4、功能块的核心组成:接口(输入/输出/输入输出)、内部变量、静态变量与临时变量

功能块(Function Block,FB)是PLC程序中实现代码复用与模块化设计的核心载体。与单纯封装算法的函数(FC)不同,功能块拥有独立的实例化数据区,这意味着每次调用功能块时,系统都会为其分配独立的内存空间来保存内部状态。理解功能块的内部结构——尤其是接口定义、内部变量、静态变量与临时变量的区别与使用场景——是掌握功能块封装技术的基础,也是从“能写程序”迈向“会设计程序”的关键分水岭。本章将系统拆解功能块的各个组成要素,并结合工程案例说明如何合理规划变量资源。

功能块(FB)内部结构全景图 功能块 FB_ValveControl 接口区(Interface) b_Start r_Setpoint i_Mode 输入(VAR_INPUT) 输入(VAR_INPUT) 输入(VAR_INPUT) b_Running r_Actual 输出(VAR_OUTPUT) 输出(VAR_OUTPUT) i_ErrorCnt 输入输出(VAR_IN_OUT) 内部变量区(VAR) b_InitDone : BOOL t_Timer : TON 静态变量区(VAR_STAT) st_LastValue : REAL i_CallCount : INT 调用方 OB1 调用方 OB2 图例: 输入信号 输出信号 双向信号 接口区 内部变量 静态变量

4.1 接口区:功能块与外部世界的通信契约

核心原理:接口区是功能块与调用方之间进行数据交换的唯一通道,它定义了功能块的“外部视图”。在IEC 61131-3标准中,接口区由三部分组成:输入变量(VAR_INPUT)、输出变量(VAR_OUTPUT)和输入输出变量(VAR_IN_OUT)。输入变量由调用方提供数据,功能块内部只读;输出变量由功能块计算后返回给调用方;输入输出变量则允许调用方传入数据,功能块处理后写回同一变量,实现双向数据传递。

接口设计原则:

  • 最小化原则:接口数量应尽量精简,只暴露必要的参数。过多的接口会增加调用方的理解负担和出错概率。
  • 类型明确原则:每个接口必须明确数据类型(BOOL、INT、REAL、TIME等),避免使用隐式转换。
  • 默认值策略:输入变量应设置合理的默认值,使功能块在未全部赋值时也能安全运行。
  • 方向清晰原则:严格区分输入、输出和输入输出,不要将本应作为输出的变量设计为输入输出类型。
接口类型 关键字 数据流向 典型应用场景 注意事项
输入变量 VAR_INPUT 调用方 → FB 设定值、使能信号、模式选择 FB内部不可修改;建议设置默认值
输出变量 VAR_OUTPUT FB → 调用方 状态反馈、计算结果、报警信息 每次调用时更新;调用方只读
输入输出变量 VAR_IN_OUT 双向传递 累计值、计数器、需要回写的状态字 必须关联实际变量,不能传常量;慎用

实战经验:在西门子TIA Portal中,VAR_IN_OUT与VAR_INPUT在底层实现上有本质区别。VAR_IN_OUT传递的是变量的引用(指针),不占用额外的实例数据区内存;而VAR_INPUT和VAR_OUTPUT在FB的实例化数据区中会分配对应的存储空间。因此,当需要传递大型结构体或数组时,使用VAR_IN_OUT可以显著节省内存。

4.2 内部变量与静态变量:状态保持的两种机制

核心原理:内部变量(VAR)和静态变量(VAR_STAT)都定义在功能块的内部,外部无法直接访问。它们的核心区别在于生命周期和作用域:内部变量在每次调用开始时初始化(或保持上次调用结束时的值,取决于具体实现),而静态变量在功能块实例化后一直存在,直到程序结束或实例被删除。在IEC 61131-3中,功能块的VAR变量实际上具有“记忆”功能——因为每个FB实例都有独立的数据区,所以VAR变量在两次调用之间会保持值不变。

内部变量(VAR)的典型用途:

  • 存储中间计算结果,如PID运算中的偏差累积值。
  • 保存功能块内部的状态机当前状态。
  • 实例化内部子功能块(如定时器TON、计数器CTU)。
  • 缓存需要跨扫描周期使用的临时数据。

