3. PID 控制器基础:比例、积分、微分与数字实现
PID 控制器,说白了就是三个字:现在、过去、未来。比例看现在,积分看过去,微分看趋势。我刚开始做轨迹跟踪时,总觉得这玩意儿太简单,不就是三个系数调来调去吗?后来被一台老旧的直流电机狠狠教育了一回——调了三天,电机还是抖得像筛糠。嗯,从那以后我才真正沉下心来理解这三个参数到底在干什么。
3.1 比例 P:最直接的纠偏
比例控制最简单——误差越大,输出越大。你想想看,机器人偏离轨迹 10 厘米,和偏离 1 厘米,控制力度肯定不一样。P 就是那个放大系数。
核心公式: u(t) = Kp × e(t)
其中 e(t) 是当前误差,Kp 是比例增益。
但比例控制有个硬伤:稳态误差。我记得有一次调一个移动机器人的直线跟踪,P 给到 5 的时候,机器人离目标线总是差 2 厘米。加 P 到 8,差 1 厘米。再加到 12,系统开始震荡。这就是比例控制的局限——你永远在误差和稳定性之间做 trade-off。
| Kp 值 | 响应速度 | 稳态误差 | 稳定性 |
|---|---|---|---|
| 过小 | 慢 | 大 | 稳定 |
| 适中 | 较快 | 较小 | 良好 |
| 过大 | 快 | 小 | 震荡甚至发散 |
3.2 积分 I:消除历史欠账
积分就是来收拾比例留下的烂摊子的。它把过去所有的误差累加起来,只要还有稳态误差,积分项就会一直增长,直到把误差推回零。
核心公式: u(t) = Ki × ∫e(τ)dτ
Ki 是积分增益,积分从 0 到当前时刻。
我曾经踩过的坑:积分饱和。有一次给一个机械臂做位置控制,启动时误差很大,积分项疯狂累积。等误差归零后,积分项还保留着巨大的值,导致系统超调严重,机械臂直接撞上了限位块。从那以后,我每次用积分必加 anti-windup。
积分也不是越大越好。Ki 太大,系统会「过冲」——误差还没归零,积分已经推过头了。我个人的习惯是:先调好 P,让系统稳定,再一点点加 I,每次加完跑一次阶跃响应,看超调量。
3.3 微分 D:提前踩刹车
微分看的是误差的变化率。误差在快速增大?微分项给出反向力,提前抑制。说白了就是「预判」。
核心公式: u(t) = Kd × de(t)/dt
Kd 是微分增益。
微分对噪声极其敏感。我做过一个激光雷达导航的项目,测距数据本身就有抖动,加上微分后,控制输出高频震荡,电机嗡嗡响。后来加了低通滤波才解决。所以我的建议是:传感器噪声大的场景,D 要慎用,或者干脆不用。
| 参数 | 作用 | 副作用 | 典型应用场景 |
|---|---|---|---|
| P | 快速响应当前误差 | 稳态误差 | 所有场景的基础 |
| I | 消除稳态误差 | 积分饱和、超调 | 高精度定位、恒速控制 |
| D | 抑制超调、提高响应 | 放大噪声 | 平滑轨迹、低噪声环境 |
3.4 位置式 PID 与增量式 PID
数字 PID 有两种实现方式,我刚开始学的时候也搞混过。这里直接说区别:
- 位置式 PID:直接计算完整的控制量 u(k)。需要累加所有历史误差,容易积分饱和。
- 增量式 PID:只计算控制量的变化量 Δu(k)。不需要累加,输出是增量,天然抗积分饱和。
我的选择原则:执行器是比例阀、伺服电机这类需要绝对位置的,用位置式;步进电机、PWM 占空比这类只关心变化量的,用增量式。增量式还有一个好处——切换手动/自动模式时无冲击,因为输出是增量,不会突变。
3.5 数字 PID 的实现
把连续 PID 离散化,就得到数字 PID。这里给出最常用的两种实现代码。
位置式 PID
typedef struct {
float Kp, Ki, Kd;
float integral;
float prev_error;
float output_limit; // 输出限幅
} PID_Position;
float PID_Position_Calc(PID_Position *pid, float setpoint, float measurement) {
float error = setpoint - measurement;
// 比例项
float P = pid->Kp * error;
// 积分项(带限幅)
pid->integral += error;
if (pid->integral > pid->output_limit / pid->Ki)
pid->integral = pid->output_limit / pid->Ki;
if (pid->integral < -pid->output_limit / pid->Ki)
pid->integral = -pid->output_limit / pid->Ki;
float I = pid->Ki * pid->integral;
// 微分项
float D = pid->Kd * (error - pid->prev_error);
pid->prev_error = error;
// 合成输出
float output = P + I + D;
// 输出限幅
if (output > pid->output_limit) output = pid->output_limit;
if (output < -pid->output_limit) output = -pid->output_limit;
return output;
}
增量式 PID
typedef struct {
float Kp, Ki, Kd;
float error[3]; // e(k), e(k-1), e(k-2)
float output_limit;
} PID_Incremental;
float PID_Incremental_Calc(PID_Incremental *pid, float setpoint, float measurement) {
// 更新误差队列
pid->error[2] = pid->error[1];
pid->error[1] = pid->error[0];
pid->error[0] = setpoint - measurement;
// 计算增量
float delta = pid->Kp * (pid->error[0] - pid->error[1])
+ pid->Ki * pid->error[0]
+ pid->Kd * (pid->error[0] - 2*pid->error[1] + pid->error[2]);
// 增量限幅
if (delta > pid->output_limit) delta = pid->output_limit;
if (delta < -pid->output_limit) delta = -pid->output_limit;
return delta; // 外部累加这个增量到当前输出
}
注意:增量式 PID 返回的是 Δu,不是 u。外部调用需要维护一个全局变量累加:output += PID_Incremental_Calc(...)。我见过有人直接把增量当绝对输出用,结果系统完全失控。
3.6 PID 知识体系总览
下面这张图把 PID 的核心脉络串起来了,建议你保存下来,调参时对照着看。
这张图把 P、I、D 三个分支和两种实现方式的关系画清楚了。调参时记住一个原则:先 P 后 I 再 D。先把比例调稳,再用积分消除静差,最后用微分改善动态响应。别一上来三个参数一起调,那样你永远不知道谁在起作用。