ST语言中临时变量(TEMP)在上升沿指令中失效的存储区修正

发布于 2026-03-17 02:25:00 · 浏览 5 次 · 评论 0 条

在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(即bInputFALSETRUE的上升沿被捕捉)。

实际行为: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=TRUEQ=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;

✅ 优势:

  • 无需新增变量,仅调整声明位置;
  • rTriggerCLK_PREV在每次扫描后自动保留,上升沿判断100%可靠;
  • 所有IEC 61131-3兼容PLC原生支持。

⚠️ 注意:

  • 若该POU被多处调用(如FB实例化),需确认rTrigger是否需独立状态——此时应改用VAR_IN_OUTVAR声明在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>&nbsp;&nbsp;trig : R_TRIG;<br>&nbsp;&nbsp;trig(CLK:=arr[i]);<br>END_FOR; 每次循环迭代都新建trig,状态无法保持 trig声明移至循环外VAR区,循环内复用同一实例
在IF...THEN分支内声明 IF mode=1 THEN<br>&nbsp;&nbsp;r1 : R_TRIG;<br>&nbsp;&nbsp;r1(CLK:=x);<br>ELSIF mode=2 THEN<br>&nbsp;&nbsp;r2 : R_TRIG;<br>&nbsp;&nbsp;r2(CLK:=y);<br>END_IF; 分支内声明等同VAR_TEMP,状态不保留 统一在VAR区声明r1, r2,分支内仅调用
使用全局变量但未初始化 VAR_GLOBAL<br>&nbsp;&nbsp;g_rTrig : R_TRIG;<br>END_VAR 全局变量若未显式初始化,厂商实现可能设为随机值,首周期上升沿判断不准 显式初始化:g_rTrig : R_TRIG := (CLK := FALSE);

五、验证方法:用PLC内置工具确认修复效果

  1. 在线监控法

    • 在PLC编程软件中,对rTrigger实例(方案1)或bInput_Prev(方案2)添加在线监控;
    • 强制bInputFALSETRUE,观察rTrigger.CLK_PREV是否从FALSETRUE(即正确更新),且下次扫描前保持TRUE
  2. 扫描计数器法(推荐):
    创建全局变量GVL_ScanCounter : UINT;,在主循环第一行执行GVL_ScanCounter := GVL_ScanCounter + 1;
    在测试程序中,当bOutput=TRUE时记录GVL_ScanCounter值;
    多次触发,确认输出周期严格等于输入上升沿所在周期(无延迟、无跳变)。

  3. 信号发生器注入法(实验室级):
    用函数发生器输出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/1500R_TRIGVAR_TEMP中可能“偶发工作”,实为CLK_PREV初始化巧合,不可依赖;
  • 倍福TwinCAT:严格遵循标准,VAR_TEMPR_TRIG必然失效,报编译警告;
  • 罗克韦尔Studio 5000:ST编辑器会高亮提示“Instance declared in TEMP section may lose state”;
  • 国产PLC(如汇川、信捷):多数已兼容标准,但低端型号可能存在初始化行为不一致,一律按方案1处理最稳妥。

八、终极检查清单(部署前必做)

在将含边沿检测的ST代码下载至PLC前,请逐项确认:

  1. R_TRIG(或F_TRIG, TON等状态型FB)的实例声明是否位于VARVAR_IN_OUT或FB内部VAR区?
    → ✅ 是;❌ 否(仍在VAR_TEMP或循环/分支内)。

  2. 所有用于保存历史状态的变量(如bPrev, nCount)是否均未声明为VAR_TEMP
    → ✅ 是;❌ 否。

  3. 若使用手动实现,bPrev := bCurrent赋值是否位于整个逻辑块最后一行
    → ✅ 是(确保本次判断基于旧值,更新发生在判断后);❌ 否(顺序颠倒将导致逻辑错误)。

  4. 全局变量(如GVL_ScanCounter)是否已在全局数据块(GDB)中正确定义并初始化?
    → ✅ 是;❌ 否(未初始化的全局变量值不确定)。

完成以上四步,上升沿失效问题将彻底根除。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文