文章目录

ST输入输出参数:VAR_INPUT 只读与 VAR_OUTPUT 引用的传递机制

发布于 2026-03-20 00:30:44 · 浏览 3 次 · 评论 0 条

在结构化文本(ST)编程中,VAR_INPUTVAR_OUTPUT 是定义程序块(如 FB、FC 或 PRG)接口的核心语法。它们不是简单的变量声明,而是隐含特定数据流向与内存访问规则的契约——理解其底层传递机制,是避免逻辑错误、调试失效、信号丢失等典型自动化故障的关键。


一、先明确:ST 中的“参数” ≠ “普通变量”

许多工程师初学 ST 时,习惯把 VAR_INPUT 当作“可读可写”的本地变量,把 VAR_OUTPUT 当作“只写缓存区”,这是根本性误解。ST 参数的本质是调用时由调用方提供的一段内存地址的绑定关系,而非独立分配的存储空间。它的行为取决于两点:

  1. 参数声明关键字VAR_INPUT / VAR_IN_OUT / VAR_OUTPUT);
  2. 调用时实参的类型与来源(字面量、全局变量、局部变量、数组元素、结构体成员等)。

下面以一个标准功能块 FB_MotorCtrl 为例展开说明:

FUNCTION_BLOCK FB_MotorCtrl
VAR_INPUT
    Start: BOOL;          // 输入:启动命令
    Stop: BOOL;            // 输入:停止命令
    SpeedSet: REAL;        // 输入:设定转速(单位:rpm)
END_VAR
VAR_OUTPUT
    Running: BOOL;         // 输出:运行状态
    ActualSpeed: REAL;     // 输出:实际转速
    Fault: WORD;           // 输出:故障代码
END_VAR
VAR
    _internalState: INT;   // 私有变量,仅本块可见
END_VAR

当在主程序中调用该功能块时:

PROGRAM PLC_PRG
VAR
    Motor1: FB_MotorCtrl;
    CmdStart: BOOL := FALSE;
    CmdStop: BOOL := FALSE;
    SetRpm: REAL := 1500.0;
    IsRunning: BOOL;
    MeasuredRpm: REAL;
    ErrCode: WORD;
END_VAR

Motor1(
    Start := CmdStart,
    Stop := CmdStop,
    SpeedSet := SetRpm,
    Running => IsRunning,
    ActualSpeed => MeasuredRpm,
    Fault => ErrCode
);

注意:此处 => 是 ST 中输出参数的赋值箭头,它不表示“等于”,而表示“将本块内部的输出变量值,复制到右侧指定的目标地址”。


二、VAR_INPUT 的“只读性”:不是语法限制,而是语义约束

VAR_INPUT 声明的变量,在块内部允许被读取,禁止被写入——这是 IEC 61131-3 标准强制规定的语义规则,编译器会在编译时报错,例如:

// ❌ 编译错误:无法向输入参数 'Start' 赋值
Start := TRUE;

// ❌ 编译错误:不能对输入参数取地址并修改
ADR(Start)^ := 1;

// ✅ 允许:仅读取
IF Start AND NOT Stop THEN
    _internalState := 1;
END_IF

但需特别注意:“只读”仅作用于块内部对参数名的直接赋值操作。它不阻止通过指针间接修改——前提是该输入参数本身来自一个可写的左值(lvalue)。例如:

// 假设调用时传入的是可写变量:
Motor1(Start := CmdStart, ...);

// 在 FB_MotorCtrl 内部,以下代码仍非法(编译报错):
// Start := FALSE;  // ❌ 明确禁止

// 但若通过指针绕过语法检查(极不推荐,且多数平台禁用):
// P_Start: POINTER TO BOOL;
// P_Start := ADR(Start);  // ⚠️ 即使编译通过,也违反语义契约,导致调用方意外被修改
// P_Start^ := TRUE;       // ❌ 实际可能改写 CmdStart,造成隐蔽逻辑错误

因此,VAR_INPUT 的“只读”本质是编译期强制的单向数据流声明:数据只能从调用方 → 功能块内部,不可反向。这保障了模块化设计中的可预测性——你永远可以确信,调用一个 FB 不会悄悄改写你的命令变量。


三、VAR_OUTPUT 的“引用传递”真相:其实是值复制,但有关键优化

这是最常被误解的机制。许多资料称 VAR_OUTPUT 是“引用传递”,但严格来说:ST 中所有参数默认都是值传递(pass-by-value),VAR_OUTPUT 也不例外。区别在于:

  • VAR_INPUT:调用时,将实参的当前值复制进功能块的输入区;
  • VAR_OUTPUT:执行完后,将功能块内输出变量的最终值复制回调用方指定的目标地址。

所谓“引用”,仅体现在语法糖 => 的右侧必须是一个可写左值(lvalue),即必须能被赋值的地址,例如:

合法实参(lvalue) 非法实参(rvalue)
IsRunning(变量名) TRUE(字面量)
MyArray[2](数组元素) 100 + 50(表达式)
Motor.Status.Running(结构体成员) GetFlag()(函数返回值)

验证方式很简单:尝试把输出连到一个常量或表达式,编译器立即报错 Expected lvalue

