简介:在西门子博途(TIA Portal)环境下,PID控制是工业自动化中最常用的闭环调节算法之一。这份“西门子 博途 PID SCL 源代码”以结构化文本SCL实现了一个完整的PID功能块,涵盖手动/自动切换、比例积分微分分量独立输出、积分限幅与保持、死区设置、过程变量标定等实用逻辑,变量定义清晰,适合正在学习博途PLC编程或需要移植PID算法的工程师参考。资源压缩包共1个doc文档,大小仅19KB,文档内展示了完整的FUNCTION_BLOCK源码及输入输出变量说明,便于直接对照学习和复制使用。目前已有4180人学习下载。通过研读这段代码,读者可以理解PID算法中比例增益、积分时间、微分时间与采样周期的配合关系,掌握积分保持、微分延迟、干扰前馈等工程细节;同时,代码采用S7_Optimized_Access优化访问模式,对理解博途高级语言编程与模块化封装也有帮助,可直接迁移到温度、压力、流量等常见过程控制场景中。
1. 为什么我最后还是自己写了一份 PID 的 SCL 源代码
入行头几年,我一直觉得博途自带的 PID_Compact 已经是标准答案了,直到现场碰到一个温度回路:滞后大、惯性强,PID_Compact 默认参数跑起来要么超调要么震荡,在线自整定又像一个黑匣子,调了半天也不知道它内部到底怎么算的。那时候才意识到,很多人需要的不是一块“能用”的 PID 块,而是一份看得懂每一行、能自己改逻辑、能对着公式调试的 SCL 源代码。西门子 博途 里用 SCL 写 PID,不是炫技,而是当自带块不够透明、不好移植、不好教学时,把控制权拿回自己手里的一条可靠路径。本文不贴某个现成库的下载,而是把一套可复现的 PID SCL 代码拆开讲清楚:怎么实现、参数怎么设、坑在哪、怎么验证。
这份方案适合谁?一类是刚接触博途的自动化工程师,想弄明白 PID 内部到底做了什么;另一类是设备集成商和工艺工程师,场景里需要把 PID 块做成自己的标准库,或者想绕开自带块的行为限制。读完可以直接照着敲一遍,再按自己的过程特性改结构。
2. 为什么自己写而不是用现成块:黑匣子与明算账
2.1 博途自带 PID 块的局限与我们自己的取舍
博途里 PID_Compact 和 PID_3Step 很好用,接口封装得也够省心。但我在实际项目里遇到过三类麻烦:
第一,块的行为不完全可控。PID_Compact 内部有多套算法切换逻辑,比如从手动切到自动的时候它有自己的无扰处理,但细节不透明。一旦曲线怪起来,你很难说清楚是参数问题还是块内部逻辑在起作用。
第二,移植和复用不方便。如果一个项目要交给另一个厂家维护,或者要把控制逻辑从 S7-1200 移植到别的环境,自带块的背景 DB 结构不透明,连在线监控都得靠专门的面板。
第三,教学和定制受限。想给学生或者新同事讲清楚“积分项到底怎么累加的”,拿 PID_Compact 讲不了底层。
所以我的取舍是:常规回路用自带块,遇到需要定制逻辑、需要透明计算、需要跨平台移植的场合,就写一份自己的 SCL 实现。常见做法是,把一个完整 PID 做成一个 FB,内部把 P、I、D 三项拆开,方便单独观测每一项的贡献。
2.2 位置式与增量式:先选型再动笔
写代码之前先定算法结构。PID 输出形式有两派,位置式和增量式。
位置式的公式是:
y(k) = Kp * e(k) + Ki * Σe + Kd * (e(k) - e(k-1))输出值 y 直接对应执行机构的绝对位置,比如阀门的开度 0-100%。它的好处是语义直观、抗积分饱和可以做在输出限幅上,坏处是积分项是误差的累加和,一旦长期偏差,积分会越攒越多,需要对积分单独限幅。
增量式的公式是:
Δy(k) = Kp * (e(k) - e(k-1)) + Ki * e(k) + Kd * (e(k) - 2*e(k-1) + e(k-2))输出是增量,比如步进电机每次走多少步,或者控制信号每次变化多少。它的好处是天然不带历史积分,执行机构本身有保持功能,位置转成绝对量后才累加。
我给出一份位置式的实现。原因有三个:温度、压力这类慢过程的执行器大多是 0-100% 模拟量阀位,位置式语义和阀位一一对应,调试的时候看得清楚;位置式实现简单,每一行代码都能和我们手算的公式对齐;抗积分饱和放在积分项上做,代码直观,可以单独开关。
如果你要控制的是步进电机这类需要增量输出的场景,则把公式换成增量式推导即可,后面的限幅和模式切换思路通用。
3. 最小可用的 SCL PID 代码:从公式到 FB
3.1 完整 FB 代码:位置式 PID 的 SCL 实现
一个最基础但完整可用的 PID FB 如下,先不考虑自整定和复杂前馈,只把闭环骨架搭起来:
FUNCTION_BLOCK "FB_PID_SCL" // 接口定义 // 输入侧 Setpoint : REAL; // 设定值 ProcessValue : REAL; // 过程值(反馈) Kp : REAL; // 比例增益 Ti : REAL; // 积分时间,单位秒,0表示关闭积分 Td : REAL; // 微分时间,单位秒,0表示关闭微分 OutputMin : REAL; // 输出下限,默认0.0 OutputMax : REAL; // 输出上限,默认100.0 ManualEnable : BOOL; // 手动模式使能 ManualValue : REAL; // 手动时的输出值 CycleTime : REAL; // 调用周期,单位秒 // 输出侧 Output : REAL; // PID输出 IsIntegralActive : BOOL; // 积分是否激活(未饱和) // 静态变量(FB背景数据块里保存) VAR LastError : REAL; // 上一次误差 IntegralSum : REAL; // 积分累加和 LastOutput : REAL; // 上一次输出,用于手动切换无扰 END_VAR // --- 主逻辑开始 --- // 1. 手动/自动选择 IF ManualEnable THEN // 手动模式:输出直接等于手动值 Output := ManualValue; LastOutput := Output; // 手动期间积分清零,防止切回自动时跳变 IntegralSum := 0.0; LastError := 0.0; ELSE // 自动模式:计算误差 #Error := Setpoint - ProcessValue; // 2. 比例项 #ProportionalTerm := Kp * #Error; // 3. 积分项 IF Ti > 0.0 THEN #IntegralRate := #Error * CycleTime / Ti; // 积分累加 #TempIntegralSum := IntegralSum + #IntegralRate; // 积分限幅:限幅到输出范围的0.8倍,给比例和微分留出空间 #IntegralMax := (OutputMax - OutputMin) * 0.8; IF #TempIntegralSum >= #IntegralMax THEN IntegralSum := #IntegralMax; IsIntegralActive := FALSE; ELSIF #TempIntegralSum <= (-1.0 * #IntegralMax) THEN IntegralSum := -1.0 * #IntegralMax; IsIntegralActive := FALSE; ELSE IntegralSum := #TempIntegralSum; IsIntegralActive := TRUE; END_IF; // 若积分被限幅,就暂停累加,防止饱和 // 这里采用的条件积分法:只在未限幅时才累加 IF NOT IsIntegralActive THEN IntegralSum := IntegralSum; // 保持当前积分值 END_IF; ELSE IntegralSum := 0.0; END_IF; #IntegralTerm := Kp * IntegralSum; // 4. 微分项 IF Td > 0.0 THEN // 不完全微分:加低通滤波,防止测量噪声放大 #DerivativeTerm := (Td / (Td + CycleTime)) * (#PreviousDerivative) + (Td * (ProcessValue - #PreviousPV) / CycleTime); #PreviousDerivative := #DerivativeTerm; #PreviousPV := ProcessValue; ELSE #DerivativeTerm := 0.0; END_IF; // 5. 输出求和与限幅 #RawOutput := #ProportionalTerm + #IntegralTerm + #DerivativeTerm; IF #RawOutput >= OutputMax THEN Output := OutputMax; ELSIF #RawOutput <= OutputMin THEN Output := OutputMin; ELSE Output := #RawOutput; END_IF; // 更新上一次误差 LastError := #Error; END_IF; END_FUNCTION_BLOCK3.2 代码里必须讲清楚的逻辑细节
这段代码我一行一行拆过,有几个地方是新手容易抄错或者理解偏的。
积分限幅的取值。上面的代码把积分单独限幅在输出范围的 0.8 倍,这是一个工程经验值。如果积分限幅等于输出限幅,那么当积分提前饱和时,比例项和微分项的任何波动都会被“封死”在饱和值上,输出失去调节能力。留出 0.8 倍区间,比例项仍然有反应空间。这个值不是固定的,慢回路可以放到 0.9,快回路建议 0.5-0.7。
为什么积分项前要乘 Kp。我这里为了公式直观,积分项写成了Kp * IntegralSum。如果按教科书标准式写,积分项是Kp/Ti * 积分,Ti 越大积分作用越弱。代码里我用#IntegralRate := #Error * CycleTime / Ti来累加,最后再乘 Kp,这样 Kp 和 Ti 的语义与西门子自带块的参数语义一致。如果你习惯另一套表示,比如直接存一个 Ki,也可以,但同一个项目里参数含义一定要统一,否则换人调试会出大问题。
调用周期 CycleTime 的获取。不要自己拍脑袋填 0.5 或 1 秒,博途 OB30 定时中断可以在 OB 里通过#CycleTime参数读实际扫描周期。常见做法是,在 OB30 里调用这个 FB,并把 OB30 的执行周期传入。如果做不到,则在 FB 内部用RD_SYS_T或系统时钟来推算,但精度差一些,慢回路够用,快回路不建议。
3.3 在博途里建立与调用 FB 的步骤
常见做法是新建 FB,语言选择 SCL,把代码粘进去,接口和静态变量按上面定义填好。调用时注意三件事:
- FB 的实例化背景 DB 会被自动分配,在线监控时可以直接看内部变量。
- 调用周期要和实际的 OB 周期一致,比如 OB30 若设成 100ms,CycleTime 填 0.1,单位是秒。
- 建议把
ManualEnable和ManualValue接到 HMI 上手自动切换控件,方便现场调试。
调用示例代码片段:
// 在 OB30 或 FB 内部调用 "FB_PID_SCL".Instance( Setpoint := "HMI_Setpoint_Temp", ProcessValue := "AI_Temp_Actual", Kp := 2.5, Ti := 120.0, Td := 10.0, OutputMin := 0.0, OutputMax := 100.0, ManualEnable := "HMI_Manual_Mode", ManualValue := "HMI_Manual_Valve", CycleTime := 0.1, Output => "AQ_Valve_Control" );调用时不必把所有参数都每次赋值,但注意,FB 内部静态变量(比如IntegralSum)会保持上次调用的值,这正是 PID 需要的记忆效应。如果你的调用周期中途改变了,比如从 100ms 改成 200ms,必须同时改 CycleTime,否则积分和微分算出来全错,这是最容易翻车的地方。
4. 参数怎么设:从开环测试到闭环整定的三步法
4.1 必调参数组:Kp、Ti、Td 与输出限幅的协同关系
PID 参数不是孤立调的,工程上我习惯先做开环测试,再做闭环整定。
开环测试的做法是:手动模式,给一个阶跃的 ManualValue,观察过程值变化曲线。比如阀门从 30% 跳到 50%,记录温度从多少度开始变化、花了多少时间到达新的平衡点。从这条曲线里能估算出一个关键参数:过程增益 Kp_process = (温度变化量)/(阀门变化量)。这个值用来估算闭环 PID 的初始 Kp。经验公式是Kp_close = 1 / (Kp_process * 延迟时间系数),延迟越小可以给的 Kp 越大,延迟越大则 Kp 只能给得保守些。
Ti 的初始值可以取过程时间常数的 1/2 到 1/3。Td 初始值取纯滞后时间的 0.5 倍左右,如果滞后时间测出来很小,Td 可以先给 0,后面再加。
这些初始值不是玄学,它们从过程机理上来:积分时间太长,消除静差太慢;太短,容易引入低频振荡。微分时间太长,会把测量噪声放大,阀门来回动作,执行器寿命缩短。
4.2 手/自动切换的无扰处理
闭环调试前,有一个必踩的坑:手动切自动瞬间输出跳变。上面代码里的处理方式是在手动期间把IntegralSum清零、LastError清零,这样切换的瞬间,比例项重新计算,输出从当前手动值附近开始逐渐调整,不会跳。
但这种方式在一种情况下仍有微小跳变:手动值和当前 PID 自动输出值差很远时,比例项会产生一个较大的初始输出。更讲究的做法是,手动期间持续计算“假想的自动输出”,切换瞬间再做输出追踪,这就要加一个输出偏移修正。常见做法是:
// 在手动模式下持续计算自动输出,但不执行 #TempAutoOutput := #ProportionalTerm + #IntegralTerm + #DerivativeTerm; // 计算手动输出与自动输出的偏差 #OutputBias := ManualValue - #TempAutoOutput; // 在自动输出上叠加偏置 #AutoOutputWithBias := #TempAutoOutput + #OutputBias; // 切换时用这个带偏置的值进行初始化这个偏置项可以慢慢衰减到零,也可以用SWITCH_TIME做软切换。我一般会保留偏置,直到出现一次有效的自动运行稳定段后再清零,这样现场操作员手动干预完之后切回自动,曲线基本看不出换挡的痕迹。
4.3 正反作用与设定值变化率
现实世界里 PID 不总是“误差增大则输出增大”。如果被控对象是制冷系统,误差增大时要关小阀门,这时 Kp 要取负值,或者说输出方向和误差方向相反。写代码时这个逻辑不应该放在 PID 内部,而是在外部把 ProcessValue 取反或者把 Setpoint 取反。我见过有人直接把 Kp 填成负数,虽然数学上等价,但 Ti 和 Td 正负号容易弄混,建议还是保持 Kp 为正,用接口前取反的方式来处理正反作用。
另一个容易忽略的是设定值变化率限制。如果 HMI 上操作员把设定值从 50 直接改成 80,误差突然变大,PID 的比例项会瞬间给一个很大的输出,阀门猛开,容易超调。常见做法是在 FB 外面加一个斜坡发生器,设定值按照 0.5-2 度/秒的速率变化,而不是直接阶跃。斜坡时间太长影响响应速度,太短没效果,一般根据过程时间常数的 1/5 来取。
5. 西门子博途 SCL PID 常见问题排查:现象、原因、解决
5.1 现象:积分饱和,输出长时间卡死在最大或最小值,回不来
原因:误差一直存在,积分项持续累加,即使输出已经到限幅值,积分仍然在涨。等到误差反向时,积分需要很长时间才能“消化”,导致输出不能及时退出饱和区——这在水箱液位、温度控制这类带执行机构饱和特性的场合非常常见。
解决:代码里已经做了积分限幅,但注意限幅机制本身要正确。上面的条件积分法有一个隐蔽问题:IF NOT IsIntegralActive THEN IntegralSum := IntegralSum;这句是无效赋值,正确做法是积分项只有在未饱和时才参与累加,饱和时保持积分值不变。很多人在这一步抄错,写成“饱和时继续累加”,导致积分饱和完全没解决。排查时在线看IntegralSum是否长期停在限幅边界,如果是,检查累加条件是否写反。
5.2 现象:阀门频繁抖动,执行器寿命明显缩短,输出波形毛刺多
原因:微分项对测量噪声敏感。温度变送器在 0.1 度级别波动时,微分项输出可能被放大几十倍。Td 越大,噪声放大越明显。常见误区是只调低 Td 到 0 就不管了,但比例项本身也可能因为测量噪声而抖动。
解决:在过程值进入 PID 前做一个一阶低通滤波,滤波时间常数取扫描周期的 3-5 倍。代码里也可以像本文一样实现不完全微分:#DerivativeTerm := (Td / (Td + CycleTime)) * #PreviousDerivative + (Td * (ProcessValue - #PreviousPV) / CycleTime)。这个式子把纯微分变成了惯性微分,高频噪声被削弱,而稳态时微分项仍然为零,不影响精度。另外检查变送器模块的滤波设置,比如模拟量模块的输入滤波时间,可设 2-4 档。
5.3 现象:曲线低频振荡,周期在几分钟以上,无论怎么调 Kp 都不稳定
原因:积分作用过强(Ti 太小)或者外部干扰有周期性负载变化。最常见的是 Ti 设得太小,导致积分项在低频段产生 180 度相移,和比例项叠加后形成等幅振荡。另一个原因是 PID 的上游或者下游还有另一个控制器在联动,比如温度控制和流量控制串在一起,两个回路互相影响。
解决:先把 Td 置 0、Ti 置一个很大的数(比如 99999),只留比例项,看曲线是否还能稳定。如果比例项单独稳定,说明是积分参数的问题,逐步缩小 Ti,直到出现衰减振荡后回退 30%。如果比例项单独也不稳定,检查外部联动回路,必要时把另一个回路的工作模式改为手动,隔离出来单独整定。
5.4 现象:SCL 编译报错,变量类型不匹配,REAL 和 TIME 混用
原因:博途 SCL 对类型检查严格。初学者经常把T#2S这类 TIME 常量直接赋值给 REAL 变量,或者用 DINT 去给 REAL 做运算,导致编译不过。
解决:接口里统一用 REAL 表示物理量,周期用 REAL 的秒数,不要用 TIME。如果必须在 HMI 上显示成“秒 毫秒”的格式,在 HMI 端做格式转换,型别转换函数REAL_TO_TIME在 SCL 里别乱用。另外布尔量做IF条件时,SCL 要求必须是 BOOL 类型,不要用IF #Error > 0 THEN之外的类型做逻辑判断。
5.5 现象:程序下载到 PLC 后第一次运行,PID 输出直接跳到了最大,然后慢慢回落
原因:背景 DB 初始化时IntegralSum和LastError都是 0,手动模式切到自动的瞬间,误差等于当前过程值减设定值,比例项直接给一个大的输出。如果过程值离设定值很远,这个初始输出会非常大。
解决:下载后手动运行一段时间,或者加一个启动保护逻辑:当误差绝对值大于某个阈值时,强制 PID 处于手动状态,直到误差进入阈值范围内才允许切自动。常见做法是:
IF ABS(#Error) > 20.0 THEN Output := ManualValue; // 误差过大时,保持手动值 ELSE // 正常PID计算 END_IF;阈值取过程量程的 10%-15%,进入范围后正常投入自动。这个保护在现场非常重要,尤其当操作员误操作把设定值设得离谱时,能避免设备冲击。
6. 用三条曲线验证你的 SCL PID 是否及格
写完代码、跑通编译、参数也调了一轮之后,怎么知道这个 PID 是“能用”还是“凑合”?我习惯用三条曲线来验收。
第一条是设定值阶跃响应。设定值做一次小幅度阶跃,幅度取量程的 5%-10%,观察过程值的上升曲线。合格标准:上升过程有轻微超调(1%-3%)后快速回落,无振荡;不合格的表现是:超调超过 10%,或者出现二次以上过零振荡。这里可以直接关掉微分,只看纯比例加积分的效果,如果这都不合格,说明 Kp 和 Ti 的比值不对,需要回调。
第二条是扰动抑制。在过程稳定的情况下,手动给执行机构一个 5% 的阶跃扰动,模拟外部干扰,观察过程值能否在几十秒(按时间常数估算)内拉回设定值。合格标准:最大偏差不超过量程的 3%-5%,恢复时间不超过两个时间常数。这里考察的核心是积分作用,如果拉回太慢,Ti 偏大,可以缩小 20% 再测。
第三条是手动自动切换的连续性。在自动稳定时切手动,改变一下输出值,再切回自动,观察输出值是否有跳变,过程值是否有明显扰动。合格标准:切换瞬间输出值变化不超过 1%-2%,且无阶跃。这一条能验证背地里IntegralSum是否被正确同步,很多做无扰切换做不好的人,问题就出在手动时没有持续跟踪“假想的自动输出”。
还有一个进阶技巧我经常教别人:不要把 PID 当万能药。对于大滞后过程,单回路 PID 效果有限,常见的做法是在 SCL 里加一个 Smith 预估器,用数学模型预估滞后后的过程值替代当前反馈,但这需要建立相对准确的过程模型,否则不如把 PID 调保守一点。还有前馈控制,比如温度控制中,进料流量变化是可测的,把流量变化折算成前馈量叠加到输出上,能让 PID 的主调工作量大幅减少。
我自己的习惯是,SCL 写 PID 之前先问三个问题:过程滞后多少、时间常数多少、有没有可测的干扰源。这三个问题决定了你代码里需要哪些结构——是否需要微分先行、是否需要前馈项、积分限幅要放到多保守。代码本身很容易复制,但控制方案的选择才是真正决定项目成败的部分。
希望这份从公式到代码到调试路径的完整笔记,能帮你在下一次需要自己写 PID 时少走几段弯路。
本文还有配套的精品资源,点击获取