ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32自平衡小车实战:串级PID与姿态控制全解析

STM32自平衡小车实战:串级PID与姿态控制全解析

简介:这份资料面向嵌入式初学者与DIY爱好者,围绕基于STM32微控制器的两轮自平衡小车展开,提供从硬件设计到软件实现的完整制作参考。包内包含原理图、控制源码、使用说明与开发笔记四大模块:原理图讲解STM32选型、陀螺仪与加速度计连接及电机驱动电路;源码涵盖数据采集、PID算法与电机控制逻辑;使用说明指导装配、固件烧录与调试运行;开发笔记则记录故障排查与性能优化经验。资源共1430个文件,以c与h源码、d与o编译中间文件、crf与uvproj工程配置、pdf文档及hex固件等为主,压缩包约53.19MB,工程结构完整便于按模块查阅。目前已有4138人学习下载,适合希望深入理解STM32应用、PID闭环控制与自平衡系统实现的中级开发者参考实践。

1. 从一块总往一边倒的板子说起:这套 STM32 自平衡小车资料到底能干什么

很多人第一次碰两轮自平衡小车,都是被同一个画面劝退的:上电、松手、板子往一边栽,电机要么不转,要么猛转一下直接冲出去。看起来是玄学,其实是姿态环、速度环、电机驱动三件事没对齐。这份「平衡小车之家资料」就是围绕 STM32 把这条链路完整走一遍的制作教程包,从硬件接线、传感器读取,到 PID 串级控制、电机 PWM 输出,再到调参上车,属于那种「照着做能立起来、拆开看能改」的工程型资料。适合刚学完 STM32 外设、想找一个能跑闭环的真实项目练手的开发者,也适合带学生做课程设计、想把控制算法落到实处的同学。它不教你点灯,直接把你扔进一个会倒的系统里,逼你把反馈环调通。

2. 系统架构与硬件链路:一块板子上到底跑了哪些东西

自平衡小车的本质是一个倒立摆:车体是摆杆,轮子是支点,控制目标是把倾角稳在零点附近,同时响应前进后退的速度指令。整套系统按「感知—决策—执行」三段划分,STM32 处在中间做实时闭环。资料里的工程结构基本围绕这三段展开,理解了这个分层,后面看代码就不会迷路。

2.1 三层结构:感知层、控制层、执行层

感知层的核心是姿态传感器。常见做法是用 MPU6050 这类六轴模块,读出三轴加速度和三轴角速度,再用互补滤波或卡尔曼滤波融合出俯仰角。加速度计静态准但动态抖,陀螺仪动态稳但会漂移,两者互补才得到可用的角度。资料里通常还会接编码器测轮速,构成速度反馈。

控制层是 STM32 的主循环加定时器中断。角度环跑在最内层,频率最高(常见 100Hz~200Hz),速度环套在外面(常见 20Hz~50Hz),形成串级 PID。角度环负责「别倒」,速度环负责「别跑飞」,两个环的输出叠加后送给电机。

执行层是电机驱动。资料里多为直流减速电机配 TB6612 或 L298N 驱动板,STM32 出 PWM 给驱动芯片,再驱动电机。编码器信号回到定时器的编码器接口模式,硬件计数,省 CPU。

层级典型器件STM32 外设作用
感知层MPU6050I2C读加速度、角速度,融合出倾角
感知层霍尔/光电编码器TIM 编码器模式测左右轮转速与方向
控制层STM32 主控SysTick / TIM 中断跑串级 PID,算 PWM 占空比
执行层TB6612 / L298NTIM PWM 输出 + GPIO驱动左右电机正反转与调速

这张表建议先对着自己的硬件核一遍,引脚对不上,后面调参全是白费。

2.2 主循环与中断的职责划分

一个容易翻车的地方是把所有逻辑塞进while(1)。正确做法是:主循环只做低实时性的事(比如串口打印、按键扫描、OLED 刷新),高实时性的姿态采样和 PID 计算放进定时器中断。下面是一个典型的中断骨架,用 TIM3 做 5ms 周期中断,即 200Hz:

