1. 蓝牙遥控STM32小车开发概述
第一次尝试用STM32做蓝牙遥控小车时,我遇到了不少坑——从蓝牙模块频繁断连到电机驱动电流不足,从PID参数调不好到手机APP控制延迟。经过三个版本的迭代,现在这台能负重1kg、控制距离30米的小车已经能稳定执行各种指令。本文将完整呈现从零开始打造蓝牙遥控小车的全过程,特别适合有51单片机基础想进阶STM32的开发者。
蓝牙遥控小车的核心在于实现"手机指令→蓝牙传输→STM32解析→电机控制"的完整链路。相比传统的红外遥控方案,蓝牙4.0 BLE模式功耗更低(待机电流<10μA),而经典蓝牙模式(如HC-05模块)则能提供更稳定的持续数据传输。我最终选择了兼具性价比和稳定性的HC-05模块,它支持AT指令配置,传输速率可达2Mbps,完全满足小车控制需求。
2. 硬件系统设计与选型
2.1 主控芯片选型对比
STM32F103C8T6(蓝色药丸板)是我的首选,72MHz主频、64KB Flash、20KB RAM的配置对于小车控制绰绰有余。相较STC89C52,STM32的定时器资源更丰富(4个通用定时器+2个高级定时器),PWM输出精度更高(16位vs 8位),且内置硬件正交编码器接口,后期扩展编码电机非常方便。
关键提示:购买时注意区分STM32F103的正版与克隆版,正版芯片丝印清晰,底部有激光刻蚀的批次号,而克隆版常使用GD32等兼容芯片,虽然便宜但PWM输出可能不稳定。
2.2 蓝牙模块选型实战
测试过三种主流蓝牙模块后,得出以下对比数据:
| 模块型号 | 协议版本 | 传输距离 | 峰值电流 | 配对时间 | 参考价格 |
|---|---|---|---|---|---|
| HC-05 | 蓝牙2.0 | 30米 | 40mA | 3秒 | 25元 |
| JDY-31 | BLE4.2 | 50米 | 15mA | 1秒 | 35元 |
| CC2541 | BLE4.0 | 20米 | 12mA | 2秒 | 45元 |
最终选择HC-05的原因在于:
- 经典蓝牙模式无需处理复杂的GATT协议栈
- 支持SPP协议,可直接当作串口使用
- 丰富的社区支持(遇到问题容易找到解决方案)
2.3 电机驱动电路设计
采用L298N双H桥驱动模块时,务必注意以下参数计算:
- 电机堵转电流:I = V/R = 12V/0.5Ω = 24A
- L298N最大持续电流:2A(需加散热片)
- 实际工作电流:空载约0.3A,负重1kg时约1.2A
我在第二版小车中改用TB6612FNG驱动芯片,体积更小且效率更高(典型效率90% vs L298N的70%)。关键布线要点:
- 电机电源与逻辑电源隔离
- 每个电机并联104电容滤除电刷噪声
- PWM频率设置在8-10kHz(避免可闻噪声)
3. 软件架构与核心代码实现
3.1 蓝牙通信协议设计
定义简洁高效的通信协议是保证实时性的关键。我的协议格式如下:
code复制[头字节0xAA][命令字节][数据字节][校验和]
例如前进指令:AA 01 64 6F(0x01代表前进,0x64是速度值100,校验和=0xAA^0x01^0x64)
在STM32端使用DMA+串口中断接收:
c复制void USART1_IRQHandler(void) {
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) {
DMA_Cmd(DMA1_Channel5, DISABLE);
uint16_t len = 32 - DMA_GetCurrDataCounter(DMA1_Channel5);
USART_ReceiveData(USART1); // 清除IDLE标志
protocol_parse(rx_buf, len);
DMA_SetCurrDataCounter(DMA1_Channel5, 32);
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}
3.2 电机PID控制实现
采用位置式PID算法控制电机转速,关键参数:
c复制typedef struct {
float Target; // 目标转速
float Kp, Ki, Kd; // PID参数
float Error; // 当前误差
float Integral; // 积分项
float LastError; // 上次误差
} PID;
void PID_Calc(PID *pid) {
pid->Error = pid->Target - Encoder_GetSpeed();
pid->Integral += pid->Error;
if(pid->Integral > 200) pid->Integral = 200;
if(pid->Integral < -200) pid->Integral = -200;
float output = pid->Kp * pid->Error +
pid->Ki * pid->Integral +
pid->Kd * (pid->Error - pid->LastError);
pid->LastError = pid->Error;
Motor_SetOutput(output);
}
调试技巧:先设Ki=0,逐渐增大Kp直到出现振荡,然后取该值的60%作为Kp,再慢慢增加Ki改善稳态误差。
4. 手机APP开发要点
4.1 Android蓝牙控制实现
使用Android Studio开发控制APP时,关键流程包括:
- 蓝牙权限声明:
xml复制<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
- 设备搜索与连接:
java复制BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (!adapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
Set<BluetoothDevice> pairedDevices = adapter.getBondedDevices();
if (pairedDevices.size() > 0) {
for (BluetoothDevice device : pairedDevices) {
if (device.getName().equals("HC-05")) {
ConnectThread connectThread = new ConnectThread(device);
connectThread.start();
break;
}
}
}
4.2 控制界面优化
通过重写onTouchEvent实现虚拟摇杆控制:
java复制@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX() - centerX;
float y = event.getY() - centerY;
double radius = Math.sqrt(x*x + y*y);
if(radius > deadZone) {
double angle = Math.atan2(y, x);
int speed = (int)(radius / maxRadius * 100);
sendCommand(angleToDirection(angle), speed);
} else {
sendCommand(STOP, 0);
}
return true;
}
5. 常见问题与解决方案
5.1 蓝牙连接不稳定
现象:手机显示已连接但小车无响应
排查步骤:
- 用串口助手查看HC-05的TX输出是否正常
- 检查STM32串口接收中断优先级(需高于定时器中断)
- 测量模块供电电压(不得低于3.3V)
5.2 电机启动抖动
解决方案:
- 在PWM启动时加入软启动:
c复制for(int i=0; i<100; i++) {
pwm = i;
delay_ms(10);
}
- 在电机两端并联反向二极管吸收反电动势
- 确保电源功率足够(建议10W以上)
5.3 控制延迟大
优化方向:
- 将蓝牙模块波特率从9600提升到115200
- 简化通信协议(去掉冗余字段)
- 在STM32端启用硬件CRC校验
- 手机APP减少UI刷新频率
6. 进阶改进方案
6.1 加入MPU6050姿态检测
通过I2C读取陀螺仪数据实现跌倒检测:
c复制void MPU6050_Read(float *pitch, float *roll) {
int16_t acc_x = (I2C_ReadByte(ACCEL_XOUT_H) << 8) | I2C_ReadByte(ACCEL_XOUT_L);
int16_t acc_z = (I2C_ReadByte(ACCEL_ZOUT_H) << 8) | I2C_ReadByte(ACCEL_ZOUT_L);
*pitch = atan2(acc_x, acc_z) * 180 / PI;
if(*pitch > 45) Motor_Stop(); // 倾角过大时紧急停止
}
6.2 移植FreeRTOS实现多任务
创建三个主要任务:
- 蓝牙通信任务(优先级最高)
- 电机控制任务
- 传感器采集任务
在CubeMX中配置FreeRTOS后,任务创建示例:
c复制void StartDefaultTask(void const * argument) {
for(;;) {
vTaskDelay(100);
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
osThreadCreate(osThread(defaultTask), NULL);
7. 项目总结与心得
经过三个版本的迭代,这台小车的总成本控制在150元以内,但实现了远超这个价位水平的性能。最大的收获是理解了实时系统设计中的优先级划分——蓝牙中断必须最快响应,而电机控制算法的计算可以适当延后。
一个出乎意料的问题是电源干扰:最初使用廉价的DC-DC模块给STM32供电时,每当电机启动就会导致单片机复位。后来改用独立的LM1117稳压电路后问题彻底解决。这也印证了硬件设计中的一个真理:电源部分的成本绝对不能省。
