☰
基于STM32F103C8T6的两轮自平衡小车:姿态解算与PID调参实战
2026/10/20 6:24:38 网站建设 项目流程

做两轮自平衡小车这事,说难不算难,说容易也真的容易翻车。我记得第一次把基于STM32F103C8T6的两轮自平衡小车装好后,满怀期待地松手,结果电机“嗡”一声就把车甩出去了,传感器数据在屏幕上跳得像心电图。后来反复查资料、改代码、调参数,才慢慢摸清楚整个系统的脾气。这篇文章我就从选型、机械、姿态解算、PID控制到实际调参,把这辆小车的完整实现思路和踩坑经验整理出来,给正准备动手或者已经卡在某个环节的朋友做个参考。

1. 选型逻辑:为什么STM32F103C8T6是新手入门自平衡小车的最佳起点

1.1 这个芯片跑控制闭环到底够不够

先说结论:STM32F103C8T6对于两轮自平衡小车来说,性能是绰绰有余的。它是一颗Cortex-M3内核的32位单片机,最高主频72MHz,Flash 64KB,RAM 20KB。很多人一看到“64KB Flash”心里就打鼓,觉得会不会不够用,实际上自平衡小车的代码量也就几千行,包含姿态解算和PID控制的完整工程编译出来通常在20KB到30KB左右,Flash余量非常充裕。

真正决定性能的是控制闭环的实时性要求。一个典型的自平衡控制循环是这样的:读取MPU6050的加速度计和陀螺仪数据,做一次姿态融合得到当前倾角,把角度和角速度送入PID控制器计算输出,最后通过定时器更新PWM占空比驱动电机。这个循环需要在1到5毫秒内重复执行一次,如果控制周期拖到10毫秒以上,小车就会明显“迟钝”,很容易倒。72MHz的主频处理这些计算完全是小事,即使加上编码器速度环,整个闭环依然能稳稳跑在1kHz以内。

相比常见的8位单片机或者某些开发板,STM32F103C8T6的优势在于外设齐全且定时器精度高。PWM输出、I2C读取传感器、串口调试、编码器接口这些功能都可以用硬件外设完成,不占用CPU中断资源去模拟时序,这对控制系统的稳定性非常重要。而且这块芯片的生态非常成熟,网上资料多,遇到问题基本都能搜到解决方案,对新手极其友好。

1.2 外设资源盘点与引脚分配参考

做自平衡小车,最怕的就是引脚分配不合理。我习惯先把资源规划表列出来再接硬件,这样能避免后面调试时发现两个外设抢同一个引脚的问题。

功能模块使用的芯片外设典型引脚备注
MPU6050姿态传感器I2C1PB6(SCL)、PB7(SDA)也可以接软件I2C,但硬件I2C更稳
左侧电机PWMTIM2的CH1/CH2PA0、PA1与编码器接口分开
右侧电机PWMTIM3的CH1/CH2PA6、PA7或者使用TIM4
电机方向控制任意GPIOPA3、PA4、PB0、PB1输出高低电平切换正反转
串口调试USART1PA9(TX)、PA10(RX)通过USB转TTL连接电脑
编码器输入定时器编码器模式TIM4的CH1/CH2如果做速度环才需要

有一个非常容易踩的坑:PB3、PB4、PA15这些引脚默认是JTAG调试引脚,如果把它们当成普通GPIO用,会发现IO怎么都拉不高,因为芯片上电后这些引脚被调试外设占用了。建议把电机方向控制、指示灯等功能放在没有复用冲突的引脚上,实在要用这些引脚,需要先用代码关闭JTAG复用功能。

1.3 最小系统板的选购与检查要点

市面上常见的STM32F103C8T6最小系统板有两种形态:一种是带USB口的老款风格板子,另一种是只有邮票孔和排针的精简板。两种都适合做小车,但有几个细节要检查。

第一是晶振。大部分板载8MHz晶振,但有些精简板用的是16MHz或者靠芯片内部RC振荡器。固件里的系统时钟配置必须和实际晶振匹配,否则串口波特率会乱、I2C时序会错,小车根本没法正常工作。建议买回来先跑一个串口打印例程,确认系统时钟是否准确。

