简介:这套基于STM32的两轮平衡小车资料面向电赛备赛学生、嵌入式入门开发者及机器人爱好者,提供一套可落地的完整项目方案。资源以MCU源码为核心,包含大量C语言与头文件(.c/.h),覆盖姿态解算、电机控制、蓝牙通信等关键模块;硬件部分提供原理图(.schdoc)、PCB生产图纸及CAD文件,并附BOM表和姿态传感器设计说明,可支撑从打样到调试的全流程。压缩包共221个文件,除源码和图纸外,还包含蓝牙遥控器APK及源码、Keil工程配置(.uvproj)与备份文件,整体约29.34MB,结构划分清晰,便于按需查阅。目前已有120人学习下载,对于想快速搭建两轮平衡车原型或理解平衡控制算法的读者,是一份值得参考的工程资料。
1. 两轮平衡车的关键不在电机,在 5ms 实时闭环
两轮平衡车最容易让人误判的地方,是以为难点在电机驱动或机械结构。实际上电机驱动和结构件只要按图纸做都能转,真正决定车能不能站稳的,是 STM32 在每一个 5ms 控制周期里,能不能把 MPU6050 的姿态数据解算完、把三层闭环跑完。这套资料的主控工程直接以 IMU_AHRS 命名,一眼就能看出重心在姿态参考系统上;配套的 TWABV 工程、蓝牙遥控器 APK 及源码、CAD 生产图纸和 BOM 表,意味着拿到手能从硬件图纸一路做到 App 层改造。准备参加电赛、做毕设,或者想把 stm32 项目从「点灯」推进到「实时控制」的人,这套资料值得拆开看一遍。
2. 硬件电路设计细节:供电树、传感器接线与 PCB 避坑
2.1 器件选型与工程文件的对应关系
从资料正文里能看到bmp180.__i、uarts.__i、IMU_AHRS.uvproj、TWABV.uvproj这些编译中间文件和工程文件,说明这套设计里有三个关键外围:BMP180 气压计、UART 串口(用于调试和蓝牙)、以及独立的主控工程和遥控器工程。主控 MCU 是 STM32F103 系列,具体型号以 Keil 工程设置里 Device 一栏为准,常见做法是 STM32F103C8T6,48 脚封装,72MHz 主频,足够跑姿态解算和三层控制环。
姿态传感器用的是 MPU6050 六轴(加速度计+陀螺仪),BMP180 挂在同一条 I2C 总线上,地址分别是 0x68 和 0x77,互不冲突。电机驱动选 TB6612FNG 这类低压 H 桥,相比 L298N 它的导通压降小得多,7.4V 锂电池供电时电机端电压更足,堵转时发热也小。蓝牙模块用 HC-05,走 UART 透传,和手机 APK 配对后直接收发协议帧。
2.2 供电树设计与 PCB 布局要点
整车的供电路径是:7.4V 两节锂电池 → 5V LDO 给 MCU、蓝牙模块和传感器 → MPU6050/BMP180 模块本身带 3.3V 稳压的话就无需额外 LDO。电机不经过 LDO,直接从电池正极取电,由 TB6612 的 VM 引脚供电,避免电机大电流把 MCU 电压拉垮。
PCB 布局上,电机驱动部分的地和信号地要单点连接。PWM 占空比突变时,电机电流会在功率地上产生毫伏级压降,如果功率地和 MPU6050 的地是同一段走线,加速度计读数会出现周期性跳变,表现就是车身高频抖动。电机焊盘附近放 100nF 陶瓷电容加 10μF 电解电容,吸收换向尖峰。另外 MPU6050 尽量靠近车体几何中心,且保证芯片 Z 轴垂直安装面,否则后续姿态解算要额外做安装角补偿。
2.3 接线表与上电检查步骤
以常见 STM32F103C8T6 蓝色 pill 板为参考,接线表如下:
| 外设 | 信号 | MCU 引脚 | 说明 |
|---|---|---|---|
| MPU6050 | SDA / SCL | PB7 / PB6 | I2C1,地址 0x68 |
| BMP180 | SDA / SCL | PB7 / PB6 | 同一条 I2C 总线,地址 0x77 |
| TB6612 | PWMA / AIN1 / AIN2 | PA8 / PB12 / PB13 | TIM1_CH1 输出 PWM |
| TB6612 | PWMB / BIN1 / BIN2 | PA9 / PB14 / PB15 | TIM1_CH2 输出 PWM |
| 编码器 | A 相 / B 相 | PA6 / PA7 | TIM3 编码器模式 |
| HC-05 | TXD / RXD | PA10 / PA9 | USART1,注意交叉连接 |
注意 STM32F103 的硬件 I2C 有时会有总线锁死问题,如果解算时读到的加速度值全为零,优先怀疑 I2C 总线状态。我一般直接改用软件模拟 I2C,把 SCL 和 SDA 配置为开漏输出加上拉电阻,稳定性反而更好,尤其是在电机电磁干扰大的环境下。
上电后别急着下载程序,先量三处电压:电池端 7.4V、LDO 输出端 5.0V、模块 VCC 端 3.3V。然后用串口线接 USART1,打开 115200 波特率终端,正常启动时能看到初始化日志,包括 MPU6050 自检通过、BMP180 气压值、蓝牙模块状态。哪一步没打印就查哪一路,比焊完直接下程序再猜问题省时间。
3. 姿态解算落地:互补滤波与 AHRS 模块化设计
3.1 为什么姿态解算要单独做成 AHRS 模块
IMU_AHRS这个工程名拆开看是 IMU(惯性测量单元)加 AHRS(姿态参考系统),说明这套代码把「传感器驱动」和「姿态解算」分成了两层。传感器驱动只负责读原始数据,解算层只负责把加速度和角速度变成稳定的俯仰角,这样换传感器或者换算法都不影响上层控制代码。
BMP180 在平衡车里的作用容易被忽略。气压计能感知高度变化,常见做法是用它做悬空检测:当车被提起或者翻倒时,气压高度突变,同时编码器脉冲也停止,MCU 据此关闭电机输出,避免轮子空转加速。另一个用途是起步辅助,测量水平面高度差,防止车在斜坡上误判零位。
3.2 三种姿态解算方案怎么选
MPU6050 官方提供了 DMP 固件,F103 可以直接读四元数,很多工程图省事就挂 DMP。但 DMP 输出频率受 FIFO 配置影响,和 5ms 控制周期对不齐的话,读取时刻会抖动。卡尔曼滤波在小车这种线性近似系统里效果确实好,但协方差矩阵需要调 4 个参数,静态收敛速度和动态响应是矛盾的,调试成本高。互补滤波代码量最小,只有一个加权公式,权重系数有明确的物理意义,平衡车只关心俯仰角 pitch 和横滚角 roll,互补滤波完全够用。这也是资料里 IMU_AHRS 工程最可能采用的方式。
3.3 互补滤波核心代码与参数说明
以下代码逻辑对应资料中 AHRS 模块的实现思路,可在工程里IMU_AHRS工程目录下搜索AHRS相关源文件对照:
#define ALPHA 0.98f #define RAD_TO_DEG 57.29578f static float g_fPitch = 0.0f; // 融合后的俯仰角,单位:度 static float g_fGyroY = 0.0f; // 陀螺仪 Y 轴角速度,单位:度/秒 // gyroY_raw: MPU6050 原始 Y 轴角速度,FS_SEL=1 时灵敏度 65.5 LSB/(°/s) // accX_raw / accZ_raw: 原始加速度值,直接参与 atan2 计算 // dt: 控制周期,单位秒,5ms 中断时传 0.005f void AHRS_Update(short gyroY_raw, short accX_raw, short accZ_raw, float dt) { float accel_pitch = atan2f((float)accX_raw, (float)accZ_raw) * RAD_TO_DEG; g_fGyroY = (float)gyroY_raw / 65.5f; g_fPitch = ALPHA * (g_fPitch + g_fGyroY * dt) + (1.0f - ALPHA) * accel_pitch; }互补滤波的本质是一个一阶低通滤波和一个一阶高通滤波并联:加速度计计算出的角度长期稳定但短期噪声大,陀螺仪积分出的角度短期精确但会随时间漂移。ALPHA越大越信任陀螺仪,动态响应越好,但静态漂移也越明显;ALPHA越小越信任加速度计,静态稳定但动态会显得迟钝。dt必须和实际中断周期严格一致,否则陀螺仪积分项会引入固定偏差。读加速度计的accX_raw和accZ_raw用atan2f计算俯仰角,比先用反正切再拼接要更抗坐标轴串扰。
3.4 姿态解算结果怎么验证
代码下进去先别急着装车,把g_fPitch通过串口打印出来,频率 100Hz。车体水平放置时角度应接近 0,误差超过 2° 就检查加速度计零偏和安装方向。用手把车绕 Y 轴前倾 20°,串口数值应立即跟到 20° 附近,松手回到 0°,回程超调超过 3° 说明ALPHA偏大。静止 30 秒,角度漂移如果超过 1°,检查陀螺仪零偏补偿,一般要在初始化时采集 100 组陀螺仪数据取平均作为零偏,在积分前减掉。这步做完再进入控制环调试,否则 PID 参数再准也救不回来。
4. 三层闭环实现:直立、速度、转向的参数整定
4.1 控制拓扑与时序分配
两轮平衡车的控制结构是三个并联反馈环:直立环用 PD 控制车身俯仰角,速度环用 PI 控制轮子平均速度,转向环用 P 控制左右轮差速。三者输出叠加后分别给左右电机,而不是串联结构。直立环必须跑在最高优先级,资料里的 5ms 周期刷新一次是常见配置;速度环积分的本质是让车通过整体前倾或后倾来调节前进速度,周期放 20ms 甚至 50ms 都没问题,太长车会晃但不会倒;转向环和直立环同步刷新即可。一个常见的错误是把三个环算完再一起输出 PWM,直立环本身计算量小,但 UART 发送调试信息会打断时序,我一般会把串口打印放在主循环里用计数器降频到 20Hz。
4.2 Balance.c 核心实现代码
以下为简化后的控制逻辑,对应工程中 Balance 或 Control 模块:
#define PWM_MAX 720 int g_iBalancePwm = 0; int g_iSpeedPwm = 0; int g_iTurnPwm = 0; // 参数由外部整定,初始值参考 4.3 表 float g_fKAngle = 800.0f; float g_fKAngleSpeed = 2.5f; float g_fKSpeed = 30.0f; float g_fKSpeedI = 0.4f; float g_fKTurn = 1.8f; static int g_iSpeedIntegral = 0; // 5ms 周期调用 void Balance_Control(float pitch, float gyroY, int speedLeft, int speedRight, float turnRate) { int speedError = (speedLeft + speedRight) / 2; // 目标速度为 0 g_iSpeedIntegral += speedError; if (g_iSpeedIntegral > 1500) g_iSpeedIntegral = 1500; if (g_iSpeedIntegral < -1500) g_iSpeedIntegral = -1500; g_iBalancePwm = (int)(g_fKAngle * pitch + g_fKAngleSpeed * gyroY); g_iSpeedPwm = (int)(g_fKSpeed * speedError + g_fKSpeedI * g_iSpeedIntegral); g_iTurnPwm = (int)(g_fKTurn * turnRate); int pwmL = g_iBalancePwm + g_iSpeedPwm - g_iTurnPwm; int pwmR = g_iBalancePwm + g_iSpeedPwm + g_iTurnPwm; if (pwmL > PWM_MAX) pwmL = PWM_MAX; if (pwmL < -PWM_MAX) pwmL = -PWM_MAX; if (pwmR > PWM_MAX) pwmR = PWM_MAX; if (pwmR < -PWM_MAX) pwmR = -PWM_MAX; Motor_SetPWM(pwmL, pwmR); }注意几个量纲问题:pitch单位是度,gyroY单位是度/秒,PWM_MAX对应定时器 ARR 值的一半,比如定时器 7200 分频 10 后满量程 720。这个表里g_fKAngleSpeed直接作用在角速度上,相当于 PD 控制的微分项,数值远小于比例项是正常的。积分限幅必须比 PWM 限幅小,否则速度环积分饱和后松开遥控器,车会以最大速度冲向一边。转向环的turnRate我习惯用陀螺仪 Z 轴角速度,比左右轮编码器差更平滑,也避免了差速计算在大占空比时溢出。
4.3 PID 参数三步整定法
| 参数 | 作用 | 建议初值 | 调大后的现象 |
|---|---|---|---|
| KAngle | 直立环比例项,决定恢复力 | 600 ~ 1000 | 车身高频抖振,电机发烫 |
| KAngleSpeed | 直立环微分项,提供阻尼 | 1.5 ~ 3.5 | 回中变慢,转向迟钝 |
| KSpeed | 速度环比例项 | 20 ~ 50 | 前后周期震荡,幅度变大 |
| KSpeedI | 速度环积分项 | 0.3 ~ 0.6 | 回中粘滞,容易原地抽动 |
| KTurn | 转向环比例项 | 1.0 ~ 2.5 | 急转向时车身侧倾明显 |
第一步只跑直立环,把速度环输出和转向环输出直接注释掉。手持车体在桌面正上方,KAngle从 300 开始往上加,车会开始自动找平衡点。如果车身来回摆动幅度越来越大,多半是KAngle符号反了,把角度反馈取反即可。加到车能在松手后原地站 2 秒以上,再加入KAngleSpeed,直到手推车身能迅速回正且不抖。第二步打开速度环,空载时看车是否会在原地缓慢移动,速度环积分项能消除静态误差,但积分增益过大会导致车周期性前冲后撤。第三步打开转向环,手机蓝牙发送左转/右转指令,观察转弯半径是否平滑。整个调试过程建议把蓝牙遥控和串口调参分开,避免遥控数据帧在控制周期内打断直立环。
4.4 蓝牙遥控链路与 APK 源码改造
蓝牙模块走 UART 透传,MCU 在串口接收中断里解析协议帧。协议格式是典型的 8 字节帧:AA 55 04 01 00 00 00 1A,前面AA 55是帧头,04表示后续数据长度,01是命令字,后两个字节分别代表前后速度(-100 ~ 100)和左右转向(-100 ~ 100),末位是校验和,等于前 7 字节累加取低 8 位。手机端 APK 源码直接搜AA 55就能定位到数据打包函数,下载下来的 APK 可以先安装看效果,再用 jadx 反编译确认协议细节,最后对比源码修改。需要改包名的话,Android Studio 里 Refactor 一下包名即可,但要同步改 manifest 里的声明以及蓝牙权限,Android 12 以上还需要在运行时申请BLUETOOTH_CONNECT权限,否则扫描不到 HC-05。
HC-05 上电后如果一直慢闪,说明没有配对。按住模块上的按键上电进入 AT 模式,此时快闪,串口发AT+UART=115200,0,0改波特率,AT+ROLE=1设为主机或从机,AT+PSWD=1234设置配对码。APK 扫描到 HC-05 后输入 1234 即可连接,连上后指示灯变为双闪。常见问题是 HC-05 默认波特率 9600 而源码里初始化 115200,串口打印全是乱码,这个先查波特率再查接线。
5. 源码复现与排错清单
资料里IMU_AHRS.uvproj是主控工程,TWABV.uvproj从命名看对应遥控器侧 MCU 固件,.uvgui_*.bak这类文件是 Keil 界面布局的备份,不影响编译可以忽略。先用 Keil 打开主控工程,确认 Device 型号和仿真器设置,ST-Link 选 SWD 模式,烧录前检查Utilities里的 Flash Download 配置。
| 现象 | 原因 | 处理 |
|---|---|---|
| 烧录报错 No Flash Algorithm Found | 芯片型号选错或 Pack 未安装 | 安装对应 Device Family Pack,重新选择芯片 |
| SWD 无法连接目标 | 程序里把 SWD 引脚复用为 GPIO | BOOT0 拉高复位进入 ISP 后重新连接 |
| 串口打印乱码 | 波特率不匹配 | 检查代码里 UART 初始化和终端设置 |
| 蓝牙模块一直慢闪不配对 | 未进入 AT 模式或配对码错误 | 按住按键上电进入 AT,重新配对 |
| 电机发出高频尖锐声 | PWM 频率过低或直立环比例过大 | 检查 PWM 频率是否低于 10kHz,降低 KAngle |
最后分享一个调试技巧:先用逻辑分析仪抓电机 PWM 输出,确认占空比极限值,再在直立环代码里加一个条件断点,当pitch大于 30° 时强制关闭电机输出。这样即使参数整定失败,车也只是倒下而不是飞出去。车能站稳之后,把串口输出的pitch用 pySerial 导成 CSV,画个曲线看稳态误差和超调量,比肉眼调参快得多。
提示:排错时优先关闭蓝牙串口中断。遥控器高频数据会在 5ms 控制周期里插入串口处理,导致直立环计算被延迟,车身会莫名抖动。把蓝牙数据接收改为主循环轮询,问题通常立刻消失。
本文还有配套的精品资源,点击获取