简介:这份资料包面向嵌入式初学者与DIY爱好者,围绕基于STM32微控制器的两轮自平衡小车展开,提供从硬件设计到软件实现的完整制作参考。内容涵盖原理图中的STM32选型、传感器连接与电机驱动电路,源码中的陀螺仪与加速度计数据采集、PID算法实现及电机控制逻辑,以及装配烧录的使用说明和记录故障排查与性能优化的开发笔记,帮助读者理解实时控制与反馈调节的落地方式。压缩包共1430个文件,以c与h源码、d与o编译中间文件、crf与uvproj等Keil工程配置为主,另含pdf文档、hex固件与map映射文件,整体约53.19MB,工程结构完整便于按模块查阅。已有4138人学习下载,适合希望系统掌握STM32应用、传感器融合与PID控制的中级开发者对照实践。
1. 从一堆散件到能站住的车:这套 STM32 自平衡小车资料到底值不值得拆
很多人第一次碰两轮自平衡小车,都是先买齐电机、驱动、陀螺仪、车架,焊完板子烧进程序,结果车一上电就往一边倒,调了三天 PID 还是原地抽搐。问题往往不在代码,而在于你手里缺一份把机械装配、姿态解算、串级控制、参数整定串成一条线的完整资料。这套基于 STM32 的两轮自平衡小车制作教程,走的就是这条线:从硬件选型清单、原理图与 PCB、MPU6050 姿态读取、卡尔曼与互补滤波、串级 PID 到整车调试,按制作顺序铺开,适合刚学完 STM32 外设、想拿一个能跑起来的闭环项目练手的同学,也适合带课程设计、毕业设计的从业者当参考底稿。它解决的不是“教你写一行 PID”,而是“让你知道每一环为什么这么接、参数为什么这么给”。
2. 硬件与姿态链路:先搞清楚车为什么会倒
自平衡小车的本质是一个倒立摆:车体偏离竖直方向时,控制器要让轮子往倾倒方向加速,把重心“追”回来。这条链路里,姿态角是眼睛,PID 是大脑,电机驱动是腿,任何一环延迟或噪声过大,整车就会抖或者直接趴下。这套资料的价值在于它把这条链路按模块拆开了,你能看到每个模块的输入输出边界,而不是拿到一个黑匣子工程。
2.1 硬件清单与选型理由
资料里的硬件方案是常见的入门配置,核心几件套如下:
| 模块 | 典型型号 | 作用 | 选型注意 |
|---|---|---|---|
| 主控 | STM32F103 系列 | 跑控制环、读传感器 | 主频 72MHz 够用,定时器要留出编码器和 PWM |
| 姿态传感器 | MPU6050 | 输出三轴加速度与角速度 | 关注量程配置和零偏,I2C 速率别拉太高 |
| 电机驱动 | 双路 H 桥模块 | 驱动左右直流减速电机 | 看持续电流,堵转电流要留余量 |
| 编码器 | 霍尔或光电编码器 | 测轮速,做速度环 | 每转脉冲数决定速度环分辨率 |
| 电源 | 锂电池组 + 降压 | 给电机和主控分别供电 | 电机供电与逻辑供电要分开滤波 |
选型上最容易翻车的是电源。电机启动瞬间的电流冲击会把主控电压拉低,表现为陀螺仪数据跳变、程序复位。常见做法是电机电源和逻辑电源各走一路,逻辑侧加足够的去耦电容,这一点资料里在电源部分有对应说明,照着接能省掉很多“玄学复位”。
2.2 MPU6050 初始化与数据读取
姿态链路的第一步是把原始数据稳定读出来。下面这段是典型的初始化与读取骨架,量程和滤波参数按实际车体调整:
# 伪代码示意:MPU6050 初始化与读取流程(对应 C 工程里的寄存器配置) # 1. 唤醒设备,退出睡眠 write_reg(MPU6050_PWR_MGMT_1, 0x00) # 2. 陀螺仪量程 ±2000dps,加速度 ±2g write_reg(MPU6050_GYRO_CONFIG, 0x18) write_reg(MPU6050_ACCEL_CONFIG, 0x00) # 3. 数字低通滤波 DLPF,抑制电机振动高频噪声 write_reg(MPU6050_CONFIG, 0x03) # 4. 采样率分频,1kHz/(1+4)=200Hz write_reg(MPU6050_SMPLRT_DIV, 0x04) def read_attitude(): ax, ay, az = read_accel() # 原始加速度 gx, gy, gz = read_gyro() # 原始角速度 angle_acc = atan2(ay, az) * 57.3 # 加速度算倾角,单位度 angle_gyro = gx / 16.4 # 角速度积分,16.4 对应 ±2000dps return angle_acc, angle_gyro逻辑说明:加速度计静态准但动态噪声大,陀螺仪动态好但会积分漂移,所以后面必须做融合。参数说明:GYRO_CONFIG=0x18对应 ±2000dps,灵敏度 16.4 LSB/(°/s),这个值直接决定角速度换算系数,写错整车响应会差一个数量级;CONFIG=0x03是 DLPF 带宽约 44Hz,车体振动大的时候可以再降,但降太多会引入相位延迟,反而让控制环更容易振荡。读取频率要和后面的控制周期对齐,常见做法是姿态更新 200Hz、控制环 100Hz 左右。
2.3 互补滤波与卡尔曼滤波怎么选
资料里两种融合都给了,新手容易纠结用哪个。互补滤波一句话:高频信陀螺、低频信加速度,实现简单、计算量小,参数就一个系数;卡尔曼滤波在噪声模型匹配时更平滑,但要调过程噪声和观测噪声,调不好反而发散。我的建议是先用互补滤波把车调站起来,再换卡尔曼对比波形,别一上来就啃卡尔曼,否则车抖了你分不清是滤波问题还是 PID 问题。
// 互补滤波核心:alpha 越大越信陀螺仪 float angle = 0.0f; void complementary_filter(float acc_angle, float gyro_rate, float dt) { // 0.98 是常见起点,车体振动大就适当降低 angle = 0.98f * (angle + gyro_rate * dt) + 0.02f * acc_angle; }dt是采样周期,必须和实际定时器中断周期一致,写成固定值而实际中断有抖动,积分项就会累积误差。alpha的物理含义是时间常数,0.98 在 100Hz 下对应约 0.5 秒的信任切换窗口,车体越重、振动越小可以往 0.99 靠,但别超过 0.995,否则加速度计几乎不起纠正作用,角度会慢慢漂走。
3. 串级 PID 与电机闭环:让车“追”回来的控制结构
姿态角算出来只是知道了“歪了多少”,真正让车动起来的是控制环。这套资料采用的是角度环 + 速度环的串级结构,外环输出给内环,内环再输出 PWM。理解这个结构比抄参数重要得多,因为参数是跟车体质量、轮径、电机特性绑定的,换辆车就得重调。
3.1 串级结构:为什么不能只用一个 PID
只用一个角度环,车能站住但会慢慢跑偏,因为角度环不关心轮子转多快。加上速度环后,外环根据“车在往哪跑”给出一个目标倾角,内环再去追这个倾角,车就能既站住又不大范围漂移。资料里的结构大致是:速度环输入是编码器测得的轮速与目标速度之差,输出叠加到角度环的目标值上;角度环输入是融合后的倾角与目标倾角之差,输出 PWM。
// 串级 PID 骨架,控制周期 5ms float pid_angle(float target, float feedback); float pid_speed(float target, float feedback); void control_loop(void) { float speed_out = pid_speed(target_speed, measured_speed); // 外环 float angle_target = speed_out; // 外环输出即内环目标 float pwm = pid_angle(angle_target, fused_angle); // 内环 set_motor_pwm(pwm); }参数说明:内环(角度环)响应要快,P 给大、I 给小甚至不给,D 用来抑制超调;外环(速度环)响应慢,P 小、I 慢慢积。常见翻车是内外环周期一样快,两个环互相打架,车会低频摆动。正确做法是内环周期明显快于外环,比如内环 5ms、外环 20ms,让内环先把姿态稳住,外环再慢慢修正位置。
3.2 编码器测速与死区处理
速度环的输入质量直接决定车会不会“一顿一顿”。编码器测速常见两种方式:定时器编码器模式和外部中断计数。前者硬件自动计数,不占 CPU,推荐优先用。测速时要注意低速死区:轮子几乎不转时,固定周期内脉冲数为零,速度环会误以为车停了而猛加输出,表现为原地小幅抖动。
// 定时器编码器模式读速,单位:脉冲/周期 int16_t read_encoder_speed(TIM_TypeDef *TIMx) { int16_t cnt = (int16_t)__HAL_TIM_GET_COUNTER(TIMx); __HAL_TIM_SET_COUNTER(TIMx, 0); // 读完清零,下周期重新计 return cnt; }逻辑说明:每个控制周期读一次并清零,得到的就是本周期脉冲增量,乘以换算系数就是轮速。参数说明:清零时机必须和读取在同一临界区,否则中断里又计数会导致丢脉冲;如果脉冲数很小,可以拉长测速周期或者用 M/T 法混合测速,别硬用固定周期短采样,量化误差会把速度环带偏。
3.3 电机 PWM 与方向控制
驱动层看着简单,坑却不少。H 桥模块一般两路 PWM 加两路方向,或者一路 PWM 加两路方向。资料里的接法要和你手上的驱动板对应,方向引脚电平反了,车会正反馈直接倒地。
void set_motor_pwm(float pwm) { // pwm 范围 -1000 ~ 1000,正负代表方向 if (pwm >= 0) { HAL_GPIO_WritePin(DIR_L_PORT, DIR_L_PIN, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim_pwm, TIM_CHANNEL_1, (uint32_t)pwm); } else { HAL_GPIO_WritePin(DIR_L_PORT, DIR_L_PIN, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim_pwm, TIM_CHANNEL_1, (uint32_t)(-pwm)); } }参数说明:PWM 上限不要一上来给满,先限到 60% 左右调参,避免车一冲就撞墙;方向切换时最好留一个短暂的全零死区,防止 H 桥上下管直通。左右电机的机械特性不会完全一致,资料里如果有左右轮单独系数,别嫌麻烦,各给一个微调系数,车走直线会稳很多。
4. 上车调试与避坑:那些调了三天才想明白的事
纸面参数再漂亮,上车就是另一回事。这一章按“现象 → 原因 → 解决”记录几条高频翻车点,都是这套资料对应的制作流程里最容易卡住的地方。
4.1 上电就倒、方向反了
现象:车一上电,往倾倒方向加速,越加速倒得越快。原因:角度环极性反了,或者电机方向接反,控制器在“帮倒忙”。解决:先把车拿在手里倾斜,观察轮子转向是否朝着倾斜方向转;如果不是,先改电机方向引脚,再检查角度环输出符号。别同时改 PID 和方向,否则你永远不知道是哪一项起了作用。
4.2 原地高频抖动
现象:车能站住,但轮子高频小幅抖动,声音刺耳。原因:角度环 D 过大,或者陀螺仪噪声没滤干净,微分项把噪声放大了。解决:先把 D 降到 0,确认 P 能让车缓慢回正,再一点点加 D;同时检查 DLPF 配置和机械连接,传感器没固定牢,振动会直接进微分项。
4.3 车慢慢跑偏、越跑越快
现象:车能站,但持续往一个方向加速,最后撞墙。原因:速度环没起作用,或者编码器方向与电机方向不一致,速度环形成正反馈。解决:单独测速度环,手推轮子看输出是否反向修正;检查编码器计数方向,左右轮符号要统一到“前进为正”的约定上。
4.4 调参时车突然复位
现象:加大 PWM 或者电机急停时,主控复位、陀螺仪数据跳变。原因:电机电流冲击拉低电源,或者地线走线不合理。解决:电机电源与逻辑电源分开,逻辑侧加去耦电容,电机线远离传感器信号线;PWM 变化加斜率限制,别让占空比瞬间跳变。
4.5 换了电池或地面就站不住
现象:同一套参数,换块电池或者换到地毯上就抖。原因:电池电压变化影响电机实际出力,地面摩擦改变等效阻尼。解决:参数按“中等电量、硬地面”整定,留一点裕量;如果资料里有电压补偿环节,把电池电压采样接进来做前馈,能明显改善不同电量下的表现。
5. 从能站到能跑:用阶跃响应验证你的控制环
车能站住只是及格线,真正判断这套资料你有没有吃透,是看你能不能定量地说出“我的控制环响应如何”。我一般会做一个简单的阶跃测试:给角度环一个小的目标倾角阶跃,用串口把融合角度和 PWM 输出按固定周期打出来,看超调量、上升时间和稳态误差。这比盯着车看靠谱得多,也是排查“感觉能站但说不清为什么”的后悔药。
具体做法是加一个调试通道,控制周期里把关键变量打包发出去:
// 每 5ms 发一帧:融合角度、目标角度、PWM、轮速 void debug_send(void) { uint8_t buf[16]; memcpy(&buf[0], &fused_angle, 4); memcpy(&buf[4], &angle_target, 4); memcpy(&buf[8], &pwm_out, 4); memcpy(&buf[12], &measured_speed, 4); uart_send(buf, 16); // 上位机按小端 float 解析 }参数说明:发送频率别和控制周期抢资源,5ms 一帧在 115200 波特率下大约占一半带宽,够用;上位机解析时注意 float 字节序和帧头对齐,丢帧会导致波形错位,看起来像控制环在抽风。拿到波形后看三件事:阶跃后有没有明显超调(有就加 D 或降 P)、回到稳态要多久(太长说明 P 偏小)、稳态有没有持续偏差(有就补一点 I)。这套方法对速度环同样适用,只是阶跃给在目标速度上。
还有一个容易被忽略的点:机械中值。车体不可能绝对对称,重心偏一点,角度环就会一直带着一个固定偏差去补偿。资料里如果有“机械中值”标定步骤,一定要做,把车放在水平面上读融合角度,把这个值作为目标角度的零点,比你在代码里硬写 0 靠谱。做完这一步再调 PID,你会发现参数好调很多。
从那以后我每次碰自平衡类项目,都强制先跑一遍阶跃波形再动车,哪怕只是改了一个滤波系数。波形不会骗人,车会。希望帮到你。
本文还有配套的精品资源,点击获取