PID 控制:从基本原理到嵌入式实现

7467 字
37 分钟
PID 控制:从基本原理到嵌入式实现

假设手边有一块加热台,想让它停在 60 ℃。最直接的办法是低于 60 ℃就开,到了就关。程序确实好写,但断电以后,加热片和底板里的热量还会继续传到测温点,温度往往还要往上走一截。等温度掉下来再开,又开始下一轮。

换成 PWM 呢?给一个固定占空比,确实可能在某个温度附近停住。可一开风扇,或者放上一个冷工件,刚才合适的占空比就不够了。

PID 就是在处理这件事:温度还差多少、这个差距持续了多久、现在升温有多快,合起来决定下一次给多少功率。下面一直用这个加热台作例子,公式和代码也按同一套单位来写。响应曲线里会标明哪些来自厂商资料,哪些是数值仿真;这里没有把仿真当成实测。

1. 应用范围与控制目标#

1.1 什么时候适合用 PID#

PID 适合这样的闭环任务:有可测的结果、有可以连续或分档调节的执行量,目标会变化,外界也会来捣乱。温控只是其中一种,电机转速、液位、气压和电流环也都符合这个模式。它不是“让设备立刻到目标”的魔法公式;对有明显延迟和热惯性的加热台,首要目标通常是不过冲、稳得住,并能在负载变化后回到设定温度。

1.2 本文的例子和约定#

本文以低压直流加热台为例:温度用 ℃ 表示,控制输出归一化到 0~1,对应 PWM 占空比。先看闭环里每个量的角色,再解释 P、I、D 的取舍,最后落到定时任务、代码、整定和保护上。

2. 闭环控制基础与性能指标#

2.1 先理解闭环:PID 到底控制什么#

以低压直流加热台为例:

符号含义加热台中的实例
r(t)r(t)设定值,setpoint60 ℃
y(t)y(t)测量值,process variable传感器测得的温度
e(t)e(t)误差目标温度减去测量温度
u(t)u(t)控制量经过限幅的 PWM 占空比
G(s)G(s)被控对象的动态模型加热功率到温度的关系

定义误差为:

e(t)=r(t)−y(t)e(t)=r(t)-y(t)

PID 闭环控制框图,包含误差计算、PID、限幅执行器、被控对象和传感器反馈
PID 闭环控制框图,包含误差计算、PID、限幅执行器、被控对象和传感器反馈

图 1:本文绘制的负反馈框图。控制器期望输出与执行器实际输出应区分,后者可能受到占空比、电流或电压限制。

顺着图走一圈:目标是 60 ℃,传感器读到 50 ℃,相减得到 10 ℃的误差;PID 算出占空比,功率级开始加热,下一次再读温度。开风扇以后,温度下降会重新变成误差,控制器就有了加大输出的依据。

本文约定占空比越大,温度越高。如果接的是制冷器,方向就反过来了。上电调参前可以先给一点输出,看读数往哪边走;方向接反时,误差会越调越大。

3. P、I、D 原理及响应对比#

3.1 比例 P:对当前误差直接作出反应#

uP(t)=Kpe(t)u_P(t)=K_p e(t)

先只留 P。若 Kp=0.02 ∘ ⁣C−1K_p=0.02\ {^\circ\!\mathrm C}^{-1},差 10 ℃就给 20% 占空比,差 5 ℃就给 10%。快接近目标时,供热也跟着减小。

麻烦在于,加热台即使到了 60 ℃也一直在散热,维持温度还得供电。可纯 P 在误差为零时,输出也变成了零。没有额外的偏置或前馈,温度就可能停在目标下面一点,用这点误差换取保温所需的功率。

AVR221 原图中纯 P 控制的阶跃响应,最终测量值低于目标
AVR221 原图中纯 P 控制的阶跃响应,最终测量值低于目标

图 2:裁自 Microchip / Atmel AVR221,2016 年版第 5 页,Figure 1-3。红色虚线是目标,蓝线最后仍低一点。原图未标横轴单位,这里只比较曲线形状。

可以用一个简单模型说明。对单位负反馈、稳定且无积分的对象,若直流增益为 G(0)G(0),设定值为幅度 RR 的阶跃,且闭环稳定、没有饱和,则:

