实战解析:基于STM32F103与PID算法的智能小车精准运动控制
1. 从零搭建智能小车硬件平台
第一次玩STM32智能小车时,我对着淘宝买来的散件堆了满桌——电机、轮子、电池、主板,活像乐高积木灾难现场。这里分享几个硬件选型避坑经验:STM32F103C8T6最小系统板绝对是性价比之王,价格不到一杯奶茶钱却自带硬件PWM和定时器外设。电机驱动建议用L298N这种经典模块,我实测带载两个N20减速电机毫无压力,注意要在电机两端并联104电容消除火花干扰。
焊接时最容易翻车的是电源部分。我的血泪教训是:必须独立供电!开发板用USB供电,电机驱动单独接7.4V锂电池。有次偷懒共用一个电源,电机启动瞬间的电流波动直接导致单片机重启。硬件连接关键点:
- 电机驱动IN1~IN4接STM32的GPIO(如PA0~PA3)
- ENA/ENB接定时器PWM输出引脚(如TIM2_CH1)
- 电机输出端记得用万用表确认正反转逻辑
提示:上电前务必用逻辑分析仪检查PWM信号,我曾遇到因引脚复用配置错误导致电机抽搐的情况
2. STM32CubeMX的PID控制工程配置
打开CubeMX时,新手常被密密麻麻的选项吓到。其实配置PID控制只需关注三个关键点:
2.1 定时器PWM生成
以TIM2为例,在Clock Configuration里确认APB1 Timer Clock达到72MHz后:
- 选择Channel1为PWM Generation CH1
- Prescaler设为71(72MHz/(71+1)=1MHz)
- Counter Period设为999(1MHz/(999+1)=1kHz PWM频率)
- 勾选自动重装载预装载
// 生成的PWM启动代码 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 500); // 50%占空比2.2 编码器接口配置
若使用带编码器的电机,需配置另一个定时器为Encoder Mode。我推荐TIM3:
- 选择TI1和TI2引脚
- Encoder Mode选择"Encoder Mode TI1 and TI2"
- 计数器周期设为65535(16位最大值)
2.3 中断优先级管理
在NVIC Settings里给定时器中断合理分配优先级:
- PWM定时器中断优先级可设低些(如PreemptionPriority=1)
- 编码器定时器中断需要更高响应速度(PreemptionPriority=0)
- SysTick保持默认最高优先级
3. PID算法的代码实现与调参
网上很多PID例程直接套用公式,但实际小车控制需要处理三个特殊问题:
3.1 增量式PID实现
位置式PID容易积分饱和,我改良的增量式代码如下:
typedef struct { float target_val; // 目标值 float actual_val; // 实际值 float err; // 当前误差 float err_last; // 上次误差 float Kp,Ki,Kd; // PID系数 float integral; // 积分项 float output; // 输出 }PID; float PID_realize(PID *pid, float actual_val) { pid->err = pid->target_val - actual_val; float increment = pid->Kp*(pid->err-pid->err_last) + pid->Ki*pid->err + pid->Kd*(pid->err-2*pid->err_last+pid->err_next); pid->output += increment; pid->err_last = pid->err; return pid->output; }3.2 参数整定实战技巧
调参时记住口诀:"先比例后积分,微分最后加"。我的经验值:
- 空载测试:Kp=0.5, Ki=0.01, Kd=0
- 带载起步:Kp=2.0, Ki=0.05, Kd=0.1
- 高速运行:Kp=1.2, Ki=0.03, Kd=0.3
用手机慢动作视频辅助观察小车运动状态:
- 出现抖动:减小Kp或增大Kd
- 到达目标速度慢:增大Ki
- 过冲严重:减小Ki或增大Kd
3.3 抗积分饱和处理
在电机堵转时加入积分限幅:
// 在PID_realize函数中加入 if(fabs(pid->integral) > INTEGRAL_MAX) { pid->integral = (pid->integral > 0) ? INTEGRAL_MAX : -INTEGRAL_MAX; }4. 运动控制高级技巧
让小车走出直线其实比想象中困难——两个电机特性总有微小差异。这里分享几个进阶玩法:
4.1 差速转向的平滑处理
普通差速转向会产生"齿轮感",我加入S曲线加速算法:
void smooth_turn(float left_speed, float right_speed) { static float left_actual = 0, right_actual = 0; const float step = 0.05f; // S曲线逼近目标速度 left_actual += (left_speed - left_actual) * step; right_actual += (right_speed - right_actual) * step; motorPidSetSpeed(left_actual, right_actual); }4.2 原地旋转的精准控制
通过陀螺仪(如MPU6050)辅助修正旋转角度:
void rotate_to_angle(float target_angle) { float current_angle = get_mpu6050_yaw(); while(fabs(current_angle - target_angle) > 2.0f) { // 2度误差范围 float err = target_angle - current_angle; float rotate_speed = constrain(err * 0.5f, -MAX_ROTATE_SPEED, MAX_ROTATE_SPEED); motorPidSetSpeed(rotate_speed, -rotate_speed); current_angle = get_mpu6050_yaw(); } motor_stop(); }4.3 速度规划算法
突然的加速会导致轮胎打滑,建议采用梯形速度规划:
| 阶段 | 时间(ms) | 加速度(m/s²) | 实现方法 |
|---|---|---|---|
| 加速段 | 0-500 | 0.5 | 线性增加PWM占空比 |
| 匀速段 | 500-1500 | 0 | 维持PID输出 |
| 减速段 | 1500-2000 | -0.3 | 指数衰减目标速度 |
void trapezoidal_speed_control(float target_speed) { static uint32_t start_time = HAL_GetTick(); float elapsed = HAL_GetTick() - start_time; if(elapsed < 500) { // 加速阶段 current_speed = target_speed * (elapsed / 500.0f); } else if(elapsed < 1500) { // 匀速阶段 current_speed = target_speed; } else if(elapsed < 2000) { // 减速阶段 current_speed = target_speed * (1 - (elapsed-1500)/500.0f); } motorPidSetSpeed(current_speed, current_speed); }调车时发现一个有趣现象:当PWM频率设为1kHz时电机噪音最小,但响应速度会稍慢。后来改用TIM1的高级定时器生成15kHz PWM,不仅消除了可闻噪音,电机响应也更迅捷。这提醒我们,PID控制不仅是算法问题,硬件特性同样关键。
