函数与函数块
SCL 支持三种程序组织单元 (POU, Program Organization Unit):FUNCTION、FUNCTION_BLOCK 和 PROGRAM。此外还支持 METHOD、PROPERTY 和 INTERFACE 等面向对象扩展。正确选择 POU 类型是结构化 PLC 编程的基础。
1. FUNCTION (FC) — 无记忆逻辑函数
FUNCTION(无记忆函数)适合纯组合逻辑运算:输入相同则输出永远相同,不依赖内部状态。
声明语法
FUNCTION 函数名 : 返回数据类型
VAR_INPUT
输入变量名 : 数据类型;
// ...
END_VAR
VAR_OUTPUT
输出变量名 : 数据类型;
// ...
END_VAR
VAR_IN_OUT
输入输出变量名 : 数据类型;
// ...
END_VAR
VAR
临时变量 : 数据类型;
// ...
END_VAR
// 函数体代码
// 必须给函数名赋值
函数名 := 返回值;
END_FUNCTION
示例:温度单位转换
FUNCTION CelsiusToFahrenheit : REAL
VAR_INPUT
celsius : REAL;
END_VAR
CelsiusToFahrenheit := celsius * 9.0 / 5.0 + 32.0;
END_FUNCTION
示例:多输出 FC
FUNCTION MinMax : VOID
VAR_INPUT
value1 : REAL;
value2 : REAL;
value3 : REAL;
END_VAR
VAR_OUTPUT
minVal : REAL;
maxVal : REAL;
END_VAR
minVal := value1;
maxVal := value1;
IF value2 < minVal THEN minVal := value2; END_IF;
IF value3 < minVal THEN minVal := value3; END_IF;
IF value2 > maxVal THEN maxVal := value2; END_IF;
IF value3 > maxVal THEN maxVal := value3; END_IF;
END_FUNCTION
使用指导
- 适用场景:数学运算、数据转换、校验计算、逻辑判断
- 无持久记忆:每次调用初始化为默认值,不保留历史状态
- 全局变量:可通过访问全局变量
"全局变量名"来获取外部状态 - 返回值:必须给函数名赋值,否则返回未定义值
2. FUNCTION_BLOCK (FB) — 带记忆函数块
FUNCTION_BLOCK(带记忆函数块)适合需要保持状态的逻辑:定时器、计数器、PID 调节器、轴控制等。
声明语法
FUNCTION_BLOCK 函数块名
VAR_INPUT
输入变量 : 数据类型;
// 由调用方赋值
END_VAR
VAR_OUTPUT
输出变量 : 数据类型;
// 由 FB 内部赋值,调用方读取
END_VAR
VAR_IN_OUT
输入输出变量 : 数据类型;
// 引用传递
END_VAR
VAR
私有变量 : 数据类型;
// 内部状态,保持持久
END_VAR
// 执行逻辑
// 可以访问和修改 VAR 和 VAR_OUTPUT 变量
END_FUNCTION_BLOCK
示例:边缘检测 (R_TRIG)
FUNCTION_BLOCK R_TRIG
VAR_INPUT
CLK : BOOL;
END_VAR
VAR_OUTPUT
Q : BOOL;
END_VAR
VAR
M : BOOL;
END_VAR
Q := CLK AND NOT M;
M := CLK;
END_FUNCTION_BLOCK
示例:累加平均器
FUNCTION_BLOCK MovingAverage
VAR_INPUT
x : REAL;
enable : BOOL;
reset : BOOL;
END_VAR
VAR_OUTPUT
average : REAL;
count : UINT;
END_VAR
VAR
sum : REAL;
n : UINT;
END_VAR
IF reset THEN
sum := 0.0;
n := 0;
average := 0.0;
count := 0;
ELSIF enable THEN
sum := sum + x;
n := n + 1;
count := n;
IF n > 0 THEN
average := sum / INT_TO_REAL(n);
END_IF;
END_IF;
END_FUNCTION_BLOCK
使用指导
- 适用场景:定时器、计数器、状态机、轴控制、PID 调节、滤波器
- 持久记忆:
VAR和VAR_OUTPUT变量在两次调用之间保持值不变 - 实例化:必须创建实例才能使用(见第 8 节)
- 生命周期:实例数据存储在背景数据块 (Instance DB) 中
3. PROGRAM — 主程序组织单元
PROGRAM 是 PLC 执行循环的顶层入口,由操作系统周期性调用。
声明语法
PROGRAM 程序名
VAR_INPUT
// ...
END_VAR
VAR_OUTPUT
// ...
END_VAR
VAR
// 私有变量,保持持久
END_VAR
// 主循环逻辑
END_PROGRAM
示例:主循环
PROGRAM MainCycle
VAR_INPUT
emergencyStop : BOOL;
END_VAR
VAR_OUTPUT
systemReady : BOOL;
END_VAR
VAR
rTrig1 : R_TRIG;
alarmActive : BOOL;
END_VAR
// 急停检查
IF emergencyStop THEN
systemReady := FALSE;
// 调用安全停止逻辑
StopAllAxes();
RETURN;
END_IF;
// 边缘检测
rTrig1(CLK := emergencyStop);
IF rTrig1.Q THEN
alarmActive := TRUE;
END_IF;
systemReady := TRUE;
// 调用其他 FC/FB
MainSequence();
END_PROGRAM
使用指导
- 顶级入口:在 Darra PLC 项目中,
OB1是默认的主 PROGRAM - 持久记忆:
VAR变量在扫描周期之间保持值 - 不自带参数传递:通常通过全局变量
"全局DB".变量与其他 POU 通信 - 可调用:内部可以调用 FC、FB 实例和其他 POU
4. METHODS — FB 方法
声明语法
FUNCTION_BLOCK 函数块名
VAR_INPUT
// ...
END_VAR
VAR
// 私有变量
END_VAR
METHOD 方法名 : 返回类型
VAR_INPUT
参数名 : 数据类型;
END_VAR
VAR_OUTPUT
// ...
END_VAR
// 方法体
方法名 := 计算结果;
END_METHOD
END_FUNCTION_BLOCK
示例:带方法的 FB
FUNCTION_BLOCK AxisControl
VAR_INPUT
position : REAL;
velocity : REAL;
END_VAR
VAR
currentPos : REAL;
targetPos : REAL;
enabled : BOOL;
END_VAR
METHOD Enable : BOOL
VAR_INPUT
enable : BOOL;
END_VAR
enabled := enable;
Enable := enabled;
END_METHOD
METHOD MoveTo : BOOL
VAR_INPUT
target : REAL;
speed : REAL;
END_VAR
targetPos := target;
velocity := speed;
currentPos := currentPos + 0.001; // 模拟运动
MoveTo := ABS(targetPos - currentPos) < 0.01;
END_METHOD
METHOD GetPosition : REAL
GetPosition := currentPos;
END_METHOD
// FB 主执行体(在每个扫描周期被调用)
IF enabled THEN
currentPos := position;
END_IF;
END_FUNCTION_BLOCK
调用方式
VAR
axis1 : AxisControl;
atTarget : BOOL;
END_VAR
axis1.Enable(enable := TRUE);
atTarget := axis1.MoveTo(target := 100.0, speed := 10.0);
方法重载
SCL 支持方法重载,通过参数数量或类型区分:
FUNCTION_BLOCK Calculator
METHOD Add : REAL
VAR_INPUT
a : REAL;
b : REAL;
END_VAR
Add := a + b;
END_METHOD
METHOD Add : REAL
VAR_INPUT
a : REAL;
b : REAL;
c : REAL;
END_VAR
Add := a + b + c;
END_METHOD
END_FUNCTION_BLOCK
5. PROPERTIES — FB 属性 (GET / SET)
声明语法
FUNCTION_BLOCK 函数块名
VAR
_value : REAL;
_min : REAL;
_max : REAL;
END_VAR
PROPERTY Value : REAL
GET
Value := _value;
END_GET
SET
IF _min <= Value AND Value <= _max THEN
_value := Value;
END_IF;
END_SET
END_PROPERTY
PROPERTY ClampedValue : REAL
GET
ClampedValue := _value;
END_GET
END_PROPERTY
END_FUNCTION_BLOCK
使用示例
VAR
sensor : FB_Sensor;
v : REAL;
END_VAR
// SET——调用属性的 SET 访问器
sensor.Value := 25.5;
// GET——调用属性的 GET 访问器
v := sensor.Value;
使用指导
- GET 只读:只声明
GET不声明SET即为只读属性 - SET 验证:可在
SET中添加范围检查、触发事件等额外逻辑 - 命名约定:私有变量建议加
_前缀(如_value)以区别于属性名
6. VAR_IN_OUT — 引用参数传递
声明语法
VAR_IN_OUT 实现引用传递 (pass by reference),调用方必须传入变量(非字面量),FB/FC 内部对该变量的修改会直接反映到调用方。
FUNCTION_BLOCK FB_Controller
VAR_IN_OUT
// 引用传递实际变量,函数内修改直接反映到外部
refValue : REAL;
refArray : ARRAY[0..9] OF INT;
END_VAR
VAR_INPUT
gain : REAL;
END_VAR
refValue := refValue * gain;
END_FUNCTION_BLOCK
调用方式
VAR
ctrl : FB_Controller;
processVar : REAL := 10.0;
data : ARRAY[0..9] OF INT;
END_VAR
// 必须传入变量,不能传常量
ctrl(refValue := processVar,
refArray := data,
gain := 2.0);
// processVar 已被修改为 20.0
与 VAR_OUTPUT 区别
| 特性 | VAR_OUTPUT | VAR_IN_OUT |
|---|---|---|
| 传递方式 | 值传递(内部复制) | 引用传递(直接操作原变量) |
| 可传字面量 | 是 | 否(必须传变量) |
| 内部修改 | 影响 FB 内部副本 | 直接修改调用方变量 |
| 适用场景 | 计算结果返回 | 需要修改调用方数据 |
7. 调用约定 — 位置绑定 vs 命名绑定
位置绑定 (Positional)
按参数声明顺序直接填入值,不写参数名:
// 假设 FC 声明为:FUNCTION Calc : REAL (a:REAL, b:REAL, op:INT)
result := Calc(10.0, 20.0, 1); // 位置绑定
优点:简洁 缺点:参数顺序必须完全正确,参数多时易错,可读性差
命名绑定 (Named)
显式指定参数名,不依赖顺序:
result := Calc(a := 10.0, b := 20.0, op := 1);
result := Calc(op := 1, a := 10.0, b := 20.0); // 顺序无关
优点:可读性好,允许中间参数使用默认值,顺序任意 缺点:稍长
混合使用
// 允许前几个参数位置绑定,其余命名绑定
result := Calc(10.0, 20.0, op := 1);
推荐做法
- 参数 ≤ 3 个:可选位置绑定,适合简单辅助函数
- 参数 ≥ 4 个:强制命名绑定,提高可维护性
- FB 调用:始终使用命名绑定,提升代码可读性
8. FB 实例化
单实例 (Single Instance)
VAR
// 声明一个 FB 实例
myTimer : TON;
elapsed : BOOL;
END_VAR
// 调用实例(隐式传递输入参数)
myTimer(IN := trigger, PT := T#5S);
elapsed := myTimer.Q;
多实例 (Multi-Instance)
在另一个 FB 中声明多个实例:
FUNCTION_BLOCK FB_MultiAxisController
VAR
// 多实例声明
axisX : AxisControl;
axisY : AxisControl;
axisZ : AxisControl;
END_VAR
// 分别调用每个实例
axisX(position := xPos, velocity := 100.0);
axisY(position := yPos, velocity := 100.0);
axisZ(position := zPos, velocity := 100.0);
END_FUNCTION_BLOCK
实例数组 (Array of Instances)
FUNCTION_BLOCK FB_ArrayController
VAR
// 实例数组(最多 8 个轴)
axes : ARRAY[1..8] OF AxisControl;
i : INT;
END_VAR
FOR i := 1 TO 8 DO
axes[i](position := posArray[i],
velocity := velArray[i]);
END_FOR;
END_FUNCTION_BLOCK
实例化指导
| 方式 | 语法 | 存储位置 | 适用场景 |
|---|---|---|---|
| 单实例 | timer : TON; | 调用方背景 DB | 少量使用 |
| 多实例 | 在 FB 内声明其他 FB | 当前 FB 的背景 DB | 模块化封装 |
| 实例数组 | axes : ARRAY[1..N] OF FB_Type; | 当前 FB 的背景 DB | 同类设备批量控制 |
9. 递归支持
递归函数
FUNCTION Factorial : DINT
VAR_INPUT
n : DINT;
END_VAR
IF n <= 1 THEN
Factorial := 1;
ELSE
Factorial := n * Factorial(n := n - 1);
END_IF;
END_FUNCTION
限制与注意事项
- 有限递归深度:Darra PLC 运行时限制递归深度,默认上限为 32 层(可通过编译器选项调整)
- 无栈溢出保护:深度超过限制时行为未定义,设计时需确保递归深度可控
- 不建议在循环中断中使用:递归可能增加扫描周期抖动
- 替代方案:对于深度不确定的递归,优先使用迭代(
FOR/WHILE循环)替代
迭代替代示例
FUNCTION FactorialIter : DINT
VAR_INPUT
n : DINT;
END_VAR
VAR
i : DINT;
result : DINT;
END_VAR
result := 1;
FOR i := 2 TO n DO
result := result * i;
END_FOR;
FactorialIter := result;
END_FUNCTION
10. 内联函数
当前版本不支持内联函数(inline 关键字)。以下替代方案可供参考:
| 需求 | 替代方案 |
|---|---|
| 避免函数调用开销 | 将逻辑直接写在调用处 |
| 代码复用 | 使用 FUNCTION 封装,编译器优化 |
| 宏定义效果 | 使用全局常量 + 表达式 |
11. INTERFACE / IMPLEMENTS
接口声明
INTERFACE I_MotionControl
METHOD
Enable : BOOL;
Disable : BOOL;
MoveTo : BOOL;
GetState : INT;
END_INTERFACE
实现接口
FUNCTION_BLOCK FB_ServoAxis IMPLEMENTS I_MotionControl
VAR
enabled : BOOL;
state : INT;
END_VAR
METHOD Enable : BOOL
enabled := TRUE;
state := 1;
Enable := TRUE;
END_METHOD
METHOD Disable : BOOL
enabled := FALSE;
state := 0;
Disable := TRUE;
END_METHOD
METHOD MoveTo : BOOL
VAR_INPUT
target : REAL;
speed : REAL;
END_VAR
// 运动控制逻辑
MoveTo := TRUE;
END_METHOD
METHOD GetState : INT
GetState := state;
END_METHOD
// FB 主执行体
IF enabled THEN
// 周期执行逻辑
END_IF;
END_FUNCTION_BLOCK
多态调用
VAR
// 接口引用变量
motion : I_MotionControl;
servo1 : FB_ServoAxis;
status : BOOL;
END_VAR
// 将 FB 实例赋给接口引用
motion := servo1;
status := motion.Enable();
12. 访问修饰符 (Access Specifiers)
| 修饰符 | 作用域 | 说明 |
|---|---|---|
PUBLIC | 任意位置 | 默认访问级别,外部可读写 |
PRIVATE | 本 FB 内部 | 仅当前 FB 的方法可访问,子类不可见 |
PROTECTED | 本 FB 及其子类 | 当前 FB 和继承它的子类可访问,外部不可见 |
示例
FUNCTION_BLOCK FB_BaseController
VAR
// 默认 PUBLIC
command : INT;
PRIVATE
_internalState : INT;
_errorCode : INT;
PROTECTED
_protectedCounter : INT;
_mode : INT;
END_VAR
METHOD PUBLIC Reset : BOOL
_internalState := 0; // OK:本 FB 内可访问 PRIVATE
_errorCode := 0;
Reset := TRUE;
END_METHOD
METHOD PROTECTED CalcError : BOOL
// 仅供本 FB 和子类调用
CalcError := _errorCode <> 0;
END_METHOD
END_FUNCTION_BLOCK
使用指导
PRIVATE:封装内部实现细节,防止外部意外修改PROTECTED:为子类预留扩展点,同时对外部隐藏实现PUBLIC:公开 API 接口,保持稳定
13. 对比总结表
| 特性 | FUNCTION (FC) | FUNCTION_BLOCK (FB) | PROGRAM | METHOD |
|---|---|---|---|---|
| 持久记忆 | 无 | 有(VAR 变量保持) | 有 | 继承 FB 记忆 |
| 实例化 | 不需要 | 必须实例化 | 编译时自动 | 绑定 FB 实例 |
| 背景数据块 | 无 | 有(Instance DB) | 有 | 共享 FB 的 DB |
| 返回值 | 必须(可 VOID) | 无 | 无 | 可选 |
| 调用方式 | 直接调用 | 实例调用 | 系统调用 | 实例.方法() |
| 适用场景 | 纯函数运算 | 带状态模块 | 顶层循环 | 模块化操作 |
| 重载 | 不支持 | 不支持 | 不支持 | 支持 |
| 接口实现 | 不支持 | 支持 IMPLEMENTS | 不支持 | 接口成员 |
| 属性 | 不支持 | 支持 PROPERTY | 不支持 | 不支持 |
| 访问修饰符 | 不适用 | 支持 | 不适用 | 支持 |
| 递归 | 有限支持 | 有限支持 | 有限支持 | 有限支持 |
| VAR_IN_OUT | 支持 | 支持 | 支持 | 支持 |
选择指南
| 如果... | 选择... |
|---|---|
| 只需要一个计算结果,不依赖历史值 | FUNCTION |
| 需要保持状态(计时、计数、累计) | FUNCTION_BLOCK |
| 需要多个相同模块(多轴、多温区) | FUNCTION_BLOCK + 多实例 |
| 需要顶层循环入口 | PROGRAM |
| 需要封装 FB 的特定操作 | METHOD |
| 需要对外暴露可控属性 | PROPERTY |
| 需要多态和接口契约 | INTERFACE + IMPLEMENTS |
附:完整示例 — 带属性的限幅器 FB
FUNCTION_BLOCK FB_Limiter
VAR_INPUT
rawValue : REAL;
reset : BOOL;
END_VAR
VAR_OUTPUT
clampedValue : REAL;
limitActive : BOOL;
END_VAR
VAR
PRIVATE
_lower : REAL := 0.0;
_upper : REAL := 100.0;
PROTECTED
_clampCount : UDINT;
END_VAR
PROPERTY LowerLimit : REAL
GET
LowerLimit := _lower;
END_GET
SET
_lower := Value;
END_SET
END_PROPERTY
PROPERTY UpperLimit : REAL
GET
UpperLimit := _upper;
END_GET
SET
_upper := Value;
END_SET
END_PROPERTY
METHOD PUBLIC ResetCount : BOOL
_clampCount := 0;
ResetCount := TRUE;
END_METHOD
METHOD PROTECTED GetClampCount : UDINT
GetClampCount := _clampCount;
END_METHOD
// 主执行体
IF reset THEN
_clampCount := 0;
END_IF;
IF rawValue < _lower THEN
clampedValue := _lower;
limitActive := TRUE;
_clampCount := _clampCount + 1;
ELSIF rawValue > _upper THEN
clampedValue := _upper;
limitActive := TRUE;
_clampCount := _clampCount + 1;
ELSE
clampedValue := rawValue;
limitActive := FALSE;
END_IF;
END_FUNCTION_BLOCK
调用示例
VAR
limiter : FB_Limiter;
result : REAL;
active : BOOL;
END_VAR
// 配置限幅
limiter.LowerLimit := -50.0;
limiter.UpperLimit := 150.0;
// 周期调用
limiter(rawValue := RawSignal,
reset := FALSE);
result := limiter.clampedValue;
active := limiter.limitActive;
// 读取钳位次数(需要 PROTECTED 方法——仅供子类扩展)
// limiter.GetClampCount(); // 外部不可调用,编译错误