4、直流有刷电机闭环控制:PID控制原理、位置式PID与增量式PID、速度闭环控制实验

各位同学,欢迎来到第四章。前面我们讲了开环控制,说白了就是给多大电压转多快,但实际中负载一变,转速就掉下来了。这时候,我们就需要闭环控制。今天要聊的PID,是电机控制里最经典、最实用的算法,没有之一。

我刚开始做电机驱动时,觉得PID不就是个比例积分微分嘛,网上代码一搜一大把。结果真到项目里,电机要么抖得像筛子,要么响应慢得像蜗牛。后来才明白,PID这东西,参数调好了是神器,调不好就是灾难。今天我就把这几年的经验掰开揉碎了讲给你听。

4.1 PID控制原理——说白了就是“纠偏”

PID的全称是比例-积分-微分控制。你想想看,我们要让电机转速稳定在1000转/分,但实际只有800转,差了200转。怎么办?

  • 比例(P):差多少补多少。差200转,我就加对应的电压。但比例太大容易超调,电机冲过头又回调,来回震荡。
  • 积分(I):消除静差。有时候比例控制下,电机就是差那么几转上不去,积分项会慢慢累积误差,直到把这点偏差吃掉。
  • 微分(D):预测未来。如果误差突然变大,微分项会提前刹车,防止系统震荡。

公式长这样:

u(t) = Kp * e(t) + Ki * ∫e(t)dt + Kd * de(t)/dt

其中e(t)是目标值与实际值的偏差。嗯,这里要注意,积分项是累积误差,如果积分时间太长,就会出现“积分饱和”——误差累积太大,撤不回来了。我在项目中遇到过,电机启动时积分项疯狂累积,等转速到了目标值,积分项还在输出,结果转速冲过头了。

核心要点:PID的本质就是根据过去(积分)、现在(比例)、未来(微分)的误差来调整输出。说白了,就是让电机听话。

4.2 位置式PID与增量式PID——两种实现方式

实际写代码时,PID有两种实现方式:位置式和增量式。很多新手搞不清区别,我简单说一下。

4.2.1 位置式PID

位置式PID直接计算当前时刻的控制量u(k)。公式如下:

u(k) = Kp * e(k) + Ki * ∑e(i) + Kd * [e(k) - e(k-1)]

这里∑e(i)是从开始到现在的所有误差之和。你想想看,如果系统跑了一天,这个累积和会非常大。一旦出现故障,输出u(k)可能瞬间跳变,电机就会猛冲一下。我曾经在调试时遇到过,位置式PID的积分项累积到几万,一复位电机直接飞车,吓得我赶紧切了电源。

警告:位置式PID的积分项需要做限幅处理,否则积分饱和会带来严重的安全隐患。我建议在代码里加一个积分上限,比如限制在±1000以内。

4.2.2 增量式PID

增量式PID只计算当前时刻相对于上一时刻的控制量增量Δu(k)。公式如下:

Δu(k) = Kp * [e(k) - e(k-1)] + Ki * e(k) + Kd * [e(k) - 2*e(k-1) + e(k-2)]

最终输出:u(k) = u(k-1) + Δu(k)

增量式的优点很明显:

  • 没有积分累积,不会出现积分饱和
  • 输出变化平缓,即使e(k)突变,Δu(k)也不会太大
  • 容易实现手动/自动切换,切换时无冲击

我个人习惯在速度闭环控制中用增量式PID。为什么?因为速度控制对平滑性要求高,增量式天然就有“软启动”的效果。位置式PID更适合位置控制,比如机械臂的关节角度控制。

小技巧:如果你刚开始调PID,建议先用增量式。它更安全,不容易出事故。等经验丰富了,再根据场景选择位置式。

4.3 速度闭环控制实验——手把手带你调参数

理论讲完了,咱们来点实际的。下面是一个基于STM32的直流有刷电机速度闭环控制实验。硬件很简单:STM32F103、L298N驱动、带编码器的直流电机、12V电源。

4.3.1 硬件连接

STM32引脚 连接对象 说明
PA0 编码器A相 定时器2通道1
PA1 编码器B相 定时器2通道2
PA6 PWM输出 定时器3通道1
PB0 电机方向1 GPIO输出
PB1 电机方向2 GPIO输出

4.3.2 代码实现——增量式PID速度控制

