4、直流电机PID控制基础:位置式PID与增量式PID、STM32实现速度闭环
做机器人运动学仿真,最终还是要落到电机控制上。而电机控制,绕不开PID。
说实话,PID这东西,教科书上讲得玄乎,什么“积分饱和”、“微分先行”,初学者一看就头大。但实际用起来,核心逻辑就一句话:看偏差,算输出,调回来。
这一章,我们就把它拆开揉碎。我会结合STM32实战,把位置式PID和增量式PID的区别讲清楚,再带你手写一个速度闭环。
4.1 从“偏差”到“控制量”:PID到底在干什么?
想象一下,你要让电机转速稳定在1000rpm。实际转速可能是950,也可能是1050。这个差值,就是偏差 e(t)。
PID要做的,就是根据这个偏差,算出一个控制量 u(t),去驱动电机,让偏差趋近于零。
公式长这样:
u(t) = Kp * e(t) + Ki * ∫e(t)dt + Kd * de(t)/dt
拆开看:
- 比例项(Kp):当前偏差有多大,我就给多大劲。反应快,但会有静差。
- 积分项(Ki):把过去的偏差累加起来。消除静差,但容易超调。
- 微分项(Kd):预测偏差的变化趋势。抑制震荡,但对噪声敏感。
我的习惯: 调参时,先把Ki和Kd设为0,只调Kp让系统不震荡。然后加一点Ki消除静差。最后,如果系统有超调,再加一点点Kd。别一上来就三个参数一起调,容易乱。
4.2 位置式PID vs 增量式PID:到底选哪个?
这是初学者最容易纠结的问题。我直接说结论:看执行器。
4.2.1 位置式PID
位置式PID输出的是控制量的绝对值。比如,直接输出PWM占空比50%。
公式离散化后:
u(k) = Kp * e(k) + Ki * ∑e(i) + Kd * [e(k) - e(k-1)]
特点:
- 输出与过去所有状态有关,计算量大。
- 积分项容易饱和(windup),需要做抗饱和处理。
- 一旦输出异常,电机可能直接飞车。
4.2.2 增量式PID
增量式PID输出的是控制量的增量。比如,这次比上次增加5%的占空比。
公式:
Δu(k) = Kp * [e(k) - e(k-1)] + Ki * e(k) + Kd * [e(k) - 2*e(k-1) + e(k-2)]
特点:
- 输出是增量,误动作影响小。
- 不需要累加积分项,计算量小。
- 容易实现手动/自动切换无冲击。
| 对比项 | 位置式PID | 增量式PID |
|---|---|---|
| 输出形式 | 控制量绝对值 | 控制量增量 |
| 积分饱和 | 容易发生,需处理 | 不易发生 |
| 误动作影响 | 大,可能飞车 | 小,只影响当前步 |
| 计算量 | 较大 | 较小 |
| 适用场景 | 伺服、舵机等位置控制 | 步进电机、速度环等 |
我曾经踩过的坑: 做四轴飞行器时,用了位置式PID控制电机转速。结果积分饱和,电机直接满转,飞机差点翻。后来换成增量式PID,配合限幅,才稳下来。所以,速度环推荐用增量式。
4.3 STM32实现速度闭环:手写增量式PID
好了,理论说完了,上代码。我们用STM32的定时器做编码器接口,读取电机速度,然后用增量式PID计算输出,控制PWM占空比。
4.3.1 硬件准备
- STM32F103C8T6(蓝板子)
- 直流减速电机带霍尔编码器
- L298N或TB6612电机驱动
4.3.2 代码实现
先定义PID结构体:
typedef struct {
float Kp;
float Ki;
float Kd;
float target; // 目标值
float last_error; // e(k-1)
float prev_error; // e(k-2)
float output; // 输出增量
float max_output; // 限幅
} PID_IncTypeDef;
核心计算函数:
float PID_Inc_Calc(PID_IncTypeDef *pid, float feedback) {
float error = pid->target - feedback;
float p_term = pid->Kp * (error - pid->last_error);
float i_term = pid->Ki * error;
float d_term = pid->Kd * (error - 2*pid->last_error + pid->prev_error);
pid->output = p_term + i_term + d_term;
// 限幅
if (pid->output > pid->max_output) pid->output = pid->max_output;
if (pid->output < -pid->max_output) pid->output = -pid->max_output;
// 更新历史误差
pid->prev_error = pid->last_error;
pid->last_error = error;
return pid->output;
}
在主循环中调用:
int main(void) {
PID_IncTypeDef speed_pid;
speed_pid.Kp = 0.5;
speed_pid.Ki = 0.1;
speed_pid.Kd = 0.02;
speed_pid.target = 1000; // 目标转速 1000rpm
speed_pid.max_output = 500;
while (1) {
uint32_t pulse = TIM_GetCounter(TIM2); // 读取编码器脉冲
float speed = pulse / 4.0f; // 简单换算为rpm
float inc = PID_Inc_Calc(&speed_pid, speed);
// 将inc叠加到当前PWM占空比
TIM_SetCompare1(TIM3, TIM_GetCompare1(TIM3) + (int16_t)inc);
delay_ms(10);
}
}
关键点: 增量式PID的输出是增量,需要累加到当前PWM值上。别忘了做限幅,防止PWM溢出。
4.4 速度闭环的完整流程
为了让你看得更清楚,我画了一张流程图。它展示了从目标速度到实际速度的整个闭环过程。
4.5 调参实战:从震荡到稳定
代码写好了,怎么调?我分享一个实战步骤:
- 先调Kp:从小到大加,直到系统出现等幅震荡。记录此时的Kp和震荡周期Tu。
- 加Ki:设置为0.5*Kp,观察静差是否消除。如果超调大,减小Ki。
- 加Kd:设置为0.125*Kp,抑制超调。如果噪声大,减小Kd。
- 微调:三个参数互相影响,需要反复试。
我的小技巧: 用串口打印目标值和实际值,画成曲线。眼睛看曲线调参,比看数字直观得多。STM32的DMA+串口,每秒能传几百个点,够用了。
嗯,这一章的内容就到这里。位置式和增量式的区别,你心里应该有数了。速度闭环的代码,也直接能用。剩下的,就是拿个电机,动手调一调。参数没有标准答案,只有最适合你系统的答案。