PID控制模拟

参考资料

几个控制参数

我们要分清“PID 控制器本身”和“整个闭环系统”

  • 对于 PID 控制器(你的代码逻辑),它其实只有一个核心输入:误差 $e(t)$
    • $e(t) = \text{Set Point} (目标值) - \text{Process Variable} (实际测量值)$。
    • 输出:控制量 $u(t)$(比如 PWM 的占空比)。
  • 对于整个仿真系统(闭环回路):为了模拟真实世界,仿真器通常有 3 个输入1 个输出
    • 输入 1:设定值 (Set Point, SP) —— 你希望温度达到的度数(如 $120^\circ\text{C}$)。
    • 输入 2:测量噪声 (Measurement Noise) —— 模拟传感器的抖动。
    • 输入 3:过程扰动 (Process Disturbance) —— 模拟外部环境的变化。
    • 输出:实际温度 (Process Variable, PV) —— 传感器最终读到的数值

为什么仿真器里会有“噪声”这个选项

噪声是 PID 算法(尤其是 D 项)的“天敌”。

  • 传感器的真实情况:你的 PT100 即使在恒温下,ADC 读数也会有 $\pm0.1^\circ\text{C}$ 的跳动。
  • D 项的放大效应:
    • $D$ 项是求导(变化率)。
    • 如果噪声让温度在 1 毫秒内跳动了 $0.1$ 度,导数就是 $0.1 / 0.001 = 100$!
    • 这会导致你的控制输出(PWM)像抽风一样剧烈抖动,即便温度其实很稳定。

所以在真实 CEMS 开发中,我们在把数据传给 PID 之前,必须先经过你思维导图里的“双重滤波算法”(算术平均或一阶滞后),这就是为了干掉噪声。

你在仿真器里尝试开启一点 Noise,然后观察:

  • 当你 $D$ 值很小时,曲线还算平滑。
  • 当你把 $D$ 调大,你会发现输出信号(通常是底下的那个条)变得非常杂乱。
  • 这就是为什么在工业现场,很多工程师宁愿只用 PI 控制,也不轻易开 D 的原因

“扰动 (Disturbance)”又是什么

噪声是“眼睛(传感器)”看花了,而扰动是“身体”被踢了一脚。

  • 场景:你的发热片正在稳稳地保持 $120^\circ\text{C}$。
  • 扰动:这时候采样泵突然开启,冷烟气快速流过,带走了热量;或者你打开了机箱风扇。
  • PID 的任务:它必须立刻感知到这个由于外部干扰导致的温度下降,并迅速加大功率补偿回来。

PID 网页仿真器

1
pid_sim.html

这个仿真器模拟了发热片控温过程:它有热惯性(升温慢)、有散热(自然冷却)、有噪声

如何使用这个仿真器学习?

当你打开这个网页后,请尝试以下“实验步骤”:

实验 1:感受“静差”

  1. 将 $K_i$ 和 $K_d$ 设为 0。
  2. 慢慢调大 $K_p$。你会发现蓝线(实际温度)永远追不上红虚线(目标温度)。
  3. 原理:因为散热的存在,当误差变小时,$K_p$ 产生的热量正好抵消了散热,温度就不再上升了。

实验 2:感受“积分的力量”

  1. 在实验 1 的基础上,慢慢调大 $K_i$。
  2. 你会看到蓝线开始“缓慢但坚定”地向红线靠拢,最终完美重合。
  3. 副作用:调大 $K_i$ 后,你会发现蓝线冲过了头(超调)。

实验 3:感受“噪声的恐惧”

  1. 保持一个稳定的 PID 参数。
  2. 调大 “环境噪声” 滑块。
  3. 此时再调大 $K_d$。你会发现蓝线变得非常抖动。
  4. 结论:在有噪声的环境下,$K_d$ 不能开太大,或者需要先对温度进行滤波。

从“网页仿真”到 C 语言的跨越

