3. 位置式PID实现:从公式到代码,再到抗积分饱和
好,咱们今天来啃一块硬骨头——位置式PID的实现。
说实话,很多新手一上来就写PID代码,结果飞控在天上乱晃,自己还不知道问题出在哪。我当年也踩过这个坑,调了三天三夜,最后发现是积分项在作怪。嗯,今天咱们就把这事彻底讲明白。
3.1 位置式PID公式推导
先看公式。位置式PID的输出,说白了就是三项加起来:
u(k) = Kp * e(k) + Ki * Σe(i) + Kd * [e(k) - e(k-1)]
这里:
- u(k):当前时刻的控制输出(比如油门值、舵机角度)
- e(k):当前时刻的误差(目标值 - 当前值)
- Σe(i):从开始到现在所有误差的累加和
- Kp, Ki, Kd:比例、积分、微分系数
为什么叫“位置式”?因为它的输出直接对应执行机构的绝对位置。比如你控制一个舵机,输出值就是舵机应该转到的角度。这和增量式PID不一样,增量式输出的是“这次要比上次多转多少”。
核心要点:位置式PID的输出是绝对值,所以一旦积分项累积过大,输出就会饱和。这就是后面要讲的积分饱和问题。
3.2 C语言实现位置式PID
直接上代码。我个人习惯把PID参数和状态封装成一个结构体,这样清晰又好维护。
// pid.h
typedef struct {
float Kp; // 比例系数
float Ki; // 积分系数
float Kd; // 微分系数
float target; // 目标值
float feedback; // 反馈值(当前值)
float error; // 当前误差
float last_error; // 上一次误差
float integral; // 积分累加和
float output; // 输出值
float output_limit; // 输出限幅(防止溢出)
} PID_TypeDef;
// pid.c
float PID_Calculate(PID_TypeDef *pid, float target, float feedback) {
// 1. 计算误差
pid->error = target - feedback;
// 2. 积分累加
pid->integral += pid->error;
// 3. 计算微分
float derivative = pid->error - pid->last_error;
// 4. 位置式PID公式
pid->output = pid->Kp * pid->error
+ pid->Ki * pid->integral
+ pid->Kd * derivative;
// 5. 输出限幅
if (pid->output > pid->output_limit) {
pid->output = pid->output_limit;
} else if (pid->output < -pid->output_limit) {
pid->output = -pid->output_limit;
}
// 6. 更新状态
pid->last_error = pid->error;
return pid->output;
}
我的小技巧:积分项最好单独做限幅,不然积分累加会变得非常大。我一般会在结构体里加一个 integral_limit 字段,在累加后直接截断。
3.3 积分饱和问题
积分饱和,说白了就是积分项“撑爆了”。
你想想看,当系统长时间达不到目标值时(比如电机堵转、舵机卡死),误差一直存在,积分项就会一直累加。等障碍解除后,这个巨大的积分值会让输出瞬间冲到极限,导致系统严重超调,甚至震荡。
我在项目中遇到过最典型的情况:四旋翼落地时,桨叶被草缠住,积分项疯狂累加。等桨叶挣脱后,飞控直接给了一个满油门,飞机差点窜上天。嗯,从那以后我再也不敢忽视积分饱和了。
注意:积分饱和不是参数调得不好,而是位置式PID的固有问题。只要用位置式,就必须考虑抗积分饱和。
3.4 抗积分饱和方法
常用的方法有三种,我按推荐程度排序:
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 积分限幅 | 给积分项设置最大值 | 实现简单 | 可能影响稳态精度 |
| 积分分离 | 误差大时停止积分 | 响应快,超调小 | 阈值难调 |
| 变积分系数 | 根据误差大小动态调整Ki | 效果最好 | 实现复杂 |
我最常用的是积分限幅 + 积分分离的组合。 为什么?因为简单可靠,嵌入式系统最怕花里胡哨的算法。
来看改进后的代码:
float PID_Calculate_AntiWindup(PID_TypeDef *pid, float target, float feedback) {
pid->error = target - feedback;
// 积分分离:误差大于阈值时,不积分
if (fabs(pid->error) < pid->integral_threshold) {
pid->integral += pid->error;
// 积分限幅
if (pid->integral > pid->integral_limit) {
pid->integral = pid->integral_limit;
} else if (pid->integral < -pid->integral_limit) {
pid->integral = -pid->integral_limit;
}
} else {
// 误差太大,积分清零或保持
pid->integral = 0; // 也可以保持原值,看需求
}
float derivative = pid->error - pid->last_error;
pid->output = pid->Kp * pid->error
+ pid->Ki * pid->integral
+ pid->Kd * derivative;
// 输出限幅
if (pid->output > pid->output_limit) {
pid->output = pid->output_limit;
} else if (pid->output < -pid->output_limit) {
pid->output = -pid->output_limit;
}
pid->last_error = pid->error;
return pid->output;
}
避坑指南:我曾经在积分分离的阈值上吃过亏。阈值设太大,积分分离没效果;设太小,系统稳态误差消不掉。我的经验是:阈值取目标值的5%-10%,然后根据实际响应微调。
3.5 总结与建议
位置式PID,说白了就是“把过去、现在、未来的误差都算进去”。
- 比例项管现在,误差越大输出越大
- 积分项管过去,消除稳态误差
- 微分项管未来,抑制超调
但积分项是把双刃剑。用好了,系统稳如老狗;用不好,系统疯如野马。抗积分饱和不是可选项,而是必选项。
最后说一句:代码写完了,一定要在仿真环境里跑一跑。我见过太多人直接上真机,结果炸机了才回来改代码。嗯,别问我怎么知道的。
下节预告:增量式PID。你会发现它天生没有积分饱和问题,但又有新的坑等着你。