ST(Structured Text)是IEC 61131-3标准定义的高级文本编程语言,广泛用于PLC(可编程逻辑控制器)开发。在工业自动化领域,ST因其接近高级语言的表达力而被用于复杂算法、数据处理和状态管理。但ST对递归调用的支持极为有限——它不是语法禁止,而是由底层运行时环境(RT)和资源约束共同决定的“事实不可行”。本文不讲理论假设,只说你实际在TIA Portal、Codesys、GX Works3或Logix Designer中写ST时,能否递归、为什么不能、误写后会发生什么、以及替代方案如何落地。
一、ST语言本身不禁止递归语法,但PLC运行时不支持
IEC 61131-3标准文档(第3版)未明文禁止函数(FUNCTION)或功能块(FUNCTION_BLOCK)的自调用。语法上,以下代码在编译器中可能通过语法检查:
FUNCTION factorial : DINT
VAR_INPUT
n : INT;
END_VAR
IF n <= 1 THEN
factorial := 1;
ELSE
factorial := n * factorial(n - 1); // ← 语法合法,但运行时崩溃
END_IF
表面看,factorial(n - 1) 是一次函数自调用,符合递归定义。但关键在于:PLC没有操作系统级的栈空间管理机制。通用PC的C程序调用factorial(10)会分配10层栈帧,每层存局部变量和返回地址;而PLC的循环任务(Cyclic Task)以固定周期(如10 ms)反复执行,其堆栈是静态预分配、大小固定、无溢出保护的。
主流PLC平台实测结果如下:
| 平台 | 是否允许编译含递归的ST代码 | 运行时行为(n=5时) | 堆栈深度限制(典型值) |
|---|---|---|---|
| Siemens TIA Portal v18(S7-1500) | 编译失败,报错 Error 427: Recursive call not allowed |
— | — |
| Codesys 3.5(Beckhoff CX9020) | 编译通过,但下载后CPU进入STOP模式 | 立即报 Stack overflow during execution |
~16 层(硬编码) |
| Mitsubishi GX Works3(iQ-R系列) | 编译拒绝,提示 Cannot call function recursively |
— | — |
| Rockwell Logix Designer(Studio 5000 v34) | 不支持FUNCTION递归;FB可“伪递归”但需手动控制调用链 | 第2次调用即触发 Task watchdog timeout |
无显式提示,依赖任务周期超时 |
结论明确:所有商用PLC平台均通过编译器拦截或运行时强制终止来阻止递归执行。这不是ST语言缺陷,而是确定性实时系统的基本设计取舍——你不能接受一个电机控制任务因栈溢出而卡死1秒。
二、为什么PLC必须禁用递归?三个硬性约束
1. 栈空间物理固化,无法动态增长
PLC固件在启动时为每个任务分配一块连续RAM区域作为堆栈。例如S7-1500的循环任务默认栈大小为16 KB,且不可配置增大。该空间需同时容纳:
- 所有全局变量(DB块映射)
- 临时局部变量(TEMP区)
- 函数调用的参数副本与返回地址
递归每深入一层,就消耗固定字节数(如INT参数+返回地址≈8字节)。当n=2000时,仅参数就占16 KB,必然覆盖相邻内存,导致变量错乱、通信中断甚至硬件复位。
2. 无栈溢出检测与异常处理机制
PC端C程序遇到栈溢出会触发SIGSEGV信号,OS可捕获并打印core dump;PLC运行时环境(如SIEMENS RTX或Codesys Runtime)不提供任何异常捕获API。一旦越界,直接改写相邻DB块或I/O映像区。现象表现为:
- 某个温度传感器读数突变为
-32768(INT最小值,常为内存踩踏标志) - 输出继电器无规律通断(输出映像区被覆盖)
- Modbus TCP连接频繁断开(通信缓冲区损坏)
此类故障极难复现与定位,因为触发条件依赖于编译器变量布局顺序和当前内存使用率。
3. 违反确定性实时性要求
IEC 61131-3核心目标是保证最坏执行时间(WCET)可预测。递归的执行时间随输入线性/指数增长:
- 线性递归(如阶乘):WCET ∝ n
- 树形递归(如斐波那契):WCET ∝ φⁿ(φ≈1.618)
而PLC任务周期是硬实时约束(如运动控制任务周期必须≤1 ms)。若某次扫描因递归耗时2.3 ms,系统将:
- 跳过本次任务执行(欠采样)
- 或触发看门狗复位(取决于配置)
无论哪种,都意味着控制律失效,可能引发机械碰撞或工艺废品。
三、常见“伪递归”陷阱与识别方法
开发者常误以为以下写法是“安全递归”,实则隐患更隐蔽:
❌ 陷阱1:功能块(FB)实例自引用
// FB_Counter 定义
FUNCTION_BLOCK FB_Counter
VAR
next : FB_Counter; // ← 错误:声明同类型FB实例
count : INT;
END_VAR
METHOD Run
IF count < 10 THEN
count := count + 1;
next.Run(); // ← 表面像递归,实际创建新实例链
END_IF
风险:每次next.Run()创建全新FB实例,占用独立内存。count < 10看似安全,但若逻辑改为count < 1000,将耗尽PLC用户内存(S7-1500典型用户内存仅2 MB),最终下载失败或运行时Memory full报警。
❌ 陷阱2:事件驱动型“递归调用”
// 在OB1中
IF StartButton THEN
CallMyAlgorithm();
END_IF
// 在FB_Algorithm中
METHOD Execute
// ... 处理逻辑
IF Done THEN
// 触发下一轮处理
ProgramInstance.StartButton := TRUE; // ← 通过置位启动信号间接调用
END_IF
表面无函数调用,但本质是异步重入。若Execute执行时间超过扫描周期,StartButton将持续为TRUE,导致算法被重复触发,形成逻辑风暴。现象:CPU负载100%、通信延迟飙升、HMI响应冻结。
✅ 正确识别方式:查编译器输出与符号表
- 在TIA Portal中:编译后打开“诊断日志”,搜索
recursive或stack关键字; - 在Codesys中:右键PLC项目 → “Online” → “Show Runtime Statistics”,观察
Stack Usage是否随任务执行持续增长; - 在GX Works3中:监控软元件
D*(数据寄存器)中与函数相关的地址范围,若出现非预期的批量写入,即存在隐式递归。
四、安全替代方案:四种工业级可行路径
当业务逻辑天然具递归结构(如树遍历、分治排序、嵌套状态机),必须用以下方法重构:
方案1:迭代展开(推荐指数★★★★★)
将递归逻辑转化为WHILE循环+显式栈(数组模拟)。以二叉树中序遍历为例:
// 原始递归(禁止!)
METHOD InOrderRecursive
IF self.Left <> NULL THEN
self.Left.InOrderRecursive();
END_IF
self.Process();
IF self.Right <> NULL THEN
self.Right.InOrderRecursive();
END_IF
// 替代:迭代+栈数组(安全)
TYPE TREE_NODE REFERENCE TO STRUCT
Value : INT;
Left : REFERENCE TO TREE_NODE;
Right : REFERENCE TO TREE_NODE;
END_STRUCT
METHOD InOrderIterative
VAR
stack : ARRAY[0..999] OF REFERENCE TO TREE_NODE; // 静态栈,最大1000层
top : INT := -1;
current : REFERENCE TO TREE_NODE := self;
END_VAR
WHILE (current <> NULL) OR (top >= 0) DO
WHILE current <> NULL DO
top := top + 1;
stack[top] := current;
current := current.Left;
END_WHILE
IF top >= 0 THEN
current := stack[top];
top := top - 1;
current.Process();
current := current.Right;
END_IF
END_WHILE
优势:栈大小可控(ARRAY[0..999])、WCET可计算(最多2×节点数次循环)、内存占用恒定。
方案2:状态机拆解(推荐指数★★★★☆)
将递归步骤映射为状态转移。例如快速排序的递归分区:
// 递归版(禁止)
METHOD QuickSortRec
IF low < high THEN
pivotIndex := Partition(low, high);
QuickSortRec(low, pivotIndex - 1);
QuickSortRec(pivotIndex + 1, high);
END_IF
// 替代:状态机控制分区队列
TYPE SORT_TASK STRUCT
low : INT;
high : INT;
END_STRUCT
METHOD QuickSortStateMachine
VAR
taskQueue : ARRAY[0..255] OF SORT_TASK;
queueHead, queueTail : INT := 0;
currentTask : SORT_TASK;
END_VAR
// 初始化:入队首个任务
IF NOT initialized THEN
taskQueue[0].low := 0;
taskQueue[0].high := arraySize - 1;
queueTail := 1;
initialized := TRUE;
END_IF
// 每次扫描处理一个任务
IF queueHead < queueTail THEN
currentTask := taskQueue[queueHead];
queueHead := queueHead + 1;
IF currentTask.low < currentTask.high THEN
pivot := Partition(currentTask.low, currentTask.high);
// 入队左子区间(模拟递归第二行)
IF queueTail < 256 THEN
taskQueue[queueTail].low := currentTask.low;
taskQueue[queueTail].high := pivot - 1;
queueTail := queueTail + 1;
END_IF
// 入队右子区间(模拟递归第三行)
IF queueTail < 256 THEN
taskQueue[queueTail].low := pivot + 1;
taskQueue[queueTail].high := currentTask.high;
queueTail := queueTail + 1;
END_IF
END_IF
END_IF
优势:完全消除函数调用开销,WCET = 单次Partition耗时 + 固定队列操作,适合实时性严苛场景。
方案3:分段执行(推荐指数★★★☆☆)
将长递归分解为多个扫描周期完成。适用于无法预估深度但允许延时的场景(如配方解析):
METHOD ParseRecipeStepByStep
VAR
state : (IDLE, PARSE_HEADER, PARSE_STEP, PARSE_SUBSTEP, DONE);
stepIndex, substepIndex : INT;
currentLine : STRING[256];
END_VAR
CASE state OF
IDLE:
IF trigger THEN
stepIndex := 0;
state := PARSE_HEADER;
END_IF
PARSE_HEADER:
currentLine := ReadLine(0);
IF currentLine <> '' THEN
ParseHeader(currentLine);
state := PARSE_STEP;
END_IF
PARSE_STEP:
IF stepIndex < totalSteps THEN
currentLine := ReadLine(stepIndex + 1);
ParseStep(currentLine);
stepIndex := stepIndex + 1;
// 下次扫描继续,避免单次超时
ELSE
state := DONE;
END_IF
END_CASE
关键:每个CASE分支确保执行时间<任务周期的50%,通过状态保持实现“跨周期递归”。
方案4:外部计算卸载(推荐指数★★★☆☆)
将递归计算移至上位机(IPC/SCADA),PLC仅负责实时I/O与指令下发:
- 上位机用Python/C#实现递归算法,结果通过OPC UA或MQTT发布;
- PLC订阅结果主题,收到后直接执行动作;
适用场景:AI质检路径规划、多机器人协同调度等超复杂逻辑。
五、终极检查清单:上线前必做5项验证
在将含替代方案的代码投入产线前,执行以下验证:
- 栈用量实测:在TIA Portal中启用“Runtime Trace”,记录
OB1执行期间Stack Usage峰值,确认<70%分配值; - 最坏路径WCET测试:用逻辑分析仪抓取PLC输入中断到输出响应的全链路时间,验证是否≤任务周期×0.8;
- 内存泄漏扫描:连续运行72小时,监控PLC Web服务器中的
Memory Utilization曲线,应无持续上升趋势; - 边界压力测试:将算法输入设为理论最大值(如数组长度=65535),观察是否触发
Hardware Interrupt或Diagnostic Buffer报错; - 故障注入验证:人为断开一次通信,确认算法状态机能自动恢复而非卡死在中间状态。
六、附录:各平台递归相关错误码速查
当意外触发递归限制时,快速定位依据:
Siemens TIA Portal:
Error 427 → 递归调用被编译器拒绝
Error 8092 → 运行时栈溢出(需检查OB块调用链)
Codesys:
Runtime Error 0x0000000A → Stack overflow
Runtime Error 0x0000001F → Task watchdog timeout due to excessive computation
Mitsubishi:
Code 4110 → Recursive function call detected
Code 4125 → User memory allocation failed (indirect recursion)
Rockwell:
Major Error 0x0000000E → Task exceeded its watchdog time
Minor Error 0x0000001A → Invalid address access (stack corruption)
暂无评论,快来抢沙发吧!