在 STM32 中,为了让代码整洁、可复用(比如你以后可能同时控制 3 路加热,每路都要一个 PID),我们通常会定义一个 结构体 (Struct)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/* PID 控制器结构体定义 */
typedef struct {
    float Target;           // 目标值 (Set Point)
    float Measured;         // 实际测量值 (Process Variable)
    
    float Kp;               // 比例系数
    float Ki;               // 积分系数
    float Kd;               // 微分系数
    
    float Error;            // 当前误差
    float LastError;        // 上一次误差
    float Integral;         // 误差累积值 (积分项)
    
    float OutputMax;        // 输出限幅最大值 (如 100% PWM)
    float OutputMin;        // 输出限幅最小值 (如 0%)
    float IntegralMax;      // 积分限幅 (防止积分饱和)
    
    float Output;           // 最终计算出的控制输出
} PID_Controller;

/* PID 计算函数原型 */
void PID_Compute(PID_Controller *pid) {
    // 1. 计算当前误差
    pid->Error = pid->Target - pid->Measured;
    
    // 2. 计算积分 (累加误差)
    pid->Integral += pid->Error;
    
    // 3. 积分限幅 (抗饱和逻辑,这就是我们在网页里讨论过的)
    if (pid->Integral > pid->IntegralMax) pid->Integral = pid->IntegralMax;
    if (pid->Integral < -pid->IntegralMax) pid->Integral = -pid->IntegralMax;
    
    // 4. 计算输出 = P + I + D
    pid->Output = (pid->Kp * pid->Error) + \
                  (pid->Ki * pid->Integral) + \
                  (pid->Kd * (pid->Error - pid->LastError));
    
    // 5. 输出限幅 (防止 PWM 溢出)
    if (pid->Output > pid->OutputMax) pid->Output = pid->OutputMax;
    if (pid->Output < pid->OutputMin) pid->Output = pid->OutputMin;
    
    // 6. 保存本次误差,供下次微分计算使用
    pid->LastError = pid->Error;
}

为什么这段 C 代码比网页版更“高级”

  • 面向对象思想:通过 PID_Controller 结构体,你可以轻松创建 Heater_PIDPump_PID 等多个实例。
  • 抗饱和机制:专门加入了 IntegralMax,防止系统在长时间达不到目标时产生的“失控”现象。
  • 指针传递:使用 *pid 指针传递,运行效率极高,符合 STM32 这种嵌入式环境的要求。

PID 控制注意事项

积分饱和—— 最隐蔽的“定时炸弹”

  • 现象:你的目标是 $120^\circ\text{C}$,但此时发热片断电了。PID 发现温度没升,积分项($I$)就会疯狂累积。当电源恢复时,累积的巨大积分值会让加热器保持 $100%$ 功率输出,直到温度冲到 $200^\circ\text{C}$ 都降不下来。
  • 解决方案:积分限幅。给 Integral 设一个上限和下限(比如最大只允许占输出的 $20%$)。

微分突变—— “瞬间心梗”

  • 现象:当你突然把目标温度从 $100^\circ\text{C}$ 改为 $150^\circ\text{C}$ 时,误差 $e(t)$ 瞬间产生了一个巨大的台阶。因为微分项 $D$ 计算的是误差的变化率,这会导致输出 $u(t)$ 瞬间产生一个巨大的尖峰脉冲。
  • 后果:这个瞬间电流脉冲可能击穿你的 MOSFET 或导致电源保护性重启。
  • 解决方案:微分先行 。在计算 $D$ 项时,不计算误差的变化,而是计算测量值(PV)的变化。因为温度本身不会瞬间跳变,这样输出就会平滑得多。

采样周期不固定 —— “算法的基石动摇”

  • 现象:PID 的公式是基于时间的 $dt$。如果你在 while(1) 循环里随意调用 PID 函数,循环里其他代码执行时间的变化(比如 Modbus 响应久了点)会导致 $dt$ 不稳定。
  • 后果:$I$ 和 $D$ 的计算逻辑会完全乱套,系统产生不可预测的震荡。
  • 解决方案:定时中断。必须把 PID 计算放在 STM32 的硬件定时器中断里(比如固定每 $100\text{ ms}$ 执行一次)。

