简介:面向西门子S7-1200/1500 PLC自动化工程师的增量式PID程序包,围绕工业现场闭环控制中的参数整定、误差处理与控制量输出等问题,适合具备一定博图编程基础、希望将PID算法用于实际项目调试的技术人员参考。压缩包共49个文件,约1.26MB,以SCL源码、DB数据块、XML工程配置和博图项目归档文件为主,并包含少量图标、索引及日志类辅助文件,导入TIA Portal后可直接查看程序块、变量与工程结构。已有752人学习下载,程序经过实际项目验证,无密码且带注释,便于对照理解增量式PID的误差差分、比例积分微分项更新和输出限幅逻辑,也可据此调整采样周期与PID参数,作为设备改造、课程设计或控制方案复用的参考模板。
1. 增量式PID程序适合谁:一套在 S7-1200/1500 上反复使用的控制逻辑
一个有意思的现象:不少在博途里已经用工艺对象跑通闭环的项目,最后又把工艺对象删掉,改成自己写的一个 FB。原因在于切换和限幅,工艺对象本身没有问题。这份西门子增量式PID程序就是这种思路的产物——一套纯 SCL 手写的增量式 PID 功能块,S7-1200 和 S7-1500 通吃,已经在实际项目里长期运行。它把输出拆成两部分:增量和累加量。需要频繁手动、自动切换以及反复限幅的场合适合使用;执行机构是电动调节阀、伺服、变频器的场合也适合。同一套差分公式放到单片机上,只需把 Ts 换成定时器中断周期,逻辑无需改动。下面从公式讲到代码,再讲到现场中最容易出错的地方。
2. 增量式还是位置式:先把差分公式和选型边界定死
2.1 三项差分各自在补什么
位置式离散 PID 的经典写法是这样(用 Ti、Td 形式,和后面代码一致):
u(k) = Kp·e(k) + Kp·(Ts/Ti)·Σe(j) + Kp·(Td/Ts)·[e(k) − e(k−1)]
增量式就是把它前后两个周期相减,Δu(k) = u(k) − u(k−1):
Δu(k) = Kp·[e(k) − e(k−1)] + Kp·(Ts/Ti)·e(k) + Kp·(Td/Ts)·[e(k) − 2e(k−1) + e(k−2)]
三个项的含义都变了。比例项不再正比于当前偏差,而正比于偏差的变化量——偏差稳定不变时,P 项输出零,这一点第一次看会觉得反直觉。积分项只跟本次偏差有关,因为 Σe(k) − Σe(k−1) 正好等于 e(k),历史累加被差分消掉了。微分项变成二阶差分,对噪声的放大倍数比位置式更高,这是增量式最容易被忽视的副作用。
差分消掉积分累加器带来一个直接好处:增量式天生不存在积分饱和。但这话只对了一半。累加器并没有消失,它从“积分项内部”搬到了“输出端”——你总得把 Δu 累加成 u 才能送给模拟量通道。所以抗饱和的工作只是换了个位置做,并没有省掉。很多人听信“增量式不用管积分饱和”,结果 acc 一路累加到几万,阀门早就到顶了,退回来时还要再爬半天,这就是典型的翻车现场。
还有一个特性值得单独说:无扰切换。位置式做手自动切换,得把积分项预置成“当前手动输出对应的积分值”,算错一点就跳变。增量式不需要这套换算,因为累加器 acc 本身就是输出的绝对值,手动时让它跟着手动值走,切回自动时第一个增量叠加在手动输出上,天然连续。这个特性在频繁切手动的间歇式生产设备上非常值钱。
代价也得说清楚。acc 是保存在背景数据块里的状态量,CPU 断电重启或者下载块之后会回到初始值,输出直接归零。要么在数据块里给 acc 设保持性,要么上电时从阀位反馈回读一次做初值。另一条代价是增量式对采样周期的稳定性更敏感——Ts 在公式里是分母,周期抖一下,等效增益就跟着抖。
2.2 什么执行机构配增量式,什么必须用位置式
判断标准只有一条:执行机构本身是不是一个积分器。如果它接到的是“走一步”的指令,增量式天然匹配;如果它接的是“停在这里”的绝对位置,增量式算完还得自己累加。
| 执行机构 | 推荐形式 | 原因 |
|---|---|---|
| 步进电机 / 伺服定位 | 增量式(只出 Δu) | 脉冲数即位置,增量直接下发,不需要软件累加 |
| 电动调节阀(带位置反馈) | 增量式 + 累加 | 阀门动作有惯性,增量限幅能当“限速”用 |
| 变频器 4-20mA 调速 | 位置式或增量式累加 | 电流信号是绝对量,必须由程序维护 acc |
| 加热器周波 / PWM 占空比 | 位置式 | 占空比是绝对比例,累加没有物理意义 |
| 比例阀 / 电液伺服 | 增量式 + 累加 | 阀芯位移对流量积分,增量控制更贴合物性 |
表里最容易搞错的是变频器和调节阀这两行。PLC 的模拟量输出通道输出的是绝对电压或电流,不管你内部算的是增量还是位置,最后都得给一个绝对值。所以这份 FB 里留了IncOnly开关:给伺服走TRUE,只输出本周期的 Δu,让驱动器自己累加;给模拟量通道走FALSE,输出累加后的工程量。
顺带提一句单片机的场景。同一套差分式子在 MCU 上跑,Ts换成定时器中断周期,acc换成一个全局 float,逻辑一字不改。差别在于单片机通常直接出 PWM,输出就是绝对占空比,所以更接近位置式;但只要保留 acc 做限幅,代码仍然可以复用。
2.3 一个 FB 该暴露哪些接口
接口设计比公式更容易出问题。我见过把Ts写成 FB 内部常量的版本,结果同一个块被两个不同周期的 OB 调用,参数全乱。正确的做法是把采样周期做成输入引脚,由调用方保证和 OB 周期一致。
接口分三区。VAR_INPUT放所有会被人改的东西:PV、SP、Kp、Ti、Td、Ts、手自动标志、手动值、输出上下限、增量限幅、IncOnly。VAR_OUTPUT放需要回给 HMI 或上位的东西:累计输出、本周期增量、当前偏差、饱和标志。VAR放纯内部状态:e1、e2 和 acc。
几个细节值得展开。输出上下限用工程量(比如 0.0 到 100.0)而不是原始的 0~27648,这样 HMI 上直接填百分比,量程换算留在 AI/AO 通道的块里做,职责清楚。饱和标志Sat一定要回传,否则操作员看到阀门不动只会报修,不知道是输出已经顶到上限。dOutMax单独做一个增量限幅引脚,比拿输出限幅去打补丁有效得多——它管的是“每周期最多走多远”,本质上是给执行器限速。
3. 在博途里把 FB 写出来:SCL 源码逐段拆解
3.1 建块与多重背景
在项目树里添加新块,选函数块 FB,语言选 SCL,勾上“优化的块访问”。优化访问下变量按符号寻址,改接口不用重新编译整个调用链,调试期能省不少时间。
多个回路建议用多重背景而不是每个回路单独建背景 DB。做法是再建一个 FB 作为“回路容器”,把 PID 的 FB 作为静态变量声明进去,一个容器对应一个回路。这样回路数量变化时只需要改容器的调用,不必在 OB 里贴一堆单独的 DB 名。如果需要掉电保持,在背景 DB 里把acc那一行勾上“保持”,注意只勾 acc,e1、e2 不要勾——保留陈年偏差只会在重启后造成一次冲击。
接口声明部分的完整代码如下,直接照着填:
FUNCTION_BLOCK "FB_IncPID" { S7_Optimized_Access := 'TRUE' } VERSION : 0.1 VAR_INPUT PV : Real; // 过程量反馈,工程量 SP : Real; // 设定值,工程量 Kp : Real; // 比例增益 Ti : Real; // 积分时间(s),填 0 表示取消积分 Td : Real; // 微分时间(s),填 0 表示取消微分 Ts : Real; // 采样周期(s),必须与调用 OB 的周期一致 Auto : Bool; // TRUE=自动,FALSE=手动 ManOut : Real; // 手动模式下的输出值,工程量 OutMax : Real; // 输出上限 OutMin : Real; // 输出下限 dOutMax : Real; // 单周期增量限幅,正值 IncOnly : Bool; // TRUE=只输出增量,FALSE=输出累加值 END_VAR VAR_OUTPUT Out : Real; // 输出,工程量 Inc : Real; // 本周期增量 Err : Real; // 当前偏差 Sat : Bool; // TRUE=输出已到限幅 END_VAR VAR e1 : Real; // e(k-1) e2 : Real; // e(k-2) acc : Real; // 输出累加器 firstScan : Bool := TRUE; END_VAR接口定义完,重点在程序体。下面这段是核心逻辑,注释写在了每一段前面,可以直接抄。
BEGIN // 1) 偏差:统一按 SP - PV,符号不要中途翻,否则积分方向会反 #Err := #SP - #PV; // 2) 首次扫描对齐历史偏差,避免开机瞬间微分项炸出一个大增量 IF #firstScan THEN #e1 := #Err; #e2 := #Err; #acc := #ManOut; // 从当前实际输出起步,实现无扰开机 #firstScan := FALSE; END_IF; // 3) 采样周期兜底,正常不会走到,防止除零把 CPU 拉进 STOP IF #Ts <= 0.0 THEN #Ts := 0.1; END_IF; // 4) 比例项:偏差的变化量 #Inc := #Kp * (#Err - #e1); // 5) 积分项:只与本次偏差有关,Ti=0 时跳过 IF #Ti > 0.0 THEN #Inc := #Inc + #Kp * #Ts / #Ti * #Err; END_IF; // 6) 微分项:二阶差分,Td=0 时跳过 IF #Td > 0.0 THEN #Inc := #Inc + #Kp * #Td / #Ts * (#Err - 2.0 * #e1 + #e2); END_IF; // 7) 增量限幅:给执行器限速,抑制突发扰动 IF #Inc > #dOutMax THEN #Inc := #dOutMax; END_IF; IF #Inc < -#dOutMax THEN #Inc := -#dOutMax; END_IF; // 8) 手自动:手动时把累加器钉在手动值上,切回自动天然无扰 IF #Auto THEN #acc := #acc + #Inc; ELSE #acc := #ManOut; #Inc := 0.0; END_IF; // 9) 输出限幅 + 饱和标志。钳位本身就完成了抗饱和回算 #Sat := FALSE; IF #acc > #OutMax THEN #acc := #OutMax; #Sat := TRUE; END_IF; IF #acc < #OutMin THEN #acc := #OutMin; #Sat := TRUE; END_IF; // 10) 历史偏差移位放最后,顺序错一步整条曲线就废 #e2 := #e1; #e1 := #Err; // 11) 按执行机构类型决定输出什么 IF #IncOnly THEN #Out := #Inc; // 伺服/步进:只吃增量 ELSE #Out := #acc; // 模拟量通道:吃绝对位置 END_IF; END_FUNCTION_BLOCK第 9 段是整段代码里最容易被跳过、后果最严重的地方。acc一旦被钳在OutMax,本周期算出来的正增量就被丢弃了,下一周期重新从限幅值往上算——这正是增量式抗饱和的天然优势,不需要额外的“积分回算”逻辑。前提是钳位必须写在累加之后、输出之前,写反了就等于没做。
第 10 段的移位顺序也有讲究。必须先e2 := e1再e1 := #Err,反过来写两个变量会同时变成当前偏差,微分项恒为零,表现为“微分怎么调都没反应”。这种 bug 在趋势图上看不出来,只能靠读代码抓。
第 8 段的手动分支里,acc := #ManOut每个周期都刷新一次,所以从手动切回自动的那一拍,acc恰好等于最后一次手动输出,第一个增量叠加上去就是连续的。注意ManOut的单位要和acc一致,都是工程量,不要一个填百分比一个填 0~27648。
3.2 参数含义与现场改法
| 参数 | 类型 | 常用取值 | 改动影响 |
|---|---|---|---|
| Kp | Real | 0.5 ~ 5.0 | 加大加快响应,过大引起等幅振荡 |
| Ti | Real | 5 ~ 1200 s | 减小加快消静差,过小引起低频摆动 |
| Td | Real | 0 ~ 120 s | 加大抑制超调,对噪声敏感,流量/压力一般设 0 |
| Ts | Real | 0.1 ~ 2.0 s | 必须等于调用 OB 周期,改周期必须同步改这个值 |
| dOutMax | Real | 满量程的 1% ~ 5% | 减小相当于给阀门限速,太大会拖慢响应 |
| OutMax/OutMin | Real | 执行器量程 | 按阀门/变频器实际行程填,不要用默认 100 |
Ti和Td填 0 是有效的“关闭”语义,代码里做了判断,不会被当成除零。这一点和某些工艺对象的约定不同,改参数时注意别把 0 当成“无穷大”来理解。
3.3 放到循环中断 OB 里调用
调用位置比代码本身更关键。新建一个循环中断 OB(OB30 或 OB35,不同 CPU 编号略有差异),把周期设成 100 ms,然后在里面调用:
// OB30 循环中断,周期 100000 us = 100 ms // Ts 必须与此处周期严格一致,改一处必须改两处 "DB_IncPID_Loop1"(PV := "AI_Temp".Value, SP := "HMI_DB".TempSP, Kp := "HMI_DB".Kp, Ti := "HMI_DB".Ti, Td := "HMI_DB".Td, Ts := 0.1, Auto := "HMI_DB".AutoMode, ManOut := "HMI_DB".ManOut, OutMax := 100.0, OutMin := 0.0, dOutMax := 3.0, IncOnly := FALSE, Out => "AQ_Valve".Value, Inc => "HMI_DB".IncView, Err => "HMI_DB".ErrView, Sat => "HMI_DB".SatAlarm);为什么不能用 OB1?OB1 的扫描周期取决于程序长度和通信负载,可能 8 ms 也可能 25 ms,Ts写死 0.1 而实际周期在抖,等效积分增益就在抖。表现是稳态时被调量在设定值附近小幅爬行,操作员会说“这阀门自己会动”。把调用挪进循环中断 OB,周期由系统保证,曲线立刻干净。这条经验值一次返工。
另外,模拟量输入块自带的滤波会引入额外滞后,如果 AI 通道的滤波时间设得比Ts还大,PID 拿到的 PV 永远是“旧数据”,再怎么调参数都追不上。常见做法是把 AI 滤波关掉或者压到Ts的十分之一以内,需要平滑就在 FB 内部对 PV 做一阶滤波。
4. 参数整定与限幅:让输出不打架的四个开关
4.1 采样周期怎么定
经验规则是Ts取被控对象时间常数的 1/10 到 1/20。对象越慢,周期可以越长;但不是越长越好,周期太长会让微分项失效、积分项步进过大。工程上的起点可以照这张表:
| 被控对象 | Ts 起点 | 说明 |
|---|---|---|
| 流量 | 0.1 ~ 0.2 s | 响应快、噪声大,Ts 必须小,微分基本不用 |
| 压力 | 0.2 ~ 0.5 s | 介于流量和温度之间 |
| 温度 | 1.0 ~ 2.0 s | 热惯性大,Ts 太小只是白烧 CPU |
| 液位 | 2.0 ~ 5.0 s | 对象最慢,注意别让 Ts 超过容器的过渡时间 |
周期一旦定下就不要随手改。改Ts等于同时改了积分和微分的等效强度,原来调好的Ti、Td全部失效。我一般会在 HMI 上把Ts做成只读显示,跟 OB 周期对一眼就能确认有没有配错。
4.2 Kp、Ti、Td 的起手值
不要从零开始试。下面这张表是不同对象类型的常用起手区间,先按表填一组,再按“先 P、后 I、最后 D”的顺序微调:
| 对象 | Kp | Ti (s) | Td (s) |
|---|---|---|---|
| 温度(热惯性) | 0.5 ~ 2.0 | 300 ~ 1200 | 30 ~ 120 |
| 压力 | 1.0 ~ 3.0 | 30 ~ 120 | 0 ~ 10 |
| 流量 | 1.0 ~ 5.0 | 5 ~ 30 | 0 |
| 液位 | 1.0 ~ 4.0 | 60 ~ 600 | 0 |
调的时候把Ti先设成一个很大的值(比如 9999),Td设 0,只留比例。逐步加大Kp直到被调量开始出现等幅小幅振荡,然后退回到振荡临界值的 60% 左右。接着把Ti从大往小收,收到静差消失且不出现慢周期摆动为止,一般取临界值的 1.5 到 2 倍留裕度。Td最后加,流量和压力回路建议直接不加——噪声经过二阶差分放大之后,输出会变成一串尖刺,看着像干扰,其实是微分在自激。
4.3 三条限幅线各管什么
限幅要分三层,混在一起就调不明白。
第一层是输出限幅OutMax/OutMin,对应执行器的物理行程。阀门 0~100%、变频器 0~50 Hz,按实际填。这一层保证输出不会跑到物理上不存在的位置。
第二层是增量限幅dOutMax,管的是每个周期最多走多远。它相当于给执行器装了个限速器,对突加扰动特别有效——没有它的时候,一个阶跃扰动会让增量项一次性算出个巨大的 Δu,阀门“咣”一下到底,然后靠积分慢慢往回爬,超调量大得吓人。填法上取满量程的 1%~5% 起步,慢慢放大到响应速度可接受为止。
第三层是饱和标志Sat。它不改变输出,只告诉上层“我现在顶着了”。接到 HMI 上做报警,或者送到顺序控制里做联锁条件——比如升温阶段输出长时间饱和,就说明加热功率选小了,改 PID 参数是治不好的。
4.4 手自动无扰的两个前提
无扰切换不是一行代码的事,它依赖两个前提同时成立。
第一个前提是手动期间acc持续跟随。代码里手动分支每个周期都执行acc := #ManOut,所以acc和实际输出始终一致。如果只在切换的那一拍赋值,操作员在手动状态下改了阀门开度,acc还是老值,切回自动立刻跳变。
第二个前提是手动期间历史偏差要正常移位。看代码第 10 段,移位是放在手自动判断之外的,也就是说手动时e1、e2照常更新。这个细节看起来不起眼,但如果手动时冻结了e1,切回自动的第一拍e(k) − e(k−1)里减的是几分钟前的偏差,比例项会给出一个完全脱离现实的增量,表现就是“一切自动阀门就猛冲一下”。
还有一个容易被忽略的点:ManOut由谁写。如果 HMI 上既有手动输入框,又有阀门开度显示,两者必须绑到同一个变量。两个变量各写各的,操作员看到的是显示值,实际生效的是输入框旧值,切自动时的冲击就解释不清了。
5. 常见问题排查:五个把现场搞停机的坑
5.1 输出毛刺与周期抖动
现象:被调量稳态下不来,趋势图上是一层薄薄的毛刺,阀门一直有轻微的“嗡嗡”动作声。
原因:FB 被放在 OB1 里调用,扫描周期随通信负载波动,而Ts写的是固定值。周期短的时候等效积分增益变大,周期长的时候变小,输出就在设定值附近来回找。另一个常见来源是 AI 通道滤波时间大于Ts,PID 拿到的是滞后数据。
解决:把调用整体挪进循环中断 OB,Ts与 OB 周期严格对齐;把 AI 通道滤波压到Ts/10以内,需要平滑就在 FB 内部做一阶滤波。挪完之后先别急着调参数,观察半小时趋势确认毛刺消失,再动Kp。
5.2 开机冲击与切换跳变
现象:设备上电或者从手动切自动的瞬间,阀门直接冲到限幅,几秒后才慢慢回落。
原因:firstScan逻辑缺失,e1、e2初值为 0,第一个周期的微分项算出e(k) − 0 + 0,等于把当前偏差当成了巨大的偏差变化;同时acc初值为 0,与实际阀位不符。两个错误叠加,冲击就避不开。
解决:补上首扫描对齐(代码第 2 段),把e1、e2都赋成当前偏差、acc赋成当前实际输出;acc在背景 DB 里设保持性,或者上电时从阀位反馈回读一次。切换跳变则按 4.4 的两个前提逐条核对,多数情况是ManOut变量绑错。
现象还有一种:只有“手动切自动”跳,“自动切手动”不跳。这基本可以确定是acc在新旧模式之间没同步,重点查手动分支是不是每周期都执行。
5.3 量纲、精度与参数不生效
现象一:偏差方向始终反着,越调越大,最后顶到限幅。原因是 PV 和 SP 的工程量纲不一致,一个来自 AI 通道的 0~27648 原始值,一个来自 HMI 的 0~100 百分比。解决是把量纲统一在 FB 入口处,AI 通道做归一化,FB 内部一律用工程量,不要在半路混着算。
现象二:HMI 上把Kp从 1.5 改成 1.8,曲线毫无变化。原因通常是 FB 内部又对Kp做了一次赋值,或者 HMI 写的是另一个同名变量,改完下个周期就被覆盖回去。解决办法是搜一遍整个项目里Kp的写操作,只保留 HMI 一处。
现象三:参数在小数点后被抹掉,Ti填 0.5 变成 0,积分直接失效。这是数据类型问题,字面量写成整数会走隐式转换。所有实数运算的字面量一律带小数点(写2.0不写2),整数除法的地方显式REAL()转换。这类 bug 编译器不报错,只能靠看趋势图发现,属于典型的黑匣子问题。
提示:调试期在 HMI 上同时放 PV、SP、Out、Inc 四条趋势,比只看 PV 一条快十倍。看到 Inc 有尖刺而 PV 平滑,问题在微分或者噪声;看到 Out 到顶而 Inc 还在正方向累加,问题在限幅位置写反了。
6. 进阶验证:用阶跃响应把参数试出来,再挂到串级上
参数靠手拧能拧出个能用的结果,但拧不出可复现的结果。换一台设备又得重来一遍,这活儿干多了会烦。我后来的习惯是先做一次开环阶跃,把对象的三个特征量读出来,再套公式算初值。
做法是切手动,输出从当前值突加 10%(比如阀门从 30% 到 40%),记录 PV 曲线直到稳定。从曲线上读三个数:稳态增量 ΔPV 与输出增量 Δu 的比值就是对象增益 K;PV 到达稳态值 63.2% 所用的时间就是时间常数 T;从输出阶跃到 PV 开始动之间的延迟就是纯滞后 τ。拿到这三个数,用一阶加纯滞后模型套简化整定公式:Kp = T / (K·(λ + τ)),Ti = T,Td = 0,其中 λ 取 τ 或 0.5T,λ 越大越稳但越慢。
| 阶跃响应特征 | 说明 | 整定倾向 |
|---|---|---|
| τ/T < 0.1 | 滞后小,易控 | 可以提高 Kp,缩短 Ti |
| τ/T ≈ 0.3 | 一般惯性对象 | 按公式取值即可 |
| τ/T > 0.5 | 大滞后 | 降低 Kp,加大 Ti,考虑加前馈 |
| 曲线有反向 | 对象非最小相位 | 不要用微分,串级或前馈更靠谱 |
微分项对噪声敏感,通常做法是在 FB 入口对 PV 加一阶滤波,而不是直接关掉 Td。一阶滤波的 SCL 写法很短,串在 PID 之前即可:
// PV 一阶滤波,Tf 建议取 Ts 的 2~5 倍 #pvFilt := #pvFilt + (#Ts / (#Tf + #Ts)) * (#PV - #pvFilt);#Tf越大越平滑,代价是相位滞后增加。温度这种慢对象可以放心用,流量回路要把Tf压得很小,否则滤波带来的延迟会直接吃掉稳定裕度,参数怎么调都在摆。
验证环节我一般做两次测试。第一次是设定值阶跃:SP 加 5%,看超调量和调节时间,超调控制在 10% 以内、两个周期内稳定下来就算过关。第二次是扰动测试:保持 SP 不变,手动把负载改一档(比如开大旁路阀),看输出多快能把 PV 拉回来、有没有持续振荡。两次都过,参数才算真的能用。只做设定值阶跃不做扰动测试,是很多人参数“看着挺好、一上负载就摆”的原因。
串级是这套东西的自然延伸。主回路用位置式算出一个副回路的设定值,副回路用这份增量式 FB,周期压到主回路的三分之一到五分之一。要点是主回路的输出限幅要和副回路的量程对上,而且主回路必须做抗饱和——副回路顶到限幅时,主回路如果还在积分,退出来又是一次大滞后。工程上一般给主回路加一个“副回路饱和则冻结积分”的逻辑。
血泪教训是采样周期这件事。早年图省事,把 PID 调用放在 OB1 里,Ts 写 0.2,觉得只要不太快就没事。结果一台设备在通信负载一上来就开始摆,查了两天才发现扫描周期在 15 ms 到 40 ms 之间跳。从那以后我每次新建回路,第一件事就是把调用拖进循环中断 OB,把 Ts 和 OB 周期对一遍,再往下写代码。这个动作花不到一分钟,能省掉的返工不止一次。希望帮到你。
本文还有配套的精品资源,点击获取