// TIM3 更新中断,5ms 触发一次,200Hz void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 1. 读姿态:I2C 读 MPU6050,融合出当前俯仰角 angle_now = Get_Angle(); // 2. 角度环:目标 0 度,误差 = 目标 - 当前 float angle_err = 0.0f - angle_now; angle_pwm = PID_Angle_Calc(angle_err); // 3. 速度环:每 5 次中断(25ms)算一次,避免和角度环抢带宽 speed_tick++; if (speed_tick >= 5) { speed_tick = 0; int speed_now = Read_Encoder_Speed(); // 编码器脉冲换算成速度 speed_pwm = PID_Speed_Calc(0 - speed_now); } // 4. 叠加输出,限幅后给电机 int pwm_out = angle_pwm + speed_pwm; pwm_out = Limit(pwm_out, -PWM_MAX, PWM_MAX); Motor_Set(pwm_out); } }

这段代码的关键点有三个。第一,角度环和速度环频率不同,速度环降频是为了让内环有足够带宽,这是串级 PID 的常规做法,不是偷懒。第二,angle_err的目标值写死 0,是因为机械中值需要单独标定,实际工程里会有一个angle_offset参与计算,否则车永远往一边偏。第三,Limit限幅必须有,否则积分饱和后车会突然暴冲,这是新手最常见的翻车点。

2.3 姿态解算:互补滤波为什么够用

资料里如果用的是互补滤波,别嫌它土。对自平衡小车这种采样快、姿态变化不剧烈的场景,互补滤波的精度完全够,计算量还小。核心公式就一行:

// 互补滤波:陀螺仪积分 + 加速度计校正 // dt 为采样周期,alpha 为信任陀螺仪的权重,常见 0.96~0.98 float Complementary_Filter(float acc_angle, float gyro_rate, float dt) { static float angle = 0.0f; // 陀螺仪积分预测 float gyro_angle = angle + gyro_rate * dt; // 用加速度计角度做低通校正 angle = alpha * gyro_angle + (1.0f - alpha) * acc_angle; return angle; }

alpha越大越信任陀螺仪,响应快但漂移校正慢;越小越信任加速度计,稳但动态噪声大。我一般从 0.98 起调,车抖就往下调一点。dt必须和实际中断周期一致,写错这个值,滤波出来的角度会系统性偏移,表现为车总是往一个方向缓慢加速,很多人以为是 PID 问题,其实是dt填错了。

3. 从零跑起来:编译、烧录与第一次上电

拿到资料先别急着改代码,按「能编译、能烧录、能读到数据、能转起来」四步走,每一步都验证过再进下一步,比一上来就调 PID 省时间得多。

3.1 工程导入与编译环境

资料里的工程多为 Keil MDK 工程(.uvprojx),少数是 STM32CubeIDE 或 IAR。先确认芯片型号和工程里选的一致,比如 STM32F103C8T6 和 C8T6 的 Flash 大小不同,选错会烧录失败。打开工程后先做一次全编译,看有没有缺头文件、缺启动文件。常见报错是cannot open source input file "stm32f10x.h",说明器件支持包没装,去 Pack Installer 里补上对应系列即可。

编译通过后检查三个配置:调试器选 ST-Link 还是 J-Link,要和手头硬件一致;Flash 算法要匹配芯片;下载后是否复位运行要勾上。这三项错一个,就会出现「编译成功但板子没反应」的黑匣子现象。

3.2 烧录与串口自检

烧录前先把电机电源断开,只给主控供电,避免调试时车突然冲出去。烧录成功后,先别装车,用串口助手看姿态数据。资料里一般会留一个调试输出口,把angle_now、gyro_rate、编码器读数打出来。判断传感器是否正常的方法很直接:把板子静止放平,角度应该稳定在某个值附近小幅波动;绕轴转动,角速度应该有正负变化且回零。