下面是我常用的增量式PID代码框架。你直接复制到工程里,改一下参数就能用。

// pid.h
typedef struct {
    float Kp;
    float Ki;
    float Kd;
    float target;      // 目标值
    float actual;      // 实际值
    float error;       // 当前误差
    float last_error;  // 上一次误差
    float prev_error;  // 上上次误差
    float output;      // 输出值
    float delta;       // 增量
} PID_TypeDef;

// pid.c
void PID_Init(PID_TypeDef *pid, float Kp, float Ki, float Kd) {
    pid->Kp = Kp;
    pid->Ki = Ki;
    pid->Kd = Kd;
    pid->target = 0;
    pid->actual = 0;
    pid->error = 0;
    pid->last_error = 0;
    pid->prev_error = 0;
    pid->output = 0;
    pid->delta = 0;
}

float PID_Calc(PID_TypeDef *pid, float target, float actual) {
    pid->target = target;
    pid->actual = actual;
    
    // 更新误差
    pid->prev_error = pid->last_error;
    pid->last_error = pid->error;
    pid->error = pid->target - pid->actual;
    
    // 计算增量
    pid->delta = pid->Kp * (pid->error - pid->last_error) +
                 pid->Ki * pid->error +
                 pid->Kd * (pid->error - 2*pid->last_error + pid->prev_error);
    
    // 累加输出
    pid->output += pid->delta;
    
    // 输出限幅,防止PWM溢出
    if (pid->output > 999) pid->output = 999;
    if (pid->output < -999) pid->output = -999;
    
    return pid->output;
}

4.3.3 参数整定——我的“三步法”

调PID参数是门手艺活。我总结了一个三步法,适合新手快速上手:

  1. 先调P:Ki和Kd设为0,只给Kp。从小到大慢慢加,直到电机出现轻微震荡。然后取当前Kp值的60%作为最终Kp。比如Kp加到10时开始震荡,那就取6。
  2. 再加I:保持Kp不变,从0开始加Ki。观察稳态误差是否消除。如果电机出现低频震荡,说明Ki太大了,减半再试。我一般Ki取Kp的1/10到1/5。
  3. 最后加D:如果系统响应慢或者超调大,加一点Kd。Kd通常很小,从0.1开始试。加多了电机反而会高频抖动。

经验之谈:我曾经调一个高速电机,Kp加到20都不震荡,但响应还是慢。后来发现是编码器分辨率太低,反馈延迟太大。换了高分辨率编码器后,Kp降到5就搞定了。所以,硬件基础决定PID的上限。

4.3.4 实验步骤

  1. 下载代码到STM32,打开串口助手,波特率115200。
  2. 发送命令:SET 1000,设置目标转速为1000转/分。
  3. 观察串口打印的实际转速。如果震荡,减小Kp;如果响应慢,增大Ki。
  4. 用手捏住电机轴,模拟负载变化。看转速能否快速恢复。
  5. 记录不同参数下的响应曲线,对比效果。

注意:实验时手不要碰到电机轴旋转部分,小心受伤。另外,PWM占空比不要长时间超过90%,否则驱动芯片容易过热。

4.4 避坑指南——我踩过的那些坑

最后分享几个我实际项目中遇到的坑,希望能帮你少走弯路。

  • 积分饱和:我曾经在位置式PID中没做积分限幅,电机启动时积分累积到几千,等转速到了目标值,积分项还在输出,结果转速冲过头了。解决方案:加积分限幅,或者改用增量式PID。
  • 微分噪声放大:Kd对高频噪声非常敏感。如果编码器信号有毛刺,微分项会把噪声放大,导致电机抖动。我建议在编码器输入加一个RC低通滤波,或者在代码里做一阶低通滤波。
  • PWM频率选择:直流有刷电机的PWM频率一般选10kHz-20kHz。频率太低,电机会有啸叫声;频率太高,驱动芯片开关损耗大。我习惯用16kHz,人耳听不到,效率也合适。
  • 死区补偿:电机在低速时,由于摩擦力,PWM占空比太小电机不转。这时候需要加一个死区补偿,比如PWM输出小于5%时直接给5%。

好了,这一章的内容就到这里。PID控制是电机控制的基石,你把它吃透了,后面学FOC、伺服控制都会轻松很多。下一章我们讲电流闭环控制,到时候会用到更高级的PI控制器。记得动手做实验,光看是学不会的。