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 里调用。
缓冲站是产线里非常典型的一段逻辑:工件从入口进来,按先进先出排队;出口取走最老的工件;同时要统计“当前有几件”“最老一件等了多久”“缓冲站占用率”。
进料:整体后移,新件放 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
| 区块 | 名称 | 类型 | 说明 |
|---|
| 输入 | Load | Bool | |
| 输入 | Unload | Bool | |
| 输入 | Reset | Bool | |
| 输入 | CycleTime | Time | |
| 输出 | Count | Int | |
| 输出 | Utilization | Int | |
| 输出 | FirstAge | Time | |
| 输出 | Full | Bool | |
| 输出 | Empty | Bool | |
| 静态 | Slots | ARRAY[1..8] OF STRUCT | |
| 静态 | LoadPrev | Bool | |
| 静态 | UnloadPrev | Bool | |
| 静态 | CountInt | Int | |
| 临时 | i | Int | |
程序结构
导入顺序以下表为准:被依赖的块/类型先导入;本页已按依赖排好。
- 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;
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;
25 UnloadPrev : Bool;
26 CountInt : Int := 0;
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
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
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 次免费调用。
- 复制提示词(包含完整 SCL 源码和任务说明)。
- 粘贴给已配置 sim MCP 的 Agent,让它先
scl_validate,再执行自己写的 scl_run_scenario。
- 看 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 云仿真 | PASS | 2026-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 上完成。两者不能互相替代。