传感器断线或短路 —— “逻辑致盲”

  • 现象:CEMS 现场震动大,PT100 探头可能脱落。
  • 后果:如果探头断了,ADC 读数可能变为 0 或最大值。PID 以为温度极低,会疯狂加热,导致发热片烧毁甚至起火。
  • 解决方案:安全保护逻辑。在 PID 运行前加一行:if (temp < -10 || temp > 300) { Stop_All_Heaters(); }

进阶版 PID 代码框架(带坑位修复)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void PID_Compute_Advanced(PID_Controller *pid) {
    float error = pid->Target - pid->Measured;
    
    // 1. 比例项
    float P_out = pid->Kp * error;
    
    // 2. 积分项 + 抗饱和限幅 (Anti-Windup)
    pid->Integral += pid->Ki * error;
    if (pid->Integral > pid->IntMax) pid->Integral = pid->IntMax;
    if (pid->Integral < -pid->IntMax) pid->Integral = -pid->IntMax;
    
    // 3. 微分项 (采用微分先行:计算测量值的变化,避免目标值改变时的冲击)
    float D_out = pid->Kd * (pid->LastMeasured - pid->Measured); // 注意这里是 PV-PV
    
    // 4. 总输出
    pid->Output = P_out + pid->Integral + D_out;
    
    // 5. 输出限幅
    if (pid->Output > 100) pid->Output = 100;
    if (pid->Output < 0) pid->Output = 0;
    
    // 保存状态
    pid->LastMeasured = pid->Measured;
}

安全监控逻辑

例如,如果功率输出 $100%$ 持续了 5 分钟,温度还没上升 $5^\circ\text{C}$,这说明什么?(可能是发热片坏了或探头掉了)。在这种情况下,程序应该怎么做?(报警并切断电源)。

在工业现场,一套成熟的控制系统,安全监控逻辑的代码量往往比 PID 核心算法还要多。对于 CEMS 这种需要 24 小时无人值守的设备,必须假设“硬件总会坏”,并用软件筑起最后一道防线。

工业级安全监控逻辑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
typedef enum {
    SAFE_OK = 0,
    ERR_SENSOR_FAULT,    // 传感器断线或短路
    ERR_HEATER_BROKEN,   // 发热片坏了(有输出但不升温)
    ERR_THERMAL_RUNAWAY, // 热失控(没输出但温度狂飙,可能是固态继电器击穿短路)
    ERR_OVER_TEMP        // 极高温度保护
} SafetyStatus;

typedef struct {
    float LastTemp;
    uint32_t PowerOnTime;      // 满功率持续计数器
    uint32_t RunawayTime;      // 热失控计数器
    SafetyStatus Status;
    uint8_t  SafetyCutoff;     // 1: 切断输出, 0: 正常
} Safety_Monitor;

/**
 * @brief 工业级安全逻辑检查
 * @param monitor 安全结构体
 * @param currentTemp 实际温度
 * @param currentOutput 当前PID输出占空比 (0-100)
 */
void Safety_Check(Safety_Monitor *monitor, float currentTemp, float currentOutput) {
    // 1. 传感器有效性硬检查 (Hard Limit)
    // PT100 断线通常读出极大值或极小值
    if (currentTemp < -20.0f || currentTemp > 250.0f) {
        monitor->Status = ERR_SENSOR_FAULT;
        monitor->SafetyCutoff = 1;
        return;
    }

    // 2. 极高温度立即切断 (防止起火)
    if (currentTemp > 180.0f) {
        monitor->Status = ERR_OVER_TEMP;
        monitor->SafetyCutoff = 1;
        return;
    }

    // 3. 堵转/发热片损坏检测 (Stall Detection)
    // 如果连续 60 秒满功率输出,温度上升却不足 2°C,判定为发热片损坏或环境极寒
    if (currentOutput > 95.0f) {
        monitor->PowerOnTime++;
        if (monitor->PowerOnTime > 600) { // 假设 100ms 调用一次,600即60秒
            if (currentTemp - monitor->LastTemp < 2.0f) {
                monitor->Status = ERR_HEATER_BROKEN;
                monitor->SafetyCutoff = 1;
            }
            monitor->PowerOnTime = 0; // 重置检查周期
            monitor->LastTemp = currentTemp;
        }
    } else {
        monitor->PowerOnTime = 0;
    }

    // 4. 热失控检测 (Runaway Detection) - 非常重要!
    // 如果 PID 输出为 0,但温度仍在以异常速度持续上升,判定为驱动管(MOSFET)击穿短路
    if (currentOutput < 1.0f && currentTemp > 50.0f) {
        if (currentTemp - monitor->LastTemp > 5.0f) { // 没加热还 60s 涨 5度
             monitor->RunawayTime++;
             if (monitor->RunawayTime > 600) {
                 monitor->Status = ERR_THERMAL_RUNAWAY;
                 monitor->SafetyCutoff = 1;
             }
        }
    } else {
        monitor->RunawayTime = 0;
    }
}

