STM32两轮自平衡小车实战:从姿态解算到PID调参与硬件细节 做两轮自平衡小车几乎是每个玩单片机的开发者都会想碰一次的项目。用STM32F103C8T6做主控配一块MPU6050姿态传感器再加上两个带编码器的直流减速电机就能搭出一台能自己站稳的小车。零件不算多但姿态解算、PID控制、电机驱动和机械结构全都被串在一起属于典型的“小项目、大系统”。这篇文章不打算照着开发板教程念一遍而是按我做这个项目时的真实顺序展开先把小车到底在控制什么讲清楚再讲传感器数据怎么处理、PID怎么往下调然后落到硬件组装和底层驱动的细节最后整理一份常见故障排查清单。适合刚接触闭环控制的新手也适合车已经能跑、但总是站不稳想搞明白原因的玩家。1. 先把项目看透两轮小车到底在解什么题1.1 从倒立摆说起小车为什么要不断“找重心”两轮自平衡小车本质上就是一个移动的倒立摆。把小车放在水平地面上重心在轮轴上方只要车体偏离垂直方向哪怕一点点重力就会产生一个让它继续倾倒的力矩。没有外力干预它是不可能自己站稳的。要让这种不稳定的系统保持直立唯一的办法就是主动把“支撑点”送回重心下方。你可以试试在手掌上立一把长扫把扫把往左歪你就把手往左移扫把往右倒你就把手跟过去。小车也是这么干的两个轮子就是那双手只是它的反应速度要比你用手掌快得多而且每一丝偏差都要在毫秒内修正。这也决定了整个项目的控制本质系统不是“保持静止”而是“动态平衡”。小车时刻都在偏离和纠正之间循环看起来静止实际上每时每刻都在高频修正。1.2 完整控制链路传感器、算法、执行器怎么配合把这个过程拆开就是一条典型的闭环控制链传感器MPU6050提供车体倾斜角度和角速度编码器提供轮子转速。控制器STM32F103C8T6读取反馈运行姿态解算和PID算法。执行器计算得到的PWM值送给电机驱动芯片控制电机输出扭矩。控制链路可以这样描述车体倾斜 → 角度变化 → 主控计算新PWM → 电机加速或减速 → 车体姿态变化 → 再次读取角度。只要这个循环跑得足够快、足够稳小车就能站住。实际操作中控制周期一般取5ms到10ms。也就是每5毫秒完成一次“读取传感器→解算→PID→输出PWM”的完整流程。控制周期不能太长因为系统本身很不稳定等太久才做出反应车早就倒了也不能一味追求太快太快的周期会把传感器噪声和结构振动当成真实姿态反而让小车发抖。1.3 为什么是STM32F103C8T6资源和算力刚好合适STM32F103C8T6是一颗Cortex-M3内核的单片机主频72MHz64KB Flash20KB SRAM。很多人一开始会怀疑这颗芯片跑姿态解算和PID会不会吃力实际完全不用担心。一套最简单的互补滤波加串级PID即使把浮点运算都裸写出来在72MHz主频下一次完整计算也用不了1ms。而平衡小车的控制周期通常是5ms到10ms算力冗余非常大。真正重要的不是算力而是它的外设配置F103有多个16位定时器可以输出高频PWM还能把定时器配置成编码器接口模式直接读取增量式编码器的A/B相信号不需要额外硬件电路。再加上这颗芯片的资料和例程积累非常多遇到问题基本都能搜到答案成本也很低。综合算力、外设、生态和价格它是做两轮平衡小车非常合适的选择。1.4 整体方案选型传感器、电机、驱动怎么配按照常见做法我会把硬件划分为几个模块主控STM32F103C8T6最小系统板外接一个USB转串口模块用于日志输出。姿态传感器MPU6050六轴传感器通过I2C接口连接。它自带陀螺仪和加速度计满足平衡控制需求。电机N20之类的小型直流减速电机带霍尔编码器减速比1:30左右。这个减速比配合小车重量能让轮子输出足够的扭矩。电机驱动TB6612FNG或者DRV8833这类双路驱动芯片输入PWM和方向信号输出驱动电机。电池2S锂电电压约7.4V直接给电机驱动供电再通过稳压模块给单片机和传感器供电。这些参数不是固定答案市面上不同小厂的电机和模块参数会有差异但总体思路是一致的扭矩够、响应够快、反馈够准。把这一套凑齐硬件基础就具备了。2. 姿态解算别急着调PID先把角度搞准2.1 陀螺仪和加速度计各自能干什么姿态解算是整个项目的地基。角度算得不准后面PID调得再好也白搭。MPU6050里有两种传感器各自有各自的脾气。陀螺仪测量的是角速度它告诉你车体“正在以多快的速度往哪边转”。对角速度做积分就能得到角度变化量。它的优点是响应快几乎没有延迟缺点是积分过程会不断累积误差时间一长角度就会漂移哪怕只是很小的零点偏置几分钟也能漂出几十度。加速度计测量的是当前的重力加速度向量。通过计算向量和车体坐标轴的夹角可以直接得到倾斜角。它的优点是没有累积漂移长期来看非常稳定缺点是对振动和运动加速度敏感轮子一颠、电机一震角度数据就会剧烈跳动。一个管短期、一个管长期两者一结合就是姿态解算的基本思路。2.2 互补滤波为什么够用系数怎么取把陀螺仪的短期准确性和加速度计的长期稳定性结合起来最简单的办法就是互补滤波。它的核心逻辑是角度主要由陀螺仪积分得到因为它平滑、响应快再用加速度计计算的倾角不断把积分结果往回拉防止它漂走。伪代码可以这样表示angle 0.98 * (angle gyro_y * dt) 0.02 * acc_angle;这里0.98和0.02就是两个互补权重两者相加等于1。权重越偏向陀螺仪角度越平滑但对漂移的修正越慢权重越偏向加速度计角度越能跟上真实值但噪声也更明显。常见取值范围在0.95到0.99之间。我习惯从0.98开始试如果发现角度静止时仍然慢慢漂就适当减小权重比如0.97如果发现角度噪声大、小车抖动就加大权重比如0.99。很多教程会直接建议用卡尔曼滤波因为它理论上能根据噪声特性自适应融合。但卡尔曼需要维护状态协方差矩阵调参不如互补滤波直观代码也复杂很多。对于两轮平衡小车这种场景互补滤波的精度已经足够用。我的建议是先用互补滤波把整个系统跑通等有余力再尝试替换成卡尔曼。2.3 原始数据读取与零点校准别让漂移毁掉整个闭环MPU6050通过I2C读取原始寄存器使用前需要做两件事配置量程和标定零点。常用的量程配置是陀螺仪±2000°/s加速度计±2g。在这个量程下陀螺仪灵敏度是16.4 LSB/(°/s)加速度计灵敏度是16384 LSB/g。也就是说原始值除以这些灵敏度系数就能得到物理量float gyro_degs gyro_raw / 16.4f; float acc_angle atan2(acc_y, acc_z) * 57.3f;注意加速度计算角度时要选对轴。MPU6050的安装方向不同使用哪个轴的公式也不同。最稳妥的办法是上电后把小车放在水平地面上观察串口打印的角度如果是0度附近说明轴的映射是对的如果差90度或者角度正负反了就换轴或者调整符号。零点校准也很关键。上电后让小车完全静止连续采样200到2000个点分别取陀螺仪三轴和加速度计三轴的平均值作为零点保存下来。后续每次读取原始值后先减掉对应的零点再做换算。陀螺仪的零偏校准是最值得投入时间的它直接决定角度长时漂移的快慢。加速度计则还要注意安装面是否水平安装底座稍微歪一点静止角度就会有固定的偏差这个偏差会被PID当成真实的倾斜误差来处理。2.4 控制周期与数据同步定时器中断比主循环更可靠角度和角速度的读取最好和控制周期严格同步。我见过很多调不稳的情况最后发现问题出在主循环里主循环执行一遍代码的时间不固定传感器读取、串口打印、PID计算混在一起导致每次采样间隔忽长忽短。PID把这种周期抖动当成姿态变化小车就会莫名其妙地发抖。可靠的做法是用定时器中断控制节奏。比如设定一个5ms的定时器中断每次中断里做“读传感器→姿态解算→PID计算→更新PWM”。主循环只处理串口打印、按键、遥控这些不紧急的事情。这样可以保证每个控制周期的间隔基本恒定。另外要注意陀螺仪和加速度计的数据要尽量在同一周期内读到。如果上一帧读陀螺仪、下一帧才读加速度计相当于把两个不同时刻的状态混合在一起姿态解算的准确性会打折扣。3. PID控制让小车有“直觉”地校正姿态3.1 平衡环为什么用PD而不是纯P姿态角度算出来后下一步就是通过PID把角度拉回零点。最核心的平衡环通常只需要PD两个环节。比例项P和当前角度成正比车体倾斜角度越大输出的纠正力矩就越大。理论上只靠P也能让小车在某些情况下保持直立但实际会发现小车会在零位附近来回摆动幅度越来越大最后发散倒掉。原因很简单P只是根据“现在歪了多少”输出力但小车在向零位恢复的过程中会带上速度到达零位时速度还在就会冲过零点歪向另一边。如果没有阻尼这个来回就会一直持续下去。微分项D解决的就是这个问题。D和角速度成正比角速度越大输出的反向阻尼越强相当于在车轮上加了“刹车”。D项可以抑制速度让车体在靠近零点时放慢速度避免冲过头。实际代码可以写成pwm_balance kp_balance * angle kd_balance * gyro_y_deg;这里直接用陀螺仪y轴输出的角速度作为微分项比用角度差分更平滑也不会因为差分引入额外噪声。这是很实用的小技巧。有一个特别容易踩的坑是符号方向。如果角度正方向和PWM正方向不匹配平衡环会变成正反馈车往左边歪你却给一个让轮子把车推得更往左歪的力小车会瞬间倒下去。第一次上电调试时建议把车拿在手里手动倾斜车体感受电机输出的力矩是不是在对抗你的手。方向不对就翻转PWM符号。3.2 速度环为什么平衡环之外还要加一个外环纯平衡环能让小车像“钉子”一样原地站住吗不能。原因在于车体重心不可能刚好落在轮轴正上方总会有微小的偏移。这个偏移会让平衡环认为车在倾斜于是输出一个让轮子“追赶”的力。小车开始往前或往后移动越移越斜速度也跟着越来越快直到追不上倒掉。做过纯平衡环调试的人会看到一种奇怪的现象小车站得很直但整台车在匀速往前跑最后摔出去。速度环就是为了管住这个“跑”。它通过编码器读取轮子的实际转速目标是让轮子速度为0。如果车正在往前跑速度环就输出一个修正量作用到平衡环的目标角度上让车体稍微向后倾斜产生一个反向的加速度把车速拉回0。这是典型的串级控制结构// 速度环输出作为平衡环的目标角度偏置 pwm_speed kp_speed * (target_speed - speed) ki_speed * speed_integral; target_angle angle_balance pwm_speed; // 最后平衡环按 target_angle 计算PWM速度环的周期可以比平衡环慢一些比如20ms执行一次。因为速度变化比姿态变化慢没必要每个控制周期都算。速度环的积分项需要做限幅防止累计值过大导致输出饱和。3.3 转向环让小车可以转向又不影响平衡转向控制和平衡控制可以分开处理这是两轮平衡小车一个非常巧妙的特性。转向靠两个轮子的差速实现左轮和右轮一个加速一个减速车体就会绕着垂直轴转动。这个转动和车体的前后倾斜是解耦的也就是说只要左右PWM对称地一增一减对平衡环的直立效果基本没有影响。转向环通常用MPU6050的Z轴角速度作为反馈。简单的PD控制就能让车头对准目标方向pwm_turn kp_turn * (turn_target - gyro_z_deg);最终输出到左右电机时把转向量和平衡量叠加pwm_left pwm_balance pwm_turn; pwm_right pwm_balance - pwm_turn;这个结构特别方便想遥控小车时只需要通过蓝牙或者无线模块给主控发一个目标转向角速度转向环会负责执行想让小车保持直线就把目标转向角速度设为0。顺便还能纠正两个电机转速不完全一致导致的车体缓慢旋转问题。3.4 参数整定从P到D再到速度环的完整顺序调参是整个项目里最考验耐心的环节。我的建议是一次只动一个变量从最内层往外层调先把传感器角度数据通过串口打印出来确认角度和实际倾斜方向一致。只开平衡环把D先设为0从一个比较小的P开始。手持小车倾斜车体感受电机输出是否正确对抗倾斜。逐步增加P直到小车能短暂直立但会明显摆动再增加D让摆动被阻尼抑制。平衡环能站住几秒之后再开速度环。先给一个很小的P观察小车是否还在匀速跑再逐步加I消除速度静差。最后调转向环目标转向角速度设为0观察车体是否稳定朝前。下面的参数范围是我在一台常见N20电机小车上试出的示意区间具体值需要根据你的硬件调整但数量级可以参考环节参数示意范围平衡环Kp_balance100 ~ 200平衡环Kd_balance20 ~ 60速度环Kp_speed5 ~ 15速度环Ki_speed0.1 ~ 0.5转向环Kp_turn2 ~ 10调参时一定从小往大加每加一次就要观察几秒钟。参数一下子给太大小车会像抽风一样猛甩很容易伤到电机和结构。4. 硬件组装与底层驱动细节4.1 机械结构重心高度、轴距和传感器安装机械结构决定了一个系统能调得多稳定很多调参难题其实是机械问题引起的。重心越低越好。重心越低等效的“摆长”越短同样的倾角对应更小的重力力矩控制起来更容易稳定。相反如果电池、主控板全部架得很高重心抬高小车会对微小干扰非常敏感必须用更大的比例增益才能拉住系统更容易振荡。轴距也要适中。轴距太短转向灵活但前后方向的稳定性差轴距太长车身笨重电机负担大。对于F103C8T6这种小板子加上两块电池和N20电机一般车体宽度在15cm到20cm之间比较合适。传感器的安装位置最好尽量靠近车轮轴线。因为加速度计对运动加速度敏感如果安装位置离旋转轴太远车体摆动时传感器会受到额外的离心加速度干扰导致角度数据出现波动。结构固定同样重要。螺丝没拧紧、电池盒晃动、传感器用双面胶粘而没有固定牢都会让姿态数据里混入高频振动分量表现就是小车原地发抖。4.2 电机驱动与PWM输出死区、频率和方向电机驱动的底层细节里最容易忽略的是死区补偿。直流减速电机存在静摩擦力PWM值小到一定程度电机根本不会转。如果PID输出正好落在死区范围内车体明明收到力轮子却不动小车的响应就会变得迟钝甚至僵住。我调试时遇到过这样的现象小车像“冻住”一样保持一个姿势在原地轻微颤抖不前进也不倒下。死区处理分两步。第一步是在输出PWM时加一段判断if (abs(pwm) dead_zone) { pwm 0; }第二步是越过死区后做一个线性映射把死区阈值外的输出按比例放大避免输出跳变。死区阈值需要根据实际电机来测在空载状态下从0开始慢慢增加PWM记下轮子刚好开始转动的那个值这就是死区边界。PWM频率也要留意。常见做法是配置到10kHz到20kHz之间。频率太低的PWM会让电机产生明显的“咔咔”声和扭矩波动频率太高则受驱动芯片开关速度限制。另外PWM频率不要太靠近传感器的采样频率否则容易产生差频干扰。电机接线方向同样会影响系统。两个电机的电源极性接反、编码器AB相接反都会让速度反馈变成负值速度环方向反了小车只会越跑越快。上电前先用一个小测试程序让两个轮子同时正转确认方向和代码里的定义一致能省下大量排查时间。4.3 编码器读数与速度测量正交解码的实际用法测速是速度环的基础。N20电机上的霍尔编码器一般是增量式的输出A/B两路方波。转动一圈会产生一定数量的脉冲经过减速器后车轮端的脉冲数和电机轴端的脉冲数不一样。比如电机轴一圈11个脉冲对减速比1:30那么车轮每转一圈大约是330个脉冲。STM32F103C8T6的定时器支持正交编码器模式可以直接把A、B两相接在定时器的两个通道上由硬件自动判断方向和计数。配置起来非常方便核心代码大致是这样TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM4, 0);启动后TIM4的计数器值会自动跟随编码器变化正转时递增反转时递减。控制程序每个周期读取一次计数器的当前值和上次的值做差就能得到这个周期内轮子转过的脉冲数。换算成转速时要记得除法顺序和单位float speed_rps (float)(pulse_delta) / pulses_per_rev / dt;如果需要线速度再乘以轮子周长。这个换算关系到速度环参数的量级很多人的Kp_speed调不出一致的效果就是因为单位没统一。编码器读数可以直接用但最好做一点软件滤波。最简单的是一阶低通滤波或者取几次读数的平均值。速度环对噪声不像平衡环那么敏感适度的平滑可以避免速度环路输出抖动让小车跑起来更稳。4.4 电源与信号完整性电机一跑角度就乱怎么解决电源问题经常被忽略却是姿态数据污染的重要原因。电机启动和加减速时瞬时电流可以达到几百毫安甚至更高这会让电池电压产生明显的毛刺。如果单片机和MPU6050直接和电机共用电源这些毛刺会直接进入传感器供电导致角度读数抖动。我习惯把电源分成两个层级电机部分直接从电池取电单片机、传感器和无线模块从稳压出来的3.3V/5V取电。稳压模块后面再加一个100uF电解电容和0.1uF瓷片电容分别滤除低频纹波和高频噪声。驱动信号的走线也尽量短粗。PWM信号在高频翻转时长导线会引入额外的寄生电感和噪声。电机驱动模块放在靠近电机的位置主控板到驱动板的信号线不要太长。如果电机运行的时候串口打印的数据明显变乱首先检查的就是共地是否可靠。需要在电源、单片机、驱动板之间建立共同的地平面而不是让回路绕一大圈。4.5 控制周期与调度定时器中断里的代码结构整个控制循环安排在一个固定周期的定时器中断里是保证稳定性的关键。核心结构大致如下void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { read_imu_and_update_angle(); pid_balance_and_speed(); set_motor_pwm(); TIM_ClearITPendingBit(TIM3, TIM_IT_Update); } }中断函数里只做控制相关的事不打印、不延时、不等待。有些新手会在中断里加串口打印日志一个print的耗时可能比整个控制计算还长直接破坏了周期稳定性。速度环如果比平衡环慢可以用一个分频计数器实现比如平衡环每5ms执行一次速度环每4个周期执行一次也就是20ms更新一次。转向环也类似。主循环只需要处理开机自检、按键状态、遥控指令、串口调试命令这些都不影响控制稳定性。这样设计之后整个系统的响应节奏非常稳定调参时至少能排除“周期抖动”这个变量。5. 常见问题与排查技巧实录5.1 小车高频抖动、电机高频啸叫怎么查高频抖动是平衡小车调参阶段最常见的现象。用手摸一下车体感觉到的是一种细微但频率很高的振动同时电机可能发出尖锐的啸叫。第一件事不是调参数而是确认角度数据本身干不干净。把滤波后的角度通过串口打印出来观察静止状态下角度曲线是否有明显毛刺。如果传感器数据本身就是乱的问题在接线、供电、滤波而不是PID。如果角度数据很平滑但小车仍然高频抖动可能性最大的是Kd过大。微分项对噪声敏感虽然我建议直接用陀螺仪角速度但当角速度信号本身带高频噪声时Kd会把噪声放大成电机的剧烈抖动。处理方法是先大幅减小Kd甚至临时设为0看抖动是否消失。还有两个容易被忽略的原因PWM频率过低引起的电机扭矩波动以及机械结构松动引起的共振。前者把PWM频率调到10kHz以上能明显改善后者需要拧紧螺丝、固定传感器和电池。5.2 一上电就往一个方向猛冲甚至直接翻倒这个问题的原因优先级非常明确先查极性再查零点最后查环路方向。如果平衡环输出符号反了小车会以肉眼可见的速度朝一个方向“义无反顾”地倒下去。判断方法是手持小车稍稍倾斜感受电机输出是帮忙还是捣乱。如果输出方向反了把平衡环的PWM符号取反即可。编码器方向反了也会导致类似现象但表现不同小车不是直接倒而是先直立然后逐渐加速跑起来越跑越快最后翻倒。这是因为速度反馈方向反了速度环实际是在“帮倒忙”。检查编码器方向很简单手动向前转动轮子看串口打印的速度是正值还是负值和预期对照。还有一种情况是加速度计的零点偏差太大。传感器安装不是水平的或者水平校准时没有真正放平都会让静止角度输出一个很大的常量偏移平衡环会把这个偏移当成真实姿态持续输出大PWM。排查时建议把速度环先关掉只保留平衡环和转向环确认小车能站住之后再打开速度环逐项检查。5.3 站立一段时间后慢慢倒下角度在缓慢漂移小车前期能站住过了十几秒甚至几十秒才倒下多半和姿态漂移有关。陀螺仪的零偏校准不彻底是首要怀疑对象。哪怕只残留1°/s的零偏积分60秒就会漂出1度车体为了修正这个假角度会慢慢地往一个方向移动最终超出电机能纠正的范围。另一个容易出问题的地方是互补滤波的权重。如果你把权重调到了0.995这种高度信任陀螺仪的值加速度计的修正作用会变得非常弱零偏漂移很难被拉回来。适当减小权重比如降到0.98让加速度计更积极地纠正漂移。还需要注意温度的影响。传感器刚上电和运行几分钟后的温度不一样零偏也会有变化。如果你的项目对稳定性要求高可以上电后运行一分钟再执行校准而不是上电立即采集零点。5.4 小车前后“逛荡”来回振荡停不下来这是速度环和平衡环互相作用的结果通常表现为车体没有剧烈抖动而是在一个角度范围内缓慢来回摆动像喝醉酒一样。速度环的比例系数Kp过大会造成这种“逛荡”。因为速度环输出的是对平衡环目标角度的修正这个修正本身是有限幅的Kp一旦过大修正量反复在正负极限之间切换等于不断给平衡环一个振荡的指令。速度环的积分项太强也会让系统产生低频振荡。积分会把长期速度偏差累积起来一旦累积值偏大输出饱和后就会出现“压过头”的情况。给速度环积分加限幅是必须的操作一般把积分输出限制在目标角度修正范围的30%以内比如目标角度限幅±10度积分输出就限幅在±3度以内。如果速度环和平衡环的执行周期相差太小两个反馈回路容易出现耦合振荡。可以适当放慢速度环的执行周期让内外环有明确的时间差。5.5 一个能提高排查效率的调试顺序把这四类问题综合起来我整理了一个下面的速查方式现象优先排查项验证动作上电猛冲平衡环极性、编码器方向手持倾斜测试、手动转轮看速度高频抖动Kd过大、传感器供电噪声减小Kd、查看串口角度毛刺缓慢漂移陀螺仪零偏、滤波权重静止观察角度变化、重新校准前后逛荡速度环Kp/Ki、内外环周期降低Kp、加积分限幅、放慢速度环周期调试时不妨把目标拆成几个阶段传感器数据可信 → 单环能站 → 速度不跑 → 转向可控。每一步都要验证通过再进入下一步这样能把问题限定在很小的范围内而不是所有环节混杂在一起无从下手。结尾最后分享一个我从这个项目里收获最大的习惯每次只改动一个变量。调PID时不要在拧Kp的同时又去改滤波系数也不要顺手改了PWM频率否则任何现象都解释不清。我会在调参之前先把要动的参数写在笔记里改完观察一段时间再记录结果。整个两轮自平衡小车做完最大的收获不是会调PID而是养成了一层层拆解问题的耐心。如果你第一次调试就卡住了不用急先把数据打出来让问题说话它总会告诉你是哪一环错了。