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 的核心脉络串起来了,建议你保存下来,调参时对照着看。

PID 控制器知识体系 PID 控制器知识体系 PID 控制器 比例 P Kp × e(t) 积分 I Ki × ∫e(τ)dτ 微分 D Kd × de/dt 特性 快速响应当前误差 存在稳态误差 特性 消除稳态误差 注意积分饱和 特性 抑制超调 对噪声敏感 位置式 PID 输出绝对控制量 u(k) 增量式 PID 输出控制增量 Δu(k) 调参顺序:先 P → 再加 I → 最后加 D(如果需要)

这张图把 P、I、D 三个分支和两种实现方式的关系画清楚了。调参时记住一个原则:先 P 后 I 再 D。先把比例调稳,再用积分消除静差,最后用微分改善动态响应。别一上来三个参数一起调,那样你永远不知道谁在起作用。