第9章

PID 工程实践

学完这一章你应该能

  • 写出一个能在真实项目里用的 PID:限幅、抗积分饱和、微分滤波、设定值权重、无扰切换;
  • 用前馈和串级结构改善控制效果;
  • 按模型整定参数(SIMC),或者用继电反馈法自动测出临界参数;
  • 根据现象(振荡、抖动、稳态误差)判断原因并调整;
  • 按清单检查一个控制回路能否上线。

前置知识:第6章 PID 原理、第8章 数字 PID 和采样周期。

教科书上的 PID 公式只有一行,可直接把它写进程序,接到真实设备上,往往会遇到这些问题:一上电输出就顶到满、温度冲过头好几十度、电机嗡嗡响、改一下设定值执行器猛地一抖……这一章逐个解决它们。本章所有数字都可以在页面里的 PID 实验台上复现。

9.1 一个能用的 PID 长什么样

$$u=\underbrace{K_p(b\,r-y)}_{\text{比例:设定值权重 }b}+\underbrace{K_i\int(r-y)\,dt}_{\text{积分:带抗饱和}}-\underbrace{K_d\frac{dy_f}{dt}}_{\text{微分:对滤波后的测量值}}+u_{ff},\qquad u\ \text{限幅到}\ [u_{\min},u_{\max}]$$

和课本公式相比,多了这几样东西:

改动解决什么问题详见
输出限幅 + 抗积分饱和执行器到头后积分继续累积,造成大超调9.2
微分作用于测量值、加低通滤波设定值突变时的微分冲击;噪声被放大9.3
设定值权重 \(b\)(或设定值斜坡)设定值阶跃时超调大9.4
前馈 \(u_{ff}\)反馈只能「事后补救」,已知的变化应该提前补偿9.5
无扰切换手动切到自动、改参数时输出跳变9.9

9.2 积分饱和与抗饱和

现象:设定值大幅改变时,误差很大,控制量很快顶到上限,执行器已经尽全力了,可积分项还在继续累积。等输出终于接近设定值、误差开始反号时,积分已经攒了一大堆,要很久才能「退」回来,于是输出冲过头很多,还可能来回振荡。这叫积分饱和(windup)。

真实后果:第13章的温度控制例子,目标 200 °C,没有抗饱和时会冲到 234.6 °C。对 3D 打印机喷头、锅炉、化学反应釜,这样的超调可能直接损坏设备。

常用的抗饱和方法:

  1. 条件积分(clamping):输出饱和时停止积分,但如果误差的方向会让积分往回退,就继续积分。简单可靠,本笔记的代码都用这种。
  2. 反算法(back-calculation):把「限幅前后的差」反馈给积分器,饱和时积分自动往回拉:
    $$\frac{dI}{dt}=K_ie+\frac{1}{T_t}(u-u_{\text{raw}})$$
    \(T_t\) 是跟踪时间常数,常取 \(\sqrt{T_iT_d}\) 或 \(T_i/2\) 左右。
  3. 积分限幅:直接限制积分项的大小。最粗糙,限值不好定。
  4. 用增量式 PID(第8章):对输出限幅就自然不会饱和。

条件积分不是在所有场合都最好:如果饱和只持续很短的时间,饱和期间完全停止积分,反而会让后面收尾变慢。第13章电机速度环的例子里,饱和只有 3 ms,条件积分的调节时间是 37 ms,反算法(\(T_t\approx T_i\))只要 16 ms。饱和时间长(升温、大行程定位)时,条件积分的效果很好。

在实验台上试:执行器限幅只有 ±1.5,PID 参数较大。先不勾「抗积分饱和」(超调约 44%),再勾上(超调约 2%)。

9.3 微分项的处理

在实验台上试:① 取消「微分作用于测量值」,看右图设定值阶跃时 u 的尖峰(可达 200 以上,被限幅截断);② 恢复后,把测量噪声加到 0.01,看控制量抖动;③ 勾上「微分低通滤波」,抖动明显减小。

9.4 设定值处理

设定值阶跃时超调大,但又不想降低增益(降低增益会让抗扰动变差),可以只改变「设定值进入控制器的方式」:

