4. 增量式PID实现:公式推导、代码实现与对比分析

各位同学,今天我们来聊聊增量式PID。

说实话,很多初学者一上来就写位置式PID,结果发现积分项调起来特别痛苦。我当年做四旋翼悬停控制时,就吃过这个亏——积分饱和导致电机直接堵转,差点炸机。后来换成增量式,问题迎刃而解。

增量式PID,说白了就是只计算控制量的变化量,而不是绝对值。你想想看,这对嵌入式系统多友好——输出不会突变,执行器不会受惊吓。

4.1 增量式PID公式推导

我们从位置式PID开始推导。位置式公式大家都熟悉:

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

那么,第k-1时刻的控制量是:

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

增量Δu(k) = u(k) - u(k-1),两式相减:

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

嗯,这里要注意:积分项的Σe(i) - Σe(i-1) 正好等于 e(k),所以积分项变得极其简洁。

再整理一下,写成标准形式:

Δu(k) = A * e(k) + B * e(k-1) + C * e(k-2)

其中:

系数 表达式 物理意义
A Kp + Ki + Kd 当前误差权重
B -(Kp + 2*Kd) 上一时刻误差权重
C Kd 上两时刻误差权重

你看,三个系数都是常数,只要算一次就能反复用。我在项目里经常把A、B、C提前算好存起来,省得每次循环都做浮点乘法。

核心优势:增量式PID的输出是Δu,不是u本身。这意味着即使你算错了,输出也只是变化一点点,不会直接飞到极限值。

4.2 C语言实现增量式PID

直接上代码。这是我个人习惯的写法,结构体封装,清晰好维护:

typedef struct {
    float Kp;      // 比例系数
    float Ki;      // 积分系数
    float Kd;      // 微分系数
    
    float A;       // 预计算系数 A = Kp + Ki + Kd
    float B;       // 预计算系数 B = -(Kp + 2*Kd)
    float C;       // 预计算系数 C = Kd
    
    float e_k;     // e(k)
    float e_k1;    // e(k-1)
    float e_k2;    // e(k-2)
    
    float out_max; // 输出限幅
    float delta_max; // 增量限幅
} IncrementalPID;

void IncrementalPID_Init(IncrementalPID *pid, float Kp, float Ki, float Kd, 
                          float out_max, float delta_max) {
    pid->Kp = Kp;
    pid->Ki = Ki;
    pid->Kd = Kd;
    
    // 预计算系数,减少实时计算量
    pid->A = Kp + Ki + Kd;
    pid->B = -(Kp + 2.0f * Kd);
    pid->C = Kd;
    
    pid->e_k = 0.0f;
    pid->e_k1 = 0.0f;
    pid->e_k2 = 0.0f;
    
    pid->out_max = out_max;
    pid->delta_max = delta_max;
}

float IncrementalPID_Calc(IncrementalPID *pid, float setpoint, float feedback) {
    // 1. 计算当前误差
    pid->e_k = setpoint - feedback;
    
    // 2. 计算增量
    float delta = pid->A * pid->e_k 
                + pid->B * pid->e_k1 
                + pid->C * pid->e_k2;
    
    // 3. 增量限幅——我吃过亏的地方
    if (delta > pid->delta_max) delta = pid->delta_max;
    if (delta < -pid->delta_max) delta = -pid->delta_max;
    
    // 4. 更新历史误差(注意顺序!)
    pid->e_k2 = pid->e_k1;
    pid->e_k1 = pid->e_k;
    
    return delta;
}

避坑指南:我曾经在更新历史误差时写反了顺序,先更新e_k1再更新e_k2,结果微分项完全乱套。记住:先更新老的,再更新新的

实际使用时,你需要在主循环里累加这个增量:

float current_output = 0.0f;  // 当前输出值

void ControlLoop(void) {
    float delta = IncrementalPID_Calc(&pid, target, sensor);
    current_output += delta;
    
    // 输出限幅
    if (current_output > pid.out_max) current_output = pid.out_max;
    if (current_output < -pid.out_max) current_output = -pid.out_max;
    
    SetMotorPWM(current_output);
}

4.3 位置式与增量式对比

很多同学问我:到底该用哪个?我的回答是——看场景。

下面这张表是我多年调参经验的总结:

对比项 位置式PID 增量式PID
输出形式 绝对控制量 u(k) 控制增量 Δu(k)
积分饱和 容易发生,需要额外处理 天然抗饱和(无积分累加)
执行器特性 适合比例执行器(如阀门开度) 适合积分执行器(如步进电机)
手动/自动切换 输出会突变,需做bumpless 切换平滑,增量从0开始
计算量 需要累加所有历史误差 只需最近3个误差值
故障影响 输出错误直接导致执行器飞车 单次错误只产生一个增量扰动
适用场景 伺服位置控制、温度控制 电机速度控制、无人机姿态

重要提醒:增量式PID虽然抗积分饱和,但它没有积分项的累加记忆。如果你需要系统在稳态时消除静差,必须在外部做积分累加,就像上面代码里的current_output变量那样。

举个例子:我做过一个云台稳定项目,用的是增量式PID。因为云台电机是积分特性——给一个PWM增量,电机就转一个角度。如果用位置式,每次都要计算绝对角度,积分项稍微调不好就震荡。换成增量式后,调参变得特别直观:P控制响应速度,I消除静差,D抑制抖动。

反过来,如果是做恒温箱的温度控制,我建议用位置式。因为加热器是比例执行器——给100%功率就全功率加热。增量式在这里反而别扭,因为你得把增量累加成绝对功率值,多此一举。

好了,这一章的内容就到这里。下一章我们聊聊PID参数整定的实战技巧——怎么用示波器看曲线调参,那才是真正的手艺活。