第二是板载LDO的压差和输出能力。有的板子用AMS1117-3.3,输入5V输出3.3V,电流余量不大,带MPU6050和天线模块还行,但不要指望它给其他大功率外设供电。电机驱动必须单独接电池电源,不要把电机电流拉到单片机LDO上。

第三是BOOT0引脚状态。正常跑程序BOOT0要拉低。有些板子把BOOT0做成跳帽,如果跳帽插错位置,ST-Link烧录正常,但上电后程序不运行,容易被误判为芯片坏了。

2. 自平衡的物理本质与控制原理拆解

2.1 小车本质上是一根倒立摆

靠两个轮子让车身立住,物理模型完全可以看成一根倒立摆。把车身简化成一根杆子,重心在轮轴上方,杆子天生就会往一侧倒。想让杆子不倒,唯一办法是给底座施加一个水平方向的加速度,让支撑点去“追”重心。

拿扫帚立在手掌上做例子:扫帚往左倒,你的手就往左移动,扫帚就往右回;手移动的速度越快,扫帚回正得越快。两轮小车也是这样,电机驱动轮子左右移动,车身倾角就作为控制反馈,让轮子始终往车身倾倒的方向加速移动。轮子移动的速度和车身的倾角、倾角变化速率之间,存在一个可以量化的比例关系,这就是PID控制器要干的事情。

这里有个初学者容易理解偏的地方:小车不是“等车身倒了一定角度再纠正”,而是要从倾角和角速度里预测出倾倒趋势,提前给轮子加速。如果等肉眼可见偏了再去控制,系统延迟早就让车倒了。这也是为什么传感器采样频率和控制频率都要尽量高的原因。

2.2 测量、决策、执行三个环节闭环

整套系统可以拆成三个环节。首先是测量环节,MPU6050输出车身当前的倾角和角速度;然后是决策环节,主控根据倾角偏差计算出电机需要的目标加速度;最后是执行环节,电机驱动芯片把控制信号转成电机两端的电压和电流,让车轮产生名副其实的推挤动作。

三个环节中任何一环延迟过大,整辆车都会失去稳定。我实测下来,控制循环的执行时间对稳定性影响特别明显:同样是P=20、D=150的参数,在5ms周期下小车能勉强立住但来回摆,把周期压到2ms之后,车身明显变“硬”,晃动幅度立刻小了很多。所以代码里不要用堵塞式的延时,尽量用一个定时器中断或者一个固定频率的任务调度来跑主循环。

2.3 三个控制增益的直觉理解

在动手调参之前,先把比例项、微分项和积分项这三者到底在干什么搞清楚,后面能少走很多弯路。

比例项(P)负责“根据当前倾角偏差给多大加速度”。车身偏1度给多大输出,就是P。P太小,车身倾斜了轮子却反应不大,结果就是车身慢慢倒下去;P太大,轮子反应过度,车身会绕着平衡点来回震荡,甚至出现危险的猛烈抖动。

微分项(D)负责“根据角速度提前抑制趋势”。等于说它站在比例项前面观察变化速度,如果车身正在快速朝一边倒,即使当前角度偏差还不大,微分项也会先给出一个反向输出把它拦住。这就是阻尼的效果。D太小车会来回晃;D太大,车会被“僵住”,同时放大传感器噪声,表现为高频发抖。

积分项(I)处理的是长期偏差,比如重心略微偏向右侧导致小车总是慢慢往右跑,这时候积分项会累计这个误差逐渐修正输出。但在平衡车上,积分项用不好的话副作用很明显:车身每次快回正时积分残留会把它再推出去,造成周期性摆动。所以很多双轮平衡方案不用I,或者只把它用在速度环上而不是角度环上。

3. 硬件搭建:元器件选型与接线参考

3.1 一套不用反复返工的核心器件组合

硬件方案我前后试过几种,最终稳定下来的是这么一套。STM32F103C8T6最小系统板做主控,MPU6050模块负责姿态检测,电机驱动用TB6612FNG,电机用带霍尔编码器的N20减速电机,电池用两节18650串联的7.4V锂电。

TB6612FNG是很值得推荐的驱动方案,它对比常见的L298N优势很明显:体积小、导通压降小、发热低。L298N在低电压下容易因为压降太大导致电机参数大打折扣,而TB6612在7.4V输入下,输出到电机的电压损失很小,控制效果更直接。如果手头只有L298N,也不是不能用,只是要把电池电压稍微抬高一点,或者接受电机响应变慢的现实。