三阶对象、Z–N 参数下:\(b=1\) 时超调 52.7%,\(b=0.5\) 时 17.0%,\(b=0\) 时 2.7%(上升时间从 1.5 s 变长到 3.1 s)。在实验台上拖动「设定值权重 b」验证,并勾上「中途加负载扰动」,看扰动响应不随 \(b\) 变化。

9.5 前馈

反馈控制要等误差出现了才动作,永远是「事后补救」。如果某个变化是事先知道或者可以测量的,就应该直接补偿它,这就是前馈。前馈不在回路里,不影响稳定性,剩下的误差再由反馈收拾。

判断要不要加前馈:扰动大、能测量、反馈回路又慢的时候,前馈最有用。前馈只能补偿你知道的东西,模型不准时补偿不完全,所以总是和反馈一起用。

9.6 串级控制

两个(或多个)回路套起来:外环的输出作为内环的设定值。

串级的好处:内环把它包住的那部分扰动和非线性就地消掉(比如电机的反电动势、执行器的非线性),外环面对的是一个更简单、更「听话」的对象。

设计规则:

  • 内环要比外环快得多,带宽一般差 3~10 倍;
  • 先整定内环,再整定外环;整定外环时把内环近似看成一个增益为 1 的一阶惯性环节;
  • 内环的采样频率也要更高;
  • 外环的输出(内环的设定值)要限幅,比如限制最大速度、最大电流。

第13章的电机案例给出一个具体的串级设计。

9.7 参数整定方法

方法一:先测模型,再用公式(推荐)

大多数过程对象(温度、液位、压力、电机速度)的阶跃响应都是「先延迟一会儿,再像一阶惯性环节那样上升」,可以近似成一阶加纯延迟(FOPDT)模型:

$$G(s)\approx\frac{K}{Ts+1}e^{-Ls}\qquad K:\text{稳态增益},\ T:\text{时间常数},\ L:\text{纯延迟}$$

做一次阶跃测试就能得到 \(K\)、\(T\)、\(L\),方法见第10章。然后用 SIMC 整定规则(Skogestad):

$$K_p=\frac{T}{K(\tau_c+L)},\qquad T_i=\min\big(T,\ 4(\tau_c+L)\big),\qquad K_i=\frac{K_p}{T_i}$$

\(\tau_c\) 是你希望的闭环时间常数,默认取 \(\tau_c=L\):响应较快,同时有足够的鲁棒性。想更稳就取大一些。对积分型对象 \(\dfrac{K'}{s}e^{-Ls}\)(比如液位、电机转角):\(K_p=\dfrac{1}{K'(\tau_c+L)}\),\(T_i=4(\tau_c+L)\)。

例 对象 \(\dfrac{e^{-s}}{5s+1}\)(\(K=1\),\(T=5\),\(L=1\)),取 \(\tau_c=1\):\(K_p=\dfrac{5}{1\times2}=2.5\),\(T_i=\min(5,8)=5\),\(K_i=0.5\)。闭环超调 4.2%,调节时间约 6 s。作为对比,Z–N 阶跃响应法(\(K_p=0.9T/(KL)\),\(T_i=3.33L\))给出的 PI 参数超调 55%。

方法二:Z–N 法(了解即可)

Z–N 法有两种:第6章讲的临界比例度法,和这里的阶跃响应法(\(K_p=1.2T/(KL)\)、\(T_i=2L\)、\(T_d=0.5L\) 给 PID)。它们的目标是「衰减比 4:1」,参数很激进,超调常在 50% 以上,现在一般只当作起点。

方法三:继电反馈自整定

想测临界增益,又不想冒险把系统调到等幅振荡?用继电反馈(Åström–Hägglund):把控制器临时换成一个开关,误差为正时输出 \(+d\),为负时输出 \(-d\)。大多数对象会自动进入一个稳定的小幅振荡:

$$K_u\approx\frac{4d}{\pi a},\qquad T_u=\text{振荡周期}$$

