跳到正文
Learn-SCL
SCL 示例库/SCL 缓冲站管理:进料、出料、停留时间与占用率

SCL 缓冲站管理:进料、出料、停留时间与占用率

SCL 先进先出缓冲站:8 槽位结构体数组、进料/出料、停留时间与占用率;含完整源码和云仿真场景。

进阶 S7-1200 S7-1500 数组 结构体 FIFO 统计 Time 1 块 / 84 行 可仿真 TIA 已验证

一句话:用 Array[1..8] of Struct 存槽位:1 号位最新、Count 号位最老;进料把 1..7 号位整体后移到 2..8 号位、新件放 1 号位;出料清空 Slots[Count]、其他槽位不动。

导入到我的 TIA 项目

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

TiaLink 设置

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

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

缓冲站是产线里非常典型的一段逻辑:工件从入口进来,按先进先出排队;出口取走最老的工件;同时要统计“当前有几件”“最老一件等了多久”“缓冲站占用率”。

SCL 缓冲站 8 槽位 FIFO:进料新件放 1 号位,出料清空最老的 Count 号位
进料:整体后移,新件放 1 号位;出料:清空 Count 号位,其他槽位不动。

概述

FB_BufferStation 用 8 个槽位的结构体数组模拟一个 FIFO:

  • Slots[1] 是最新工件,Slots[8] 是最老工件;
  • 进料上升沿:整体后移,新工件放到 1 号位;
  • 出料上升沿:清空当前最老槽位(下标 = Count),其他槽位不动;
  • 每个扫描周期给所有已装载槽位累加 CycleTime;
  • 循环统计 Count、FirstAge,并计算 Utilization、Full、Empty。

原工程 Buffer.scl 用 2×100 槽位、TOD/日期和 UDT/DB 做更完整的统计;本页把主线抽出来,缩小到 8 槽位,并把日期差改成 CycleTime,便于在云仿真里逐扫描验证。

你将学到

  • 结构体数组 Array[1..8] of Struct ... END_STRUCT;
  • 用 FOR ... BY -1 做整体后移;
  • 用上升沿避免“按住按钮连续进料”;
  • 在循环中同时做计数、最老工件和占用率;
  • 如何用场景验证进料/出料/复位。

接口与变量

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

FB_BufferStation FB

区块名称类型说明
输入LoadBool
输入UnloadBool
输入ResetBool
输入CycleTimeTime
输出CountInt
输出UtilizationInt
输出FirstAgeTime
输出FullBool
输出EmptyBool
静态SlotsARRAY[1..8] OF STRUCT
静态LoadPrevBool
静态UnloadPrevBool
静态CountIntInt
临时iInt

程序结构

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

  1. FB_BufferStation FB 缓冲站 FIFO 与统计 · 84 行 · 5 段

FB_BufferStation FB

缓冲站 FIFO 与统计 · 84 行 · 源文件 blocks/FB_BufferStation.scl

查看 / 复制完整块源码
FB_BufferStation.scl
1FUNCTION_BLOCK "FB_BufferStation"
2{ S7_Optimized_Access := 'TRUE' }
3VERSION : 0.1
4   VAR_INPUT
5      Load : Bool;        // 上升沿:进料一个工件
6      Unload : Bool;      // 上升沿:出料一个工件
7      Reset : Bool;       // 清空缓冲站
8      CycleTime : Time;   // 本次扫描经过的时间
9   END_VAR
10 
11   VAR_OUTPUT
12      Count : Int;        // 当前缓存数量
13      Utilization : Int;  // 占用率 0..100
14      FirstAge : Time;    // 最老工件的停留时间
15      Full : Bool;        // 缓冲站已满
16      Empty : Bool;       // 缓冲站为空
17   END_VAR
18 
19   VAR
20      Slots : Array[1..8] of Struct
21         Loaded : Bool;   // 该位置有工件
22         Age : Time;      // 已停留时间
23      END_STRUCT;
24      LoadPrev : Bool;    // Load 上一周期状态
25      UnloadPrev : Bool;  // Unload 上一周期状态
26      CountInt : Int := 0; // 内部计数:1 号位最新,CountInt 号位最老
27   END_VAR
28 
29   VAR_TEMP
30      i : Int;            // 循环变量
31   END_VAR
32 
33BEGIN
34   REGION 复位
35      IF #Reset THEN
36         FOR #i := 1 TO 8 DO
37            #Slots[#i].Loaded := FALSE;
38            #Slots[#i].Age := T#0MS;
39         END_FOR;
40      END_IF;
41   END_REGION
42 
43   REGION 进料移位
44      IF #Load AND NOT #LoadPrev THEN
45         FOR #i := 8 TO 2 BY -1 DO
46            #Slots[#i] := #Slots[#i - 1];
47         END_FOR;
48         #Slots[1].Loaded := TRUE;
49         #Slots[1].Age := T#0MS;
50      END_IF;
51      #LoadPrev := #Load;
52   END_REGION
53 
54   REGION 出料移位
55      IF #Unload AND NOT #UnloadPrev THEN
56         // 1 号位最新、CountInt 号位最老:清空最后一个已占用槽位,其他槽位不动。
57         IF #CountInt > 0 THEN
58            #Slots[#CountInt].Loaded := FALSE;
59            #Slots[#CountInt].Age := T#0MS;
60         END_IF;
61      END_IF;
62      #UnloadPrev := #Unload;
63   END_REGION
64 
65   REGION 统计数量与停留时间
66      #CountInt := 0;
67      #FirstAge := T#0MS;
68      FOR #i := 1 TO 8 DO
69         IF #Slots[#i].Loaded THEN
70            #Slots[#i].Age := T_ADD(IN1 := #Slots[#i].Age, IN2 := #CycleTime);
71            #CountInt := #CountInt + 1;
72            #FirstAge := #Slots[#i].Age; // 后面的位置更老,循环结束时就是最老工件
73         END_IF;
74      END_FOR;
75   END_REGION
76 
77   REGION 输出
78      #Count := #CountInt;
79      #Utilization := (#Count * 100) / 8;
80      #Full := #Count >= 8;
81      #Empty := #Count = 0;
82   END_REGION
83END_FUNCTION_BLOCK
84 