为什么这才是工业级?

  • 容错时间窗口:它不是一发现异常就报错,而是观察一段时间(如 60 秒),避开了传感器抖动或冷风吹过导致的干扰。
  • 全链路覆盖:它不仅管传感器,还通过“能量守恒”逻辑监控 MOSFET 和发热片的健康。
  • 状态机思维:它将错误分类,方便你在串口屏(HMI)上精准弹出错误代码(如:E01: 传感器断线E03: 固态继电器击穿)。

首次调试

真实硬件有热惯性(升温后停不下来)和非线性(电压波动),不像仿真器那么听话。建议采用“渐进式经验法”(也叫试凑法)。不要去算复杂的数学公式,按照下面的步骤操作,既安全又能快速见效:

第一步:安全第一(调试前的硬限制)

在写下第一行 PID_Compute 之前,先在代码里做两个限制,防止把发热片烧红:

  1. 输出限幅:将 OutputMax 先设为 50%(比如 PWM 是 1000,先限制在 500)。等调稳了再放开。
  2. 采样时间固定:确保你的 PID 函数是在定时器中断(如 100ms 一次)里跑的,这比什么都重要。

第二步:确定比例系数 $K_p$(找“劲头”)

目标:让温度动起来,允许有差距,但不能震荡。

  1. 将 $K_i$ 和 $K_d$ 设为 0
  2. 从一个很小的 $K_p$ 开始(比如 0.11.0,取决于你的量程)。
  3. 观察温度曲线:
    • 现象 A:温度升得很慢,离目标值还差十万八千里就不动了。—— 调大 $K_p$
    • 现象 B:温度猛冲,过了目标值还在狂跳(震荡)。—— 调小 $K_p$
  4. 最终状态:让温度能快速上升到目标值的 80%~90% 左右,曲线平滑,不产生剧烈来回摆动

第三步:确定积分系数 $K_i$(消“静差”)

目标:把最后那点“够不着”的温差磨平。

  1. 保持刚才调好的 $K_p$,开始加入微小的 $K_i$(比如从 0.01 开始)。
  2. 观察曲线:
    • 现象 A:温度在 $K_p$ 停下的位置慢悠悠地往目标值蹭,蹭得很久才到。—— 稍微调大 $K_i$
    • 现象 B:温度很快到了目标值,但直接“冲过了头”(超调),然后反复好几次才稳住。—— 调小 $K_i$
  3. 最终状态:温度能稳稳地停在目标线上,误差为 0。

第四步:确定微分系数 $K_d$(加“刹车”)

注意:对于发热片这种大惯性系统,$K_d$ 往往可以设为 0。

  1. 如果你的系统在加入 $K_i$ 后,超调(冲过头)还是无法接受。
  2. 试着加入一点点 $K_d$(比 $K_i$ 还要小)。
  3. 作用:它会预测温度上升的趋势,在快到目标时提前减小电流。

核心辅助工具:串口示波器

一定要用图形化软件看曲线! 光看串口打印的数字,你根本分不清是“震荡”还是“正常的漂移”。

  • 推荐工具:VOFA+
  • 做法:在程序里 printf("%f,%f\n", Target, CurrentTemp);
  • 看图说话:
    • 锯齿太多?—— 滤波没做好或 D 太大。
    • 波浪太大?—— P 太大或 I 太大。
Licensed under CC BY-NC-SA 4.0