跳到正文
Learn-SCL
SCL 示例库/SCL ID 校验:数组查表与路径许可

SCL ID 校验:数组查表与路径许可

两个 FC 完成 8 个罐 ID 的查表、源/目标有效性检查和罐↔装车台路径许可,非法 ID 用 IN_OUT 清 0。

进阶 S7-1200 S7-1500 FC 数组 FOR IN_OUT 路径许可 2 块 / 110 行 可仿真 TIA 已验证

一句话:把 8 个罐 ID 装进 Array[1..8],用 FOR 循环查表;源和目标的 ID 不在表里也不等于装车台 ID 时,用 IN_OUT 把对应 ID 清 0,再按罐↔罐、罐↔装车台、装车台↔罐三个许可位拦截路径。

导入到我的 TIA 项目

通过 TiaLink 把本示例的 2 个 SCL 块作为“外部源”导入你当前打开的 TIA Portal 项目:自动生成块、编译,并清理外部源。导入后不会自动保存;FB 示例导入后需要你在 OB 里调用。

TiaLink 设置

默认 http://localhost:8080。也可以在本页地址后加 ?tialink= 临时指定。

TiaLink 启动时若设置了 --api-token,这里要填相同的值;只保存在本机浏览器 localStorage,不会上传。

罐区、装车台这类工段经常用“ID”表示设备:HMI 上选一个源和一个目标,PLC 先确认两个 ID 都在当前工段里,再检查这条路径允不允许。这个示例用两个 FC 把“查表”和“许可”拆开,重点不是业务,而是数组、FOR、IN_OUT 和 FC 返回值。

8 个罐 ID 与装车台 ID 的查表,以及罐到罐、罐到装车台、装车台到罐、装车台到装车台的路径许可矩阵
先判断源/目标是否在场,再按路径许可决定是否清 0。

概述

  • FC_IsTankID:在 Array[1..8] of UInt 里线性查找一个 ID,返回 Bool。
  • FC_ID_CHECK:把 8 个罐 ID 装进数组,检查源/目标是否“在场”(罐 ID 或装车台 ID),非法的一侧用 IN_OUT 清 0;两侧都有效时,再按三个许可位检查路径。

路径许可有三个布尔输入:

输入 含义
AllowTankToLoading 允许 罐 → 装车台
AllowLoadingToTank 允许 装车台 → 罐
AllowTankToTank 允许 罐 → 罐

装车台 → 装车台永远不允许。被禁止时清哪一侧,沿用原工程的约定:罐→装车台清目标,装车台→罐清源,罐→罐清源。这样 HMI 上总能保留一侧有效 ID,方便显示“是哪条路径被拒绝”。

原工程 FC_ID_CHECK.scl 用全局常量 FC_ID_CHECK_ARRAY 当数组上界,并且 FC_IsTankID 在循环结束后无条件写 FALSE——这是一个真实的返回值 bug。本页改成 1..8 字面量,并修复返回值;讲解里保留原写法对照。

你将学到

  • 用 Array[1..8] of UInt + FOR 做线性查找;
  • FC 的返回值怎么写、什么时候用 RETURN 提前结束;
  • IN_OUT 参数如何把“清 0”写回调用方;
  • 为什么“源/目标都有效”要作为一个前置条件,再检查路径;
  • 一个真实 bug:循环里写 TRUE、循环后又无条件写 FALSE。

接口与变量

下表由构建器从 SCL 源码的接口声明自动生成;页面上展示的代码就是导入 TIA 的同一份源码。

FC_IsTankID FC

区块名称类型说明
输入idUInt
输入/输出TARRAY[1..8] OF UInt
临时idxInt
返回值Ret_ValBool

FC_ID_CHECK FC

区块名称类型说明
输入ID_T1UInt
输入ID_T2UInt
输入ID_T3UInt
输入ID_T4UInt
输入ID_T5UInt
输入ID_T6UInt
输入ID_T7UInt
输入ID_T8UInt
输入ID_VUInt
输入AllowTankToLoadingBool
输入AllowLoadingToTankBool
输入AllowTankToTankBool
输入/输出ID_SourceUInt
输入/输出ID_TargetUInt
临时TARRAY[1..8] OF UInt
临时SourceIsTankBool
临时TargetIsTankBool
临时SourceValidBool
临时TargetValidBool

程序结构