ess=R1+KpG(0)e_{\mathrm{ss}}=\frac{R}{1+K_pG(0)}

这个模型里,增大 KpK_p 能把残差压小。但加热和测温都有滞后,增益太大又容易来回振荡。所以看到“还差几度”时,不能一直加 P。上式也有适用条件;带积分的对象等其他系统,结论可能不同。

3.2 积分 I:把持续存在的误差积累起来#

uI(t)=Ki∫0te(τ) dτu_I(t)=K_i\int_0^t e(\tau)\,\mathrm d\tau

假如温度一直停在 58 ℃,P 的输出已经不再变了,I 还会把这 2 ℃的误差一拍一拍累加进去,慢慢把占空比抬高。到了 60 ℃,新增的积分变成零,之前攒下来的输出还在,可以继续保温。

例如误差持续为 2 ℃,Ki=0.005 (∘ ⁣C⋅s)−1K_i=0.005\ ({^\circ\!\mathrm C}\cdot\mathrm s)^{-1},那么积分输出每秒增加 0.01,即一个百分点的占空比。

AVR221 中 P、纯 I 和 PI 的响应对比
AVR221 中 P、纯 I 和 PI 的响应对比

图 3:AVR221 第 5 页,Figure 1-4。绿色 PI 曲线靠近目标,蓝色纯 I 曲线慢得多。这张原图省略了坐标刻度,只用来说明趋势。

前提是目标确实够得着,而且闭环能稳定下来。若加热器已经全功率,温度还是上不去,积分继续加也变不出更多功率,反而会留下一个很大的内部数值。后面单独处理它。

3.3 微分 D:对变化速度作出反应#

理想的误差微分形式为:

uD(t)=Kdde(t)dtu_D(t)=K_d\frac{\mathrm de(t)}{\mathrm dt}

比如温度只有 55 ℃,但升温速度很快,继续给大功率就可能冲过头。这时 D 可以先减一点输出。它用的是眼下的变化速度,传感器迟到的那部分信息仍然补不回来。

D 对噪声也很敏感。温度读数只跳了 0.1 ℃,若两次采样只隔 1 ms,差分算出来就是 100 ℃/s。控制器看见的“快速变化”可能全是噪声,所以实际使用时通常要给微分加滤波。

还有一个容易碰到的尖峰:目标从 40 ℃改成 60 ℃,实物温度还没动,误差已经跳了 20 ℃。直接对误差微分,就会产生 derivative kick。常见做法是把 D 改接到测量值上:

uD(t)=−Kddy(t)dtu_D(t)=-K_d\frac{\mathrm dy(t)}{\mathrm dt}

目标不变时,误差的变化就是测量值变化的相反数,两种写法相同。目标跳变时,测量微分不会直接响应这个跳变;P 项仍然会变。Control Guru把这两条信号的区别画得很清楚。

AVR221 中 P、D、PD 控制的阶跃响应
AVR221 中 P、D、PD 控制的阶跃响应

图 4:AVR221 第 6 页,Figure 1-5。PD 改变了过渡过程,最终仍和 P 留着相近的残差。D 并不负责提供恒定的保温功率。

3.4 为什么很多工程系统只用 PI#

先把 PI 调到能用,是个省事的起点。若超调和恢复速度已经能接受,就可以先不加 D。尤其当读数本身一直跳动时,加 D 很可能先得到一条抖得更厉害的 PWM 曲线。

只有确实还缺一点阻尼、测量信号也够干净时,再试带滤波的 D,并同时记录温度和输出。

3.5 连续 PID:先统一参数形式#

3.5.1 并联形式#

本文使用以下增益定义:

u(t)=ub+Kpe(t)+Ki∫0te(τ) dτ+Kdde(t)dtu(t)=u_b+K_pe(t)+K_i\int_0^t e(\tau)\,\mathrm d\tau +K_d\frac{\mathrm de(t)}{\mathrm dt}

ubu_b 是偏置或前馈基值。忽略偏置,并在零初始条件下取拉普拉斯变换,理想控制器为:

C(s)=Kp+Kis+KdsC(s)=K_p+\frac{K_i}{s}+K_ds