电机选择上,TT马达(130电机加减速箱)也能跑,但控制精度确实有限。TT马达的减速比常见的是1:48,空载转速偏高,齿隙比较大,低速时响应不均匀,会出现电机明明给了PWM却在死区附近不转的情况。N20电机体积小、减速比高、带编码器,价格稍贵但控制效果好很多。如果预算有限,至少选一个带霍尔编码器的电机,后面做速度环会轻松得多。

3.2 电气接线详细参考

接线是新手翻车的第一高发区。我整理过一张自己实测没问题的接线表,照着接基本不会出乱子。

模块引脚连接到说明
MPU6050VCC3.3V注意千万不要接5V,模块内置稳压也不建议长期超压
MPU6050GNDGND(电源地)必须与电机驱动、单片机共地
MPU6050SCLPB6硬件I2C
MPU6050SDAPB7硬件I2C
TB6612VM电池正极7.4V电机供电
TB6612VCC5V逻辑供电
TB6612PWMA/PWMBPA0/PA6两路PWM输入
TB6612AIN1/AIN2任选GPIO左电机方向
TB6612BIN1/BIN2任选GPIO右电机方向
TB6612AOUT1/AOUT2左电机电机正负极
TB6612BOUT1/BOUT2右电机电机正负极
编码器VCC/GND/OUT_A/OUT_B5V、GND、定时器引脚如果只做单环可以先不接

有个特别容易被忽略的点:MPU6050的GND和电机驱动的GND、单片机的GND必须连接在一起,形成完整共地。很多人装上之后发现传感器数据乱跳、电机不转,查了半天最后发现就是没共地。电机启动瞬间电流很大,会在地线上产生电压波动,如果不共地,单片的逻辑参考电位会和传感器不一致,I2C通信直接被打乱。

3.3 机械装配里决定成败的细节

电路接好只是开始,机械装配决定这辆车好不好调。第一个重点是重心位置:电池和电路板安装位置尽量低,重心越靠近轮轴,小车越容易稳定。第二个重点是把MPU6050安装在车身的几何中心附近,并且与车身刚性固定。用双面胶粘在软绵绵的塑料壳上,电机振动会直接干扰传感器,姿态数据全毁。

轮距和轮子材质也有讲究。轮距太大,转向不灵活;轮子太窄或者表面太光滑,在小幅度调整时容易打滑,导致控制失效。我用的是一对直径65mm左右的橡胶轮,配合N20电机自带的联轴器,抓地力和响应速度比较均衡。

如果条件允许,在车身前方装一个小支撑轮或者防倾倒架子,调试阶段能少摔无数次。这个架子不用电机驱动,只在软件还没调好前防止车子直接拍地上摔坏传感器。

4. 姿态信息获取:从MPU6050原始数据到稳定姿态角

4.1 加速度计和陀螺仪为什么必须互补

MPU6050内部有一个三轴加速度计和一个三轴陀螺仪。加速度计测的是“当前加速度矢量”,静止时它输出的就是重力加速度的分量,所以能直接算出车身倾角,也就是arctan(a_y / a_z)这类公式。它的优点是长期稳定,没有漂移,缺点是噪声大,而且当车身有水平加速度时,输出的加速度不纯粹是重力分量,算出来的角度会被污染。

陀螺仪测的是角速度,对角速度做积分就能得到角度变化。它的优点是短时间非常准确,不受水平加速度干扰,缺点是有零漂,时间一长积分积累的误差会越来越大,角度会慢慢飘到十万八千里。

所以要用互补滤波或者卡尔曼滤波把两者融合:短时间信任陀螺仪积分,长时间用加速度计角度拉回来。这就是姿态解算的核心思想。

4.2 一阶互补滤波的实现原理与代码

最经典的互补滤波实现就是一行核心公式:

angle = ALPHA * (angle + gyroRate * dt) + (1 - ALPHA) * accAngle;

其中ALPHA是一个0到1之间的系数,通常取0.9到0.98。ALPHA越接近1,系统越信任陀螺仪,角度响应快但长期漂移大;ALPHA越接近0,系统越信任加速度计,角度稳定但受到振动干扰大。实际调试时我通常先设成0.95,再根据波形微调。