静态变量(VAR_STAT)的典型用途:

  • 记录功能块被调用的总次数(调试用)。
  • 保存上一次运行的关键参数,用于变化率检测。
  • 存储需要在整个程序生命周期内保持的配置参数。

重要区别:在CODESYS和博途中,VAR变量与VAR_STAT变量在功能块实例中的存储位置不同。VAR变量存储在实例数据区,而VAR_STAT变量存储在静态数据区。当使用“多重背景”或“参数实例”方式调用FB时,VAR_STAT变量在所有实例间共享,而VAR变量则每个实例独立。这一点在封装库函数时尤其需要关注。

4.3 临时变量:高效但需谨慎的短期存储

核心原理:临时变量(VAR_TEMP)在功能块每次调用时在局部数据栈上分配内存,调用结束后立即释放。它们不占用实例数据区,因此不保持状态。临时变量主要用于存储计算过程中的中间值、循环索引、临时标志位等。由于临时变量不参与状态保持,它们的使用可以显著减少实例数据区的内存占用。

临时变量的使用规范:

  1. 必须初始化:临时变量在每次调用时不会自动初始化,其初始值是未定义的(可能是上次调用残留的数据)。因此,在使用前必须显式赋值。
  2. 避免状态依赖:不能假设临时变量在两次调用之间有任何关联。
  3. 适合存储:循环变量、算术运算的中间结果、临时标志位。
  4. 不适合存储:需要跨扫描周期保持的数据、需要被多个方法共享的数据。
// 临时变量使用示例(结构化文本ST)
FUNCTION_BLOCK FB_CalcAverage
VAR_INPUT
    r_Value1 : REAL;
    r_Value2 : REAL;
END_VAR
VAR_OUTPUT
    r_Average : REAL;
END_VAR
VAR_TEMP
    r_Sum : REAL;  // 临时变量,仅用于本次调用
END_VAR

r_Sum := r_Value1 + r_Value2;  // 必须先赋值再使用
r_Average := r_Sum / 2.0;

4.4 案例拆解:电机控制功能块的变量规划

场景背景:某自动化产线需要封装一个电机控制功能块,要求支持启动/停止、速度设定、过流报警、运行时间累计等功能。系统使用西门子S7-1500 PLC,扫描周期约10ms,电机由变频器驱动,通过模拟量输出控制速度。

初始设计(存在问题的版本):

FUNCTION_BLOCK FB_MotorCtrl
VAR_INPUT
    b_Start : BOOL;
    b_Stop : BOOL;
    r_SpeedSet : REAL;
END_VAR
VAR_OUTPUT
    b_Running : BOOL;
    r_SpeedAct : REAL;
    b_Fault : BOOL;
END_VAR
VAR
    b_Init : BOOL := FALSE;
    t_RunTimer : TON;
    r_SpeedRamp : REAL;
    i_AlarmCount : INT;
END_VAR

问题分析:

  • 运行时间累计值i_AlarmCount放在VAR区,每次实例化后从0开始,无法实现断电保持。
  • 速度斜坡r_SpeedRamp在每次扫描时计算,但缺少对上次值的记忆,导致斜坡不平滑。
  • 缺少输入输出变量,无法将累计运行时间回传给HMI显示。

优化后的设计:

FUNCTION_BLOCK FB_MotorCtrl
VAR_INPUT
    b_Start : BOOL := FALSE;
    b_Stop : BOOL := FALSE;
    r_SpeedSet : REAL := 0.0;
    r_RampRate : REAL := 100.0;  // 斜坡速率 单位/s
END_VAR
VAR_OUTPUT
    b_Running : BOOL;
    r_SpeedAct : REAL;
    b_Fault : BOOL;
    w_Status : WORD;  // 状态字
END_VAR
VAR_IN_OUT
    t_RunHours : REAL;  // 累计运行时间(小时),断电保持
END_VAR
VAR
    b_Init : BOOL := FALSE;
    t_RunTimer : TON;
    r_SpeedRamp : REAL;
    st_PrevSpeed : REAL;  // 上一次速度值
END_VAR
VAR_STAT
    i_CallCount : INT;  // 调用次数统计
    st_LastFaultCode : INT;  // 上次故障代码