FB_BufferStation / 复位 — 清空所有槽位

FB_BufferStation.scl · 复位
34   REGION 复位
35      IF #Reset THEN
36         FOR #i := 1 TO 8 DO
37            #Slots[#i].Loaded := FALSE;
38            #Slots[#i].Age := T#0MS;
39         END_FOR;
40      END_IF;
41   END_REGION

目的:把缓冲站恢复到空状态。

条件 → 动作:Reset=1 → 所有 Slots[i].Loaded=FALSE,Age=T#0MS。

为什么:调试、换型、故障恢复都需要一个确定的初始状态。

注意:复位只清槽位,不清 LoadPrev/UnloadPrev;如果复位时按钮还按着,需要额外的“等待释放”逻辑。

FB_BufferStation / 进料移位 — 新工件放到 1 号位

FB_BufferStation.scl · 进料移位
43   REGION 进料移位
44      IF #Load AND NOT #LoadPrev THEN
45         FOR #i := 8 TO 2 BY -1 DO
46            #Slots[#i] := #Slots[#i - 1];
47         END_FOR;
48         #Slots[1].Loaded := TRUE;
49         #Slots[1].Age := T#0MS;
50      END_IF;
51      #LoadPrev := #Load;
52   END_REGION

目的:在 Load 上升沿把一个新工件插入队首。

条件 → 动作:Load=1 且 LoadPrev=0 → FOR i := 8 TO 2 BY -1 DO Slots[i] := Slots[i-1];然后 Slots[1].Loaded=TRUE、Age=0。

为什么:从后往前复制,避免覆盖还没搬走的数据;整体后移让 1 号位始终是最新工件。

注意:数组只有 8 个槽位;满的时候再进料会丢掉最老的工件(Slots[8] 被覆盖)。如果业务不允许丢件,要在进料前检查 Full。

FB_BufferStation / 出料移位 — 清空最老工件

FB_BufferStation.scl · 出料移位
54   REGION 出料移位
55      IF #Unload AND NOT #UnloadPrev THEN
56         // 1 号位最新、CountInt 号位最老:清空最后一个已占用槽位,其他槽位不动。
57         IF #CountInt > 0 THEN
58            #Slots[#CountInt].Loaded := FALSE;
59            #Slots[#CountInt].Age := T#0MS;
60         END_IF;
61      END_IF;
62      #UnloadPrev := #Unload;
63   END_REGION

目的:在 Unload 上升沿把最老工件移出。

条件 → 动作:Unload=1 且 UnloadPrev=0 且 CountInt>0 → 清空 Slots[CountInt](Loaded=FALSE、Age=0)。

为什么:1 号位最新、CountInt 号位最老;缓冲站是紧凑存放的,最老件就在最后一个已占用槽位。清空它即可,其他槽位不需要移动。

注意:不要用“整体前移/后移”处理出料。对未满的缓冲站,移位会把最新件搬到别的位置,甚至留下最老件;空站出料时 CountInt=0,直接跳过。

FB_BufferStation / 统计数量与停留时间 — 一次循环算三个值