TI 实时控制参考手册中的并联 PID 原始框图,包含 P、I、D 三条支路
TI 实时控制参考手册中的并联 PID 原始框图,包含 P、I、D 三条支路

图 5:裁自 TI《Real-Time Control Reference Guide》,2021 年 10 月版第 24 页,Figure 8。P、I、D 各走一条支路,最后相加。原始 PDF。

图上的理想微分器在高频处会一直放大信号。实际常用的写法会多一个滤波时间常数:

C(s)=Kp+Kis+Kds1+TfsC(s)=K_p+\frac{K_i}{s}+\frac{K_ds}{1+T_fs}

TfT_f 决定微分支路的滤波强度,MathWorks 的 pid 文档使用的就是这套并联参数。注意上式仍对误差微分,后面的 C 代码则按前面讨论的方式改成测量微分。

3.5.2 标准形式与并联形式不能直接抄参数#

另一种常见表达是:

C(s)=Kp(1+1Tis+Tds)C(s)=K_p\left(1+\frac{1}{T_is}+T_ds\right)

在相同理想结构下:

Ki=KpTi,Kd=KpTdK_i=\frac{K_p}{T_i},\qquad K_d=K_pT_d

抄参数时尤其容易在这里弄反。“积分增益 2”和“积分时间 2 s”差别很大:增大 KiK_i,积分更快;增大 TiT_i,积分反而更慢。先看清调参界面或代码到底收哪一种。

如果输入误差单位为 ℃、输出为 0~1 的占空比,三项增益的量纲分别为:

参数单位
KpK_p∘ ⁣C−1{^\circ\!\mathrm C}^{-1}
KiK_i(∘ ⁣C⋅s)−1({^\circ\!\mathrm C}\cdot\mathrm s)^{-1}
KdK_ds/∘ ⁣C\mathrm s/{^\circ\!\mathrm C}

若把输出单位从 0~1 改为 0~100,保持同样控制作用时增益的数值也要相应换算。

3.6 从曲线看控制器的作用#

AVR221 中 P、PI、PID 三种控制器的响应对比
AVR221 中 P、PI、PID 三种控制器的响应对比

图 6:AVR221 第 6 页,Figure 1-6。原资料这组示例中,PI 消除了残差,PID 的超调更小。图中没有给出完整模型和增益,不能据此提取一组实物参数。

这种对比图很直观,但读完往往还有个问题:换个参数会怎样?为方便自己跑一遍,下面另选一个写得清楚的二阶对象:

G(s)=1(s+1)2⟺y¨+2y˙+y=uG(s)=\frac{1}{(s+1)^2} \quad\Longleftrightarrow\quad \ddot y+2\dot y+y=u

目标从 0 跳变为 1,全部初始状态为零。控制周期 Ts=0.01 sT_s=0.01\ \mathrm s,微分作用于测量值,滤波时间常数 Tf=0.05 sT_f=0.05\ \mathrm s。对象通过步长 0.001 s 的前向欧拉法积分,控制输出在两次更新之间保持不变。本组仿真不加输出限幅、噪声和额外纯延迟。

同一二阶对象上 P、PI、PID 控制器的单位阶跃响应对比
同一二阶对象上 P、PI、PID 控制器的单位阶跃响应对比

图 7:本文数值仿真,参数在图例中,和上一张厂商图不是同一组实验。

这次 P 的稳态值是 2/32/3,可以直接用前面的稳态误差公式核对。PI 把残差补上了,不过也带来了超调;再加合适的测量微分,过渡过程又变了。换一组增益,曲线的好坏顺序也可能改变。

看曲线时,除了多久升上去,还要看最高冲到哪里、多久才停稳、最后差多少。“2 秒稳定”要说清楚容差,比如进入目标 ±2% 后不再跑出去;否则两个人口中的“稳定”可能差很多。

加热台还得试一次放上冷工件,看看温度跌下去以后多久恢复。目标变化与负载变化走过的信号通道不同,MathWorks 的跟踪与抗扰示例专门比较了这件事。启动曲线好看,不一定代表受扰以后也恢复得好。

4. 离散化、采样与 C 实现#

4.1 位置式计算#

设采样周期为固定的 TsT_s,使用当前误差的后向矩形积分和一阶后向差分:

ek=rk−ykIk=Ik−1+KiTsekDk=KdTs(ek−ek−1)uk=ub+Kpek+Ik+Dk\begin{aligned} e_k&=r_k-y_k\\ I_k&=I_{k-1}+K_iT_se_k\\ D_k&=\frac{K_d}{T_s}(e_k-e_{k-1})\\ u_k&=u_b+K_pe_k+I_k+D_k \end{aligned}

每次算出来的都是完整输出,所以叫位置式。得到 1.2 也只能输出 100% 占空比,这一步限幅放在写 PWM 之前。

网上的代码有时只有 sum += ki * error,看起来少乘了一个周期,其实可能在初始化时就算进 ki 了。例如 Arduino PID Library 的 SetTunings()会把连续 KiK_i 乘上周期、KdK_d 除以周期。对照代码时要连初始化一起看。本文代码保留连续增益,在计算时显式带入 TsT_s。

4.2 增量式从哪里来#

将同一位置式在相邻时刻相减,偏置恒定时得到:

Δuk=Kp(ek−ek−1)+KiTsek+KdTs(ek−2ek−1+ek−2)uk=uk−1+Δuk\begin{aligned} \Delta u_k={}&K_p(e_k-e_{k-1})+K_iT_se_k\\ &+\frac{K_d}{T_s}(e_k-2e_{k-1}+e_{k-2})\\ u_k={}&u_{k-1}+\Delta u_k \end{aligned}

这个等价关系用的是同一套固定周期、理想误差微分、无饱和的公式。换成测量微分或加上滤波以后,先写出新状态方程,再相减,才能知道到底等价到哪一步。

增量式也要处理饱和。u[k-1] 存的是限幅前的计算值,还是上次实际给出去的值,会直接影响下一拍。Arm CMSIS-DSP 的 PID 文档给出了这种递推结构;其中 Ki、Kd 已按离散公式使用,移植时同样要先换算周期。

4.3 对测量微分并增加一阶滤波#

采用:

D(s)=−Kds1+TfsY(s)D(s)=-\frac{K_ds}{1+T_fs}Y(s)

用后向欧拉替代 ss,整理得到:

Dk=TfTf+TsDk−1−KdTf+Ts(yk−yk−1)D_k=\frac{T_f}{T_f+T_s}D_{k-1} -\frac{K_d}{T_f+T_s}(y_k-y_{k-1})

后面的 C 代码就用这一行更新 D。增大 TfT_f,曲线会平滑些,反应也会慢些。如果为了压住噪声不断加大滤波,最后可能把原本想要的微分作用也拖迟了。传感器和模拟前端已有的滤波延迟也算在环路里。

4.4 一份可移植的 C 实现#

AVR221 示例中定时调用、PID 函数与状态结构体的关系
AVR221 示例中定时调用、PID 函数与状态结构体的关系

图 8:AVR221 第 9 页,Figure 2-1。定时触发计算,参数和历史状态放进一个结构体。右侧文字在原 PDF 中已有重叠,裁图保留原样;这里主要看调用关系。

真正写程序时,PID 函数不需要知道 ADC 寄存器和 PWM 通道。外层按时送进目标与测量值,函数保留上一拍状态,返回一个输出即可。下面按这个分工写位置式 PID,D 用测量差分加一阶滤波,I 用条件积分。

integral 和 derivative 存的是已经乘过各自增益的输出分量。因此日志里把 P、I、D 加上偏置,就能和未限幅输出核对。

调用前检查输入是有限值,参数满足 Ts>0T_s>0、Tf≥0T_f\geq0、Ki≥0K_i\geq0,输出上下限顺序正确。这里仍按“输出越大,温度越高”来写;制冷等反向对象要统一处理控制方向。