\(a\) 是输出振荡的幅值。原理是第12章的描述函数法。例:三阶对象 \(\frac{1}{(s+1)^3}\)、\(d=1\),仿真得到 \(a=0.163\)、\(T_u=3.68\) s,估计 \(K_u=7.80\)(真值 8,\(T_u\) 真值 3.63 s),误差约 2%。得到 \(K_u\)、\(T_u\) 后再用 Z–N 表或更保守的规则(比如 Tyreus–Luyben:PI 取 \(K_p=K_u/2.2\),\(T_i=2.2T_u\))。

很多工业温控器的「自整定」按钮,以及 3D 打印机固件的 PID 自整定(比如 Marlin 的 M303 命令),都是这个原理:先让加热器在目标温度附近开关几个周期,测出振荡幅度和周期,再算 PID 参数。

方法四:现场手动调

没有模型、也不能做实验时:先 P 后 I 再 D(第6章)。调的时候每次只改一个参数,改动幅度按倍数来(比如 ×2 或 ×0.5),记录每次的响应。

9.8 看现象找原因

现象可能的原因怎么办
振荡越来越大增益太高;回路里延迟太大;采样太慢减小 \(K_p\)、\(K_i\);缩短采样和计算延迟
周期较长的大幅振荡,衰减很慢积分太强减小 \(K_i\)(加大 \(T_i\))
周期较短的振荡,频率接近某个机械共振比例或微分太强;机械共振减小 \(K_p\)、\(K_d\);加陷波滤波器
控制量高频抖动、执行器噪音大微分放大了噪声;测量噪声大微分滤波;减小 \(K_d\);检查传感器和接地
设定值一改,执行器猛地一抖微分冲击;比例项太强微分作用于测量值;设定值权重或斜坡
大幅改变设定值时冲过头很多积分饱和抗积分饱和;设定值斜坡
一直有稳态误差没有积分;积分被限幅卡住;执行器饱和;死区加积分;检查限幅;执行器是否够用
在设定值附近小幅来回振荡(极限环)死区、间隙、静摩擦、量化见第12章:减小积分、加死区补偿、提高分辨率
响应太慢增益太低;执行器限幅太小;设定值斜坡太缓逐步加大 \(K_p\);检查执行器能力

9.9 完整实现

下面的 C 代码实现了本章的所有要点:并联形式、设定值权重、微分作用于测量值并滤波、输出限幅、条件积分抗饱和、无扰切换。Python 版本的逻辑完全一样,已在仿真中验证(本页的实验台用的也是同样的算法)。

typedef struct {
    float kp, ki, kd;      /* 并联形式的增益 */
    float b;               /* 设定值权重:比例项用 b*r - y,取 0~1 */
    float tf;              /* 微分滤波时间常数 [s],0 表示不滤波 */
    float umin, umax;      /* 输出限幅 */
    float ts;              /* 采样周期 [s] */
    float integral;        /* 以下是内部状态 */
    float d_filt;
    float prev_y;
    int   first;
} PidCtrl;

void pid_init(PidCtrl *p, float kp, float ki, float kd, float ts, float umin, float umax)
{
    p->kp = kp; p->ki = ki; p->kd = kd; p->ts = ts;
    p->umin = umin; p->umax = umax;
    p->b = 1.0f; p->tf = 0.0f;
    p->integral = 0.0f; p->d_filt = 0.0f; p->prev_y = 0.0f; p->first = 1;
}

/* 每个采样周期调用一次:r 为设定值,y 为测量值,返回控制量 */
float pid_update(PidCtrl *p, float r, float y)
{
    float e = r - y;
    float d_raw, u_raw, u;

    /* 微分作用于测量值(设定值突变时不产生尖峰),并做一阶低通滤波 */
    d_raw = p->first ? 0.0f : -(y - p->prev_y) / p->ts;
    p->prev_y = y;
    p->first = 0;
    if (p->tf > 0.0f)
        p->d_filt += p->ts / (p->tf + p->ts) * (d_raw - p->d_filt);
    else
        p->d_filt = d_raw;

    u_raw = p->kp * (p->b * r - y) + p->ki * p->integral + p->kd * p->d_filt;

    /* 输出限幅 */
    u = u_raw;
    if (u > p->umax) u = p->umax;
    if (u < p->umin) u = p->umin;

    /* 抗积分饱和(条件积分):没有饱和,或者误差会让积分往回退时,才继续积分 */
    if (u == u_raw || (u_raw > p->umax && e < 0.0f) || (u_raw < p->umin && e > 0.0f))
        p->integral += e * p->ts;

    return u;
}