FB_BufferStation.scl · 统计数量与停留时间
65   REGION 统计数量与停留时间
66      #CountInt := 0;
67      #FirstAge := T#0MS;
68      FOR #i := 1 TO 8 DO
69         IF #Slots[#i].Loaded THEN
70            #Slots[#i].Age := T_ADD(IN1 := #Slots[#i].Age, IN2 := #CycleTime);
71            #CountInt := #CountInt + 1;
72            #FirstAge := #Slots[#i].Age; // 后面的位置更老,循环结束时就是最老工件
73         END_IF;
74      END_FOR;
75   END_REGION

目的:计算当前数量、最老工件停留时间,并给每个已装载槽位加 CycleTime。

条件 → 动作:Count=0、FirstAge=0;FOR i := 1 TO 8,如果 Slots[i].Loaded:Age += CycleTime、Count += 1、FirstAge := Age。

为什么:后面的槽位更老,循环结束时 FirstAge 自然就是最老工件的停留时间;一次循环同时完成三件事,扫描开销固定。

注意:Age 的累加必须在移位之后;否则刚进料的工件会多算一个周期。FirstAge 只代表“最老工件”,不是“平均停留时间”。

FB_BufferStation / 输出 — 占用率、满/空

FB_BufferStation.scl · 输出
77   REGION 输出
78      #Count := #CountInt;
79      #Utilization := (#Count * 100) / 8;
80      #Full := #Count >= 8;
81      #Empty := #Count = 0;
82   END_REGION

目的:把内部状态翻译成 HMI/联锁用的布尔和百分比。

条件 → 动作:Utilization := Count * 100 / 8;Full := Count >= 8;Empty := Count = 0。

为什么:上位机只关心“满没满、空没空、占用多少”,不需要读整个数组。

注意:整数除法会截断;Count=1 时占用率是 12%,不是 12.5%。如果需要小数,用 REAL 计算。

仿真验证:让 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 缓冲站管理:进料、出料、停留时间与占用率
// 复制后包含完整 SCL 源码和 MCP 参数说明

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

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

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

EXPECTED
PASS  进料两件、停留 3 秒、出料一件、复位 (6 step(s), 8 scan(s))
RESULT: ALL PASS (8 scan(s))
BASELINE JSON
{
  "sources": [
    {
      "name": "blocks/FB_BufferStation.scl",
      "source": "FUNCTION_BLOCK \"FB_BufferStation\"\n{ S7_Optimized_Access := 'TRUE' }\nVERSION : 0.1\n   VAR_INPUT\n      Load : Bool;        // 上升沿:进料一个工件\n      Unload : Bool;      // 上升沿:出料一个工件\n      Reset : Bool;       // 清空缓冲站\n      CycleTime : Time;   // 本次扫描经过的时间\n   END_VAR\n\n   VAR_OUTPUT\n      Count : Int;        // 当前缓存数量\n      Utilization : Int;  // 占用率 0..100\n      FirstAge : Time;    // 最老工件的停留时间\n      Full : Bool;        // 缓冲站已满\n      Empty : Bool;       // 缓冲站为空\n   END_VAR\n\n   VAR\n      Slots : Array[1..8] of Struct\n         Loaded : Bool;   // 该位置有工件\n         Age : Time;      // 已停留时间\n      END_STRUCT;\n      LoadPrev : Bool;    // Load 上一周期状态\n      UnloadPrev : Bool;  // Unload 上一周期状态\n      CountInt : Int := 0; // 内部计数:1 号位最新,CountInt 号位最老\n   END_VAR\n\n   VAR_TEMP\n      i : Int;            // 循环变量\n   END_VAR\n\nBEGIN\n   REGION 复位\n      IF #Reset THEN\n         FOR #i := 1 TO 8 DO\n            #Slots[#i].Loaded := FALSE;\n            #Slots[#i].Age := T#0MS;\n         END_FOR;\n      END_IF;\n   END_REGION\n\n   REGION 进料移位\n      IF #Load AND NOT #LoadPrev THEN\n         FOR #i := 8 TO 2 BY -1 DO\n            #Slots[#i] := #Slots[#i - 1];\n         END_FOR;\n         #Slots[1].Loaded := TRUE;\n         #Slots[1].Age := T#0MS;\n      END_IF;\n      #LoadPrev := #Load;\n   END_REGION\n\n   REGION 出料移位\n      IF #Unload AND NOT #UnloadPrev THEN\n         // 1 号位最新、CountInt 号位最老:清空最后一个已占用槽位,其他槽位不动。\n         IF #CountInt > 0 THEN\n            #Slots[#CountInt].Loaded := FALSE;\n            #Slots[#CountInt].Age := T#0MS;\n         END_IF;\n      END_IF;\n      #UnloadPrev := #Unload;\n   END_REGION\n\n   REGION 统计数量与停留时间\n      #CountInt := 0;\n      #FirstAge := T#0MS;\n      FOR #i := 1 TO 8 DO\n         IF #Slots[#i].Loaded THEN\n            #Slots[#i].Age := T_ADD(IN1 := #Slots[#i].Age, IN2 := #CycleTime);\n            #CountInt := #CountInt + 1;\n            #FirstAge := #Slots[#i].Age; // 后面的位置更老,循环结束时就是最老工件\n         END_IF;\n      END_FOR;\n   END_REGION\n\n   REGION 输出\n      #Count := #CountInt;\n      #Utilization := (#Count * 100) / 8;\n      #Full := #Count >= 8;\n      #Empty := #Count = 0;\n   END_REGION\nEND_FUNCTION_BLOCK\n"
    }
  ],
  "scenario": {
    "docs": [
      "blocks/FB_BufferStation.scl"
    ],
    "block": "FB_BufferStation",
    "tickMs": 100,
    "seed": {
      "CycleTime": "T#1S"
    },
    "always": [
      "Utilization >= 0",
      "Utilization <= 100",
      "!(Full && Empty)"
    ],
    "scenarios": [
      {
        "name": "进料两件、停留 3 秒、出料一件、复位",
        "steps": [
          {
            "set": {
              "Load": true
            },
            "expect": {
              "Count": 1,
              "Utilization": 12,
              "Empty": false
            }
          },
          {
            "set": {
              "Load": false
            },
            "expect": {
              "Count": 1
            }
          },
          {
            "set": {
              "Load": true
            },
            "expect": {
              "Count": 2,
              "Utilization": 25
            }
          },
          {
            "set": {
              "Load": false
            },
            "ticks": 3,
            "expect": {
              "Count": 2,
              "FirstAge": "T#6S"
            }
          },
          {
            "set": {
              "Unload": true
            },
            "expect": {
              "Count": 1,
              "Utilization": 12,
              "FirstAge": "T#5S"
            }
          },
          {
            "set": {
              "Unload": false,
              "Reset": true
            },
            "expect": {
              "Count": 0,
              "Utilization": 0,
              "Empty": true,
              "FirstAge": "T#0S"
            }
          }
        ]
      }
    ]
  }
}

