第一次接触智能小车时,我总觉得循迹功能特别神奇——为什么这小玩意儿能乖乖跟着黑线走?后来才发现,核心秘密就藏在PID差速算法里。传统固定速度纠偏就像蒙眼走路,碰到障碍才调整;而PID控制则是提前预判路线,像老司机打方向盘一样自然。这次我们要用STM32CubeMX搭建硬件框架,在Keil中实现真正的动态调速。
你可能遇到过这种情况:小车遇到急弯时,固定PWM调速会左右摇摆,最后冲出跑道。差速算法的优势在于,它能根据实时误差动态分配轮速。比如左轮遇到黑线时自动降速,右轮加速,形成转向力矩。实测下来,PID版本的小车过弯流畅度能提升60%以上。
这个项目的硬件基础很简单:STM32F103C8T6最小系统板、TB6612电机驱动、红外循迹模块。软件层面需要掌握CubeMX配置PWM输出、FreeRTOS任务调度,以及最关键的PID公式落地。别被这些术语吓到,我会用调参时的真实案例带你一步步理解。
打开CubeMX新建工程时,芯片型号千万别选错。我吃过亏:选了STM32F103C6结果发现Flash容量不够用。正确选择F103C8后,先在SYS里把Debug改成Serial Wire,否则后续没法用ST-Link调试。RCC选项卡要启用外部高速晶振(HSE),这是保证PWM精度的关键。
红外循迹模块的接线有讲究:建议用PA8和PA9这两个带内部上拉的GPIO口,避免额外焊接电阻。配置时选择GPIO_Input模式,上拉电阻(Pull-up)设为Enable。TB6612的电机控制引脚需要四个GPIO输出:AIN1/AIN2控制右电机转向,BIN1/BIN2控制左电机,记得在CubeMX里把这些引脚设为GPIO_Output模式。
差速控制的核心就是PWM波调制。以TIM2为例,在Channel2和Channel3启用PWM Generation模式。关键参数设置:
时钟树配置有个易错点:APB1总线时钟要确保是72MHz。我遇到过PWM频率异常的情况,最后发现是HCLK没配置对。建议直接点击"Clock Configuration"选项卡,让CubeMX自动计算参数,然后检查APB1 Timer Clocks是否显示72MHz。
在Middleware里启用FreeRTOS,把USE_TRACE_FACILITY和USE_STATS_FORMATTING_FUNCTIONS勾选上,方便后期调试。创建两个任务:
任务堆栈大小要留足余量。有次小车突然死机,最后发现是PID计算导致堆栈溢出。建议在FreeRTOSConfig.h里把configMINIMAL_STACK_SIZE改为128,configTOTAL_HEAP_SIZE改为10240。
原始代码里用简单的高低电平判断,实际场景会有噪声干扰。我改进后的版本加入了滑动滤波:
c复制#define SAMPLE_SIZE 5
int read_sensor_filtered(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) {
int sum = 0;
for(int i=0; i<SAMPLE_SIZE; i++){
sum += HAL_GPIO_ReadPin(GPIOx, GPIO_Pin);
osDelay(1);
}
return (sum > SAMPLE_SIZE/2) ? 10 : 6;
}
误差计算也有优化空间。原始方案直接用左右传感器差值,更科学的做法是引入误差累积和变化率:
c复制float prev_error = 0;
float integral = 0;
void pid_control_diff() {
int left = read_sensor_filtered(GPIOA, GPIO_PIN_8);
int right = read_sensor_filtered(GPIOA, GPIO_PIN_9);
float error = left - right;
integral += error * 0.02; // 假设采样周期20ms
float derivative = (error - prev_error) / 0.02;
float output = Kp*error + Ki*integral + Kd*derivative;
prev_error = error;
// 限幅处理防止超调
output = fmaxf(fminf(output, 50), -50);
set_motor_speed(base_speed + output, base_speed - output);
}
TB6612的驱动逻辑要注意死区保护。原始代码里电机转向切换时没有延时,可能导致MOS管直通。改进后的驱动函数:
c复制void motor_control(uint8_t dir, uint16_t speed, TIM_HandleTypeDef* htim, uint32_t channel) {
static uint8_t last_dir = 0;
if(dir != last_dir) {
__HAL_TIM_SET_COMPARE(htim, channel, 0); // 先停止PWM
osDelay(10); // 等待10ms
}
HAL_GPIO_WritePin(GPIOA, AIN1_Pin, dir);
HAL_GPIO_WritePin(GPIOA, AIN2_Pin, !dir);
__HAL_TIM_SET_COMPARE(htim, channel, speed);
last_dir = dir;
}
PWM占空比建议做非线性映射。实测发现电机在低速段响应不灵敏,可以建立速度曲线表:
c复制uint16_t speed_mapping(uint16_t input) {
const uint16_t curve[] = {0, 30, 60, 90, 120, 150, 180};
return curve[input / 40]; // 将0-255映射为7档速度
}
调参时记住口诀:"先比例后积分,最后再加微分"。具体步骤:
这是我调试某次比赛的参数记录:
| 参数组 | Kp | Ki | Kd | 表现描述 |
|---|---|---|---|---|
| 第一组 | 10 | 0 | 0 | 过冲严重,来回摆动 |
| 第二组 | 8 | 0.02 | 0 | 直道稳定,弯道有静差 |
| 第三组 | 12 | 0.01 | 0.3 | 综合表现最佳 |
遇到小车画龙(S形走线)时:
若小车在弯道冲出跑道:
固定PID参数难以适应复杂赛道,可以设计参数自整定策略。比如检测到连续弯道时自动增大Kp:
c复制if(fabs(error) > 8 && fabs(prev_error) > 8) {
float adaptive_Kp = Kp * 1.5;
output = adaptive_Kp*error + Ki*integral + Kd*derivative;
}
单纯循迹不够刺激,可以加入速度控制。在直道段加速,入弯前减速:
c复制float speed_planning(float curvature) {
// curvature通过最近5个误差值的标准差估算
float max_speed = 180;
return max_speed * exp(-0.5 * curvature);
}
最后分享一个调试彩蛋:在MainTask里添加以下代码,可以用蜂鸣器音调反馈实时误差值,比看波形更直观:
c复制void error_beep(float err) {
uint16_t freq = 500 + fabs(err)*100;
__HAL_TIM_SET_AUTORELOAD(&htim3, 1000000/freq);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
osDelay(50);
HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_1);
}