// 调试输出:每 100ms 打一次,别放在高频中断里 if (debug_tick >= 20) // 5ms * 20 = 100ms { debug_tick = 0; printf("angle=%.2f, gyro=%.2f, spdL=%d, spdR=%d\r\n", angle_now, gyro_rate, speed_left, speed_right); }

printf重定向到串口是常规操作,注意别在 200Hz 中断里打印,串口发送是阻塞的,会把控制周期拖垮,车会表现为间歇性抽搐。这个坑我见过不止一次,现象很像 PID 参数炸了,实际是打印拖慢了中断。

3.3 电机方向与编码器极性校准

装车之前必须单独验证电机。给一个小的正 PWM,看左右轮是不是都往前转;如果一边正一边反,改Motor_Set里对应轮的符号即可。编码器同理,手转轮子看计数是增还是减,和电机正转方向对齐。这一步不做,后面角度环一输出,两个轮子对着干,车原地打转,你会误以为是姿态解算错了。

提示:电机和编码器的方向校准一定要在架空状态下做,轮子离地,避免上电瞬间冲出去摔坏传感器。

4. 核心控制代码拆解:串级 PID 与电机输出

这一章是整套资料的重心。PID 谁都会写,但自平衡小车的 PID 有几个必须处理的细节:积分限幅、微分先行、输出叠加顺序。拆开看。

4.1 角度环 PID:位置式还是增量式

角度环建议用位置式 PID,因为需要绝对输出量直接映射到 PWM。核心结构如下:

