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 速度闭环的完整流程

为了让你看得更清楚,我画了一张流程图。它展示了从目标速度到实际速度的整个闭环过程。

增量式PID速度闭环流程图 目标速度 (rpm) 偏差 e(k) = 目标 - 反馈 增量式PID计算 Δu = Kp*Δe + Ki*e + Kd*Δ²e 输出限幅 PWM输出 + 累加 直流电机 速度反馈(编码器)

4.5 调参实战:从震荡到稳定

代码写好了,怎么调?我分享一个实战步骤:

  1. 先调Kp:从小到大加,直到系统出现等幅震荡。记录此时的Kp和震荡周期Tu。
  2. 加Ki:设置为0.5*Kp,观察静差是否消除。如果超调大,减小Ki。
  3. 加Kd:设置为0.125*Kp,抑制超调。如果噪声大,减小Kd。
  4. 微调:三个参数互相影响,需要反复试。
我的小技巧: 用串口打印目标值和实际值,画成曲线。眼睛看曲线调参,比看数字直观得多。STM32的DMA+串口,每秒能传几百个点,够用了。

嗯,这一章的内容就到这里。位置式和增量式的区别,你心里应该有数了。速度闭环的代码,也直接能用。剩下的,就是拿个电机,动手调一调。参数没有标准答案,只有最适合你系统的答案。