更重要的是:这种复制发生在功能块执行完毕后,一次性完成。这意味着:

  • 输出值在块执行过程中不会实时更新调用方变量;
  • 多个输出参数之间无执行时序依赖——它们的复制动作是原子性的(按声明顺序逐个复制,但对外表现为“同时生效”);
  • 若调用方将同一变量既作输入又作输出(如 SpeedSet => SpeedSet),则存在未定义行为(UB) ——因为输入值在块开始时已固定,而输出值在块结束时才写回,二者是否冲突取决于编译器实现,应绝对避免。

四、对比 VAR_IN_OUT:唯一真正支持“双向值传递”的参数类型

当确实需要在块内修改调用方变量,并让修改结果立即反馈给调用方逻辑时,必须使用 VAR_IN_OUT

FUNCTION_BLOCK FB_Counter
VAR_IN_OUT
    Count: UINT;   // 可读可写,值在调用前后均有效
END_VAR
VAR_INPUT
    Inc: BOOL;
END_VAR
VAR_OUTPUT
    Overflow: BOOL;
END_VAR

IF Inc THEN
    Count := Count + 1;  // ✅ 允许:修改传入的 Count 变量本身
    IF Count > 65535 THEN
        Overflow := TRUE;
        Count := 0;
    END_IF
END_IF

调用方式:

MyCounter(Inc := PushBtn, Count => CurrentCount);

此时 CurrentCount 在每次调用 MyCounter 时,先被读取(作为 Count 初始值),再被块内逻辑修改,最后原地更新(而非复制新值)。这就是真正的“双向值传递”——但它要求实参必须是可写左值,且承担被修改的风险。

✅ 正确场景:计数器、累加器、状态机上下文保持。
❌ 错误场景:用于传递配置参数(如 MaxSpeed => MaxSpeed),会破坏参数语义清晰性。


五、深层机制:内存布局与执行周期视角

PLC 扫描周期分为三个阶段:输入采样 → 程序执行 → 输出刷新。VAR_INPUT/VAR_OUTPUT 的行为必须放在这个背景下理解:

  1. 输入采样阶段:物理输入点(I0.0、AIW2 等)状态被锁存到过程映像区(PII)。
  2. 程序执行阶段
    • 所有 VAR_INPUT 的值,来自过程映像区或调用方变量的当前快照(非实时读取);
    • VAR_OUTPUT 的值在块内计算,但不立即写入过程映像区(PIQ)
    • 它们仅保存在功能块实例的数据块(DB)中对应偏移位置。
  3. 输出刷新阶段:所有 VAR_OUTPUT 值(经 => 指定的目标)被批量复制到过程映像区,再由硬件扫描写入物理输出点。

这意味着:

  • 在同一个扫描周期内,VAR_INPUT 的值绝不会因其他逻辑改变而动态更新
  • VAR_OUTPUT 的值在块执行中修改多次,只有最后一次有效;
  • 若多个功能块共用同一输出目标(如 Motor1.Running => Q0.0; Motor2.Running => Q0.0),则后执行的块覆盖先执行的块——顺序决定最终值。

六、实战避坑清单(直接可用)

风险现象 根本原因 正确做法
Start 输入在块内始终为 FALSE 调用时 Start := FALSE 字面量传入 改用变量 Start := CmdStart,确保信号源有效
Running 输出延迟一个周期才变化 => 目标是中间变量,该变量又被其他逻辑覆盖 输出直连过程映像区(如 Running => Q0.0)或确保单源驱动
功能块重复调用导致计数错乱 误用 VAR_INPUT 存储状态(如 Count: UINT 状态必须用 VARVAR_IN_OUTVAR_INPUT 仅传指令
Fault => MyAlarm 未触发报警 MyAlarm 是常量或表达式,=> 绑定失败 检查 MyAlarm 是否为可写变量;启用编译器“lvalue 检查”
两个 FB 同时写同一输出位,结果不确定 多源输出竞争,无仲裁 使用 MOVESEL 显式仲裁,或统一由一个 FB 管理输出

七、高级技巧:利用 VAR_INPUT CONST 实现只读常量注入

某些平台(如 Codesys、TwinCAT)支持 VAR_INPUT CONST,用于在实例化时注入不可变配置:

FUNCTION_BLOCK FB_PID
VAR_INPUT CONST
    Kp: REAL := 1.0;      // 编译期常量,每个实例可不同
    Ki: REAL := 0.1;
    Kd: REAL := 0.05;
END_VAR
VAR_INPUT
    SP: REAL;             // 运行时可变设定值
    PV: REAL;             // 运行时可变过程值
END_VAR

调用时:

PID_AirFlow(
    Kp := 2.5, Ki := 0.15, Kd := 0.02,
    SP := AirSP, PV := AirTemp
);

Kp/Ki/Kd 在编译时固化为该实例的只读参数,不占运行时 RAM,且无法在块内修改——这是比 VAR 更安全的配置管理方式。


八、总结:三句话抓住核心

  • VAR_INPUT单向只读快照:它保证调用方信号不被意外篡改,值在块执行开始时锁定;
  • VAR_OUTPUT单向值复制终点:它不提供实时引用,而是在块执行完毕后,将结果可靠、确定地投递到指定左值;
  • 所谓“引用”,本质是语法强制的左值绑定能力,而非 C/C++ 那样的内存地址别名;真要双向修改,请用 VAR_IN_OUT 并承担相应责任。

评论 (0)

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

扫一扫,手机查看

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