END_VAR
VAR_TEMP
    r_DeltaSpeed : REAL;
    b_RisingEdge : BOOL;
END_VAR

关键改进点:

  1. VAR_IN_OUT的使用:t_RunHours作为输入输出变量,由调用方(如OB100初始化块)传入一个保持型变量,实现断电保持。
  2. 静态变量的应用:i_CallCount用于监控功能块被调用的频率,辅助调试;st_LastFaultCode保存上次故障代码,便于故障追溯。
  3. 临时变量的合理使用:r_DeltaSpeed仅用于本次扫描的斜坡计算,b_RisingEdge用于检测启动信号的上升沿,均不需要跨周期保持。
  4. 内部变量的状态保持:st_PrevSpeed保存上一次的速度值,用于计算加速度限制。

实施效果:优化后的功能块在连续运行72小时的测试中,速度斜坡平滑无突变,累计运行时间准确,断电重启后数据不丢失。调用方代码量减少了约40%,且功能块的可读性和可维护性显著提升。

经验总结:变量类型的选择本质上是“内存效率”与“功能完整性”之间的权衡。设计功能块时,建议按以下顺序思考:首先确定哪些数据需要与外部交互(接口区),其次确定哪些数据需要跨扫描周期保持(VAR),再次确定哪些数据需要在整个生命周期内全局共享(VAR_STAT),最后将纯计算用的中间量放入VAR_TEMP。

4.5 常见误区与设计复盘

误区一:滥用VAR_IN_OUT

很多初学者将本应作为输出的变量设计为VAR_IN_OUT,理由是“这样调用方可以直接看到变化”。但VAR_IN_OUT要求调用方必须传入一个变量(不能是常量),且功能块内部可以修改该变量,这破坏了封装性。正确的做法是:只有确实需要“读-改-写”同一变量的场景才使用VAR_IN_OUT,如累计值、状态字等。

误区二:忽视临时变量的初始化

在CODESYS V3和TIA Portal V16及以上版本中,VAR_TEMP变量在每次调用时不会自动清零。如果忘记初始化,可能读到上一次调用残留的随机值,导致间歇性故障。建议在功能块开头统一对临时变量赋值。

误区三:静态变量与内部变量混用

在多实例场景下,VAR_STAT变量在所有实例间共享,而VAR变量每个实例独立。如果设计者没有意识到这一点,可能在两个电机控制实例中互相干扰。例如,一个FB_MotorCtrl实例修改了i_CallCount,另一个实例读到的也是修改后的值。

误区四:接口数量失控

一个功能块的输入输出接口超过10个时,调用方的代码会变得难以阅读和维护。此时应考虑将相关参数封装为结构体(STRUCT)或使用UDT(用户自定义类型),减少接口数量。

变量类型 内存位置 生命周期 多实例行为 典型用途
VAR_INPUT 实例数据区 与实例相同 每个实例独立 外部传入的参数
VAR_OUTPUT 实例数据区 与实例相同 每个实例独立 返回给外部的结果
VAR_IN_OUT 引用外部变量 与外部变量相同 共享外部变量 需要双向修改的数据
VAR 实例数据区 与实例相同 每个实例独立 内部状态保持
VAR_STAT 静态数据区 程序运行期间 所有实例共享 全局统计、共享配置
VAR_TEMP 局部数据栈 单次调用 不保留 中间计算、临时标志

设计复盘清单:

  • 每个接口是否有明确的用途?能否通过合并或封装减少接口数量?
  • 哪些数据需要断电保持?应通过VAR_IN_OUT引用保持型变量,而非依赖FB内部变量。
  • 是否有多实例共享数据的需求?如有,使用VAR_STAT;如无,使用VAR。
  • 临时变量是否都已在使用前初始化?
  • 功能块的实例数据区大小是否合理?过大的实例数据区会占用PLC的保持性内存资源。

掌握功能块各组成要素的本质区别与适用场景,是进行高质量PLC程序封装的前提。在实际项目中,建议先绘制功能块的接口定义表和数据流图,再动手编写代码,这样可以有效避免后期返工。下一章将深入探讨功能块的调用方式与实例化管理策略,帮助读者进一步理解如何在复杂工程中灵活运用功能块。