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参数整定的实战技巧——怎么用示波器看曲线调参,那才是真正的手艺活。