公司动态

基于STM32与FreeRTOS的平衡小车:从实时系统到PID控制实战

📅 2026/7/31 6:05:08
基于STM32与FreeRTOS的平衡小车:从实时系统到PID控制实战
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的负担会减轻代码也更简洁。从零开始将一堆散件变成一个能自主平衡、甚至听从指令行动的小车这个过程充满了挑战也极具成就感。它强迫你去理解硬件的电气特性、软件的实时调度、控制理论的经典算法。每一次调试每一次参数的微调看到小车从倒下到站稳从抖动到平稳都是对理论知识的绝佳验证。这个项目最大的价值不在于结果而在于这个完整的、跨学科的实践过程。当你亲手调通的那一刻之前所有的疑惑和挫折都会化为扎实的经验沉淀下来。