导入顺序以下表为准:被依赖的块/类型先导入;本页已按依赖排好。

  1. FC_IsTankID FC 在 1..8 的罐 ID 表里线性查找 · 30 行 · 2 段
  2. FC_ID_CHECK FC 源/目标有效性检查与路径许可 · 80 行 · 3 段

FC_IsTankID FC

在 1..8 的罐 ID 表里线性查找 · 30 行 · 源文件 blocks/FC_IsTankID.scl

查看 / 复制完整块源码
FC_IsTankID.scl
1FUNCTION "FC_IsTankID" : Bool
2{ S7_Optimized_Access := 'TRUE' }
3VERSION : 0.1
4   VAR_INPUT
5      id : UInt;   // 要查的罐号 ID
6   END_VAR
7 
8   VAR_IN_OUT
9      T : Array[1..8] of UInt;   // 罐 ID 表
10   END_VAR
11 
12   VAR_TEMP
13      idx : Int;   // 查表下标
14   END_VAR
15BEGIN
16   REGION 默认结果
17      // 先假设“不是罐”,找到再改成 TRUE;这样不会返回未赋值的 Bool。
18      #Ret_Val := FALSE;
19   END_REGION
20 
21   REGION 线性查找
22      FOR #idx := 1 TO 8 DO
23         IF #id = #T[#idx] THEN
24            #Ret_Val := TRUE;
25            RETURN;   // 找到就结束,不再继续扫后面的空位
26         END_IF;
27      END_FOR;
28   END_REGION
29END_FUNCTION
30 

FC_IsTankID / 默认结果 — 先假设“不是罐”

FC_IsTankID.scl · 默认结果
16   REGION 默认结果
17      // 先假设“不是罐”,找到再改成 TRUE;这样不会返回未赋值的 Bool。
18      #Ret_Val := FALSE;
19   END_REGION

目的:给 FC 的返回值一个确定的初值。

条件 → 动作:进入 FC 后先执行 #Ret_Val := FALSE。

为什么:SCL 的 Bool 返回值在未赋值时没有可靠默认值;先写 FALSE,后面找到才改成 TRUE,任何路径都有确定结果。

注意:原工程在循环里写 #FC_IsTankID := TRUE,循环结束后又写 #FC_IsTankID := FALSE,结果永远返回 FALSE。这是本页要重点对照的坑。

FC_IsTankID / 线性查找 — 找到就 RETURN

FC_IsTankID.scl · 线性查找
21   REGION 线性查找
22      FOR #idx := 1 TO 8 DO
23         IF #id = #T[#idx] THEN
24            #Ret_Val := TRUE;
25            RETURN;   // 找到就结束,不再继续扫后面的空位
26         END_IF;
27      END_FOR;
28   END_REGION

目的:在 8 个罐 ID 中找到目标 ID。

条件 → 动作:FOR idx := 1 TO 8,如果 id = T[idx],写 #Ret_Val := TRUE 并 RETURN;循环结束仍未找到,保持 FALSE。

为什么:RETURN 让 FC 在命中后立即结束,不再扫后面的空位;也避免“找到后又把结果覆盖掉”。

注意:数组上界必须和实际表长一致。本页用字面量 8,因为本地 SCL 仿真引擎不支持 TIA 的全局常量上界;真实项目里可以用常量,但要保证声明和循环上界同步修改。

FC_ID_CHECK FC

源/目标有效性检查与路径许可 · 80 行 · 源文件 blocks/FC_ID_CHECK.scl

