从零打造STM32平衡小车:避障与蓝牙遥控全攻略
第一次看到平衡小车稳稳立在桌面上时,那种成就感至今难忘。作为电子爱好者入门嵌入式开发的经典项目,平衡小车融合了传感器技术、控制算法和硬件设计的精华。本文将带你用STM32F103C8T6这颗性价比之王,从元器件焊接开始,一步步实现能自动避障、手机遥控的智能平衡车。不同于学院派的理论讲解,这里全是实打实的"踩坑"经验和即插即用的代码片段。
1. 硬件选型与电路设计
1.1 核心元器件清单
选择合适器件是成功的第一步,经过多次迭代测试,这套配置在性能和成本间取得了最佳平衡:
| 部件 | 型号 | 关键参数 | 单价 |
|---|---|---|---|
| 主控 | STM32F103C8T6 | 72MHz Cortex-M3, 64KB Flash | ¥15 |
| 陀螺仪 | MPU6050 | 六轴(加速度+陀螺仪), I2C接口 | ¥8 |
| 电机驱动 | TB6612FNG | 双路1.2A, 带制动功能 | ¥6 |
| 直流减速电机 | GA25-370 | 减速比1:34, 编码器500线 | ¥25/个 |
| 蓝牙模块 | HC-05 | 经典串口蓝牙, 10米传输距离 | ¥12 |
| 超声波模块 | HC-SR04 | 2cm-400cm测距, 3mm精度 | ¥5 |
避坑提示:电机务必选择带编码器的型号!早期测试使用普通电机,因缺少速度反馈导致控制效果极差。GA25-370的霍尔编码器输出为AB相脉冲,可通过STM32的编码器接口模式直接读取。
1.2 电源系统设计
平衡小车的"心脏"常被初学者忽视,实测中80%的异常重启都源于电源问题。推荐三级供电方案:
- 输入级:航模3S锂电池(11.1V)直接接入,注意正负极防反接设计
- 电机驱动级:LM2596降压模块将11.1V降至7.4V供给TB6612的VM引脚
- 控制级:
- AMS1117-3.3为STM32和传感器供电
- 单独7805为蓝牙模块提供5V电源(避免数字噪声干扰)
// 电源检测代码示例(防止低压运行) void Check_Battery(void) { float voltage = ADC_GetValue() * 3.3 / 4096 * (10+2)/2; // 分压电阻10k+2k if(voltage < 9.5) { Motor_Stop(); // 紧急停止电机 OLED_ShowString(1,1,"Low Battery!"); while(1); // 阻塞等待充电 } }1.3 PCB布局技巧
使用嘉立创EDA设计时,这几个细节决定成败:
- 电机驱动线路走线宽度≥1mm,避免大电流发热
- MPU6050周围预留15mm净空区,减少振动干扰
- 蓝牙天线区域(HC-05的金色部分)不要覆铜
- 所有数字地模拟地单点连接在稳压芯片GND脚
红色为高压线路,蓝色为数字信号线,绿色为模拟信号区域
2. 软件架构与核心算法
2.1 传感器数据融合
MPU6050的原始数据需要经过三重处理才能用于控制:
- DMP初始化(数字运动处理器):
void MPU6050_Init(void) { I2C_WriteByte(MPU6050_ADDR, PWR_MGMT_1, 0x80); // 复位设备 delay_ms(100); I2C_WriteByte(MPU6050_ADDR, PWR_MGMT_1, 0x03); // 使用Z轴晶振 // 加载DMP固件 if(!mpu_load_memory(dmp_memory, sizeof(dmp_memory))) { OLED_ShowString(1,1,"DMP Load Failed!"); while(1); } I2C_WriteByte(MPU6050_ADDR, INT_ENABLE, 0x02); // 开启DMP中断 }- 卡尔曼滤波(简化版):
角度 = 0.98*(上一角度 + 陀螺仪*dt) + 0.02*加速度计角度- 零偏校准:
- 上电静止2秒自动采集200组数据求平均值
- 运行时实时减去零偏值
2.2 串级PID控制实现
平衡车的控制分为三个闭环,像俄罗斯套娃一样层层嵌套:
直立环(PD) → 速度环(PI) → 转向环(PD)
typedef struct { float Kp,Ki,Kd; float Err,LastErr,Integral; } PID; void PID_Update(PID* pid, float current, float target) { pid->Err = target - current; pid->Integral += pid->Err; // 抗积分饱和 if(pid->Integral > 1000) pid->Integral = 1000; else if(pid->Integral < -1000) pid->Integral = -1000; float output = pid->Kp * pid->Err + pid->Ki * pid->Integral + pid->Kd * (pid->Err - pid->LastErr); pid->LastErr = pid->Err; return output; }调参口诀:先直立后速度,先比例后微分。直立环Kp从小往大调,直到小车能勉强站立;接着加Kd抑制抖动;最后加入速度环Ki让小车能抵抗轻微推力。
2.3 蓝牙遥控协议设计
HC-05模块与手机APP通信采用自定义简协议:
| 字节 | 含义 | 取值 |
|---|---|---|
| 0x55 | 帧头 | 固定0x55 |
| 0x01 | 指令类型 | 0x01:速度 0x02:转向 |
| data | 控制量 | -100~+100 |
| sum | 校验和 | 前面所有字节异或 |
Android端示例代码(MIT App Inventor导出APK):
// 当摇杆移动时 procedure 摇杆.PositionChanged(x number, y number) var speed = y * 100 var turn = x * 50 // 打包数据帧 var frame = list make a list list add item to frame = 0x55 list add item to frame = 0x01 list add item to frame = speed list add item to frame = 0x02 list add item to frame = turn list add item to frame = 异或校验(frame) // 通过蓝牙发送 call BluetoothClient.SendBytes frame end procedure3. 机械组装与调试技巧
3.1 车体结构优化
经过五次迭代验证,这些机械设计原则能显著提升稳定性:
- 重心位置:电池安装在车轮轴线下方1/3处
- 轮距选择:建议12-15cm(太窄易侧翻,太宽响应慢)
- 减震措施:
- 电机与底盘间加3mm硅胶垫
- 电路板使用尼龙柱悬浮安装
3.2 系统联调步骤
按这个顺序调试可事半功倍:
基础测试(不装车轮):
- 用USB转串口打印MPU6050原始数据
- 用手转动电机检查编码器计数方向
直立环调试:
# 简易PID参数整定脚本(需接串口) import serial ser = serial.Serial('COM3',115200) while True: ser.write(b'KP+0.1\n') # 逐步增加Kp input("小车是否开始振荡?")运动测试:
- 在光滑地板上标记2米直线
- 观察小车能否保持直线行走(偏差>30cm需调整转向环)
3.3 常见故障排查
这些"症状"和解决方案来自真实踩坑经验:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 上电瞬间电机狂转 | TB6612使能信号未初始化 | 在main()开头先拉低STBY引脚 |
| 蓝牙连接后控制延迟大 | 手机APP发送频率过高 | 限制发送间隔≥50ms |
| 超声波误触发 | 电机干扰导致误回波 | 在Trig引脚加10uF去耦电容 |
| 小车走圆圈 | 两轮转速不一致 | 在代码中对电机输出做补偿校准 |
4. 功能扩展与进阶优化
4.1 红外遥控兼容设计
除了蓝牙,可以增加红外遥控功能作为备用方案。VS1838B接收头仅需3个引脚:
// 红外解码核心逻辑(NEC协议) void EXTI_IRQHandler(void) { static uint32_t last_time = 0; uint32_t gap = TIM_GetCounter(TIM2) - last_time; if(gap > 13000 && gap < 14000) { // 起始码13.5ms ir_data = 0; for(int i=0; i<32; i++) { while(!GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0)); // 等待上升沿 uint32_t pulse = TIM_GetCounter(TIM2); ir_data |= (pulse > 1000) << i; // 1.12ms为1, 0.56ms为0 } } last_time = TIM_GetCounter(TIM2); }4.2 手机APP数据监控
通过蓝牙回传实时数据到手机,实现专业级调试:
数据打包协议:
[0xAA][类型][数据1][数据2]...[校验] 类型:0x01角度 0x02速度 0x03超声波Android端波形显示:
// 使用MPAndroidChart库 LineDataSet speedSet = new LineDataSet(speedEntries, "Speed"); speedSet.setColor(Color.BLUE); speedSet.setDrawCircles(false); LineData data = new LineData(speedSet); chart.setData(data); chart.invalidate(); // 刷新图表
4.3 性能优化技巧
当系统运行不稳定时,这些底层优化立竿见影:
定时器配置:
// 使用TIM3产生20ms中断作为控制周期 TIM_TimeBaseInitTypeDef timer; timer.TIM_Prescaler = 72-1; // 1MHz计数频率 timer.TIM_Period = 20000-1; // 20ms TIM_TimeBaseInit(TIM3, &timer); NVIC_EnableIRQ(TIM3_IRQn);DMA应用:
// 用DMA传输编码器数据,减少CPU开销 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&TIM4->CNT; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&encoder_val; DMA_Init(DMA1_Channel1, &DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE);内存优化:
// 在Keil中设置优化等级-O2 // 将频繁访问的变量定义为register类型 register float angle_err __asm__("r5");
最后分享一个真实案例:曾遇到小车在特定角度突然失控的问题,最终发现是MPU6050安装位置过于靠近电机导致磁干扰。用铜箔包裹传感器后,问题彻底解决。这提醒我们,硬件项目有时需要跳出代码思维,从物理层面寻找解决方案。