完整的读取流程大致是初始化I2C、配置MPU6050的量程和采样率、读取原始数据、换算成物理单位、做互补滤波。这里贴一个小段的参考代码框架:

#define ALPHA 0.95f #define DT 0.002f float angle = 0.0f; void IMU_Update(float gx, float gy, float gz, float ax, float ay, float az) { float accAngle = atan2f(-ax, az) * 180.0f / 3.1415926535f; angle = ALPHA * (angle + gy * DT) + (1 - ALPHA) * accAngle; }

注意代码里用的gy是绕车身前后轴旋转的角速度,方向正负一定要和机械安装对应好。我曾经因为传感器竖装、横装没调整坐标轴映射,导致融合出来的角度方向和实际倾角相反,整个PID控制全乱了。

4.3 采样周期与滤波效果的调试心得

互补滤波系数不是拍脑袋定的。正确的验证方法是用串口把angle值和加速度计直接算出的accAngle同时打印出来,用手缓慢晃动车身,观察两条波形是否贴合。如果angle明显滞后,说明ALPHA太大;如果angle噪声大、有毛刺,说明ALPHA太小。

控制周期越短,互补滤波的单步积分值越小,ALPHA可以适当调大。我的实验数据是:2ms控制周期,ALPHA取0.95时波形最平滑;10ms控制周期,ALPHA必须降到0.90,否则陀螺仪积分漂移效应会比拉回作用更明显。

另外强烈建议给MPU6050加上低通滤波,也就是利用芯片内部的DLPF配置寄存器,把传感器输出的截止频率设在42Hz左右。不加这个滤波器,高速振动会被当作真实角度变化喂给PID,导致车身高频抖动。

5. PID控制器的设计与实现

5.1 并联PID的基本结构

先区分什么叫并联PID和串联PID。并联结构就是三路分别计算再求和,公式是:

output = Kp * error + Kd * angleRate + Ki * integral;

这里error是目标角度和当前角度的偏差,angleRate是传感器直接读出的角速度,integral是误差积分。注意角度环的微分项不推荐用“误差的差商”,也就是error_pre - error,因为这样会把传感器噪声差分放大,而且等价于一个不稳定的伪微分。直接把陀螺仪输出的角速度用作微分项,物理意义更准确,效果也更好。

完整控制代码可以写成:

float balance_pid(float angle, float gyro_rate, float target_angle) { float error = target_angle - angle; float integral += error * dt; if (integral > I_LIMIT) integral = I_LIMIT; if (integral < -I_LIMIT) integral = -I_LIMIT; float output = Kp * error + Kd * (-gyro_rate) + Ki * integral; return output; }

注意gyro_rate前面的符号要跟error方向配合好。关键是建立一个闭环逻辑:车身往一个方向倾斜,输出要让轮子往同一方向加速,这样支撑点才能回到重心下方。符号搞反的话,车会猛地往倾倒方向加速,直接拍地,这是几乎所有新手都会经历一次的“反向炸车”。

5.2 角度环和速度环:从站住到稳定移动

如果只用了角度环,车可能会原地立住,但会非常“飘”,一个小扰动就让它往一个方向溜车。这是因为光平衡角度,没有解决车身整体漂移的问题。这时候就要加第二层——速度环。

速度环的思想很简单:如果车身整体在往右移动,就把目标角度微调一个偏量,让车稍微往左倾斜,产生一个反向的加速度,把车拉回原地。本质上速度环的输出不是直接给电机,而是叠加到角度环的目标角度上。

float speed_output = SpeedP * speed + SpeedI * speed_integral; float balance_target = speed_output; float duty = balance_pid(angle, gyro_rate, balance_target);

速度环通常用PI就行,比例项负责快速抑制车速,积分项负责消除长期的漂移。对于不打算做自主走动的朋友,速度环可以后加,先把角度环调到能站住,再加速度环让它“原地驻车”,最后再用遥控目标速度实现前进后退转弯,这个顺序不要乱。

5.3 控制参数的量级参考与初始化

很多朋友不知道PID参数第一次该给多大。我给自己常用的板子做了一组经验参数参考,具体数值会因机械结构、电机减速比和电池电量不同而浮动,但量级是有参考价值的。