typedef struct { float kp, ki, kd; float err, err_last, err_prev; float integral; float integral_max; // 积分限幅 float out_max; // 输出限幅 } PID_t; float PID_Angle_Calc(PID_t *pid, float target, float actual) { pid->err = target - actual; pid->integral += pid->err; // 积分限幅,防止饱和后暴冲 if (pid->integral > pid->integral_max) pid->integral = pid->integral_max; if (pid->integral < -pid->integral_max) pid->integral = -pid->integral_max; float out = pid->kp * pid->err + pid->ki * pid->integral + pid->kd * (pid->err - pid->err_last); pid->err_last = pid->err; // 输出限幅 if (out > pid->out_max) out = pid->out_max; if (out < -pid->out_max) out = -pid->out_max; return out; }

参数含义:kp决定回正力度,太小车站不住,太大高频抖动;ki消除静态偏差,但自平衡场景里角度环的ki通常给很小甚至给 0,因为积分会引入滞后;kd抑制超调,是让车「稳」的关键,但kd对噪声敏感,传感器数据脏的时候kd一大就抖。integral_max和out_max是后悔药,没有它们,一次大扰动就能让积分堆到天上,车直接飞出去。

4.2 速度环与角度环的叠加逻辑

速度环的输出不是直接给电机,而是叠加到角度环的目标值上,这是串级控制的关键。也就是说,速度环通过「让车稍微前倾或后仰」来实现加速减速,而不是直接给 PWM。很多资料里的写法是:

// 速度环输出作为角度环的目标偏移 float speed_out = PID_Speed_Calc(&pid_speed, target_speed, actual_speed); // 角度目标 = 机械中值 + 速度环修正 float angle_target = angle_offset + speed_out; float angle_out = PID_Angle_Calc(&pid_angle, angle_target, angle_now); Motor_Set((int)angle_out);

这样写的好处是速度环和角度环解耦清晰:速度环只管「该倾多少」,角度环只管「倾角到了没」。如果直接把速度环输出加到 PWM 上,两个环会互相打架,表现为车要么站不稳,要么速度响应迟钝。angle_offset是机械中值,需要实测:把车扶到刚好能站住的角度,读出来的angle_now就是它。

4.3 电机驱动与死区处理

PWM 给到驱动芯片后,小占空比时电机可能不转,这是电机的启动死区。处理办法是在输出叠加一个死区补偿:

// 死区补偿:输出不为 0 但绝对值小于死区时,抬到死区值 int Motor_Compensate(int pwm) { if (pwm > 0 && pwm < DEAD_ZONE) pwm = DEAD_ZONE; if (pwm < 0 && pwm > -DEAD_ZONE) pwm = -DEAD_ZONE; return pwm; }

DEAD_ZONE一般 100~300(按 PWM 满量程 1000 或 7200 折算),实测方法是从 0 慢慢加占空比,记录轮子刚开始转的值。不做补偿,车在零点附近会有一点点「粘滞感」,微调时反应迟钝,调参怎么调都觉得差一口气。

5. 避坑与排查:调参现场最常见的五个翻车

这一章按「现象 → 原因 → 解决」写,都是实际调车时反复出现的。

现象一:上电就朝一个方向猛冲。原因多半是角度环符号反了,或者angle_offset没标定。解决:架空车轮,手动倾斜车体,看 PID 输出是否让轮子朝「扶正」方向转;反了就取反kp符号。然后重新标定机械中值。

现象二:车能站但高频抖动,像筛糠。原因通常是kd过大或姿态数据噪声大。解决:先把kd降到 0,确认kp能让车缓慢回正,再一点点加kd;同时检查互补滤波的alpha,噪声大就降低对陀螺仪的信任权重。

现象三:静止时慢慢往一边漂,越漂越快。原因是陀螺仪零漂没校准,或dt和实际周期不符。解决:上电静止时采集几百个陀螺仪样本求均值做零偏补偿;核对定时器中断周期和代码里的dt是否一致。

现象四:一给速度指令就倒。原因是速度环输出限幅太大,或速度环ki过大导致积分饱和。解决:给speed_out加一个较小的限幅(比如 ±5 度),限制它能把角度目标拉偏多少;速度环ki从小往大调。

现象五:串口一开就抽。原因就是前面说的,printf放进了高频中断。解决:把打印移到主循环,用标志位传递数据,或者降低打印频率。

注意:每次只改一个参数,改完记录现象。同时改三个参数,出了问题你根本不知道是谁的锅,这是调参最血泪的经验。

6. 二次开发:把平衡车改成能循迹、能遥控的底盘

资料跑通之后,多数人不会停在「能站住」,而是想往上加功能。这里给两条改造路线和一个验证习惯。

第一条是加遥控。常见做法是接一个蓝牙串口模块或 2.4G 接收机,解析出前进后退和转向指令,把前进量喂给速度环的目标值,转向量差速叠加到左右轮 PWM 上。改动点集中在速度环入口和Motor_Set,不用动姿态环。注意遥控指令要做斜坡处理,直接阶跃给目标速度,车会猛点头。

第二条是加循迹。用几路红外或灰度传感器,输出偏差量,做一个外环 PD 控制器,输出转向修正量,同样以差速形式叠加到左右轮。这里的关键是循迹环的频率不能太高,否则和姿态环抢资源,一般 20Hz 足够。

改造方向改动模块新增外设注意点
蓝牙遥控速度环目标值串口蓝牙模块指令斜坡处理,避免阶跃
红外循迹差速叠加红外/灰度传感器循迹环降频,别抢姿态环带宽
上位机调参参数存储Flash 或 EEPROM参数掉电保存,省得每次重烧

验证方法我一般用一个笨办法:每加一个功能,先把原有功能回归一遍——架空看电机方向、静止看角度稳定、手推看回正。新功能引入的问题,八成会先在老功能上露出马脚。

参数持久化也值得早做。把kp、ki、kd、angle_offset存进 Flash,配合串口命令在线改参,比每次改代码重烧快一个数量级。实现上就是一段 Flash 读写加一个简单的命令解析,几十行的事,但能省下大量调参时间。

从那以后我每次调这种闭环系统,都强制先做「架空验证 + 单参数变更记录」这两步再上车,摔过的车和烧过的驱动板教会我,省这一步的代价远大于它。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进