
1. 項目緣起為什么選擇FreeRTOS來做平衡小車如果你玩過或者聽說過平衡小車大概率知道它最核心的挑戰就是“平衡”二字。一個直立的兩輪小車本質上是一個倒立擺系統它天生就是不穩定的需要不斷地檢測自身的傾斜角度然后通過驅動輪子前后移動來“追”著重心從而維持直立。這個過程對控制系統的實時性要求極高。傳統的裸機編程用一個main函數里的while(1)大循環配合定時器中斷來處理傳感器數據、執行控制算法、驅動電機在簡單任務時還能應付。但當你需要同時處理MPU6050的I2C數據讀取、編碼器脈沖計數、PID控制計算、藍牙串口通信、遙控指令解析甚至還想加個OLED屏幕顯示狀態時整個程序就會變得異常復雜中斷嵌套、時序沖突、邏輯耦合等問題會接踵而至代碼的可維護性和擴展性會急劇下降。這時候一個實時操作系統RTOS的價值就凸顯出來了。FreeRTOS作為一款輕量級、開源、在嵌入式領域應用最廣泛的RTOS它能把你的復雜任務拆分成多個獨立的“線程”在FreeRTOS里叫任務每個任務只關心自己的邏輯。比如一個任務專門以100Hz的頻率讀取MPU6050數據并做濾波另一個任務以50Hz的頻率執行姿態解算和PID控制再有一個任務負責處理串口接收到的遙控指令。FreeRTOS的內核會負責這些任務的調度、同步和通信讓你從繁瑣的時序管理中解放出來更專注于每個功能模塊本身的實現。所以基于STM32和FreeRTOS來做平衡小車不僅僅是為了“炫技”或者“堆砌技術棧”它是一個非常契合實際需求的工程實踐。它能讓你深刻理解多任務并發、實時調度、任務間通信隊列、信號量等核心概念而這些正是現代復雜嵌入式系統的基石。這個項目做完你對嵌入式系統的理解會從“單片機編程”躍升到“嵌入式軟件架構”的層面。2. 硬件平臺搭建與核心器件選型做平衡小車硬件是骨架。選型不當軟件再精巧也無力回天。這里我們基于性價比和易用性給出一個經典的方案。2.1 主控芯片STM32F103C8T6核心板為什么是它STM32F103C8T6江湖人稱“藍橋杯”或“最小系統板”價格低廉通常十幾塊錢資源對于平衡小車來說綽綽有余。它基于ARM Cortex-M3內核主頻72MHz擁有64KB Flash和20KB RAM。運行FreeRTOS和基本的控制算法完全夠用。更重要的是它的資料和社區支持是STM32系列里最豐富的幾乎你遇到的任何問題都能在網上找到答案。注意購買時建議選擇帶USB轉串口芯片如CH340的核心板這樣只需要一根USB線就能完成供電、程序下載和串口調試非常方便。2.2 姿態傳感器MPU6050平衡小車的“眼睛”和“耳朵”。MPU6050是一個集成了3軸陀螺儀和3軸加速度計的六軸運動處理傳感器。陀螺儀測量角速度可以積分得到角度但存在漂移加速度計在靜止時可以通過重力分量解算出姿態角但運動時會有嚴重干擾。我們需要通過一種算法通常是互補濾波或卡爾曼濾波將兩者的數據融合得到一個相對穩定、準確的傾角值。這是整個平衡控制的基礎。MPU6050通過I2C接口與STM32通信。市面上模塊很多建議選擇帶電平轉換通常模塊輸出3.3V和備用電池座用于保持DMP數據的版本。2.3 電機與驅動TT減速電機 TB6612FNG電機常用的是N20減速電機或普通的TT馬達黃色齒輪電機。TT電機價格便宜扭力尚可但轉速和一致性一般。對于學習項目完全足夠。如果追求更好性能可以考慮帶編碼器的直流減速電機這樣可以直接獲得速度反饋實現更精準的速度控制。驅動芯片TB6612FNG是驅動直流電機的明星芯片它比傳統的L298N效率更高、發熱更小、體積也更小巧。一片TB6612可以驅動兩個電機支持PWM調速和正反轉控制正好滿足我們兩個輪子的需求。2.4 電源管理18650鋰電池與降壓模塊小車需要移動必須使用電池供電。常見的方案是兩節18650鋰電池串聯約7.4V-8.4V。這個電壓不能直接給STM32核心板3.3V/5V和MPU60503.3V供電所以需要降壓模塊。電機供電電池電壓7.4V直接供給TB6612的VM引腳用于驅動電機。控制系統供電電池電壓先經過一個降壓模塊如LM2596降到5V這個5V給STM32核心板的5V引腳供電同時也可以給TB6612的VCC引腳邏輯供電供電。STM32核心板上的LDO會再將5V轉為3.3V供自身和MPU6050使用。實操心得電源是關鍵務必確保電機大電流和控制部分小電流的電源是分開的或者在電源入口處并聯一個大電容如470uF-1000uF來緩沖電機啟停造成的電壓波動。很多情況下小車無法穩定不是代碼問題而是電源被電機拉垮了導致單片機復位或傳感器數據異常。2.5 車架與結構可以購買現成的亞克力或鋁合金平衡小車套件車架也可以自己用洞洞板或3D打印制作。核心要求是重心要高于輪軸。這是倒立擺能立起來的前提。通常會把電池放在車體的最上方來抬高重心。完整的硬件連接清單如下模塊與STM32的連接引腳說明MPU6050SDA - PB7, SCL - PB6I2C1接口需接上拉電阻模塊通常已集成TB6612 (左電機)PWMA - PA8, AIN1 - PB12, AIN2 - PB13PA8用于PWM調速PB12/PB13控制方向TB6612 (右電機)PWMB - PA9, BIN1 - PB14, BIN2 - PB15PA9用于PWM調速PB14/PB15控制方向TB6612STBY - PC13使能引腳高電平有效可直接接VCC或MCU IO編碼器 (如有)左A相 - PA0, 左B相 - PA1使用定時器編碼器接口模式右A相 - PA6, 右B相 - PA7使用定時器編碼器接口模式串口1 (調試)PA9(TX) - USB轉串口RX, PA10(RX) - USB轉串口TX用于printf打印調試信息藍牙模塊 (可選)PA2(TX) - 藍牙RX, PA3(RX) - 藍牙TX串口2用于無線遙控3. FreeRTOS在STM32上的移植與基礎任務設計有了硬件接下來就是讓FreeRTOS在STM32上跑起來并設計好我們的任務骨架。3.1 工程創建與FreeRTOS移植現在最省事的方法不是手動移植而是使用STM32CubeMX工具。CubeMX可以圖形化配置引腳、時鐘、外設并一鍵集成FreeRTOS中間件。新建工程選擇MCU型號為STM32F103C8Tx。配置時鐘樹將HCLK設置為72MHz最大。配置外設I2C1: 用于MPU6050模式為I2C速度標準模式100kHz即可。TIM1 和 TIM8 (或 TIM3, TIM4): 配置為PWM Generation CH1/CH2用于驅動兩個電機。預分頻和周期值需要計算目標是產生一個頻率在10kHz-20kHz左右的PWM聽不到電機嘯叫為宜。USART1: 異步模式波特率115200用于調試。USART2: 異步模式波特率9600或115200用于連接藍牙模塊可選。如果使用編碼器TIM2和TIM3: 配置為Encoder Mode用于讀取編碼器脈沖。啟用FreeRTOS在Middleware選項卡中選擇FREERTOS接口選擇CMSIS_V2這是ARM為RTOS定義的通用接口標準更規范。在Configuration選項卡里可以設置內核參數比如TOTAL_HEAP_SIZE: 堆大小設為1024 * 10即10KB對于我們的任務足夠。USE_PREEMPTION: 啟用搶占式調度。生成代碼點擊生成代碼選擇MDK-ARMKeil或IDE。CubeMX會自動生成包含FreeRTOS的完整工程以及所有外設的HAL庫初始化代碼。3.2 設計核心任務與優先級在freertos.c文件或你自己創建的應用任務文件中我們創建幾個核心任務。任務優先級數字越大優先級越高在FreeRTOS默認配置下。姿態解算任務 (Task_IMU)優先級最高例如osPriorityHigh。因為傾角數據是所有控制的基礎必須及時更新。功能以固定的頻率如100Hz讀取MPU6050的原始數據進行校準、濾波如互補濾波計算出當前小車的俯仰角Pitch和角速度。實現關鍵這個任務必須嚴格定時。我們可以使用vTaskDelayUntil()這個API來實現精確的周期執行而不是簡單的vTaskDelay()。平衡控制任務 (Task_Balance)優先級高僅次于IMU任務。功能以稍低的頻率如50Hz執行。從IMU任務獲取最新的角度和角速度結合編碼器得到的速度如果有時進行直立環PD控制和速度環PI控制的串級PID計算最終輸出兩個電機的PWM控制量。任務間通信如何從IMU任務獲取數據這里就需要用到FreeRTOS的隊列Queue。IMU任務每計算完一次姿態就將數據一個結構體包含角度、角速度等發送到隊列。平衡控制任務則阻塞在這個隊列上一旦有新數據到來就立刻取出進行計算。這保證了控制任務拿到的是最新數據且不會空跑。速度測量任務 (Task_Encoder)優先級中。功能如果使用了編碼器這個任務負責定時如20Hz讀取定時器編碼器模式下的計數器值通過計算差值得到輪子速度脈沖數/時間。然后將速度值通過另一個隊列發送給平衡控制任務。注意讀取編碼器計數器是個“快照”操作可以在中斷中完成但速度計算和發送隊列放在任務中更安全避免在中斷中執行過長的操作。通信任務 (Task_Comm)優先級低。功能處理串口數據。例如解析藍牙APP發來的遙控指令前進、后退、轉向并將這些指令轉化為目標速度或轉向差速通過隊列或全局變量需注意互斥傳遞給平衡控制任務。同時也可以將小車當前的狀態角度、速度、PWM輸出等打包發回給上位機進行調試顯示。任務間通信關系圖文字描述[MPU6050] --I2C-- [Task_IMU] --(隊列:姿態數據)-- [Task_Balance] [編碼器] --定時器中斷-- [Task_Encoder] --(隊列:速度數據)-- [Task_Balance] [藍牙串口] --USART中斷-- [Task_Comm] --(隊列:遙控指令)-- [Task_Balance] [Task_Balance] --PWM輸出-- [TB6612] -- [電機]4. 核心算法實現從傳感器融合到PID控制這是平衡小車的“大腦”部分決定了它能否站穩、走好。4.1 MPU6050數據讀取與校準首先你需要一個MPU6050的驅動庫能通過I2C讀取其寄存器中的原始數據加速度計AD值、陀螺儀AD值。在開始前必須進行校準。將小車靜止水平放置連續讀取數百個加速度計和陀螺儀樣本分別求平均值這些平均值就是零偏Offset。在后續讀取中每個原始值都要減去對應的零偏。// 示例讀取加速度計原始值簡化 HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, ACCEL_XOUT_H_REG, 1, buffer, 6, 100); raw_accel_x (buffer[0] 8) | buffer[1]; accel_x (raw_accel_x - accel_offset_x) / ACCEL_SCALE; // 轉換為重力加速度g4.2 互補濾波算法這是融合加速度計和陀螺儀數據得到穩定傾角的關鍵。原理很簡單利用加速度計在長期靜態下的準確性來修正陀螺儀積分產生的漂移。// 一階互補濾波偽代碼 float a 0.98; // 互補濾波系數通常0.98左右可調 float dt 0.01; // 采樣周期對應100Hz即0.01秒 void ComplementaryFilter(float accel_angle, float gyro_rate) { static float angle 0; // 陀螺儀積分得到角度預測 angle a * (angle gyro_rate * dt) (1 - a) * accel_angle; return angle; }accel_angle由加速度計數據atan2(accel_y, accel_z)計算出的角度小車前后傾斜時主要看Y軸和Z軸。gyro_rate陀螺儀Y軸讀取的角速度值已減去零偏。系數a決定了你更相信誰。a越大相信陀螺儀動態響應好但會漂移a越小相信加速度計靜態準但動態干擾大。需要根據實際小車抖動情況調整。4.3 串級PID控制平衡小車通常采用兩個PID環內環是直立環快速響應外環是速度環消除靜差。直立環PD控制輸入當前融合后的角度angle和目標角度通常就是0度直立狀態。輸出一個用于維持直立的基本電機出力balance_out。公式balance_out Kp1 * (angle - target_angle) Kd1 * gyro_rate;為什么用PD不用PID因為角度環本身是一個“位置”環積分項I容易引入滯后和超調在快速反應的直立控制中反而不好通常只用比例P和微分D。微分項直接用了陀螺儀的角速度這相當于對角度進行了“超前矯正”能有效抑制小車倒下。速度環PI控制輸入當前編碼器測得的速度speed和目標速度來自遙控指令靜止時為0。輸出一個用于調節速度的補償量speed_out。公式speed_out Kp2 * (target_speed - speed) Ki2 * speed_integral;作用如果小車因為地面輕微傾斜或電機差異導致在直立平衡時有一個緩慢的移動趨勢速度環會檢測到這個非零的速度并通過積分項I累積一個輸出去抵消這個趨勢讓小車最終能穩定在原地。速度環的輸出是疊加在直立環輸出上的。轉向環單獨P控制輸入目標轉向差速來自遙控和當前轉向速率可以通過左右輪速差估算。輸出一個給左右輪附加的差動PWM值turn_out。最終輸出left_motor_pwm balance_out speed_out - turn_out; right_motor_pwm balance_out speed_out turn_out;注意要對left_motor_pwm和right_motor_pwm進行限幅不能超過PWM計數值范圍。PID調參順序非常重要先調直立環將速度環和轉向環的輸出置零。用手扶住小車給它一個傾斜角度觀察輪子反應方向是否正確應該往倒下的方向轉動。先調Kp1讓小車能有明顯的回正力度再調Kd1抑制小車回正過程中的抖動。目標是松手后小車能在原地小幅度高頻“抽搐”而不倒下。再調速度環直立環調好后加入速度環。將Kp2和Ki2從很小的值開始增加。你會發現小車變得更“穩”了高頻抽搐減少能更長時間站立。但Ki2太大會引起低頻前后搖擺需要耐心微調。最后調轉向環這個相對簡單確保轉向控制方向正確比例系數不要太大導致轉彎過猛即可。5. 系統整合、調試與避坑指南將所有的硬件、FreeRTOS任務、算法整合在一起才是真正的挑戰。5.1 系統初始化流程在main.c的main()函數中合理的初始化順序是int main(void) { HAL_Init(); // HAL庫初始化 SystemClock_Config(); // 配置系統時鐘 MX_GPIO_Init(); // 初始化GPIO MX_USART1_UART_Init(); // 初始化調試串口 MX_I2C1_Init(); // 初始化I2C MX_TIM1_Init(); // 初始化PWM定時器 // ... 其他外設初始化 printf(System Init OK!\r\n); // 確認串口通信正常 MX_FREERTOS_Init(); // **FreeRTOS初始化創建任務** osKernelStart(); // **啟動FreeRTOS內核調度** while (1) { // 這里不會執行到因為調度器已經接管 } }關鍵點所有外設硬件初始化必須在osKernelStart()之前完成。任務創建在MX_FREERTOS_Init()中完成。5.2 調試方法與工具串口打印大法好在關鍵任務中使用printf通過串口1打印變量值角度、角速度、PID輸出、電機PWM等。注意在FreeRTOS任務中頻繁打印會影響實時性最好能通過一個條件如按鍵觸發來開啟/關閉調試輸出或者將數據打包后由低優先級的通信任務統一發送。邏輯分析儀/示波器如果條件允許用邏輯分析儀抓取PWM波形和電機驅動IO的變化可以直觀看到控制輸出的頻率和占空比變化對于排查電機驅動問題非常有效。上位機曲線顯示這是最高效的調試方式。你可以編寫一個簡單的上位機如用Python的PyQt5或Matplotlib讓STM32通過串口定時發送一組數據時間戳、角度、速度、PWM等上位機實時繪制成曲線。這樣你能清晰地看到PID調節過程中系統的響應比如超調量、震蕩頻率、穩態誤差等。5.3 常見問題與避坑指南問題一小車一上電就“瘋轉”或朝一個方向猛沖。排查電機接線檢查TB6612的AIN1/AIN2和BIN1/BIN2是否接反或者PWM輸入線是否接觸不良。可以用一個簡單的測試程序固定輸出一個方向的PWM看電機轉向是否正確。角度極性這是最常見的原因MPU6050安裝的方向導致計算出的角度正負號與PID控制期望的相反。例如小車前傾時角度為負但你的PID控制邏輯可能期望為正。檢查互補濾波輸出的角度用手向前傾斜小車看串口打印的角度值是正是負其變化方向是否與你的控制邏輯匹配。不匹配就在角度或陀螺儀數據前加個負號。PID參數極性確保直立環的Kp1符號正確。如果角度為正前傾電機應該向前轉使小車前進以“追”上重心這個輸出應該是正還是負取決于你的電機驅動邏輯即PWM值增大是正轉還是反轉。問題二小車能站住但高頻劇烈抖動。原因直立環微分項Kd1太小或者比例項Kp1太大。微分項是抑制抖動的關鍵嘗試增大Kd1。同時過高的Kp1會讓系統過于“敏感”。也可能是電源問題電機快速正反轉導致電源電壓劇烈波動影響單片機穩定工作。務必在電源輸入端并聯大電容。問題三小車站幾秒后就緩慢地向一個方向跑走無法靜止。原因速度環沒有起作用或者參數不對。這說明直立環讓小車平衡了但存在一個微小的靜差比如地面不平、電機細微差異導致平衡點不是一個零速度點。檢查速度環編碼器讀數是否正確速度計算是否有誤速度環的輸出是否正確地疊加到了最終電機輸出上增大速度環的積分項Ki2但要小心太大會引起低頻振蕩。問題四FreeRTOS任務運行不正常某個任務好像“卡死”了。排查堆棧溢出這是RTOS新手最常犯的錯誤。在FreeRTOSConfig.h中增大configTOTAL_HEAP_SIZE或者增大具體任務的堆棧大小在xTaskCreate參數中設置。FreeRTOS提供了堆棧溢出檢測鉤子函數可以啟用它來輔助排查。任務優先級設置不當低優先級任務長時間占用CPU導致高優先級任務無法及時運行。檢查是否有任務中沒有使用vTaskDelay或阻塞式API如xQueueReceive導致其一直處于就緒態。中斷優先級沖突FreeRTOS管理的中斷優先級有要求。對于Cortex-M3/M4需要調用HAL_NVIC_SetPriority()和HAL_NVIC_EnableIRQ()來設置中斷優先級并確保系統滴答定時器Systick和PendSV中斷的優先級是最低的。問題五MPU6050數據跳動很大角度不準。排查電源和地線確保MPU6050模塊供電穩定地與STM32共地良好。I2C上拉電阻雖然模塊通常自帶但如果通信距離稍長或干擾大可以嘗試在SDA和SCL線上額外加4.7kΩ上拉電阻到3.3V。軟件濾波除了互補濾波在讀取原始數據后可以先進行一階低通濾波平滑加速度計數據。filtered_data a * filtered_data (1-a) * new_data。安裝位置盡量將MPU6050安裝在車體中心并減少電機振動傳遞到傳感器。可以在模塊和小車底板之間墊上海綿或軟膠減震。6. 功能進階與項目擴展當你的小車能夠穩穩立住之后就可以嘗試給它增加更多“技能”了這也是FreeRTOS多任務優勢的體現。6.1 增加循跡功能購買五路或七路紅外循跡模塊安裝在車體底部。創建一個新的任務Task_Trace。任務設計該任務以較低優先級如osPriorityBelowNormal運行定時如30Hz讀取紅外傳感器的IO狀態。算法根據中間、左、右等傳感器的黑白線檢測情況計算出一個偏差值然后通過修改Task_Balance中的turn_out目標值讓小車進行差速轉向從而實現沿黑線行走。任務通信Task_Trace可以通過一個全局變量或隊列將計算出的“期望轉向量”發送給Task_Balance。6.2 增加無線遙控與參數調試利用藍牙模塊如HC-05/06或Wi-Fi模塊如ESP-01S創建一個通信任務Task_Comm。協議設計定義簡單的串口通信協議。例如手機APP發送”SPD:50\n“表示設置目標速度為50”KP1:12.5\n“表示動態修改直立環的Kp參數。任務實現在Task_Comm中解析串口接收緩沖區可以在串口中斷中填充一個環形緩沖區的數據根據協議修改對應的全局變量。注意修改像PID參數這樣的關鍵變量時最好使用FreeRTOS的信號量Semaphore或互斥量Mutex進行保護防止在Task_Balance正使用該參數計算時被中途修改導致數據不一致。數據回傳Task_Comm也可以定時從其他任務獲取狀態數據打包后發送給手機APP實現實時波形顯示這將極大提升調試效率。6.3 使用DMP庫獲取更優姿態MPU6050內部自帶一個數字運動處理器DMP它可以自動完成傳感器數據融合和四元數解算直接輸出穩定的歐拉角俯仰、橫滾、偏航。使用DMP可以省去自己編寫和調試互補濾波的麻煩效果通常也更穩定。實現需要導入InvenSense公司提供的官方inv_mpu.c和inv_mpu_dmp_motion_driver.c等庫文件。初始化后DMP會通過一個FIFO輸出數據你只需要定時讀取這個FIFO即可獲得解算好的姿態。注意DMP庫的移植需要一定的耐心主要是適配I2C讀寫接口和延時函數。使用DMP后姿態解算任務Task_IMU的負擔會減輕代碼也更簡潔。從零開始將一堆散件變成一個能自主平衡、甚至聽從指令行動的小車這個過程充滿了挑戰也極具成就感。它強迫你去理解硬件的電氣特性、軟件的實時調度、控制理論的經典算法。每一次調試每一次參數的微調看到小車從倒下到站穩從抖動到平穩都是對理論知識的絕佳驗證。這個項目最大的價值不在于結果而在于這個完整的、跨學科的實踐過程。當你親手調通的那一刻之前所有的疑惑和挫折都會化為扎實的經驗沉淀下來。