在ST(Structured Text)语言中编写PLC逻辑时,使用R_TRIG(上升沿触发器)指令配合临时变量(VAR_TEMP)是常见做法。但许多工程师会遇到一个隐蔽却致命的问题:上升沿检测始终不触发,或仅在首次扫描生效、后续扫描完全失效。根本原因不是指令写错,而是ST语言中VAR_TEMP变量的生命周期特性与R_TRIG内部状态存储机制发生冲突。本文将彻底拆解该问题,给出可验证的复现步骤、底层原理分析,并提供三种经过现场验证的修正方案——全部无需修改硬件、不依赖特定品牌PLC,且完全符合IEC 61131-3标准。
一、问题复现:用最简代码暴露失效现象
以下ST代码在多数主流PLC(如西门子S7-1200/1500、倍福TwinCAT、罗克韦尔ControlLogix+Studio 5000 ST编辑器)中均可稳定复现问题:
PROGRAM Test_RisingEdge_Temp
VAR
bInput : BOOL := FALSE;
bOutput : BOOL;
END_VAR
VAR_TEMP
rTrigger : R_TRIG;
END_VAR
// 模拟输入信号:第10个扫描周期置TRUE,持续1个周期后恢复FALSE
IF GVL_ScanCounter = 10 THEN
bInput := TRUE;
ELSE
bInput := FALSE;
END_IF;
// 关键错误写法:在VAR_TEMP中声明R_TRIG实例
rTrigger(CLK := bInput);
bOutput := rTrigger.Q;
预期行为:bOutput 应在 GVL_ScanCounter = 10 的扫描周期内为TRUE(即bInput从FALSE→TRUE的上升沿被捕捉)。
实际行为:bOutput 始终为FALSE。
即使将bInput改为手动操作(如HMI按钮),现象依旧:第一次按下可能触发,第二次及之后完全无响应。
二、核心原理:为什么VAR_TEMP会导致R_TRIG失效?
R_TRIG不是纯函数,而是一个功能块(FB),其内部必须保存前一扫描周期的CLK状态(记为CLK_PREV),才能判断当前周期是否发生上升沿。其逻辑等效于:
$$ Q = CLK \land \lnot CLK\_PREV \\ CLK\_PREV := CLK $$
关键点在于:CLK_PREV必须在两次扫描之间保持值不变。这要求R_TRIG实例所在的变量存储区具备跨扫描周期的数据持久性。
而VAR_TEMP的语义定义(IEC 61131-3 Part 3, §14.2.3)明确指出:
“临时变量(
VAR_TEMP)在每次调用该程序组织单元(POU)时被重新初始化。其值在POU执行结束后不保留。”
这意味着:
- 每次PLC扫描执行
Test_RisingEdge_Temp程序时,rTrigger实例被全新构造; rTrigger.CLK_PREV被重置为初始值(通常是FALSE或未定义);- 即使
bInput当前为TRUE,因CLK_PREV总是FALSE(或随机值),Q看似应触发——但问题在于:R_TRIG的内部状态变量(包括CLK_PREV)的初始化行为由PLC厂商实现决定,多数厂商将其初始化为FALSE,导致第一次bInput=TRUE时Q=TRUE;但第二次触发时,因rTrigger被重建,CLK_PREV再次被设为FALSE,看起来仍能触发——然而,当bInput信号存在抖动、或PLC扫描周期不稳定时,CLK_PREV的随机初值会使上升沿判断失效,表现为间歇性丢失边沿。
更本质的矛盾在于:R_TRIG需要状态记忆,而VAR_TEMP禁止记忆。二者语义直接冲突。
三、三种可靠修正方案(按推荐度排序)
所有方案均满足:
- 不修改PLC硬件配置;
- 不依赖特定厂商扩展指令;
- 符合IEC 61131-3标准,可在任意合规PLC平台移植;
- 无需额外DB/UDT资源(方案1、2)。
方案1:改用VAR声明(推荐|零成本|最高兼容性)
将R_TRIG实例移至VAR区(而非VAR_TEMP),确保其生命周期覆盖整个POU调用周期:
PROGRAM Test_RisingEdge_Fixed
VAR
bInput : BOOL := FALSE;
bOutput : BOOL;
rTrigger : R_TRIG; // ← 移至此处:VAR区,非VAR_TEMP
END_VAR
// 输入模拟逻辑(同前)
IF GVL_ScanCounter = 10 THEN
bInput := TRUE;
ELSE
bInput := FALSE;
END_IF;
rTrigger(CLK := bInput); // ← 此处rTrigger状态跨扫描保持
bOutput := rTrigger.Q;
✅ 优势:
- 无需新增变量,仅调整声明位置;
rTrigger的CLK_PREV在每次扫描后自动保留,上升沿判断100%可靠;- 所有IEC 61131-3兼容PLC原生支持。
⚠️ 注意:
- 若该POU被多处调用(如FB实例化),需确认
rTrigger是否需独立状态——此时应改用VAR_IN_OUT或VAR声明在FB内部(见方案3)。
方案2:手动实现上升沿(精准可控|适合学习与调试)
绕过R_TRIG,用基础布尔运算+持久变量实现等效逻辑。此法完全透明,便于排查时序问题:
PROGRAM Test_RisingEdge_Manual
VAR
bInput : BOOL := FALSE;
bOutput : BOOL;
bInput_Prev : BOOL := FALSE; // ← 持久变量,保存上一周期状态
END_VAR
// 输入模拟逻辑(同前)
IF GVL_ScanCounter = 10 THEN
bInput := TRUE;
ELSE
bInput := FALSE;
END_IF;
// 手动上升沿检测:当前为TRUE且之前为FALSE
bOutput := bInput AND NOT bInput_Prev;
// 更新历史状态(必须放在逻辑末尾!)
bInput_Prev := bInput;
✅ 优势:
- 逻辑完全可见,无黑盒;
bInput_Prev位于VAR区,天然持久;- 可轻松扩展为带滤波的边沿检测(如增加计数器防抖)。
🔧 扩展防抖示例(5周期确认):
VAR
bInput : BOOL;
bOutput : BOOL;
bInput_Prev : BOOL := FALSE;
uDebounceCnt : UINT := 0;
uDebounceThresh : UINT := 5;
END_VAR
IF bInput AND bInput_Prev THEN
uDebounceCnt := MIN(uDebounceCnt + 1, uDebounceThresh);
ELSIF NOT bInput THEN
uDebounceCnt := 0;
END_IF;
bOutput := (uDebounceCnt >= uDebounceThresh) AND NOT bInput_Prev;
bInput_Prev := bInput;
方案3:封装为可重用FB(适合大型项目|避免重复代码)
当多个地方需上升沿检测时,创建专用功能块,将状态变量封装在FB内部:
FUNCTION_BLOCK FB_RisingEdge_Filter
VAR_INPUT
CLK : BOOL;
DebounceCycles : UINT := 0; // 0=无滤波,>0启用计数防抖
END_VAR
VAR_OUTPUT
Q : BOOL;
END_VAR
VAR
bCLK_Prev : BOOL := FALSE;
uCnt : UINT := 0;
bConfirmed : BOOL := FALSE;
END_VAR
// 防抖逻辑
IF DebounceCycles = 0 THEN
// 无滤波:直接判断
Q := CLK AND NOT bCLK_Prev;
ELSE
// 有滤波:上升沿确认需持续DebounceCycles周期
IF CLK AND bCLK_Prev THEN
uCnt := MIN(uCnt + 1, DebounceCycles);
ELSIF NOT CLK THEN
uCnt := 0;
bConfirmed := FALSE;
END_IF;
IF uCnt >= DebounceCycles AND NOT bConfirmed THEN
Q := TRUE;
bConfirmed := TRUE;
ELSE
Q := FALSE;
END_IF;
END_IF;
bCLK_Prev := CLK;
调用方式(在主程序中):
PROGRAM Main
VAR
bButton : BOOL;
bRising : BOOL;
fbEdge : FB_RisingEdge_Filter; // ← 实例声明在VAR区
END_VAR
fbEdge(CLK := bButton, DebounceCycles := 3);
bRising := fbEdge.Q;
✅ 优势:
- 状态完全隔离,多实例互不干扰;
- 一次开发,全项目复用;
- 支持灵活配置(如防抖周期)。
四、其他易踩坑场景与规避指南
| 场景 | 错误写法 | 风险 | 修正建议 |
|---|---|---|---|
| 在FOR循环内声明R_TRIG | FOR i:=1 TO 10 DO<br> trig : R_TRIG;<br> trig(CLK:=arr[i]);<br>END_FOR; |
每次循环迭代都新建trig,状态无法保持 |
将trig声明移至循环外VAR区,循环内复用同一实例 |
| 在IF...THEN分支内声明 | IF mode=1 THEN<br> r1 : R_TRIG;<br> r1(CLK:=x);<br>ELSIF mode=2 THEN<br> r2 : R_TRIG;<br> r2(CLK:=y);<br>END_IF; |
分支内声明等同VAR_TEMP,状态不保留 |
统一在VAR区声明r1, r2,分支内仅调用 |
| 使用全局变量但未初始化 | VAR_GLOBAL<br> g_rTrig : R_TRIG;<br>END_VAR |
全局变量若未显式初始化,厂商实现可能设为随机值,首周期上升沿判断不准 | 显式初始化:g_rTrig : R_TRIG := (CLK := FALSE); |
五、验证方法:用PLC内置工具确认修复效果
-
在线监控法:
- 在PLC编程软件中,对
rTrigger实例(方案1)或bInput_Prev(方案2)添加在线监控; - 强制
bInput从FALSE→TRUE,观察rTrigger.CLK_PREV是否从FALSE→TRUE(即正确更新),且下次扫描前保持TRUE。
- 在PLC编程软件中,对
-
扫描计数器法(推荐):
创建全局变量GVL_ScanCounter : UINT;,在主循环第一行执行GVL_ScanCounter := GVL_ScanCounter + 1;;
在测试程序中,当bOutput=TRUE时记录GVL_ScanCounter值;
多次触发,确认输出周期严格等于输入上升沿所在周期(无延迟、无跳变)。 -
信号发生器注入法(实验室级):
用函数发生器输出1Hz方波(占空比50%)接入PLC输入点;
监控bOutput波形——修复后应得到与输入同频、脉宽=1个PLC扫描周期的精确脉冲。
六、延伸思考:TEMP变量的合理使用边界
VAR_TEMP并非“坏设计”,其价值在于:
- 存储纯中间计算结果(如
tempSum := a + b + c;); - 作为局部缓存(如字符串处理中的索引
iPos); - 绝不用于任何需跨扫描保持状态的元件(
R_TRIG,F_TRIG,TON,TOF,CTU,CTD等)。
经验法则:
当你声明一个变量,目的是“记住上次发生了什么”,它就必须离开
VAR_TEMP。
七、品牌特异性补充说明
- 西门子S7-1200/1500:
R_TRIG在VAR_TEMP中可能“偶发工作”,实为CLK_PREV初始化巧合,不可依赖; - 倍福TwinCAT:严格遵循标准,
VAR_TEMP中R_TRIG必然失效,报编译警告; - 罗克韦尔Studio 5000:ST编辑器会高亮提示“Instance declared in TEMP section may lose state”;
- 国产PLC(如汇川、信捷):多数已兼容标准,但低端型号可能存在初始化行为不一致,一律按方案1处理最稳妥。
八、终极检查清单(部署前必做)
在将含边沿检测的ST代码下载至PLC前,请逐项确认:
-
R_TRIG(或F_TRIG,TON等状态型FB)的实例声明是否位于VAR、VAR_IN_OUT或FB内部VAR区?
→ ✅ 是;❌ 否(仍在VAR_TEMP或循环/分支内)。 -
所有用于保存历史状态的变量(如
bPrev,nCount)是否均未声明为VAR_TEMP?
→ ✅ 是;❌ 否。 -
若使用手动实现,
bPrev := bCurrent赋值是否位于整个逻辑块最后一行?
→ ✅ 是(确保本次判断基于旧值,更新发生在判断后);❌ 否(顺序颠倒将导致逻辑错误)。 -
全局变量(如
GVL_ScanCounter)是否已在全局数据块(GDB)中正确定义并初始化?
→ ✅ 是;❌ 否(未初始化的全局变量值不确定)。
完成以上四步,上升沿失效问题将彻底根除。

暂无评论,快来抢沙发吧!