用STM32F103C8T6和FreeRTOS做平衡小车,我踩过的那些坑(附完整代码)
平衡小车项目听起来简单,但真正动手做起来,从硬件选型到软件调试,每一步都可能遇到意想不到的问题。作为一个电子爱好者,我在使用STM32F103C8T6和FreeRTOS实现平衡小车的过程中,踩了不少坑,也积累了一些经验。这篇文章将分享我在这个项目中的实战经验,特别是那些容易忽略的细节和调试技巧,希望能帮助正在或计划复现这个项目的开发者少走弯路。
1. 硬件选型与电路设计
1.1 核心控制器选择
STM32F103C8T6(俗称"蓝莓派")因其性价比高、资源丰富而成为平衡小车项目的热门选择。但在实际使用中,我发现几个关键点:
- 时钟配置:默认内部RC振荡器精度不足,建议使用外部8MHz晶振并配置为72MHz主频
- GPIO分配:这款芯片只有48引脚,需要精心规划引脚用途,避免冲突
c复制// 时钟配置示例(使用HAL库)
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
1.2 传感器选择与布局
MPU6050是最常用的姿态传感器,但在实际使用中需要注意:
- 安装位置:必须安装在车体中心线上,远离电机和电源线
- 滤波电路:VCC引脚建议增加100nF电容,减少电源噪声
- I2C上拉电阻:4.7kΩ是常用值,但根据布线长度可能需要调整
提示:MPU6050的AD0引脚电平决定了I2C地址,如果使用多个传感器需要注意区分
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. FreeRTOS任务设计与调度
2.1 关键任务划分
平衡小车通常需要以下几个核心任务:
- 控制任务:最高优先级,处理MPU6050数据并计算PID输出
- 电机驱动任务:中等优先级,将控制量转换为PWM输出
- 用户界面任务:低优先级,处理显示和用户输入
- 通信任务:低优先级,处理蓝牙/WiFi等无线通信
c复制// 任务创建示例
xTaskCreate(ControlTask, "Control", 256, NULL, 4, NULL);
xTaskCreate(MotorTask, "Motor", 128, NULL, 3, NULL);
xTaskCreate(UITask, "UI", 192, NULL, 2, NULL);
xTaskCreate(CommTask, "Comm", 192, NULL, 2, NULL);
2.2 任务间通信机制
FreeRTOS提供了多种通信机制,在平衡小车项目中:
- 任务通知:最适合控制任务与中断服务程序(ISR)之间的轻量级通信
- 队列:适合通信任务接收外部命令
- 互斥锁:必须用于保护共享资源(如I2C总线)
c复制// 使用任务通知从ISR唤醒控制任务
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if(GPIO_Pin == MPU6050_INT_Pin) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(ControlTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
3. 常见问题与解决方案
3.1 I2C总线冲突问题
当多个设备共享I2C总线时(如MPU6050和OLED屏幕),会出现总线冲突。解决方案:
- 硬件方案:使用不同的I2C总线(如I2C1和I2C2)
- 软件方案:使用FreeRTOS互斥锁保护总线访问
c复制// 创建互斥锁
SemaphoreHandle_t xI2CMutex = xSemaphoreCreateMutex();
// 使用互斥锁访问I2C
if(xSemaphoreTake(xI2CMutex, portMAX_DELAY) == pdTRUE) {
HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, REG_ADDR, I2C_MEMADD_SIZE_8BIT, data, size, 100);
xSemaphoreGive(xI2CMutex);
}
3.2 串口DMA发送问题
许多开发者反映HAL库的串口DMA发送只能工作一次,解决方法:
- 清除DMA标志:在每次发送前清除相关标志位
- 重新配置DMA:发送完成后重新初始化DMA通道
c复制// 可靠的串口DMA发送函数
void USART_DMA_Send(UART_HandleTypeDef *huart, uint8_t *data, uint16_t size) {
HAL_DMA_Abort(huart->hdmatx);
huart->gState = HAL_UART_STATE_READY;
HAL_UART_Transmit_DMA(huart, data, size);
}
4. PID调参与系统稳定性
4.1 平衡控制PID结构
平衡小车通常需要两级PID控制:
- 直立环:快速响应,主要依赖陀螺仪数据
- 速度环:慢速调节,防止小车持续加速
c复制typedef struct {
float Kp, Ki, Kd;
float integral;
float last_error;
} PID_Controller;
void PID_Init(PID_Controller *pid, float Kp, float Ki, float Kd) {
pid->Kp = Kp;
pid->Ki = Ki;
pid->Kd = Kd;
pid->integral = 0;
pid->last_error = 0;
}
float PID_Update(PID_Controller *pid, float error, float dt) {
pid->integral += error * dt;
float derivative = (error - pid->last_error) / dt;
pid->last_error = error;
return pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative;
}
4.2 调参技巧
PID参数调试是平衡小车最耗时的环节,我的经验是:
- 先调直立环:从小Kp开始,逐渐增加直到小车能勉强站立
- 再调微分项:增加Kd抑制振荡
- 最后调积分项:Ki通常很小,用于消除稳态误差
- 速度环后调:直立环稳定后再加入速度环
注意:调试时最好用串口实时输出PID各项的贡献值,这比单纯观察小车行为更有效
5. 电源管理与电机驱动
5.1 电源设计要点
平衡小车的电源系统经常被忽视,但非常关键:
- 电机电源:建议使用2S锂电池(7.4V),直接驱动电机
- 控制电路电源:需要LDO稳压到3.3V
- 去耦电容:每个IC附近都要有100nF电容,电机驱动附近增加大容量电解电容
5.2 电机驱动选择
常见电机驱动方案对比:
| 驱动芯片 | 最大电流 | 控制方式 | 优点 | 缺点 |
|---|---|---|---|---|
| L298N | 2A | PWM+方向 | 便宜 | 效率低 |
| TB6612 | 1.2A | PWM+方向 | 效率高 | 电流小 |
| DRV8833 | 1.5A | PWM+方向 | 集成度高 | 需要散热 |
| BTS7960 | 43A | PWM | 大电流 | 需要双电源 |
在实际项目中,我发现TB6612是最平衡的选择,虽然电流不大,但效率高、发热小。
6. 完整代码结构
项目代码采用模块化设计,主要文件结构如下:
code复制/Drivers
/STM32F1xx_HAL_Driver # HAL库驱动
/CMSIS # Cortex核心支持
/Inc
mpu6050.h # MPU6050驱动
pid.h # PID算法
motor.h # 电机驱动
config.h # 硬件配置
/Src
main.c # 主程序
mpu6050.c # MPU6050实现
pid.c # PID实现
motor.c # 电机驱动实现
/MDK-ARM
BalanceCar.uvprojx # Keil工程文件
关键代码片段:
c复制// main.c中的任务创建
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
MX_TIM1_Init();
MPU6050_Init();
PID_Init(&balance_pid, 15.0f, 0.0f, 0.3f);
PID_Init(&speed_pid, 1.0f, 0.01f, 0.0f);
xTaskCreate(BalanceTask, "Balance", 256, NULL, 4, &BalanceTaskHandle);
xTaskCreate(MotorTask, "Motor", 128, NULL, 3, NULL);
xTaskCreate(UITask, "UI", 192, NULL, 2, NULL);
vTaskStartScheduler();
while (1) {}
}
调试这个项目最大的收获是认识到实时系统调试与传统裸机编程的不同。FreeRTOS提供了强大的功能,但也引入了新的复杂性,比如任务优先级安排不当可能导致系统死锁,资源共享处理不好会出现随机故障。最难忘的是花了三天时间追踪的一个bug,最终发现是因为I2C总线访问没有加互斥保护,导致MPU6050数据偶尔出错。
