PID控制原理:比例、积分、微分的作用与离散化实现
聊到电机控制,PID 是绕不开的核心。说实话,我入行头两年,觉得 PID 就是调三个数,没什么技术含量。直到有一次做伺服驱动器,电机在低速时抖得像筛子,我才意识到——不懂原理,调参就是瞎蒙。
这一章,咱们把 PID 的底裤扒干净。比例、积分、微分到底在干什么?怎么从连续公式变成单片机里跑的代码?位置式和增量式有什么区别?
连续域下的 PID 表达式
先看最经典的连续形式:
u(t) = Kp * e(t) + Ki * ∫e(τ)dτ + Kd * de(t)/dt
三个部分各司其职。我习惯用一个比喻来理解:
- 比例项——就像你开车时看到偏离车道,立刻打方向盘。反应快,但容易过头。
- 积分项——方向盘打正了车还在偏?那就持续加力,直到完全回正。能消除静差,但容易超调。
- 微分项——提前预判车的偏离趋势,在偏差变大之前就收油或补油。能抑制震荡,但对噪声敏感。
核心观点:比例决定响应速度,积分消除稳态误差,微分提供阻尼。三者配合,才能让电机又快又稳地到达目标位置。
比例作用:最直接的反馈
比例项的输出 = Kp × 当前偏差。偏差越大,输出越强。我在做云台电机时遇到过一个问题:Kp 调大了,电机响应快,但到位后总在目标值附近来回晃;Kp 调小了,晃是不晃了,但反应慢半拍。
为什么会这样?因为比例控制天生有静差。电机带负载时,需要一定的输出才能维持位置,而比例项只有在偏差存在时才有输出。这就导致——你永远到不了目标值,只能无限接近。
我的经验:纯比例控制适合对精度要求不高的场合,比如风机、水泵。但做伺服定位,必须加积分项。
积分作用:消除静差的利器
积分项累加过去的偏差。只要偏差存在,积分值就会一直增长,直到输出足够大,把电机推到目标位置。
但积分有个副作用——积分饱和。我在做电动工具项目时踩过这个坑:电机启动时偏差很大,积分项快速累积到上限。等电机到达目标位置时,积分值还卡在上限,导致输出迟迟降不下来,电机冲过头,来回震荡好几秒才稳定。
解决办法?限幅、积分分离、抗饱和算法。后面章节会细讲,这里先记住:积分不是越大越好。
微分作用:提前预判,抑制震荡
微分项看的是偏差的变化率。偏差变化越快,微分输出越大。说白了,它是在跟偏差的「趋势」作斗争。
我做过一个高速贴片机的运动控制,电机加减速时震荡特别严重。加了微分项之后,震荡明显被压住了。但微分对噪声极其敏感——传感器信号稍微有点毛刺,微分输出就会剧烈跳动。
注意:实际工程中,微分项通常要配合低通滤波使用。否则,高频噪声会被微分放大,反而让系统更不稳定。
PID 的离散化:从连续到数字
单片机不认识连续积分和微分。我们需要把公式变成离散的、可迭代的形式。
离散化的核心思路:
- 积分 → 累加求和:∫e(τ)dτ ≈ Ts × Σe(k)
- 微分 → 后向差分:de(t)/dt ≈ (e(k) - e(k-1)) / Ts
其中 Ts 是采样周期。采样周期怎么选?我一般遵循一个原则:采样频率至少是系统带宽的 10 倍。比如电机电流环带宽 1kHz,采样频率至少 10kHz。
离散后的位置式 PID 公式:
u(k) = Kp * e(k) + Ki * Ts * Σe(i) + Kd * (e(k) - e(k-1)) / Ts
为了方便,通常把 Ki 和 Kd 重新定义为离散域系数:
u(k) = Kp * e(k) + Ki_d * Σe(i) + Kd_d * (e(k) - e(k-1))
其中:
Ki_d = Ki * Ts
Kd_d = Kd / Ts
位置式 PID vs 增量式 PID
这是面试常考题,也是实际选型的关键。
| 对比项 | 位置式 PID | 增量式 PID |
|---|---|---|
| 输出含义 | 直接输出控制量(如占空比) | 输出控制量的增量(Δu) |
| 计算公式 | u(k) = Kp·e(k) + Ki·Σe + Kd·Δe | Δu(k) = u(k) - u(k-1) |
| 积分累积 | 需要单独累加偏差 | 隐含在输出累加中 |
| 积分饱和 | 容易发生,需额外处理 | 不易发生,天然抗饱和 |
| 执行器切换 | 切换时输出可能突变 | 切换平滑,无冲击 |
| 适用场景 | 位置环、速度环 | 电流环、步进电机控制 |
增量式 PID 的推导其实很简单:
Δu(k) = Kp * (e(k) - e(k-1)) + Ki * e(k) + Kd * (e(k) - 2*e(k-1) + e(k-2))
你看,公式里没有累加项 Σe。这意味着什么?就算你把 Δu 算错了,下一拍就能纠正回来。而位置式一旦积分累积出错,要花很长时间才能消化掉。
我的建议:做电流环控制,优先用增量式。做位置环,位置式更直观。但无论哪种,都要做输出限幅——这是保命措施。
两种 PID 的代码实现对比
下面是我在实际项目中用过的代码骨架,去掉了一些工程细节,保留核心逻辑。
位置式 PID:
typedef struct {
float Kp, Ki, Kd;
float integral;
float prev_error;
float out_max, out_min;
} PositionPID;
float PositionPID_Update(PositionPID *pid, float setpoint, float feedback) {
float error = setpoint - feedback;
pid->integral += error;
// 积分限幅
if (pid->integral > pid->out_max) pid->integral = pid->out_max;
if (pid->integral < pid->out_min) pid->integral = pid->out_min;
float output = pid->Kp * error
+ pid->Ki * pid->integral
+ pid->Kd * (error - pid->prev_error);
pid->prev_error = error;
// 输出限幅
if (output > pid->out_max) output = pid->out_max;
if (output < pid->out_min) output = pid->out_min;
return output;
}
增量式 PID:
typedef struct {
float Kp, Ki, Kd;
float error[3]; // e(k), e(k-1), e(k-2)
float out_max, out_min;
} IncrementalPID;
float IncrementalPID_Update(IncrementalPID *pid, float setpoint, float feedback) {
pid->error[2] = pid->error[1];
pid->error[1] = pid->error[0];
pid->error[0] = setpoint - feedback;
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]);
// delta 累加到外部变量,不做内部积分
return delta;
}
注意增量式的输出是 Δu,需要在外部累加:u += Δu。累加后的 u 再做限幅。
知识体系总览
下面这张图把本章的核心逻辑串起来了:
避坑指南
最后分享几个我踩过的坑,希望能帮你省点时间:
- 积分限幅一定要做。我曾经在位置式 PID 里忘了加积分限幅,电机启动时积分累积到几万,输出直接饱和,电机像疯了一样冲出去。嗯,那次差点把夹具撞坏。
- 微分项慎用。如果传感器噪声大,宁可不加微分,也别加一个没滤波的微分。否则系统会高频抖动,听起来像电机在「唱歌」。
- 采样周期要固定。离散化公式依赖固定的 Ts。如果采样周期不固定,PID 参数会「漂移」。我习惯用定时器中断来保证采样间隔一致。
- 增量式也要做输出限幅。虽然增量式不容易积分饱和,但累加后的 u 如果不限幅,一样会溢出。
好了,这一章的内容就到这里。PID 的原理和离散化是后续调参的基础,理解透了,后面调 Kp、Ki、Kd 才能有的放矢。