用STM32F103C8T6和FreeRTOS做平衡小车?我踩过的这些坑你千万别再踩了

张开发
2026/4/4 10:19:50 15 分钟阅读
用STM32F103C8T6和FreeRTOS做平衡小车?我踩过的这些坑你千万别再踩了
STM32F103C8T6平衡小车开发避坑指南FreeRTOS实战经验全解析平衡小车作为嵌入式开发的经典项目既能检验硬件设计能力又能考验实时系统开发水平。去年我用STM32F103C8T6搭配FreeRTOS完成了自己的第一个平衡小车项目从硬件选型到软件调试踩了不少坑。今天就把这些血泪教训整理成实战指南希望能帮你少走弯路。1. 硬件设计中的那些暗礁1.1 引脚冲突I2C与SPI的相爱相杀STM32F103C8T6的引脚复用功能看似灵活实则暗藏玄机。我的第一个硬件版本就栽在了I2C1和SPI1的引脚冲突上——两者都默认使用PB6和PB7引脚。当MPU6050I2C和NRF24L01SPI同时工作时数据直接乱成一锅粥。解决方案对比表方案实现方式优缺点适用场景硬件SPI重映射通过AFIO重映射SPI1到其他引脚性能最优但占用额外引脚资源引脚资源充足时首选软件SPI模拟用GPIO模拟SPI时序不占用硬件SPI但占用CPU资源低速设备或引脚紧张时更换通信接口改用其他接口如UART完全避免冲突但需硬件支持设备支持多接口时最终我选择了软件SPI方案核心代码如下// 软件SPI初始化 void Soft_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); // MOSI, SCK 推挽输出 GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // MISO 输入 GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_INPUT; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // CS 推挽输出 GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); }提示软件SPI的时钟频率不要超过1MHz否则时序容易出错。实测在500kHz下NRF24L01工作稳定。1.2 电源设计的隐形陷阱平衡小车的电机启动瞬间电流可达2-3A普通的LDO根本扛不住。我的第一个版本用AMS1117供电电机一动单片机就重启。后来改用开关电源模块如MP2307才解决问题。电源设计要点电机驱动电源与MCU电源完全隔离大容量滤波电容1000μF以上靠近电机放置锂电池电压监测不可少分压电阻ADC2. FreeRTOS任务架构优化实战2.1 任务优先级设计的艺术最初我的任务优先级简单按功能划分结果控制任务响应不及时导致小车剧烈抖动。经过多次调整最终形成了这样的优先级方案控制任务 (25) MPU6050数据处理 (20) 电机PWM输出 (15) 用户界面 (10) 无线通信 (5)关键经验控制环任务必须最高优先级传感器数据处理次之人机交互和通信可以放低优先级各任务间优先级差建议≥52.2 共享资源的保护策略当MPU6050和OLED屏共用I2C总线时如果没有保护机制I2C总线很容易死锁。FreeRTOS提供了多种同步机制// 创建I2C互斥锁 SemaphoreHandle_t xI2CSemaphore; void I2C_Init(void) { xI2CSemaphore xSemaphoreCreateMutex(); } // 使用I2C前获取锁 if(xSemaphoreTake(xI2CSemaphore, pdMS_TO_TICKS(100)) pdTRUE) { // 安全的I2C操作 HAL_I2C_Mem_Write(hi2c1, ...); xSemaphoreGive(xI2CSemaphore); } else { // 超时处理 }注意互斥锁的等待时间不宜过长否则会影响系统实时性。建议控制在50-100ms以内。3. 传感器数据处理的关键细节3.1 MPU6050的校准与滤波原始MPU6050数据噪声很大直接使用会导致小车疯狂抖动。必须经过以下处理流程静态校准上电静止2秒采集零偏动态滤波互补滤波融合加速度计和陀螺仪角度计算四元数或卡尔曼滤波// 互补滤波实现示例 float ComplementaryFilter(float accelAngle, float gyroRate, float dt) { static float angle 0; const float alpha 0.98; // 滤波系数 angle alpha * (angle gyroRate * dt) (1-alpha) * accelAngle; return angle; }3.2 编码器信号处理技巧光电编码器信号容易受到电机干扰导致计数异常。我的解决方案硬件上加10kΩ上拉电阻软件上采用双边沿触发去抖算法定期清零累计误差// 编码器计数处理 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { static uint32_t lastTime 0; uint32_t now HAL_GetTick(); // 去抖处理5ms视为抖动 if((now - lastTime) 5) { if(GPIO_Pin ENCODER_A_PIN) { if(HAL_GPIO_ReadPin(ENCODER_B_GPIO, ENCODER_B_PIN)) { encoderCount--; } else { encoderCount; } } lastTime now; } }4. 控制算法实现与调试4.1 PID参数整定方法论平衡小车需要三环PID控制角度环、速度环和转向环。调试顺序很关键先调角度环P让小车能勉强站立加入角度环D抑制振荡最后加入速度环实现自平衡典型PID参数范围参考控制环PID说明角度环20-5000.5-2快速响应速度环0.1-10.001-0.010慢速调节转向环1-500根据编码器灵敏度调整4.2 中断与任务协作模式MPU6050的数据更新频率高达1kHz如果每次更新都触发任务切换系统开销太大。我的优化方案// 外部中断回调1kHz void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { BaseType_t xHigherPriorityTaskWoken pdFALSE; // 仅通知不立即切换任务 xTaskNotifyFromISR(controlTaskHandle, 0, eNoAction, xHigherPriorityTaskWoken); // 必要时切换任务 portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } // 控制任务 void ControlTask(void *params) { while(1) { // 等待通知最多等待2ms ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(2)); // 读取传感器数据 MPU6050_Read(accel, gyro); // PID计算 float output PID_Calculate(anglePID, targetAngle, currentAngle); // 电机控制 Motor_SetOutput(output); } }这种设计既保证了实时性又避免了频繁任务切换的开销。5. 通信模块的那些坑5.1 串口DMA的发送陷阱HAL库的串口DMA发送有个大坑调用HAL_UART_Transmit_DMA()后如果上次发送未完成就再次调用会导致DMA通道挂起。解决方案是每次发送前检查状态void Safe_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { // 等待上次传输完成 while(HAL_UART_GetState(huart) HAL_UART_STATE_BUSY_TX); // 启动新传输 HAL_UART_Transmit_DMA(huart, pData, Size); }5.2 蓝牙数据解析技巧蓝牙模块发送的数据往往是不定长的我的处理方案是DMA接收空闲中断检测帧结束环形缓冲区存储接收数据专用解析任务处理协议// 空闲中断回调 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if(huart huart1) { // 将数据放入队列 xQueueSendFromISR(bleQueue, rxBuffer, NULL); // 重新启动DMA接收 HAL_UARTEx_ReceiveToIdle_DMA(huart1, rxBuffer, BUF_SIZE); } }6. 系统稳定性提升技巧6.1 看门狗使用策略平衡小车在异常状态下可能疯狂转圈非常危险。必须启用硬件看门狗// 独立看门狗初始化 void IWDG_Init(void) { hiwdg.Instance IWDG; hiwdg.Init.Prescaler IWDG_PRESCALER_32; // 约1.6s超时 hiwdg.Init.Reload 0xFFF; HAL_IWDG_Init(hiwdg); } // 任务中定期喂狗 void SafetyTask(void *params) { while(1) { HAL_IWDG_Refresh(hiwdg); vTaskDelay(pdMS_TO_TICKS(1000)); } }6.2 低电量保护实现锂电池过放会永久损坏必须实时监测电压float Get_Battery_Voltage(void) { uint16_t adcValue 0; float voltage 0; // 读取ADC值分压比2:1 HAL_ADC_Start(hadc1); adcValue HAL_ADC_GetValue(hadc1); // 计算电压参考电压3.3V12位ADC voltage (adcValue * 3.3f / 4095.0f) * 2; return voltage; }当电压低于3.3V对应锂电池3.7V时应逐渐降低电机功率直至停止。

更多文章