查看 / 复制完整块源码
FC_ID_CHECK.scl
1FUNCTION "FC_ID_CHECK" : Void
2{ S7_Optimized_Access := 'TRUE' }
3VERSION : 0.1
4   VAR_INPUT
5      ID_T1 : UInt;   // 罐 1 ID
6      ID_T2 : UInt;   // 罐 2 ID
7      ID_T3 : UInt;   // 罐 3 ID
8      ID_T4 : UInt;   // 罐 4 ID
9      ID_T5 : UInt;   // 罐 5 ID
10      ID_T6 : UInt;   // 罐 6 ID
11      ID_T7 : UInt;   // 罐 7 ID
12      ID_T8 : UInt;   // 罐 8 ID
13      ID_V : UInt;    // 装车台 ID
14      AllowTankToLoading : Bool;   // 允许 罐 -> 装车台
15      AllowLoadingToTank : Bool;   // 允许 装车台 -> 罐
16      AllowTankToTank : Bool;      // 允许 罐 -> 罐
17   END_VAR
18 
19   VAR_IN_OUT
20      ID_Source : UInt;   // 源 ID,非法时被清 0
21      ID_Target : UInt;   // 目标 ID,非法时被清 0
22   END_VAR
23 
24   VAR_TEMP
25      T : Array[1..8] of UInt;   // 罐 ID 表
26      SourceIsTank : Bool;
27      TargetIsTank : Bool;
28      SourceValid : Bool;
29      TargetValid : Bool;
30   END_VAR
31BEGIN
32   REGION 组装 ID 表
33      #T[1] := #ID_T1;
34      #T[2] := #ID_T2;
35      #T[3] := #ID_T3;
36      #T[4] := #ID_T4;
37      #T[5] := #ID_T5;
38      #T[6] := #ID_T6;
39      #T[7] := #ID_T7;
40      #T[8] := #ID_T8;
41   END_REGION
42 
43   REGION 源和目标是否在场
44      // 罐 ID 在表里,装车台 ID 单独比较;两者都算“在场”。
45      #SourceIsTank := "FC_IsTankID"(id := #ID_Source, T := #T);
46      #TargetIsTank := "FC_IsTankID"(id := #ID_Target, T := #T);
47      #SourceValid := #SourceIsTank OR (#ID_Source = #ID_V);
48      #TargetValid := #TargetIsTank OR (#ID_Target = #ID_V);
49 
50      IF NOT #SourceValid THEN
51         #ID_Source := 0;
52      END_IF;
53      IF NOT #TargetValid THEN
54         #ID_Target := 0;
55      END_IF;
56   END_REGION
57 
58   REGION 路径许可
59      // 只有在源和目标都有效时才检查路径;无效的一侧在上一步已经清 0,不会进入这里。
60      IF #SourceValid AND #TargetValid THEN
61         IF #SourceIsTank AND #TargetIsTank THEN
62            IF NOT #AllowTankToTank THEN
63               #ID_Source := 0;
64            END_IF;
65         ELSIF #SourceIsTank AND (#ID_Target = #ID_V) THEN
66            IF NOT #AllowTankToLoading THEN
67               #ID_Target := 0;
68            END_IF;
69         ELSIF (#ID_Source = #ID_V) AND #TargetIsTank THEN
70            IF NOT #AllowLoadingToTank THEN
71               #ID_Source := 0;
72            END_IF;
73         ELSIF (#ID_Source = #ID_V) AND (#ID_Target = #ID_V) THEN
74            // 装车台 -> 装车台永远不允许。
75            #ID_Source := 0;
76         END_IF;
77      END_IF;
78   END_REGION
79END_FUNCTION
80 

FC_ID_CHECK / 组装 ID 表 — 把 8 个输入装进数组

FC_ID_CHECK.scl · 组装 ID 表
32   REGION 组装 ID 表
33      #T[1] := #ID_T1;
34      #T[2] := #ID_T2;
35      #T[3] := #ID_T3;
36      #T[4] := #ID_T4;
37      #T[5] := #ID_T5;
38      #T[6] := #ID_T6;
39      #T[7] := #ID_T7;
40      #T[8] := #ID_T8;
41   END_REGION

目的:让查表函数只面对一个数组,而不是 8 个独立参数。

条件 → 动作:T[1] := ID_T1 … T[8] := ID_T8。

为什么:FC_IsTankID 的接口只需要 id 和 T 两个参数;增加罐位时只改这里和数组上界,不用改查表函数。

注意:T 是 VAR_TEMP,每个扫描周期重新组装;不要把它写成静态变量,否则改罐 ID 后要等下一次写入才生效。

FC_ID_CHECK / 源和目标是否在场 — 罐 ID 或装车台 ID

FC_ID_CHECK.scl · 源和目标是否在场
43   REGION 源和目标是否在场
44      // 罐 ID 在表里,装车台 ID 单独比较;两者都算“在场”。
45      #SourceIsTank := "FC_IsTankID"(id := #ID_Source, T := #T);
46      #TargetIsTank := "FC_IsTankID"(id := #ID_Target, T := #T);
47      #SourceValid := #SourceIsTank OR (#ID_Source = #ID_V);
48      #TargetValid := #TargetIsTank OR (#ID_Target = #ID_V);
49 
50      IF NOT #SourceValid THEN
51         #ID_Source := 0;
52      END_IF;
53      IF NOT #TargetValid THEN
54         #ID_Target := 0;
55      END_IF;
56   END_REGION

目的:先排除“源/目标根本不存在”的请求。

条件 → 动作:

  • SourceIsTank := FC_IsTankID(id := ID_Source, T := T);
  • TargetIsTank := FC_IsTankID(id := ID_Target, T := T);
  • SourceValid := SourceIsTank OR (ID_Source = ID_V);
  • TargetValid := TargetIsTank OR (ID_Target = ID_V);
  • 无效的一侧写 0。

为什么:装车台不在罐 ID 表里,必须单独比较 ID_V;只有“罐或装车台”才算在场。

注意:这里先清 0,再进入路径检查。清 0 后 ID_Source/ID_Target 已经改变,所以后面的路径判断必须使用清 0 后的值,不能再用局部副本。

FC_ID_CHECK / 路径许可 — 先确认两侧有效,再检查方向

FC_ID_CHECK.scl · 路径许可
58   REGION 路径许可
59      // 只有在源和目标都有效时才检查路径;无效的一侧在上一步已经清 0,不会进入这里。
60      IF #SourceValid AND #TargetValid THEN
61         IF #SourceIsTank AND #TargetIsTank THEN
62            IF NOT #AllowTankToTank THEN
63               #ID_Source := 0;
64            END_IF;
65         ELSIF #SourceIsTank AND (#ID_Target = #ID_V) THEN
66            IF NOT #AllowTankToLoading THEN
67               #ID_Target := 0;
68            END_IF;
69         ELSIF (#ID_Source = #ID_V) AND #TargetIsTank THEN
70            IF NOT #AllowLoadingToTank THEN
71               #ID_Source := 0;
72            END_IF;
73         ELSIF (#ID_Source = #ID_V) AND (#ID_Target = #ID_V) THEN
74            // 装车台 -> 装车台永远不允许。
75            #ID_Source := 0;
76         END_IF;
77      END_IF;
78   END_REGION

目的:根据源/目标的类型选择对应许可位,禁止的路径清 0。

条件 → 动作:

源 目标 检查 禁止时
罐 罐 AllowTankToTank 清源
罐 装车台 AllowTankToLoading 清目标
装车台 罐 AllowLoadingToTank 清源
装车台 装车台 永远禁止 清源

为什么:把“设备类型判断”和“方向许可”放在一个 IF/ELSIF 链里,顺序固定,不会出现两个分支同时清 0 的情况。

注意:外层必须先判断 SourceValid AND TargetValid。如果源已经被清 0,SourceIsTank 仍然是之前算出的 TRUE,不重新判断就会走到错误分支。原工程也是先做有效性检查,再进入路径判断。

仿真验证:让 Agent 写场景并执行(不需要博途)

推荐:让 Agent 自己设计场景

不需要你手写 scenario。把下面的提示词复制给已连接 PLC 云仿真 MCP 的 AI 助手(Claude Code / Cursor / VS Code 等),它会阅读源码、自己设计正常/边界/异常场景,并调用 scl_run_scenario 执行;新用户有 100 次免费调用。

  1. 复制提示词(包含完整 SCL 源码和任务说明)。
  2. 粘贴给已配置 sim MCP 的 Agent,让它先 scl_validate,再执行自己写的 scl_run_scenario。
  3. 看 Agent 返回的 PASS/FAIL、输入/期望/实际;需要精确复现时,再对照下方本站回归基线。
AGENT PROMPT
你已连接 PLC 云仿真 MCP(sim-cloud)……

任务:阅读下面的 SCL 示例,自己设计 3 个场景并用 scl_run_scenario 执行:
1) 正常路径
2) 边界/临界值
3) 异常、互锁或超时
每个场景请给出输入、期望、实际结果和 PASS/FAIL,并说明场景设计理由。
不要照抄现成场景。

示例:SCL ID 校验:数组查表与路径许可
// 复制后包含完整 SCL 源码和 MCP 参数说明

页面上只显示提示词开头;复制的内容包含全部块源码和 MCP 参数说明。

查看本站回归基线(固定场景,不是让你手写)

下面是本站构建时跑过的固定场景和预期输出,用来做回归对比或精确复现;不是要求你手写 scenario。推荐先让 Agent 自己写场景,再和这份基线对照。

EXPECTED
PASS  罐 101 -> 装车台 900:允许 (1 step(s), 1 scan(s))
PASS  源 ID 不在表里:清 0 (1 step(s), 1 scan(s))
PASS  目标 ID 不在表里:清 0 (1 step(s), 1 scan(s))
PASS  罐 -> 装车台被禁止:清目标 (1 step(s), 1 scan(s))
PASS  装车台 -> 罐被禁止:清源 (1 step(s), 1 scan(s))
PASS  罐 -> 罐被禁止:清源 (1 step(s), 1 scan(s))
PASS  装车台 -> 装车台:永远清源 (1 step(s), 1 scan(s))
RESULT: ALL PASS (7 scan(s))
BASELINE JSON
{
  "sources": [
    {
      "name": "blocks/FC_IsTankID.scl",
      "source": "FUNCTION \"FC_IsTankID\" : Bool\n{ S7_Optimized_Access := 'TRUE' }\nVERSION : 0.1\n   VAR_INPUT\n      id : UInt;   // 要查的罐号 ID\n   END_VAR\n\n   VAR_IN_OUT\n      T : Array[1..8] of UInt;   // 罐 ID 表\n   END_VAR\n\n   VAR_TEMP\n      idx : Int;   // 查表下标\n   END_VAR\nBEGIN\n   REGION 默认结果\n      // 先假设“不是罐”,找到再改成 TRUE;这样不会返回未赋值的 Bool。\n      #Ret_Val := FALSE;\n   END_REGION\n\n   REGION 线性查找\n      FOR #idx := 1 TO 8 DO\n         IF #id = #T[#idx] THEN\n            #Ret_Val := TRUE;\n            RETURN;   // 找到就结束,不再继续扫后面的空位\n         END_IF;\n      END_FOR;\n   END_REGION\nEND_FUNCTION\n"
    },
    {
      "name": "blocks/FC_ID_CHECK.scl",
      "source": "FUNCTION \"FC_ID_CHECK\" : Void\n{ S7_Optimized_Access := 'TRUE' }\nVERSION : 0.1\n   VAR_INPUT\n      ID_T1 : UInt;   // 罐 1 ID\n      ID_T2 : UInt;   // 罐 2 ID\n      ID_T3 : UInt;   // 罐 3 ID\n      ID_T4 : UInt;   // 罐 4 ID\n      ID_T5 : UInt;   // 罐 5 ID\n      ID_T6 : UInt;   // 罐 6 ID\n      ID_T7 : UInt;   // 罐 7 ID\n      ID_T8 : UInt;   // 罐 8 ID\n      ID_V : UInt;    // 装车台 ID\n      AllowTankToLoading : Bool;   // 允许 罐 -> 装车台\n      AllowLoadingToTank : Bool;   // 允许 装车台 -> 罐\n      AllowTankToTank : Bool;      // 允许 罐 -> 罐\n   END_VAR\n\n   VAR_IN_OUT\n      ID_Source : UInt;   // 源 ID,非法时被清 0\n      ID_Target : UInt;   // 目标 ID,非法时被清 0\n   END_VAR\n\n   VAR_TEMP\n      T : Array[1..8] of UInt;   // 罐 ID 表\n      SourceIsTank : Bool;\n      TargetIsTank : Bool;\n      SourceValid : Bool;\n      TargetValid : Bool;\n   END_VAR\nBEGIN\n   REGION 组装 ID 表\n      #T[1] := #ID_T1;\n      #T[2] := #ID_T2;\n      #T[3] := #ID_T3;\n      #T[4] := #ID_T4;\n      #T[5] := #ID_T5;\n      #T[6] := #ID_T6;\n      #T[7] := #ID_T7;\n      #T[8] := #ID_T8;\n   END_REGION\n\n   REGION 源和目标是否在场\n      // 罐 ID 在表里,装车台 ID 单独比较;两者都算“在场”。\n      #SourceIsTank := \"FC_IsTankID\"(id := #ID_Source, T := #T);\n      #TargetIsTank := \"FC_IsTankID\"(id := #ID_Target, T := #T);\n      #SourceValid := #SourceIsTank OR (#ID_Source = #ID_V);\n      #TargetValid := #TargetIsTank OR (#ID_Target = #ID_V);\n\n      IF NOT #SourceValid THEN\n         #ID_Source := 0;\n      END_IF;\n      IF NOT #TargetValid THEN\n         #ID_Target := 0;\n      END_IF;\n   END_REGION\n\n   REGION 路径许可\n      // 只有在源和目标都有效时才检查路径;无效的一侧在上一步已经清 0,不会进入这里。\n      IF #SourceValid AND #TargetValid THEN\n         IF #SourceIsTank AND #TargetIsTank THEN\n            IF NOT #AllowTankToTank THEN\n               #ID_Source := 0;\n            END_IF;\n         ELSIF #SourceIsTank AND (#ID_Target = #ID_V) THEN\n            IF NOT #AllowTankToLoading THEN\n               #ID_Target := 0;\n            END_IF;\n         ELSIF (#ID_Source = #ID_V) AND #TargetIsTank THEN\n            IF NOT #AllowLoadingToTank THEN\n               #ID_Source := 0;\n            END_IF;\n         ELSIF (#ID_Source = #ID_V) AND (#ID_Target = #ID_V) THEN\n            // 装车台 -> 装车台永远不允许。\n            #ID_Source := 0;\n         END_IF;\n      END_IF;\n   END_REGION\nEND_FUNCTION\n"
    }
  ],
  "scenario": {
    "docs": [
      "FC_IsTankID.scl",
      "FC_ID_CHECK.scl"
    ],
    "block": "FC_ID_CHECK",
    "tickMs": 10,
    "seed": {
      "ID_T1": 101,
      "ID_T2": 102,
      "ID_T3": 103,
      "ID_T4": 104,
      "ID_T5": 105,
      "ID_T6": 106,
      "ID_T7": 107,
      "ID_T8": 108,
      "ID_V": 900,
      "AllowTankToLoading": true,
      "AllowLoadingToTank": true,
      "AllowTankToTank": true
    },
    "scenarios": [
      {
        "name": "罐 101 -> 装车台 900:允许",
        "steps": [
          {
            "set": {
              "ID_Source": 101,
              "ID_Target": 900
            },
            "expect": {
              "ID_Source": 101,
              "ID_Target": 900
            }
          }
        ]
      },
      {
        "name": "源 ID 不在表里:清 0",
        "steps": [
          {
            "set": {
              "ID_Source": 555,
              "ID_Target": 900
            },
            "expect": {
              "ID_Source": 0,
              "ID_Target": 900
            }
          }
        ]
      },
      {
        "name": "目标 ID 不在表里:清 0",
        "steps": [
          {
            "set": {
              "ID_Source": 101,
              "ID_Target": 555
            },
            "expect": {
              "ID_Source": 101,
              "ID_Target": 0
            }
          }
        ]
      },
      {
        "name": "罐 -> 装车台被禁止:清目标",
        "steps": [
          {
            "set": {
              "AllowTankToLoading": false,
              "ID_Source": 101,
              "ID_Target": 900
            },
            "expect": {
              "ID_Source": 101,
              "ID_Target": 0
            }
          }
        ]
      },
      {
        "name": "装车台 -> 罐被禁止:清源",
        "steps": [
          {
            "set": {
              "AllowLoadingToTank": false,
              "ID_Source": 900,
              "ID_Target": 101
            },
            "expect": {
              "ID_Source": 0,
              "ID_Target": 101
            }
          }
        ]
      },
      {
        "name": "罐 -> 罐被禁止:清源",
        "steps": [
          {
            "set": {
              "AllowTankToTank": false,
              "ID_Source": 101,
              "ID_Target": 102
            },
            "expect": {
              "ID_Source": 0,
              "ID_Target": 102
            }
          }
        ]
      },
      {
        "name": "装车台 -> 装车台:永远清源",
        "steps": [
          {
            "set": {
              "ID_Source": 900,
              "ID_Target": 900
            },
            "expect": {
              "ID_Source": 0,
              "ID_Target": 900
            }
          }
        ]
      }
    ]
  }
}

打开 PLC 云仿真

关键 SCL 语法

写法 含义
FUNCTION "FC_IsTankID" : Bool 有返回值的 FC,接口里出现 Ret_Val
#Ret_Val := TRUE 给 FC 返回值赋值;TIA V19 和本地 SCL 仿真引擎都识别
RETURN; 提前结束 FC
VAR_IN_OUT T : Array[1..8] of UInt 按引用传入数组,FC 内可读写
FOR #idx := 1 TO 8 DO ... END_FOR 计数循环
IF ... ELSIF ... END_IF 多分支判断
#ID_Source := 0 通过 IN_OUT 写回调用方

原工程在代码里写 #FC_IsTankID := TRUE,TIA 也能编译;但本地 SCL 仿真引擎只把返回值识别为 Ret_Val,所以教学版统一写 #Ret_Val。两种写法在 TIA V19 里都能编译,本站选择能被仿真器执行的那种。

常见坑

  • 循环后无条件写 FALSE:原工程的 bug;命中后必须不再覆盖返回值,最简单的写法是命中就 RETURN。
  • 数组上界和循环上界不一致:声明 1..8、循环 1..7,第 8 个罐永远查不到。
  • 把 T 写成 VAR_TEMP 又在 FC 外使用:临时数组只在本次调用有效,不能跨周期保存。
  • 只检查目标、不检查源:源 ID 不存在时也可能放行,泵/阀会收到一个无效源。
  • 清 0 后继续用旧布尔值:SourceIsTank 是清 0 前算的;路径判断要用清 0 后的 ID_Source/ID_Target。
  • 把三个许可位当成互斥:它们可以同时为 TRUE,具体走哪条分支由源/目标类型决定。

变体与扩展

  • 罐 ID 超过 8 个时,把 Array[1..8] 改成 Array[1..N],循环上界同步修改;真实项目里用常量统一管理。
  • 需要返回“拒绝原因”时,增加 RejectReason : Int 输出:1=源无效、2=目标无效、3=路径不允许;比只看清 0 更适合 HMI 提示。
  • 需要支持更多设备类型(泵、阀、管廊)时,把“是否在场”改成枚举或 Int 类型码,再建一张“类型 → 允许路径”的表。
  • 查表也可以改成 Array[1..8] of Struct,每个元素带 ID、类型、可用标志;FC_IsTankID 只返回是否命中,类型判断放到另一个 FC。

常见问题

FC 里怎么返回 Bool?

声明 FUNCTION "FC_IsTankID" : Bool,接口里会多出一个 Ret_Val。循环前先写 #Ret_Val := FALSE,找到后写 TRUE 并 RETURN;不要像原工程那样在循环后再无条件写 FALSE,否则永远返回 FALSE。

为什么非法 ID 用 IN_OUT 清 0,而不是直接报错?

这段逻辑面对的源/目标是 HMI 或上位机写入的 ID,清 0 表示“本次请求作废”,后续泵/阀逻辑看到 0 就不会动作;同时保留另一侧的有效 ID,便于 HMI 显示是哪一侧不合法。

验证状态

项目结果日期证据
本地仿真场景PASS(7 scans)2026-11-01页面下方 `scl_run_scenario` 请求可复现
PLC 云仿真PASS2026-11-01`scl_run_scenario`(托管 MCP)
TIA 导入编译PASS(0 error / 0 warning)2026-11-01本地 SCL 仿真与 PLC 云仿真(scl_run_scenario)7 扫描通过;两个 FC 按顺序导入 TIA Portal V19 + TiaLink 1.4.0.0 的 PLC_2,编译 0 错误 / 0 警告,已保存。

本地仿真引擎验证语法、结构和逐扫描行为;TIA 编译验证在 Windows + TIA Portal + TiaLink 上完成。两者不能互相替代。

来源与许可

本页示例改编自 github.com/lopez-dev/Siemens-SCL-Source-Files 的 FC_ID_CHECK/FC_ID_CHECK.scl,原作者 Daniel Cáceres López,以 MIT 许可发布。中文注释、接口调整、仿真场景与页面讲解由 Learn-SCL 编写。

原文件用全局常量 FC_ID_CHECK_ARRAY 作为数组上界,并在 FC_IsTankID 末尾无条件写 FALSE;本站改为 1..8 字面量并修复返回值,保留查表和路径许可逻辑。

示例源码以 MIT 许可发布(上游仓库);中文讲解与页面内容版权归汇智网(Hubwiz)。官方参考:Siemens TIA Portal。

把这个示例带进你的工程

TiaLink

把 TIA Portal 接到你的智能体:导入、编译、读回核对、PLCSIM 仿真。本地运行,工程数据不出厂。

了解 TiaLink →

PLC 云仿真

不装博途,把 SCL 放到云端逐扫描执行:校验、仿真、场景验收。新用户 100 次免费。

打开云仿真 →

Learn-LAD

同一个团队的梯形图示例库:逐网络讲解、变量表、TiaLink 一键导入。

浏览 LAD 示例 →