参数经验量级说明
Kp15~35太小车立不住倒向一边,太大会震荡
Kd100~300数值通常比Kp大一个数量级,这是正常的
Ki0.01~0.2只在速度环用,角度环能不用就不用
控制周期2~5ms周期越长参数越难调,越短效果越硬

控制输出一定要做限幅,我通常限到±90%占空比。如果不限幅,积分饱和或者大偏差时电机直接满转,车会像兔子一样蹦出去,既不安全也容易损坏机械结构。

6. 调参实战与踩坑排查

6.1 先把“数据观测”这一步做到位

调参之前最重要的一件事是用串口把实时数据发到电脑上看,没有可视化数据,调参就是在盲人摸象。我在调试时用串口发送类似“angle,pwm”这样简单的文本帧,电脑端用串口助手或者自己写一个简单的Python脚本画波形都可以。

不要一上来就直接整车跑。我通常先把车轮架空,手拿着车身倾斜,观察串口输出的角度值和PWM输出值是否符号匹配。轮子应该往车身倾斜的方向加速追。这一步排除符号错误后再落地调参,效率会高很多。

落地后的调参顺序严格按照P→D→I来。先把速度环和积分项都清零,只留Kp,从5开始慢慢增加,直到车能明显抵抗倾斜,出现有节律的摆动或者“咕咕咕”的颤抖声。这时候说明P已经给到位了,再增加Kd抑制摆动。Kd从50开始逐步往上加,你会发现摆动逐渐收敛,车身变硬变稳。最后再观察车身是否缓慢漂移,决定要不要加一点速度环。

6.2 常见现象的排查对照表

现象可能原因处理办法
一松手就朝固定一侧倒电机方向或者传感器坐标方向反了确认角度正方向与PWM输出的符号匹配
车身上下快速颤抖Kd过大或传感器振动过大降低Kd,开启MPU6050内部低通滤波
来回周期性摆动,越摆越大Kp过小或者Kd过大增大Kp,或稍微减小Kd
能站立但车身慢慢向一侧溜去缺少速度环或者重心偏置加速度环PI,或者调整电池位置配平
站立状态下偶尔猛跳一下控制周期不稳定或I2C读取抖动用定时器驱动固定周期,检查I2C上拉电阻
跑一会越来越站不稳电池电压跌落导致电机响应变化换高放电倍率电池,或者加电源稳压电容

其中“越摆越大”这个现象最好玩,它很像教堂里的巨大钟摆:能量的输入节奏和系统的自然频率共振了。遇到这种情况,先别再调P,反而是把Kd加大一点,阻尼一上来共振立刻减弱。我的经验是:每一个阶段的参数改变不要超过当前值的20%,每次改完至少观察3到5秒再决定下一步。

6.3 供电、电机死区与传感器固定等“后置问题”

调车调到后期,问题往往不在PID参数上,而在硬件细节。电机死区就是个典型:PWM占空比小于5%的区间,电机因为静摩擦根本不转,但PID输出一个很小的值在累加,等累积到一定值电机突然猛转,车就像打了个嗝一样。解决办法是在代码里做死区补偿,当输出绝对值小于阈值时直接输出0,或者加一个固定偏置让电机跨过死区。

供电是另一大坑。N20电机的启动电流和堵转电流都不小,如果用普通18650电池,电压在负载下会跌落得很快。实测同一组参数,电池电压从8.4V降到7.0V时,平衡效果会明显变差,因为同样的PWM占空比对应的电机电压和加速度都变了。建议在驱动板的VM引脚处并联一个大容量的电解电容,比如470uF甚至1000uF,能显著改善瞬间大电流对系统电压的冲击。

最后再提一句传感器固定方式。MPU6050模块不要用那种很长的排针直插,车身微小时排针会像弹簧一样产生形变,让传感器读数耦合进机械振动。我最后用的是排针焊接后用热熔胶把模块和固定面粘死,收到效果特别好,原来高频抖动的问题直接消失。

这套两轮自平衡小车的项目做到现在,我自己最大的体会是:平衡控制不是“代码写对就行”,它是一个从机械到电气再到软件的完整系统闭环。先学会把每一环做扎实,再用PID把各个环节串起来,剩下的就是耐心调参和细心观察数据。硬件方案、控制代码和调参经验都在上面了,真要复刻的话,先从中断频率和传感器方向检查入手,九成问题都能提前避开。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询