pid.c
#include <stdbool.h>
typedef struct {
float kp, ki, kd;
float ts, tf;
float out_min, out_max;
float bias;
float integral;
float derivative;
float prev_y;
bool initialized;
} PID;
static float clampf(float x, float lo, float hi)
{
if (x < lo) return lo;
if (x > hi) return hi;
return x;
}
/* initial_output is the desired output at the transfer instant.
* A sufficiently large I term is allowed to cancel P during transfer.
*/
static void pid_reset(PID *p, float setpoint, float measured,
float initial_output)
{
const float u0 = clampf(initial_output, p->out_min, p->out_max);
p->integral = u0 - p->bias - p->kp * (setpoint - measured);
p->derivative = 0.0f;
p->prev_y = measured;
p->initialized = true;
}
static float pid_step(PID *p, float setpoint, float measured)
{
if (!p->initialized) {
pid_reset(p, setpoint, measured, p->bias);
}
const float error = setpoint - measured;
const float proportional = p->kp * error;
const float alpha = p->tf / (p->tf + p->ts);
p->derivative = alpha * p->derivative
- p->kd / (p->tf + p->ts) * (measured - p->prev_y);
p->prev_y = measured;
const float delta_i = p->ki * p->ts * error;
const float trial_i = p->integral + delta_i;
const float trial_u = p->bias + proportional
+ trial_i + p->derivative;
const bool pushes_high = trial_u > p->out_max && delta_i > 0.0f;
const bool pushes_low = trial_u < p->out_min && delta_i < 0.0f;
if (!pushes_high && !pushes_low) {
p->integral = trial_i;
}
const float raw_u = p->bias + proportional
+ p->integral + p->derivative;
return clampf(raw_u, p->out_min, p->out_max);
}

调用方式如下。这里的数值只是为了把 ℃、秒和 0~1 占空比接起来,实物要重新调参:

调用示例(与 pid.c 放在同一翻译单元)
static PID heater = {
.kp = 0.02f, .ki = 0.005f, .kd = 0.01f,
.ts = 0.1f, .tf = 0.3f,
.out_min = 0.0f, .out_max = 1.0f, .bias = 0.0f,
};
/* Call once after configuration and after the first valid measurement. */
static void heater_start(float target_c, float measured_c)
{
pid_reset(&heater, target_c, measured_c, 0.0f);
}
/* Call every 100 ms with a fresh, validated measurement.
* The returned duty is mapped to the hardware PWM compare register.
*/
static float heater_update(float target_c, float measured_c)
{
return pid_step(&heater, target_c, measured_c);
}

4.4.1 初始化与手自动切换#

假设手动模式下正在输出 30%,切自动时却一下跳到 80%,温度就会多冲一段。pid_reset() 用当前误差反算积分,让 P、I 和偏置之和先接上这 30%。下次调用仍会正常积一拍分,所以匹配的是切换瞬间。

这个选择更适合手动切自动。冷启动时,如果拿 0% 去匹配,它可能用负积分抵消初始 P,让升温慢下来。希望开机就体现 P 的作用,可以改成积分清零、D 清零,再把 prev_y 设成当前测量值。MathWorks 的无扰切换示例还展示了手动期间持续跟踪输出的做法。

还有个小细节:把 ki 设成零,只是停止新增积分,旧的 integral 仍会参与输出。想测试纯 P,记得同时处理这个旧状态。换参数、换方向时也一样。

4.4.2 不要把控制核心当成完整设备程序#

传感器断线返回什么?ADC 数据超范围以后关不关输出?控制任务多久没运行算故障?这些判断放在外层。尤其别把 NaN 直接传进来,它可能一路污染内部状态,普通的大小比较也拦不住它。

参数和状态尽量由同一个任务管理。串口调参时一次换完整组参数,避免刚换完 kp 就被控制中断打断,算了一拍新旧混用的结果。长时间关闭输出时,也别让积分在后台照常积着。

4.5 采样周期:不只是把代码放进定时器#

TI 原图中连续信号、采样器、离散控制器与保持器的关系
TI 原图中连续信号、采样器、离散控制器与保持器的关系

图 9:裁自 TI 参考手册第 16 页,Figure 5。右边的对象连续变化,左边的控制器只在离散时刻计算;Hold 表示两次更新之间保持控制输出。

一次控制更新,实际经历的是传感器响应、采样、计算、PWM 生效。PID 函数算得快,不代表这整条链就快。比如传感器每 200 ms 才刷新一次,主循环每 1 ms 算一次,大部分时候用的还是旧温度。

初步估算时,可以先让采样频率 fsf_s 比期望闭环带宽 fbf_b 高一个数量级,再把采样保持、滤波与计算延迟放进模型检查。这个比例只给起点,不能据此断言稳定。温控和电机电流环的时间尺度也差得很远。

