1. 我为什么放着PID_Compact和FB41不用,非要自己写一个
1.1 官方PID块不算差,但“黑盒性”在跨平台项目里很要命
很多PLC工程师应该都有过这种经历:项目要求不复杂,就是个恒压供水、恒温控制、或者风机调节,直接用系统自带的PID块,三下五除二就能跑起来。PID_Compact在博图里点几下就能生成,STEP7里也有FB41“CONT_C”这种老牌经典块,除非是没接触过PID的初学者,否则一般不会有人想自己去写一个PID算法。毕竟官方块经过了大量测试,稳定性和边界情况处理都比自己写要完善得多。
但我遇到的麻烦在于:同一套控制策略,车间里新产线用S7-1500加博图,老产线还是S7-300加STEP7。控制要求完全一样,程序却必须写两遍。博图这边用PID_Compact,STEP7那边用FB41,两边参数单位不一致,内部状态变量也不一致,限幅逻辑、手自动切换的处理方式更是各有各的脾气。最要命的是,这两个块都是“黑盒”——你只知道输入输出,内部积分项怎么变化、比例项和微分项到底怎么参与运算,出了问题根本看不透。
调试时遇到系统振荡,你只能猜是Kp大了还是积分太快了,然后反复改参数试。如果能看到P、I、D三个分量分别输出多少,就能一眼判断问题出在哪一根弦上。但这个需求官方块满足不了,至少不能满足得那么直接。
1.2 一套算法逻辑,两套程序,改一处要动两处
再说管理成本。代码写出来是给人维护的,不是写完就完事。我遇到的实际场景是:上位机画面统一用一套组态,操作员在HMI上只关心“设定值、实际值、手动自动、输出百分比”这几个点。但底层PLC程序却因为平台不同,PID的接口命名、数据类型、上下限处理全都不一样。今天甲方说要把积分时间从20秒改成15秒,我得先在博图里改PID_Compact的Ti参数,再去STEP7里改FB41的积分时间,两边还要对着换算单位,稍不留神就会改出两种行为。
后来我意识到,与其在官方块外面再包一层转换逻辑,不如直接把PID功能块写成自己的。SCL写一套算法逻辑,两个平台各自编译一下,接口保持完全一致,HMI侧和上位机侧根本不用区分你是什么PLC。这样维护成本直接降一个数量级,改参数也只需要在同一个符号名下面改。
1.3 适合谁:想要真正掌握PID内核而不是被库牵着走的人
如果你只是想快速搭个Demo,那官方块完全够用,没必要折腾。但如果你需要交付源代码给客户、需要在多个品牌或多个软件版本之间移植同一套控制算法、或者你在调试时想把控制过程的中间量看得清清楚楚,那自己写一个PID功能块就是非常值得做的事。
而且,自己写完这个块之后,你对PID的理解会有一个明显的提升。以前你只是把Kp、Ti、Td填进去,现在你会知道积分项是怎么累加的,微分滤波为什么要有惯性环节,抗积分饱和到底在防什么。这些东西看十遍理论都不如自己把代码写一遍来得管用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前先定算法形态:位置式、接口和抗饱和策略
2.1 位置式与增量式的选择
PID算法有两种最常见的实现形态:位置式和增量式。位置式直接把控制量算出来,比如输出是0~100%的阀门开度,你看到的是绝对输出值;增量式算出来的是输出变化量,需要在外部对输出做累加,常见于步进电机、电液伺服之类需要“走一步算一步”的场合。
我选择位置式,理由很简单:工业现场最常用的控制对象,比如调节阀、变频器、加热器,接收的就是绝对输出值,位置式更直观,限幅也好做,HMI上显示输出百分比也符合操作员习惯。增量式在掉电保持、积分限幅处理上反而要额外考虑累计变量的初始化,多做一层功夫。如果你后续确实需要增量式,也可以在这个逻辑基础上修改,把最终输出改成累加一个增量值。
位置式的核心是一个老掉牙但可靠的公式:
输出 = 比例项 + 积分项 + 微分项
比例项处理“当前偏差”,积分项处理“历史偏差累积”,微分项处理“偏差变化趋势”。三分量相加,最后限幅到可执行机构允许的输出范围内。
2.2 FB接口设计:输入、输出、静态变量怎么划分
功能块接口设计是自撸PID的关键一步,接口定得好不好,直接影响后续移植和维护。我的划分思路如下。
输入变量:
| 变量名 | 类型 | 说明 |
|---|---|---|
| SP | REAL | 设定值 |
| PV | REAL | 过程测量值 |
| Kp | REAL | 比例增益 |
| Ti | REAL | 积分时间(秒),为0时禁用积分 |
| Td | REAL | 微分时间(秒),为0时禁用微分 |
| Cycle | TIME | 采样周期,必须与循环中断OB周期一致 |
| OutUpLimit | REAL | 输出上限 |
| OutLoLimit | REAL | 输出下限 |
| IntUpLimit | REAL | 积分项上限 |
| IntLoLimit | REAL | 积分项下限 |
| Manual | BOOL | 手动模式使能 |
| ManualVal | REAL | 手动模式给定输出值 |
输出变量:
| 变量名 | 类型 | 说明 |
|---|---|---|
| Output | REAL | PID最终输出 |
| Error | REAL | 当前偏差 |
| PPart | REAL | 比例项分量 |
| IPart | REAL | 积分项分量 |
| DPart | REAL | 微分项分量 |
静态变量(块内自用):
| 变量名 | 类型 | 说明 |
|---|---|---|
| dCycle | REAL | 换算后的采样周期(秒) |
| Integral | REAL | 积分累加值 |
| eF | REAL | 滤波后的误差 |
| PrevEF | REAL | 上一次滤波后误差 |
把PPart、IPart、DPart全部引到输出上,是我坚持的做法。调试时在HMI或者变量表里同时看这三个值,系统出了问题能立刻定位是哪个分量在捣乱。很多官方块不提供这个透明的中间量,但自撸块完全可以做到。
2.3 采样周期为什么必须是参数而不是固定值
PID的积分项和微分项都跟时间有关:积分是误差对时间的累积,微分是误差对时间的变化率。所以功能块必须知道“距离上一次调用过去了多少时间”,这就是采样周期Cycle。
我在设计时把Cycle作为输入参数,而不是在块内部写死。原因有两个:第一,同一个功能块可以放在不同周期的循环中断里调用,比如OB30跑1秒一次,OB35跑100毫秒一次,接口一样,只要参数填对应周期即可;第二,现场调试时临时改调用周期也很常见,周期变了不用改程序,直接改块输入参数就行。
在SCL代码里,TIME类型的Cycle要换算成秒。西门子PLC的TIME本质上是32位有符号整数,单位是毫秒,所以换算方式是先把TIME转成DINT,再转成REAL,最后除以1000。这个换算是跨平台移植时最容易踩的坑之一,后面讲STEP7移植时我会再提。
3. 博图平台实现:SCL源文件写出可维护的PID功能块
3.1 外部源文件而不是在块界面里手写
在TIA Portal里创建一个FB,大多数人习惯在项目树上右键添加块,然后在SCL编辑窗口里直接写。但我更推荐用“外部源文件”的方式:在项目树里找到“外部源文件”,添加一个新的SCL源文件,把整个FUNCTION_BLOCK完整代码写在里面,然后右键“从外部源生成块”。
这样做的最大好处是:功能块的全部代码集中在一个文本文件里,可以放进版本管理工具,也可以直接复制到STEP7里做移植。在块编辑器里写代码虽然界面友好,但拷贝代码时需要处理很多看不见的格式信息,跨平台搬运时反而麻烦。外部源文件就是纯文本,干净利落。
这个自撸功能块我命名为FB_PID_DIY,源文件里完整的块结构如下。
scl复制FUNCTION_BLOCK "FB_PID_DIY"
VAR_INPUT
SP : REAL; // 设定值
PV : REAL; // 过程测量值
Kp : REAL := 1.0; // 比例增益
Ti : REAL := 20.0; // 积分时间(秒),0表示取消积分
Td : REAL := 0.0; // 微分时间(秒),0表示取消微分
Cycle : TIME := T#1S; // 采样周期,必须与循环中断OB周期一致
OutUpLimit : REAL := 100.0; // 输出上限
OutLoLimit : REAL := 0.0; // 输出下限
IntUpLimit : REAL := 100.0; // 积分项上限
IntLoLimit : REAL := 0.0; // 积分项下限
Manual : BOOL := FALSE; // 手动模式
ManualVal : REAL := 50.0; // 手动模式下输出给定值
END_VAR
VAR_OUTPUT
Output : REAL; // PID输出
Error : REAL; // 当前偏差
PPart : REAL; // 比例项分量
IPart : REAL; // 积分项分量
DPart : REAL; // 微分项分量
END_VAR
VAR
dCycle : REAL; // 采样周期(秒)
Integral : REAL; // 积分累加值
eF : REAL; // 经过一阶惯性滤波后的误差
PrevEF : REAL; // 上一次滤波后误差
PreOut : REAL; // 未加积分前的预输出
END_VAR
BEGIN
// 将毫秒单位换算成秒
dCycle := DINT_TO_REAL(TIME_TO_DINT(Cycle)) / 1000.0;
IF dCycle <= 0.0 THEN
dCycle := 1.0;
END_IF;
Error := SP - PV;
IF Manual THEN
// 手动模式:输出给定值,内部状态跟随,为无扰切换做准备
IPart := ManualVal;
Integral := ManualVal;
eF := 0.0;
PrevEF := 0.0;
Output := ManualVal;
ELSE
// 比例项
PPart := Kp * Error;
// 微分项:不完全微分,先对误差做一阶惯性滤波再求差分
// Td<=0时禁用微分,并把滤波状态清零
IF Td > 0.0 THEN
eF := eF + dCycle / (Td / 5.0 + dCycle) * (Error - eF);
DPart := Kp * Td * (eF - PrevEF) / dCycle;
PrevEF := eF;
ELSE
eF := 0.0;
PrevEF := 0.0;
DPart := 0.0;
END_IF;
// 未加积分前的预输出,用于判断积分是否需要冻结
PreOut := PPart + DPart;
// 积分项:带限幅和条件积分,避免积分饱和
IF Ti > 0.0 THEN
// 只有当输出未饱和、或偏差方向与饱和方向不一致时才累加积分
IF (PreOut < OutUpLimit OR Error <= 0.0) AND (PreOut > OutLoLimit OR Error >= 0.0) THEN
Integral := Integral + Kp * Error / Ti * dCycle;
END_IF;
// 积分项硬限幅
IF Integral > IntUpLimit THEN
Integral := IntUpLimit;
ELSIF Integral < IntLoLimit THEN
Integral := IntLoLimit;
END_IF;
IPart := Integral;
ELSE
IPart := 0.0;
END_IF;
// 合成输出并限幅
Output := PreOut + IPart;
IF Output > OutUpLimit THEN
Output := OutUpLimit;
ELSIF Output < OutLoLimit THEN
Output := OutLoLimit;
END_IF;
END_IF;
END_FUNCTION_BLOCK
3.2 核心SCL代码逐段拆解
上面这段代码看着不长,但每一段都有具体讲究。
比例项没什么好说的,就是Kp乘以当前偏差Error。唯一要注意的是单位关系:如果SP和PV是温度,单位是摄氏度,输出是百分比开度,那么Kp的单位就是“输出百分比/摄氏度”,数值取决于实际对象。
微分项我用的是不完全微分。什么是“不完全微分”?纯微分对噪声极其敏感,工程上一般不用“误差直接微分”,而是先给误差加一个一阶惯性滤波,再对滤波后的值做差分。这个滤波环节相当于把尖锐的微分尖峰削平,让微分项的变化更平滑。代码里滤波时间常数取了Td/5,这是一个实测下来比较顺手的经验值。如果你现场噪声特别大,可以把分母里的5改大一些,滤波效果更强。如果你希望微分只作用于测量值、避免设定值突变引起冲击,也可以把Error替换成PV,在代码里对应调整即可。
积分项是自撸PID里最容易出错的地方。积分累加直接写在Integral变量里,每次增加的量是Kp * Error / Ti * dCycle。注意这里用的是积分时间Ti而不是独立的积分增益Ki,这意味着比例增益Kp同时影响积分项,符合西门子PID参数的习惯。积分累加之后先做硬限幅,把积分项限制在IntUpLimit和IntLoLimit之间,这是防积分饱和的第一道保险。
更关键的是条件积分。如果在输出已经饱和、而且偏差方向还在往饱和方向走的时候继续累加积分,积分项会冲得很高,等设定值反向变化时,系统要花很长时间才能把积分消耗掉,表现为大幅超调甚至震荡。代码里的处理是:当预输出达到上限且误差为正(还在要求输出增加)时,冻结积分;当预输出达到下限且误差为负时,也冻结积分。其他情况才正常累加。
3.3 OB30里的调用方法和周期匹配
功能块本身不决定什么时候运行,要靠循环中断OB来调用。我在这个项目里放置的是OB30,周期设置为1秒,然后在OB30里调用FB_PID_DIY。
scl复制CALL "FB_PID_DIY", "DB_PID_DIY"
SP := HMI_SP,
PV := AI_Pressure,
Kp := PID_Kp,
Ti := PID_Ti,
Td := PID_Td,
Cycle := T#1S,
OutUpLimit := 100.0,
OutLoLimit := 0.0,
IntUpLimit := 100.0,
IntLoLimit := 0.0,
Manual := HMI_Manual,
ManualVal := HMI_ManualVal,
Output => AO_ValveOpen,
Error => HMI_PID_Error,
PPart => HMI_PID_P,
IPart => HMI_PID_I,
DPart => HMI_PID_D;
注意Cycle必须与OB30的循环时间严格一致。比如OB30设置为1秒,Cycle就填T#1S;如果OB30实际周期是500毫秒,Cycle却填了2秒,积分算出来的累积速度就会差4倍。这个错误很隐蔽,肉眼看不出来,只有用波形对比时才发现比例符合但积分收敛速度不对。
循环中断OB的优先级也要注意。博图里循环中断优先级比普通OB高,如果你的程序里还有其他耗时较长的OB,有可能导致循环中断不能被及时触发,实际调用周期比设定周期长。遇到这种情况,要么把其他逻辑往普通OB里挪,要么给循环中断分配一个空闲优先级。
3.4 在线监视时把P/I/D三个分量全部引出来
功能块上线调试前,建议先建立一张变量表或者HMI趋势画面,把PPart、IPart、DPart、Output、Error全部放进去。很多现场问题一眼就能看得出来:如果系统持续输出上限但Error始终不为零,看IPart是不是已经顶到积分限幅;如果Output高频抖动,看DPart是不是在来回跳;如果系统有静差,看IPart是否为零。
这个习惯帮我省了大量时间。以前用官方PID块,内部积分状态看不到,只能靠猜。现在自撸块把五脏六腑全暴露出来,调试效率完全不一样。
4. 移植到STEP7的实战记录:不是复制粘贴,是对着差异点逐个改
4.1 源文件格式与块命名的差异
在博图里能跑通的代码,搬到STEP7 V5.x并不能直接编译通过。第一步要改的就是源文件格式。TIA Portal的外部源文件习惯用带引号的块名,比如FUNCTION_BLOCK "FB_PID_DIY",同时可以带VERSION版本号。STEP7 V5.x的SCL源文件通常不带引号,块通过编号和符号名来标识,整体声明方式也更老派。
我的做法是保留一份基础源文件,专门用于跨平台搬运,里面不写平台特定的花哨语法。比如复合赋值运算符+=在TIA里能用,但为了兼容老SCL,统一写成Integral := Integral + ...这种完整形式。逻辑上宁可多写两行,也不给自己埋雷。
4.2 S7-300/400的数据类型限制
数据类型是另一个需要重点检查的地方。S7-1500和S7-1200支持64位浮点LREAL,但S7-300/400的浮点数只有32位REAL。如果你在博图里用了LREAL,移植到STEP7必须全部改成REAL。
另外,TIME转REAL这个操作在不同平台的做法不完全一样。我在博图里用的是DINT_TO_REAL(TIME_TO_DINT(Cycle)) / 1000.0,这个写法在STEP7的SCL里也是可行的,所以从设计之初就特意绕开了可能不兼容的函数。如果你贪图省事直接用TIME_TO_REAL,到了老平台上可能就要到处找替代方案。
S7-300/400的循环中断默认是OB35,周期100毫秒。博图里最常用的可能是OB30、OB31这些可按自己需求设置的循环中断。移植时要把调用代码里的OB名改过来,同时注意CPU属性里循环中断的周期配置。
4.3 背景DB、多实例与调用方式的差异
TIA Portal在调用FB时可以让你自己选择“单实例DB”“多实例”或“参数化实例”,如果调用时还没创建背景DB,系统会提示自动生成。STEP7 V5.x则更显式:你必须指定一个背景DB编号,比如CALL "FB_PID_DIY", DB100,然后在调用前确认DB100的类型匹配FB_PID_DIY。
如果你想把多个PID实例放在同一个背景DB里,TIA里用多实例就行,静态区声明几个实例变量,分散管理。STEP7里也有多实例,但语法和符号名规则跟TIA不完全一样。我的经验是:跨平台项目里尽量用单实例DB,每个回路一个DB,背景DB创建方式老派但直观,两个平台的行为一致性也更好。
还要注意的是,TIA里的FB可以开启优化访问,背景DB只按符号访问,不关心物理偏移地址。STEP7的标准访问则要考虑DB变量在内存里的排布,修改变量声明后,背景DB可能需要重新生成一次,否则可能出现变量错位的奇怪现象。所以在STEP7里修改FB接口后,记得去检查对应背景DB是否已经更新,必要时手动重新生成。
4.4 调试手段的降级:从Trace回到变量表
博图配合S7-1500的Trace功能确实好用,能直接把PPart、IPart、DPart录成波形,整定参数时非常直观。STEP7 V5.x环境下没有这种现代工具,最实用的还是变量表在线监视,把背景DB里的Input、Output、Error、PPart、IPart、DPart全部拖进来,同时监控几个关键量,比如设定值SP、过程值PV。
变量表虽然没有曲线那么直观,但能看到关键数据的变化趋势已经足够定位问题了。我的习惯是每隔一小段时间手动刷新一次变量表,记录稳定工况下的数值,再对比系统阶跃响应前后的变化。只要能确认积分项在正常累积、比例项随误差同步变化、输出没被限幅卡死,基本就能判断算法工作状态正常。
4.5 我的移植顺序
移植到STEP7时,我没有用“复制整个源文件再编译”的方式,而是按照固定顺序一步一验证。
先把TIA版本的外部源文件另存一份,批量替换掉平台相关的写法:去掉引号、换成标准SCL声明方式、确认没有LREAL、检查所有复合赋值都已展开。第二步把源文件放进STEP7的SCL源文件夹,编译成FB,根据报错信息逐个修类型不匹配和语法差异。第三步建立背景DB,在OB35里写调用代码。第四步用PLCSIM跑一遍和TIA仿真时完全相同的测试序列,比对输出值是否一致。
这个顺序看起来很笨,但实际最稳。跨平台移植最大的风险不是SCL语法本身,而是一些隐性的行为差异,比如背景DB数据不刷新、调用周期不对、字节对齐变化。只要每一步都用同样的数据做验证,就能把问题限制在一个小范围内。
5. 实测整定与常见问题:那些文档里查不到的边界经验
5.1 先空载后带载:模拟信号代替执行机构
PID功能块写完之后,千万别直接接现场执行机构去调。我在某个恒压供水项目上调试时,先把PV接到了一个由HMI控制内部变量模拟的“假压力值”上,Output接到另一个内部变量,相当于在PLC内部搭了一个纯软件环境,抛开所有传感器和执行机构来做闭环模拟。
这样做的意义是:先验证算法逻辑本身没有飞车、没有振荡、积分不会溢出。在空载环境下造一个缓慢变化的PV信号,观察Output是否跟随;再给SP一个阶跃,观察响应过程。如果空载环境下PID输出就是乱的,接上真实设备只会更乱,而且还要背着炸执行机构的风险。
调试顺序我强烈建议固定为:先纯软件模拟,再接模拟量信号源测试,最后才接真实的变送器和执行机构。省掉任何一步,都可能把简单问题复杂化。
5.2 临界比例度法在某个小项目上的整定记录
以我之前做的一个小型恒压供水回路为例,SP是压力值0~100%,PV来自压力变送器,Output控制变频器频率,间接控制水泵转速。整定用的是经典的临界比例度法:先把Ti设为0禁用积分,Td设为0禁用微分,然后慢慢增大Kp,观察系统是否出现等幅振荡。
我记录的临界参数大概是Ku等于1.8,Tu约30秒。按齐格勒-尼克尔斯整定公式,PID参数初值可以取Kp等于0.6倍Ku也就是1.08,Ti等于0.5倍Tu也就是15秒,Td等于0.125倍Tu也就是3.75秒。接上去之后试运行,系统稳定但超调略大,现场阀门和水泵的惯性比理想模型大,最后把Ti调整到20秒,Td降到1.5秒,静差控制在±0.2%以内。
这个案例想说明的是:整定公式只是给一个可靠的起点,不是精确答案。现场对象千差万别,管道长度、阀门死区、传感器滤波时间都会影响最终参数,必须在线微调。
5.3 手/自动无扰切换与微分冲击的处理
手动自动切换的坑,很多人第一次写PID都会踩。如果自动状态下积分项累积得很大,切到手动时输出直接跳到手动值,操作员可能会看到执行机构猛地一抖。更麻烦的是从手动切回自动时,如果积分项没有和当前输出对齐,输出会瞬间跳变。
代码里我在手动分支把Integral赋成了ManualVal,同时把滤波状态清零。这样切回自动时,比例项P和微分项D先计算出来,积分项从ManualVal出发,合成输出接近当前手动输出,不会产生阶跃。这个“对齐”动作是我在实际调试中反复验证过的,比单独做积分清零要可靠得多。
微分冲击是另一个容易被忽视的问题。设定值一旦阶跃变化,误差瞬间变大,如果微分项直接作用于误差,输出会打出一个尖峰。用不完全微分之后,这个尖峰被惯性滤波削掉了不少。如果你的工艺对输出瞬态要求非常严格,可以改成“微分先行”,即微分只作用于测量值PV,不作用于设定值SP,代码上把微分部分的Error改成PV变化量即可。
5.4 常见症状排查表
调试过程中我整理了一张排查表,按症状直接找原因,方便查询。
| 现场症状 | 可能原因 | 调整思路 |
|---|---|---|
| 输出持续上限但误差不为零 | 积分饱和、或执行机构下限卡死 | 检查IPart是否顶到限幅,检查条件积分逻辑是否触发 |
| 系统低频振荡 | Kp过大或Ti过小 | 适度降低Kp,或增大Ti |
| 输出高频抖动 | 微分项放大噪声、或传感器信号有毛刺 | 加强惯性滤波、增大Td分母的滤波强度,必要时对PV做一阶滤波 |
| 存在静差消不掉 | Ti为0、或系统未进入稳态、或PV有恒定偏移 | 确认Ti大于0,检查变送器零点 |
| 超调大且回稳慢 | 积分限幅设置过高、或Td偏大 | 检查IntUpLimit和IntLoLimit,适当减小Td |
| 切换手动/自动时输出跳变 | 积分状态未与输出对齐 | 在手动分支里同步Integral,必要时也同步滤波状态 |
这张表并不能覆盖所有现场情况,但可以帮你快速缩小排查范围。大部分PID问题,归根结底是某个分量没被看见,所以我还是建议把PPart、IPart、DPart全部引出来在线监视,数据比感觉可靠得多。
5.5 最后再补一句个人的小习惯
自撸PID功能块这个项目做完之后,我养成了一个习惯:每次把功能块移植到一个新平台,都在SCL源文件头部写清楚“适用平台、调用周期要求、参数单位、已知坑点”。比如我会注明“Kp单位为输出百分比/工程值,Ti单位秒,Cycle需与调用OB一致”这类信息。这看起来是个微不足道的习惯,但几个月后你再打开这段源码时,会感谢当初记下这几行字的自己。
还有一些PID功能块代码,我把它们整理成了一个可以反复使用的标准库文件,放在公司内部公共目录里。后来的项目只要复制一份,改一下接口信号和初始参数就能用,省掉了大量重复造轮子的时间。如果你也在同时维护博图和STEP7两个平台,强烈建议尽早做一套自己的标准控制功能块,不要每次都在两个软件里手工同步代码。