/* 手动切到自动时调用,让自动模式的第一个输出等于当前手动输出(无扰切换) */
void pid_bumpless(PidCtrl *p, float r, float y, float u_manual)
{
    p->prev_y = y;
    p->first = 0;
    p->d_filt = 0.0f;
    if (p->ki != 0.0f)
        p->integral = (u_manual - p->kp * (p->b * r - y)) / p->ki;
}

在单片机里的用法(read_speed、set_motor_voltage 换成你自己的硬件函数):

static PidCtrl speed_pid;
static volatile float speed_ref = 0.0f;   /* 设定值,可以由其他任务修改 */

void control_init(void)
{
    /* 1 kHz 采样,输出 ±12 V */
    pid_init(&speed_pid, 0.05f, 2.0f, 0.0f, 0.001f, -12.0f, 12.0f);
    speed_pid.tf = 0.002f;
}

/* 定时器中断,每 1 ms 进来一次:读测量 → 算控制量 → 输出,顺序和周期都要固定 */
void timer_1ms_isr(void)
{
    float w = read_speed();
    float u = pid_update(&speed_pid, speed_ref, w);
    set_motor_voltage(u);
}

上面的增益只是示意,实际要按你的电机整定(第13章)。改参数时最好先调用 pid_bumpless,避免输出跳变。

9.10 上线前检查清单

本章小结

  • 能用的 PID = 课本公式 + 限幅与抗饱和 + 微分作用于测量值并滤波 + 设定值处理 + 无扰切换。
  • 前馈补偿已知或可测的变化,不影响稳定性;串级用快的内环消掉局部扰动,内环带宽是外环的 3~10 倍。
  • 整定优先「测 FOPDT 模型 + SIMC」(\(\tau_c=L\));继电反馈法可以安全地测出 \(K_u\)、\(T_u\);Z–N 参数偏激进。
  • 按现象找原因:快振荡看增益和延迟,慢振荡看积分,抖动看微分和噪声,小幅极限环看死区和摩擦。

练习

1. 加热炉从室温升到 800 °C,PI 控制,每次升温都冲过头 50 °C,到了稳态却很稳。最可能的原因是什么?

积分饱和:升温初期误差很大,加热功率一直是 100%,积分在这段时间里不停累积。稳态时没有饱和,所以表现正常。解决:加抗积分饱和,或者设定值按斜坡上升。

2. 阶跃测试得到 \(K=2\)(°C/%),\(T=120\) s,\(L=15\) s。用 SIMC(\(\tau_c=L\))给出 PI 参数。

\(K_p=\dfrac{120}{2\times30}=2\) %/°C;\(T_i=\min(120,\ 4\times30)=120\) s;\(K_i=K_p/T_i\approx0.0167\) %/(°C·s)。

3. 设定值权重 \(b\) 从 1 改成 0.5,对负载扰动的响应会变吗?为什么?

不变。\(b\) 只出现在 \(b\,r\) 这一项里,扰动不经过 \(r\),扰动引起的误差响应只由 \(K_p\)、\(K_i\)、\(K_d\) 决定。这正是二自由度结构的好处。

4. 继电反馈测试:继电器幅值 \(d=20\%\),温度振荡幅值 \(a=2\) °C,周期 \(T_u=90\) s。估算 \(K_u\),并用 Z–N 表给出 PID 参数。

\(K_u=\dfrac{4\times20}{\pi\times2}\approx12.7\) %/°C。Z–N PID:\(K_p=0.6K_u\approx7.6\),\(T_i=0.5T_u=45\) s,\(T_d=0.125T_u\approx11\) s。温度对象通常建议更保守一些,比如把 \(K_p\) 再减半。

5. 电机位置控制用串级结构,速度环带宽 50 Hz,位置环带宽大约选多少合适?

位置环比速度环慢 3~10 倍,取 5~15 Hz 左右。位置环太快,内环来不及跟上,等效对象就不再是简单的一阶环节,容易振荡。