在结构化文本(ST)编程中,VAR_INPUT 和 VAR_OUTPUT 是定义程序块(如 FB、FC 或 PRG)接口的核心语法。它们不是简单的变量声明,而是隐含特定数据流向与内存访问规则的契约——理解其底层传递机制,是避免逻辑错误、调试失效、信号丢失等典型自动化故障的关键。
一、先明确:ST 中的“参数” ≠ “普通变量”
许多工程师初学 ST 时,习惯把 VAR_INPUT 当作“可读可写”的本地变量,把 VAR_OUTPUT 当作“只写缓存区”,这是根本性误解。ST 参数的本质是调用时由调用方提供的一段内存地址的绑定关系,而非独立分配的存储空间。它的行为取决于两点:
- 参数声明关键字(
VAR_INPUT/VAR_IN_OUT/VAR_OUTPUT); - 调用时实参的类型与来源(字面量、全局变量、局部变量、数组元素、结构体成员等)。
下面以一个标准功能块 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 的行为必须放在这个背景下理解:
- 输入采样阶段:物理输入点(I0.0、AIW2 等)状态被锁存到过程映像区(PII)。
- 程序执行阶段:
- 所有
VAR_INPUT的值,来自过程映像区或调用方变量的当前快照(非实时读取); VAR_OUTPUT的值在块内计算,但不立即写入过程映像区(PIQ);- 它们仅保存在功能块实例的数据块(DB)中对应偏移位置。
- 所有
- 输出刷新阶段:所有
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) |
状态必须用 VAR 或 VAR_IN_OUT,VAR_INPUT 仅传指令 |
Fault => MyAlarm 未触发报警 |
MyAlarm 是常量或表达式,=> 绑定失败 |
检查 MyAlarm 是否为可写变量;启用编译器“lvalue 检查” |
| 两个 FB 同时写同一输出位,结果不确定 | 多源输出竞争,无仲裁 | 使用 MOVE 或 SEL 显式仲裁,或统一由一个 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并承担相应责任。

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