打开 PLC 云仿真

关键 SCL 语法

写法 含义
Array[1..8] of Struct ... END_STRUCT 结构体数组
FOR i := 8 TO 2 BY -1 DO 倒序循环
Slots[i] := Slots[i-1] 结构体整体赋值
T_ADD Time 相加
#Slots[i].Loaded 访问数组元素成员

常见坑

  • 出料写成整体前移/后移:1 号位最新、CountInt 号位最老,出料只清空 Slots[CountInt];整体移位在未满时会搬错件。
  • 忘记更新 LoadPrev/UnloadPrev:按住按钮会每个周期都进料/出料。
  • 数组越界:Slots[1..8] 的有效下标是 1..8;循环边界写错会运行时错误。
  • 满站继续进料:会覆盖最老工件;需要丢件策略或 Full 联锁。
  • FirstAge 取错方向:1 号位最新,循环从 1 到 8,最后一次赋值才是 CountInt 号位的最老件。

变体与扩展

  • 把 8 槽位扩成 100 槽位,增加按天统计(原工程做法)。
  • 增加 PartId : DInt 字段,做工件追溯。
  • 用环形缓冲区(Head/Tail 指针)替代整体移位,扫描开销从 O(n) 降到 O(1)。
  • 把 CycleTime 换成 RD_LOC_T 的时间差,就可以在真实 PLC 上统计实际停留时间。

常见问题

SCL 怎么实现先进先出(FIFO)?

用 Array[1..8] of Struct 存槽位:1 号位最新、Count 号位最老;进料上升沿把 1..7 号位整体后移到 2..8 号位,新件放 1 号位;出料只清空最后一个已占用槽位 Slots[Count],其他槽位不动。

为什么出料不能整体移位?

对未满的缓冲站,整体前移/后移会把最新件搬到别的位置,甚至留下最老件。1 号位最新、Count 号位最老,出料清空 Slots[Count] 即可;只有进料需要从 8 往 2 复制,避免覆盖还没搬走的数据。

验证状态

项目结果日期证据
本地仿真场景PASS(8 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 云仿真场景通过;TIA Portal V19 + TiaLink 1.4.0.0 批量导入 PLC_2 编译 0/0;FIFO 修正后已重新导入并复验 0/0,已保存。

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

来源与许可

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

原文件是 2×100 槽位 + TOD/日期统计 + UDT/DB;本站简化为 8 槽位 + CycleTime,保留进料/出料/最老工件/占用率的主线。

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

把这个示例带进你的工程

TiaLink

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

了解 TiaLink →

PLC 云仿真

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

打开云仿真 →

Learn-LAD

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

浏览 LAD 示例 →