环节要确认的问题
传感器数据是否真的在每次控制更新前刷新?
ADC 与前端是否同步采样,是否存在混叠或较大的滤波延迟?
控制任务周期是否稳定,最坏计算时间是否满足截止时间?
PWM控制更新和 PWM 载波是什么关系,比较值何时生效?
数据记录日志和通信是否阻塞了控制任务?

PWM 每秒开关多少次,和 PID 每秒更新多少次,是两件事。加热器可以使用较高频率的 PWM,而控制任务慢一些;具体周期仍要看对象和测量链路。

本例 ts=0.1,就每 100 ms 调用一次。如果实际每 10 ms 调用,程序还以为过了 100 ms,单位时间内的积分就加了十倍。若使用实测间隔,积分、微分和滤波系数要一起跟着变;任务严重超时则另作故障处理。

Microchip AN937 的定时中断流程原图,包含误差累积、积分限制和微分历史更新
Microchip AN937 的定时中断流程原图,包含误差累积、积分限制和微分历史更新

图 10:裁自 Microchip AN937,2004 年版第 6 页,Figure 4。它把累积误差和差分历史放进定时中断,适合对照早期整数实现的任务分工。

这张老图也值得带着问题看:它有 error = 0? 的提前返回分支。零误差时仍可能需要积分提供保温功率,测量值也可能仍在变化,所以不能把这个分支直接搬成“误差为零就输出零”,也不能因此漏掉通用实现里的状态更新。本文代码每次都更新历史值并计算输出。

5. 加热台硬件和整定#

5.1 真实硬件还需要哪些环节#

低压直流加热控制的 MCU、栅极驱动、MOSFET、加热负载、温度采样与独立保护功能连接图
低压直流加热控制的 MCU、栅极驱动、MOSFET、加热负载、温度采样与独立保护功能连接图

图 11:低压直流加热系统的功能连接示意。箭头表示信号或作用关系,不是完整导线连接,不能直接用于投板。

MCU 给的是逻辑信号,真正承受负载电流的是功率级。MOSFET 选型、驱动电压和散热要按负载来算。采样线如果贴着大电流回路走,读数可能跟着开关动作跳;传感器如果离加热片很远,读到的又是迟到的温度。两种情况都会让调参变得费劲。

图里的独立过温切断也要保留。传感器断线、程序卡住时,仍应有办法关掉加热输出。

5.2 先记录一次对象响应#

先给一个小幅、可控的开环输出变化,同时记下时间、占空比和温度。看温度多久开始动、大约多久接近新平衡、最终升了多少。对某些温控工况,可以先试着拟合成:

G(s)≈Kτs+1e−LsG(s)\approx\frac{K}{\tau s+1}e^{-Ls}

这里 KK 是局部静态增益,τ\tau 是时间常数,LL 是延迟。比如占空比加了 0.1,稳定温度升了 8 ℃,这个工况附近的 KK 约为 80 ℃/单位占空比。若曲线一直爬升、明显振荡,或不同工况差别很大,就要换更合适的模型。

5.3 一套有顺序的手动整定方法#

先关闭 I、D,清掉旧状态,从小 P 做一点目标变化。观察温度的同时也看占空比;若占空比一直打满,只看温度曲线很难分清是参数问题,还是设备本来就只能这么快。

P 增大到响应明显快起来以后,若开始超调或振荡,就退回一些。然后一点点加 I,把持续残差补掉。热过程有时很慢,改完要等足够久再判断,否则上一组参数的热量还没散完,下一组已经换上去了。

PI 能满足要求就先用 PI。还想压超调,再试带滤波的 D,比较加入前后的温度与输出抖动。最后试试冷工件、风扇、不同温度目标,以及长时间全功率后把目标调低。最后这一项,正好能检查抗饱和有没有实际起作用。

每次只改一类参数,把周期和曲线一起保存。否则调了半天,很容易只记得“刚才那组好像更好”。

5.4 如何看待 Ziegler–Nichols#

Ziegler–Nichols 能给出一组经验初值,NI 文档介绍了相关整定方法。用之前先看清是哪一种实验、哪一种 PID 参数形式,别拿到三个数字就填进去。

