stm32实战笔记1——基于stm32f103的增量式PID电机调速(标准库)
1. 增量式PID控制基础与STM32F103硬件准备第一次接触电机闭环控制时我被各种专业术语搞得晕头转向。后来发现理解增量式PID其实就像学骑自行车——刚开始摇摇晃晃找到平衡点后就变得简单了。STM32F103这颗芯片就像是我们的自行车而增量式PID算法就是保持平衡的技巧。我手头用的TB6612电机驱动模块体积虽小但能耐不小。它的AIN1/AIN2控制电机转向PWM引脚调节转速STBY脚接高电平就能唤醒模块。实测驱动MG513电机时3.3V逻辑电平完全够用不用额外电平转换。这里有个坑要注意电机电源一定要和MCU分开供电共地就行否则PWM信号会被干扰得面目全非。编码器选用的是霍尔式13PPR分辨率配合30:1减速箱转一圈能输出390个脉冲。记得第一次接线时我把A/B相序接反了导致转速计算总是负值排查半天才发现问题。建议先用示波器确认编码器输出波形别像我一样走弯路。2. 标准库环境搭建与电机驱动实现新建工程时我习惯用Keil MDK记得勾选STM32F10x_StdPeriph_Lib。有次我漏选了GPIO库调电机驱动时死活不转最后发现是时钟没使能。建议按这个顺序初始化系统时钟配置为72MHz使能GPIO端口时钟配置电机控制引脚为推挽输出电机驱动代码我封装成了三个函数void Motor_Init() { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_2 | GPIO_Pin_5 | GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOE, GPIO_InitStructure); } void Motor_Run(uint8_t dir) { if(dir FORWARD) { GPIO_SetBits(GPIOE, GPIO_Pin_4); GPIO_ResetBits(GPIOE, GPIO_Pin_2); } else { GPIO_ResetBits(GPIOE, GPIO_Pin_4); GPIO_SetBits(GPIOE, GPIO_Pin_2); } } void Motor_Stop() { GPIO_ResetBits(GPIOE, GPIO_Pin_4); GPIO_ResetBits(GPIOE, GPIO_Pin_2); }PWM配置有个小技巧把ARR设为7199PSC设为0这样PWM频率就是72MHz/(71991)10kHz既避开人耳敏感频段又不会因频率太高导致MOS管过热。我曾试过20kHz电机发热明显增加。3. 编码器接口配置与速度测量编码器接口配置是闭环控制的关键。STM32的定时器编码器模式简直是为这类应用量身定做的我选择的是模式3TI1和TI2边沿都触发计数配合10个时钟周期的输入滤波能有效消抖。速度计算需要特别注意采样周期。我的方案是每10ms读取一次编码器计数值通过这个公式换算转速转速(rpm) (Δ计数值 × 60) / (编码器线数 × 减速比 × 采样时间)实际测试发现当电机低速运转时10ms采样会导致速度波动较大。后来我改成了滑动窗口滤波取5次采样值的移动平均效果立竿见影。编码器中断服务函数里有个细节容易忽略计数值是16位有符号数超过32767会自动转为负数。我的处理方法是int16_t Read_Encoder() { int32_t temp TIM_GetCounter(TIM4); if(temp 32767) temp - 65536; TIM_SetCounter(TIM4, 0); return (int16_t)temp; }4. 增量式PID算法实现与调参增量式PID与位置式的最大区别在于它只计算控制量的变化而不是绝对值。这带来三个好处不会产生积分饱和电机启停更平滑更易实现手动/自动无扰切换我的PID实现代码如下typedef struct { float Kp; float Ki; float Kd; int16_t max_output; int16_t min_output; int16_t last_error; int16_t prev_error; } PID_IncTypeDef; int16_t PID_Inc_Calculate(PID_IncTypeDef *pid, int16_t target, int16_t feedback) { int16_t error target - feedback; float delta pid-Kp*(error - pid-last_error) pid-Ki*error pid-Kd*(error - 2*pid-last_error pid-prev_error); pid-prev_error pid-last_error; pid-last_error error; int16_t output (int16_t)delta; if(output pid-max_output) output pid-max_output; if(output pid-min_output) output pid-min_output; return output; }调参是个耐心活我的经验是先设Ki0Kd0逐渐增大Kp直到系统开始振荡取振荡时Kp值的60%作为最终P参数慢慢增加Ki改善稳态误差最后加少量Kd抑制超调实测发现对于我的小车电机Kp0.3、Ki0.15、Kd0.05时速度阶跃响应超调5%稳态误差在±2RPM以内。5. 系统整合与性能优化把所有模块整合后主程序框架是这样的int main() { SystemInit(); Motor_Init(); PWM_Init(7199, 0); Encoder_Init(); TIM6_Init(); // 10ms定时中断 PID_IncTypeDef pid {0.3, 0.15, 0.05, 4000, -4000}; while(1) { if(need_speed_ctrl) { int16_t pwm PID_Inc_Calculate(pid, target_speed, current_speed); Set_PWMA(pwm); } } }遇到最头疼的问题是电机启动时的抖动。后来加入软启动策略前500ms让目标速度从0线性增加到设定值完美解决了这个问题。另外发现PWM占空比低于5%时电机可能不转高于95%时效率反而下降所以我在输出端做了限幅处理。6. 常见问题排查与实战技巧调试时我总结了几条黄金法则先开环测试固定占空比看电机能否正常转动再验证编码器手动旋转电机观察计数值变化最后闭环调试从较小P参数开始逐步调整有个诡异现象困扰了我一周电机偶尔会突然反转。最后发现是电源线太长导致电压跌落在电机端并联了1000uF电容后问题消失。建议大家在电机供电端一定要加足够大的滤波电容。对于需要精确控制的应用可以加入前馈控制。我在小车爬坡时发现速度会下降后来通过检测坡度角提前增加PWM输出速度稳定性提升了40%。具体实现是在PID输出上叠加一个与坡度成正比的补偿量。7. 进阶优化方向当系统要求更高性能时可以考虑改用定时器硬件PWM生成减轻CPU负担将PID计算放在定时器中断中确保采样周期精确加入抗积分饱和逻辑防止长时间误差累积实现参数自整定功能我最近尝试将控制周期缩短到5ms发现响应速度确实更快但对编码器分辨率要求也更高。另外把PID计算移到了定时器中断里主循环只做目标值更新和状态监控系统实时性明显改善。最后分享一个实用技巧用OLED实时显示目标速度、实际速度和PWM占空比调试时能直观看到系统响应过程。我通常会把曲线绘制出来这样PID参数对系统的影响一目了然。