闭环临界比例法会把系统推到持续振荡附近,测临界增益和周期。加热台若不允许过温,就不适合硬做这种实验。得到初值后也要重新检查超调、噪声、饱和恢复和不同工作点。

6. 抗积分饱和、噪声与安全#

6.1 为什么限幅之后,还需要抗积分饱和#

把目标设得很高,加热器全功率跑了很久,温度还是差一截。积分每一拍都在增加,PWM 却早已卡在 100%。这时把目标调低,明明该减功率了,程序还可能继续全功率加热:之前攒下来的积分太多,一时减不掉。

这就是积分饱和,也称 integral windup。只写 u = clamp(u, 0, 1),并没有约束内部积分状态。

目标先不可达再降低时,仅限幅与条件积分抗饱和的输出和控制量对比
目标先不可达再降低时,仅限幅与条件积分抗饱和的输出和控制量对比

图 12:本文仿真。对象为 2y˙+y=u2\dot y+y=u,PI 参数 Kp=2K_p=2、Ki=1K_i=1,周期 0.01 s,输出范围 0~1。前 8 s 的目标是不可达的 1.5,之后降为 0.4。只限幅的那条曲线在目标降低后仍持续饱和,条件积分则更快退出。

6.2 条件积分和反算#

可以先试算一次。若输出已经超上限,本次积分还想往上加,就舍弃这次积分;超下限时反过来处理。若这次变化能把输出拉回范围内,就允许它减下去。代码里把候选值叫作 trial_i 和 trial_u,正好能对上这几步。

这种写法不难检查,不过限幅边界附近的行为还受每次积分步长影响。另外,软件以为自己输出了 80%,功率级可能已经因限流只能给出 40%;只看软件里的限幅就发现不了这件事。

另一类方法把饱和前后的差值反馈到积分器:

Ik=Ik−1+Ts[Kiek+Kaw(usat,k−uraw,k)]I_k=I_{k-1}+T_s\left[K_ie_k+K_{aw} \left(u_{\mathrm{sat},k}-u_{\mathrm{raw},k}\right)\right]

例如算出 1.4,限幅后只有 1.0,差值就是 −0.4;这条反馈会把积分往回拉。实现上先用旧积分求 uraw,ku_{\mathrm{raw},k},再限幅,最后更新积分,就不会互相等着对方的新值。KawK_{aw} 的单位是 s−1\mathrm s^{-1},越大拉回得越快,仍需按离散周期检查稳定性。

如果执行器另有电流环或限流器,最好把实际执行量反馈回来;有前馈时,还要分清哪一部分输出属于 PID。MathWorks 的抗饱和示例分别展示了条件积分、反算和外部跟踪,可以对着框图查。

6.3 噪声与独立保护#

微分支路和采样链路的滤波会带来相位滞后,不能只靠把 TfT_f 调大来压住输出抖动。先核对传感器刷新率、布线和测量噪声,再决定是否真的需要 D。无论控制器参数怎样,独立过温切断、传感器断线处理和输出故障默认态都应在 PID 外层生效。

7. 检查表、总结与资料#

7.1 常见故障与排查方向#

现象优先检查
一启动输出就迅速打满反馈方向、单位换算、初始状态、目标是否可达
纯 P 稳定但总差一点保持输出所需的偏置是否存在,是否需要 I 或前馈
升温越过目标后还继续大功率积分饱和、传感器滞后、热惯性、实际执行器是否跟随命令
输出出现高频抖动测量噪声、D 增益、微分滤波、ADC 与 PWM 的采样关系
改了定时周期后效果完全不同是否遗漏 TsT_s,或重复折算了离散增益
目标改变时控制量突然尖峰是否对误差直接微分,P 项阶跃是否也需要设定值斜坡
仿真稳定,实物却振荡模型漏掉的延迟、执行器动态、死区、采样滤波与负载变化
只在某些工作点稳定对象增益随工况变化、执行器非线性,是否需要分区整定

遇到振荡,先导出一段日志:目标、测量值、限幅前后输出、P/I/D 分量和时间戳。I 一直往上爬、D 跟着噪声跳、更新周期忽长忽短,会在这些数据里表现得很不一样。

7.2 本文小结#

对加热台而言,先确认方向、采样周期和硬件保护,再从 P 或 PI 起步,通常比直接套一组“PID 参数”更可靠。P 负责眼前误差,I 补偿持续残差,D 用来处理变化趋势但会放大噪声;当输出会饱和时,积分状态必须被单独管理。曲线、日志和实际保护逻辑都要一起验收。

7.3 想继续看,可以从这些资料入手#

本文原创图的生成脚本为仓库中的 scripts/generate-pid-figures.py,需要 Python、NumPy、Matplotlib 和中文字体。运行:

Terminal window
python scripts/generate-pid-figures.py

可以重建图 1、图 7、图 11、图 12。厂商图的 PDF 页码、裁切坐标和重建命令在配图来源说明见随文的 ./pid-control-guide/images/SOURCES.txt里;裁切脚本是 scripts/crop-pid-references.py。这些图片随文章保存在本地,阅读时不用等第三方图床。

参考资料#

  1. **先看整体:**TI,Real-Time Control Reference Guide,2021 年 10 月版。第 16 页把采样与保持画在一起,第 24~25 页对照并联、串联 PID。本文图 5、图 9 来自这份手册。
  2. **从公式走到小 MCU:**Microchip / Atmel,AVR221: Discrete PID Controller on tinyAVR and megaAVR devices,2558C,2016 年 9 月。第 5~6 页看响应,第 7~8 页看离散化,第 9~11 页看定点缩放、状态保存和积分限制。本文图 2~4、图 6、图 8 从这里裁取。
  3. **读一份完整的老式实现:**Chris Valenti / Microchip,AN937: Implementing a PID Controller Using a PIC18 MCU,2004 年。第 5~6 页的主程序与中断流程、第 7~10 页的时间基准和三项计算,适合对照阅读。它的整数实现和条件分支有特定背景,本文没有直接移植。图 10 来自第 6 页。
  4. **查参数的定义:**MathWorks,pid — PID controller in parallel form。拿到一组 Kp,Ki,Kd,TfK_p,K_i,K_d,T_f 时,可以先查这里,核对形式和离散方法。
  5. **查为什么要对测量微分:**Control Guru,PID Control and Derivative on Measurement。重点看目标跳变时,误差与测量值的差别。
  6. **查积分饱和:**MathWorks,Anti-Windup Control Using PID Controller Block。有反算、条件积分、级联执行器和前馈的例子,适合查实际输出该从哪里反馈。
  7. **查切换时为什么跳一下:**MathWorks,Bumpless Control Transfer Between Manual and PID Control。比较有无输出跟踪时,手动切自动的响应。
  8. **比较两种调参目标:**MathWorks,PID Tuning for Setpoint Tracking vs. Disturbance Rejection。能解释为什么目标跟得好,负载扰动后却恢复得慢。
  9. **对照一份常见库的源码:**Brett Beauregard,Arduino PID Library / PID_v1.cpp。重点看 Compute()、SetTunings()、SetSampleTime() 和 Initialize(),对应测量微分、周期换算、限幅和初始化。
  10. **准备使用 Cortex-M 库:**Arm,CMSIS-DSP PID Motor Control。给出了递推系数和 Q15、Q31、浮点接口;定点版本要继续查各自的溢出与缩放说明。
  11. **了解经典整定:**NI,Ziegler-Nichols Autotuning Method。看经验整定怎样给初值,再拿设备的实际约束筛选。

资料检索日期:2026-10-16。原创文字、代码和原创图采用 CC BY 4.0。TI、Microchip / Atmel 的原图只做局部裁切,权利归各自所有,不包含在本文的原创许可里。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

PID 控制:从基本原理到嵌入式实现
https://nekorain.xyz/posts/pid-control-guide/
作者
NekoRain
发布于
2026-10-16
许可协议
CC BY 4.0

评论区

Profile Image of the Author
NekoRain
永远相信美好的事情即将发生 Always believe that wonderful things are about to happen
公告
这里是小雨的模拟小窝,白天调电路,晚上写点温柔碎碎念。希望你来的时候像低噪声运放一样稳定,走的时候带一点点开心呀。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
28
分类
11
标签
89
总字数
224,088
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.12.1
文章许可
CC BY-NC-SA 4.0

文章目录