Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

练习项目阶梯: L0~L3 十三题 (S7-1200 + TIA Portal + SCL)

本篇是纯动手篇: 13 道按 L0~L3 递进的练习, 从 20 行的启保停到包含通信与 HMI 的小型产线。所有题目默认在 PLCSIM + Watch table 中即可完成; 受仿真限制的部分 (3.3/3.4/3.5) 单独标注替代方案。0.1、1.1、1.2 给完整答案代码, 其余给完整变量声明 + 带 TODO 的骨架, 动手部分留给你。每题固定七栏: 目标 / 涉及知识点 / IO 与变量定义 / 提示 / 骨架或完整代码 / 验收标准 / 进阶变体。

0. 使用说明与通用约定

环境: TIA Portal V16~V20 (V21 亦适用); CPU 建议 S7-1200 1214C DC/DC/DC (G1/G2 均可, G2 需 V20+); 仿真用 TIA 内置 S7-PLCSIM (菜单 Online > Simulation, 无需额外许可); 新建工程 Trainer06, OB1 语言直接选 SCL, 每题逻辑封装为一个 FB, OB1 只做调用; 每题一个 Watch table, 命名 wt_xx。 代码约定: TIA 的 SCL 编辑器上半是块接口 (声明区), 下半是代码区。本文代码一律按声明区/代码区分开给出, 可照抄。局部变量带 # 前缀, 全局 tag/DB 名用引号。 与真机的两个关键差异:

  • PLCSIM 里 Watch table 可直接 Modify %I 输入 (仿真的核心便利)。真机上 Modify 输入映像会在下个扫描周期被物理输入覆盖 (“站不住”); 要持续固定 I/O 须用 Force (S7-1200 支持 Watch and force table, 官方手册有专章; 注意 Force 激活期间会阻碍下载, 调试完务必停用)。
  • Watch table 刷新是 PG 通信节奏 (约秒级), 看不到单个 scan cycle 内部的中间状态; 要“看见“扫描, 靠扫描计数器或定时器行为反推 — 0.1 就是练这个。

PLCSIM 能力边界速查 (决定哪些题能仿真, 出处见附录 B):

能力标准 PLCSIM说明
SCL 逻辑、IEC 定时器/计数器、数组/字符串、FB/DBL0~L2 全部题目
Watch table 修改 I/Q/M/DB可 (仅限仿真)见上
PUT/GET、开放用户通信 (TSEND_C/TRCV_C)仅 Softbus, 本机内同一台 PC 的实例间可用, 无真实 TCP/IP
Modbus TCP (MB_CLIENT/MB_SERVER)不可 (社区多方一致结论)3.4 给替代方案
Trace 录制不可 (仅 PLCSIM Advanced)用 Watch table + 变量记录替代
PID_Compact V2.x一代 S7-1200 不可; S7-1500 与 S7-1200 G2 可官方手册, 3.3 引用
Web Server / OPC UA不可官方对比表

题目一览: L0: 0.1 起保停+扫描观察 | L1: 1.1 多地启停+急停, 1.2 TON/TOF+振荡器, 1.3 CTU 装箱, 1.4 单按钮启停 | L2: 2.1 滑动平均, 2.2 SCALE_X+滞回, 2.3 协议解析, 2.4 排序+二分 | L3: 3.1 交通灯, 3.2 电机 FB 多实例, 3.3 PID 温控, 3.4 Modbus TCP, 3.5 HMI+配方, 3.6 小型产线。

L0: 建立 scan cycle 直觉 (1 题)

0.1 起保停: 电机自锁, 并观察扫描周期行为

目标: 用 SCL 实现最经典的电机自锁: 按一下启动, 电机运行并“记住“状态; 按停止立即停。再用 Watch table 验证状态如何在 scan cycle 之间被记住。 涉及知识点: scan cycle (输入采样、逻辑执行、输出写回), process image, 自锁 (self-holding), Watch table 的 Monitor/Modify, 置位/复位优先级。 IO 与变量定义 (PLC tag table):

名称地址类型说明
bStart%I0.0Bool启动按钮 (PLCSIM 中 Modify)
bStop%I0.1Bool停止按钮
bRun%Q0.0Bool电机运行
iScanCnt%MD10DInt扫描计数器 (观察用)

提示:

  • C/Python 类比: 整个程序就是一个永不退出的 while True 循环体, “记住状态“只能靠变量本身 (%Q/%M/DB/static), 没有闭包、没有事件回调。类比到此为止 — 普通程序执行完退出, PLC 程序永远循环, 任何阻塞 (长循环、死循环) 都会拖慢所有逻辑甚至触发 cycle time fault (watchdog)。
  • 自锁的本质: 输出 bRun 既是输出又被读回 (读到上一周期写入的值), 相当于 run = run OR start。两条 IF 的先后顺序决定同时按下时谁赢: 后执行的覆盖先执行的; 这里停止在后 = 停止优先。

完整代码 (OB1 直接写成 SCL, 代码区):

%MD10 := %MD10 + 1;            // 观察用: 每个扫描周期 +1, 证明程序在被周期性执行
IF %I0.0 THEN
    %Q0.0 := TRUE;             // 置位; 启动信号消失后靠下一周期读回保持
END_IF;
IF %I0.1 THEN
    %Q0.0 := FALSE;            // 后执行 → 停止优先
END_IF;

等价的一行布尔方程: %Q0.0 := (%Q0.0 OR %I0.0) AND NOT %I0.1; 语义相同, 同样停止优先。 Watch table 实验: 1) wt_l01 放 4 个量, Online & Monitor, iScanCnt 持续增长 = 你看到的是 N 个扫描周期后的结果; 2) Modify bStart=1 确认 bRun=1, 再改回 0 — bRun 仍为 1, 自锁成立 (状态记在 %Q0.0 的 process image 里); 3) Modify bStop=1, bRun 立即变 0, 改回 0 后保持 0; 4) 选做: 若 PLCSIM 窗口有单次扫描模式 (Single scan / Next scan, 待核实: 以你安装版本界面为准), 逐周期单步观察。 验收标准: 1) bStart 置 1 再回 0 (哪怕只持续一个扫描周期), bRun 保持 1。 2) bStop=1 期间即使 bStart 同时=1, bRun=0 (停止优先)。 3) 重启 PLCSIM 后 bRun=0、iScanCnt=0; 想要跨重启保持计数值该放哪里 (DB 的 retentive 属性或 M 区保持设置)。 4) 能口头回答: bRun 的“记忆“存在哪, 谁每个周期刷新它。 进阶变体: 改成启动优先 (交换两条 IF 顺序); 加热过载 %I0.2, 故障断开并自锁故障灯; 逻辑搬进 FB fbLatch, 状态放 static, OB1 调用 (为后面所有题做准备); 在 Online & Diagnostics > Cycle time 看真实扫描时间。

L1: 基础指令四连 (4 题)

1.1 多地启停 + 急停 (完整答案)

目标: 甲/乙两地都能启停同一台电机; 急停与故障随时切断。 涉及知识点: 多地控制 (启动并联、停止串联), 常闭 (NC) 信号约定, 急停的软件语义, FB + 实例 DB。 IO 与变量定义:

名称地址类型说明
bStartA / bStartB%I0.0 / %I0.1Bool甲/乙地启动, 常开瞬动
bStopA / bStopB%I0.2 / %I0.3Bool甲/乙地停止, 常闭
bEstopNC%I0.4Bool急停, 硬件常闭
bFault%I0.5Bool过载/变频器故障
bRun%Q0.0Bool运行

提示:

  • 现场约定: 停止类信号 (停止按钮、急停、安全门) 一律接常闭。断线时信号变 FALSE, 系统走向安全侧 (停机); 常开接法断线表现为“永远没按过“, 是隐患。多地控制就两条: 启动并联 (OR), 停止串联 (所有常闭健康才允许运行)。
  • 必须写进代码注释的一句话: 软件急停不等于安全回路。真正的急停要靠硬件 (安全继电器/接触器冗余或 F-PLC), 因为 PLC 有扫描延迟与单点故障; 本题只是操作逻辑层的快速停机。官方安全编程指南见附录 B。

完整代码, FB fbMotor2w 声明区:

FUNCTION_BLOCK "fbMotor2w"
VAR_INPUT
    bStartA  : Bool;   // 甲地启动 (常开)
    bStartB  : Bool;   // 乙地启动
    bStopA   : Bool;   // 甲地停止 (常闭: 未按=TRUE)
    bStopB   : Bool;   // 乙地停止 (常闭)
    bEstopNC : Bool;   // 急停 (常闭: 健康=TRUE)
    bFault   : Bool;   // 故障反馈
END_VAR
VAR_OUTPUT
    bRun         : Bool;
    bEstopActive : Bool;   // 急停已触发 (诊断用)
END_VAR
#bEstopActive := NOT #bEstopNC;
IF (NOT #bStopA) OR (NOT #bStopB) OR #bEstopActive OR #bFault THEN
    #bRun := FALSE;              // 停止/急停/故障: 后执行 → 优先级最高
END_IF;
IF (#bStartA OR #bStartB) AND #bStopA AND #bStopB AND #bEstopNC AND NOT #bFault THEN
    #bRun := TRUE;               // 自锁: 按钮松开后靠 bRun 保持
END_IF;

OB1 里用生成的单实例 DB fbMotor2w_DB 调用: "fbMotor2w_DB"(bStartA := %I0.0, bStartB := %I0.1, bStopA := %I0.2, bStopB := %I0.3, bEstopNC := %I0.4, bFault := %I0.5, bRun => %Q0.0); 验收标准: 1) 任一启动按钮脉冲即可启动; 任一停止按钮立即停止。 2) 急停期间任何启动无效; 急停释放 (信号回 TRUE) 后需重新按启动。 3) bFault=1 强制停机, bFault 回 0 后同样需重新启动。 4) 常闭语义正确: 把 bStopA Modify 成 FALSE (模拟按下或断线), 电机停。 进阶变体: 加故障复位按钮与故障锁存; 急停释放后要求 3s 复位延时 (TON, 为 1.2 热身); 启动/停止各扩展为数组的通用件 (为 3.2 铺路)。

1.2 TON/TOF 组合与振荡器 (完整答案)

目标: (a) 延时 3s 启动、命令撤销后延时 5s 停止; (b) 用两个 TON 做占空比可调的闪烁振荡器, 并解释为什么单 TON 自复位版不是方波。 涉及知识点: IEC 定时器 TON (on-delay) / TOF (off-delay) 精确语义, 多实例声明, 自复位振荡器, 占空比。 IO 与变量定义: 两个 FB, 输入输出见声明; 观察用 %M0.0 (bCmd)、%M0.1 (bBlinkEn) 由 wt_l12 Modify 驱动。 提示 (先背语义再写码, 这是面试级事实):

  • TON: IN=TRUE 后开始计时, ET 从 0 增长, ET>=PT 时 Q=TRUE; IN 变 FALSE 时 Q 立即 FALSE 且 ET 清零。TOF: IN=TRUE 时 Q 立即 TRUE; IN 变 FALSE 后 Q 保持, 直到 ET>=PT 才落 FALSE。
  • 定时器实例必须声明在 static 区 (ET 要跨周期保存); TEMP 是每周期复用的草稿区, 装不下实例数据。
  • C 类比: TON 像被主循环轮询驱动的状态机, 不像 sleep — PLC 里没有“等“, 延时只能“设好闹钟, 下个周期再看“。

完整代码, FB fbDelayRun 声明区:

FUNCTION_BLOCK "fbDelayRun"
VAR_INPUT
    bCmd      : Bool;         // 运行命令
    tOnDelay  : Time := T#3S;
    tOffDelay : Time := T#5S;
END_VAR
VAR_OUTPUT
    bRun     : Bool;
    tElapsed : Time;
END_VAR
VAR
    tonStart : TON;    // 多实例
    tofStop  : TOF;
END_VAR
#tonStart(IN := #bCmd, PT := #tOnDelay, ET => #tElapsed);  // 启动延时
#tofStop(IN := #tonStart.Q, PT := #tOffDelay);             // 停止延时
#bRun := #tofStop.Q;

FB fbBlink 声明区:

FUNCTION_BLOCK "fbBlink"
VAR_INPUT
    bEnable : Bool;
    tOn     : Time := T#700MS;
    tOff    : Time := T#300MS;
END_VAR
VAR_OUTPUT
    bLamp : Bool;
END_VAR
VAR
    tonOn  : TON;
    tonOff : TON;
END_VAR
// 灯灭阶段计 tOff, 灯亮阶段计 tOn — 到期翻转
#tonOff(IN := #bEnable AND NOT #bLamp, PT := #tOff);
#tonOn (IN := #bEnable AND #bLamp,     PT := #tOn);
IF #tonOff.Q THEN #bLamp := TRUE;  END_IF;
IF #tonOn.Q  THEN #bLamp := FALSE; END_IF;
IF NOT #bEnable THEN #bLamp := FALSE; END_IF;   // 使能断开: 确定的初始态

单 TON 振荡器陷阱 (记进笔记): #ton1(IN := NOT #ton1.Q, PT := T#1S); — Q 每秒只“亮“一个扫描周期。机理: ET 到 PT 时 Q=TRUE → 下个扫描周期 IN=NOT Q=FALSE → Q 立即回 FALSE、ET 清零; Q 的 TRUE 宽度只有 1 个 scan cycle, wt 几乎看不见, 更点不亮灯。它是“每秒产生一个脉冲沿“的工具, 不是方波。 验收标准: 1) bCmd 置 1, 3s 后 bRun=1; bCmd 置 0, bRun 保持 5s 后落 0; 用 tElapsed 在 wt 上核对时间轴。 2) tOn/tOff 改 T#2S/T#1S, 观察到 2/3 占空比。 3) bBlinkEn 断开瞬间灯立即灭 (而不是停在高电平)。 4) 能解释单 TON 版为什么不是 50% 方波。 进阶变体: 启动延时期间亮“预备“黄灯, 延时停止期间亮“惯性运行“指示; 用 fbBlink 做 1Hz 故障闪烁, 故障上升沿后闪 10 次自动停 (TON + CTU, 为 1.3 热身)。

1.3 CTU 计数装箱 + 复位 (骨架)

目标: 光电每检测一个产品计 1; 计满 12 个则“装满“灯亮、传送带暂停; 复位后继续。 涉及知识点: CTU (up counter) 语义与自带边沿检测, 电平 vs 边沿, 计数联锁, 手动/自动复位。 IO 与变量定义: bPhoto %I0.0 (光电), bReset %I0.1, bBoxReady %Q0.0 (装满), bBelt %Q0.1 (传送带)。FB fbPacker 声明区 (完整可照抄):

FUNCTION_BLOCK "fbPacker"
VAR_INPUT
    bPhoto   : Bool;
    bReset   : Bool;
    bBeltCmd : Bool;        // 上游传送带启动命令
    iBoxSize : Int := 12;
END_VAR
VAR_OUTPUT
    bBelt     : Bool;
    bBoxReady : Bool;
    iCount    : Int;
END_VAR
VAR
    trigPhoto : R_TRIG;     // 手写对照版用
    ctuBox    : CTU;
END_VAR

提示:

  • CTU 语义: CU 的上升沿使 CV+1; R=TRUE 清零 (优先于计数); CV>=PV 时 Q=TRUE; PV/CV 为 Int。CU 自带边沿检测: wt 里把 bPhoto Modify 成 1 并保持, 只会计 1 次 — 长电平只产生一个上升沿。
  • 手写对照: #trigPhoto(CLK := #bPhoto); IF #trigPhoto.Q THEN #iCnt := #iCnt + 1; END_IF; — 写一遍就知道 CTU 内部在做什么。千万别写 IF #bPhoto THEN 计数, 那是每个扫描周期都 +1。联锁语义自己定义并写进注释: 装满立即停带; 复位后自动恢复还是等待 bBeltCmd 重新给命令。
#ctuBox(CU := #bPhoto, R := #bReset, PV := #iBoxSize,
        Q => #bBoxReady, CV => #iCount);
// TODO1: 传送带联锁 — 装满即停, 复位后按你定义的语义恢复
// 参考形状: #bBelt := #bBeltCmd AND NOT #bBoxReady;
// TODO2 (选): 手写 R_TRIG + Int 计数, 验证与 CTU 等价

验收标准: 1) 连续 12 个上升沿后 bBoxReady=1 且 bBelt=0; 2) bReset 脉冲清零并按定义恢复; 3) bPhoto 恒 1 长电平只计 1 次; 4) 计到 11 时挂机 10 分钟计数不丢 (static 语义)。 进阶变体: 计满自动发 500ms 换箱脉冲 (TON/TP) 再自复位; 用 CTUD 实现“满 12 提示、超 14 报警“; 加 DInt 总箱数累计 (为 3.6 铺路)。

1.4 单按钮启停 (骨架, 两种解法)

目标: 一个按钮既启动又停止: 按一次开, 再按一次关 (toggle)。 涉及知识点: 边沿检测两种实现 (R_TRIG 与手写上次值), toggle 写法, FB 的可重入性。 IO 与变量定义: bBtn %I0.0, bRun %Q0.0。FB fbToggle 声明区:

FUNCTION_BLOCK "fbToggle"
VAR_INPUT
    bBtn : Bool;
END_VAR
VAR_OUTPUT
    bRun : Bool;
END_VAR
VAR
    trigBtn  : R_TRIG;
    bBtnLast : Bool;    // 解法 B 用 (static: 跨周期保存)
END_VAR

提示:

  • 解法 A: #trigBtn(CLK := #bBtn); IF #trigBtn.Q THEN #bRun := NOT #bRun; END_IF;
  • 解法 B (手写上次值, bEdge 声明为 TEMP):
#bEdge := #bBtn AND NOT #bBtnLast;   // 本周期=1 且上周期=0
#bBtnLast := #bBtn;                  // 必须在算完边沿之后再更新上次值
IF #bEdge THEN #bRun := NOT #bRun; END_IF;
  • 想想两行赋值交换顺序会怎样 (答案: 边沿永远检测不到, “上次值“总等于当前值)。
  • 坑: 同一 FB 在一个扫描周期内被调用两次, 两种解法都会用同一份实例状态互相踩。与 C 类比失效点: C 里无状态函数天然可重入; FB 状态在实例数据里而不在栈上, 一个实例一处状态, 调两次就是共享。

验收标准: 1) 连按 10 次状态交替且次数正确; 2) 按住不放只翻转一次 (边沿语义, wt 的 Modify 长电平同理); 3) 两种解法行为一致; 4) 能解释原因。 进阶变体: 长按 2s 停机、短按启动 (TON + 边沿); 单按钮三态 (停止/运行/点动)。

L2: 数据处理与算法 (4 题)

2.1 模拟量滑动平均滤波 (骨架)

目标: 对一路模拟量输入做 16 点滑动平均 (定长数组循环缓冲), 并正确处理启动阶段窗口未满。 涉及知识点: circular buffer, Array/Real, FOR 求和与增量求和, 浮点误差, 模拟量在 PLCSIM 中的注入。 IO 与变量定义: iRawAin %IW64 (Int, S7-1200 板载 AI, 地址以硬件组态为准)。FB fbMovAvg 声明区:

FUNCTION_BLOCK "fbMovAvg"
VAR CONSTANT
    cN : Int := 16;
END_VAR
VAR_INPUT
    iRaw : Int;              // 0..27648
END_VAR
VAR_OUTPUT
    rAvg    : Real;          // 滑动平均
    iFilled : Int;           // 已填充点数
END_VAR
VAR
    aBuf : Array[0..15] of Real;
    iIdx : Int;              // 下一个写入位置
    iCnt : Int;              // 有效样本数
END_VAR
VAR_TEMP
    i    : Int;
    rAcc : Real;
END_VAR

提示:

  • 两种实现至少各写一遍: (A) 每周期 FOR 遍历求和再除 — O(N), N=16 无所谓, 先写它; (B) 增量和: 新样本进、被挤出的旧样本出 (需自加 static rSum) — O(1), 但要考虑浮点累计误差与未填满阶段的除数。
  • 循环缓冲与 C 的 ring buffer 完全同构: 写 aBuf[iIdx], #iIdx := (#iIdx + 1) MOD cN; (iIdx 恒非负, MOD 无负数问题)。这一条类比成立。
  • 未填满时 (iCnt < cN) 平均值除以 iCnt, 否则前 15 个周期输出被稀释 16 倍。PLCSIM 注入: wt 直接 Modify %IW64 (仿真允许, 真机不可, 见 0)。
// TODO1: 写入 aBuf[iIdx], 推进 iIdx/iCnt
// TODO2: FOR 求和 (rAcc) → rAvg, 除以实际有效点数
// TODO3 (选): 改增量和 rSum, 与 FOR 版对拍, 差值应在浮点误差内

验收标准: 1) 恒定输入时 rAvg == 输入; 2) 阶跃输入后 rAvg 用 16 个周期收敛 (wt 手动改 16 次); 3) 单点尖峰 (注入一次 27648) 只引起约 1/16 幅度扰动; 4) iFilled 从 1 增长到 16 后封顶。 进阶变体: 3/5 点中值滤波 (复用 2.4 排序); 加权滑动平均; 试着把 cN 提成 VAR_INPUT — 会发现数组声明长度必须编译期常量; 但 S7-1200 支持变长数组形参 Array[] (需优化块 + 固件 >= V4.2; FC 只能声明在 Input/InOut 区, FB 只能 InOut), 把 fbMovAvg 改造成 Array[] + LOWER_BOUND/UPPER_BOUND 的通用版是很好的练习 (见 03 章 9.3)。

2.2 SCALE_X 工程量转换 + 报警滞回 (骨架)

目标: 0..27648 原始值转 0..100.0 工程量, 超过 80.0 报警, 回落到 75.0 以下才解除 (滞回)。 涉及知识点: NORM_X / SCALE_X, 工程量转换, 滞回 (hysteresis), 报警抖动。 IO 与变量定义: iRawAin %IW64。FB fbScaleAlarm 声明区:

FUNCTION_BLOCK "fbScaleAlarm"
VAR_INPUT
    iRaw   : Int;
    rMin   : Real := 0.0;
    rMax   : Real := 100.0;
    rHiLim : Real := 80.0;
    rHyst  : Real := 5.0;
END_VAR
VAR_OUTPUT
    rEng     : Real;
    bHiAlarm : Bool;
END_VAR
VAR_TEMP
    rNorm : Real;
END_VAR

提示:

  • 标准两步 (NORM_X 归一化到 0.0..1.0, 再 SCALE_X 映射): #rNorm := NORM_X(MIN := 0, VALUE := #iRaw, MAX := 27648); #rEng := SCALE_X(MIN := #rMin, VALUE := #rNorm, MAX := #rMax); 若你的版本按带 OUT 的调用形式处理, 写成 NORM_X(MIN := ..., VALUE := ..., MAX := ..., OUT => #rNorm) 即可。27648 是单极性满量程标称值; 溢程/欠程区间 (约 27649..32511 与负值区) 有专门含义, 本题先钳位处理; NORM_X/SCALE_X 不做自动限幅, 溢程输入线性外推 (待核实: 以指令帮助为准)。
  • 滞回永远是两条独立 IF (置位条件/复位条件), 不要写成一条: IF #rEng > #rHiLim THEN #bHiAlarm := TRUE; END_IF; IF #rEng < #rHiLim - #rHyst THEN #bHiAlarm := FALSE; END_IF; 为什么需要滞回: 信号带噪声在 80.0 附近抖动时, 单阈值报警每秒翻转几十次, HMI 与日志被刷爆 — 施密特触发器的软件版。
// TODO1: 原始值钳位 (<0 → 0, >27648 → 27648)
// TODO2: NORM_X + SCALE_X → rEng
// TODO3: 滞回报警 → bHiAlarm

验收标准: 1) raw = 0/13824/27648 → rEng = 0.0/50.0/100.0 (浮点误差内); 2) raw 阶跃跨过 22118 (约 80%) 报警出现; 3) 阈值附近 Modify ±50 码值抖动, 报警状态不抖; 4) 回落到 20736 (约 75%) 以下解除。 进阶变体: 加低报与报警窗口; 报警延时确认 (TON 滤短毛刺); 反向输出: 工程量 → 0..27648 写 %QW (先 NORM_X 再 SCALE_X, 即 4..20mA 输出的套路)。

2.3 简单文本协议解析: SET,1,25.5 (骨架)

目标: 输入一行逗号分隔命令字符串, 解析出命令字/通道号/数值, 非法输入返回错误码 — 3.4 通信与 3.5 配方的解析层基本功。 涉及知识点: SCL String 语义 (1-based、定长缓冲), FIND/MID/LEFT/RIGHT, 字符串比较, S_CONV/STRG_VAL 数值转换, 错误码设计。 IO 与变量定义: FB fbParseCmd 声明区 (eErr 约定: 0=正常 1=格式 2=命令字 3=数值):

FUNCTION_BLOCK "fbParseCmd"
VAR_INPUT
    sRx : String[32];        // 如 'SET,1,25.5'
END_VAR
VAR_OUTPUT
    eErr    : Int;
    iCh     : Int;
    rVal    : Real;
    bSetCmd : Bool;
END_VAR
VAR_TEMP
    iC1  : Int;              // 第一个逗号位置
    iC2  : Int;              // 第二个逗号位置 (绝对)
    sCmd : String[8];
    sCh  : String[4];
    sVal : String[12];
END_VAR

提示:

  • String 是 1-based; LEN 返回实际长度; FIND(IN1 := s, IN2 := ‘,’) 返回首个匹配位置, 找不到返回 0 — 用 0 判错。拆解套路 (SET,1,25.5): iC1 = FIND(sRx, ‘,’); sCmd = LEFT(IN := sRx, L := iC1-1); 第二个逗号: 把首逗号之后的尾巴切出来 (MID/RIGHT), 对尾巴 FIND 得相对位置再换算回绝对位置 — 经典 off-by-one 练习, 先在纸上算好参数。
  • 数值转换: S_CONV(IN := #sVal, OUT => #rVal); 或 STRG_VAL (可指定小数点/指数格式, 格式位含义以指令帮助为准, 待核实)。转换失败的行为 (OUT 保持原值还是置 0) 需要实验确认并写进注释 — 本题隐藏考点。
  • 与 Python 类比: 手写 line.split(‘,’) + float(x) + try/except。类比失效点: SCL 没有 split、正则与异常, 只有 FIND/MID 与显式错误码; String[32] 是定长缓冲 (2 字节头 + 32 字节字符区), 不是动态 str。
  • 解析通过后别忘了范围检查 (如 iCh 1..8, rVal 合理区间)。
#eErr := 0;
#bSetCmd := FALSE;
#iC1 := FIND(IN1 := #sRx, IN2 := ',');
// TODO1: 校验两个逗号都存在且顺序合法, 否则 eErr 置格式错误后 RETURN
// TODO2: 切出 sCmd/sCh/sVal (LEFT/MID/RIGHT + 偏移换算)
// TODO3: IF sCmd = 'SET' THEN 通道号与数值转换+范围检查, bSetCmd := TRUE
//        ELSE eErr 置命令字错误

验收标准: 1) ‘SET,1,25.5’ → eErr=0, iCh=1, rVal=25.5, bSetCmd=TRUE; 2) 空串、‘SET’、‘SET,1’、‘SET,1,25.5,extra’、‘SET,x,25.5’ → 各返回定义的错误码; 3) ‘GET,2’ → 命令字错误或你定义的扩展行为; 4) 转换失败行为有实验记录。 进阶变体: 支持第二条命令 RST,1; 不用 FIND 的逐字符状态机解析; 与 3.4 联动: 从 TRCV 缓冲收字节流, 按 ‘\n’ 断行后交给本 FB。

2.4 插入排序 + 二分查找: 就是普通编程 + TEMP 陷阱 (骨架)

目标: 在 SCL 里对定长 Int 数组实现插入排序与二分查找, 顺手踩一遍 TEMP 的坑。 涉及知识点: 数组参数 (VAR_IN_OUT 引用传递), WHILE/FOR, 布尔求值顺序, TEMP 的真实语义。 IO 与变量定义: FC fcSortIns 与 fcBinSearch 声明区:

FUNCTION "fcSortIns" : Void
VAR_INPUT
    iN : Int;                  // 有效元素个数 (<=16)
END_VAR
VAR_IN_OUT
    a : Array[0..15] of Int;   // 调用方数组, 引用传递
END_VAR
VAR_TEMP
    i    : Int;
    j    : Int;
    iKey : Int;
END_VAR

FUNCTION "fcBinSearch" : Void
VAR_INPUT
    iKey : Int;
    iN   : Int;
END_VAR
VAR_IN_OUT
    a : Array[0..15] of Int;
END_VAR
VAR_OUTPUT
    iPos   : Int;    // 找到: 0..15
    bFound : Bool;   // 未找到: FALSE, iPos := -1
END_VAR
VAR_TEMP
    iLo  : Int;
    iHi  : Int;
    iMid : Int;
END_VAR

提示:

  • 写法与 C 同构, 但有一个致命差异: SCL 的 AND/OR 不提供语言级短路保证 (与 C 的 && 不同)。C 版 WHILE (j >= 0) AND (a[j] > key) 在 j=-1 时若不短路就是负下标运行时错误; 必须改写成不越界的结构 (前置守卫 + 嵌套 IF, 或调整循环变量逻辑) — “普通编程“迁到 SCL 的第一个坑, 也是本题核心考点。
  • 二分查找: 经典 lo/hi/mid 三分; (lo+hi)/2 对正数无取整歧义; 未找到 iPos := -1。
  • TEMP 陷阱 (本题主角): TEMP 是每次调用共用的草稿区, 不清零、跨调用内容不保证。C 类比: 像未初始化的局部变量, 但更隐蔽 — 更像全体函数共用的一块 static scratchpad, 上次留下的值还在, 看似“能记住“, 中间插入别的调用就被改写。实验: 1) FB 里加 VAR_TEMP x : Int, 某次调用赋值, 下次调用不赋值直接读 (拷到 output 观察), 它“记得“上次; 2) 两次调用之间调另一个用大量 TEMP 的块, x 被破坏。结论: 状态一律 static (FB) / DB / M; TEMP 只放“本调用内先写后读“的量。调用方法: 测试 FB 的 static 里建 Int 数组, 乱序赋初值, 调 fcSortIns 后逐元素监控。
FOR #i := 1 TO #iN - 1 DO
    #iKey := #a[#i];
    #j := #i - 1;
    // TODO1: 后移元素, 直到 j<0 或 a[j] <= iKey (不得依赖 AND 短路!)
    // TODO2: 放回 iKey
END_FOR;

验收标准: 1) 16 个逆序元素排成升序, 已有序输入也正确; 2) 二分查找存在/不存在关键字各至少 5 组用例通过; 3) WHILE 在 j=-1 边界无运行时错误 (PLCSIM 实跑); 4) TEMP 两个实验现象亲手复现并写进注释。 进阶变体: FB static 里维护“有序插入“ (新值二分找位后搬移插入); 比较计数器统计最好/最坏情形; 对 Real 数组再写一份 — S7-1200 无泛型, 只能复制改类型, 体会 PLC 生态与软件工程的差距。

L3: 系统集成 (6 题)

L3 开始“像项目“: 状态机、FB 复用、控制算法、通信、HMI、总联调。建议每题先在纸上写清块划分再动手。

3.1 双向交通灯状态机 (骨架)

目标: 十字路口双向红绿灯, 6 相位循环, 时长可配; 任何时刻每个方向有且仅有一盏灯亮, 换向必经全红安全间隔。 涉及知识点: CASE + CONSTANT 命名状态, 相位时长表 (数组), 定时器数组 (包装 FB 手法), 输出解算与互斥自检, “未被调用的实例会冻结”。 相位表与 IO:

相位南北东西默认时长
0 南北绿绿10s
1 南北黄3s
2 全红A1s
3 东西绿绿10s
4 东西黄3s
5 全红B1s

输出 aLamp[0..5] 映射 %Q0.0~%Q0.5, 顺序: nsG/nsY/nsR/ewG/ewY/ewR; 行人请求 %I0.0 (选做)。 小 FB fbPhT (每相位一个定时器的包装, 声明+代码全给):

FUNCTION_BLOCK "fbPhT"
VAR_INPUT
    bRun : Bool;    // 本相位激活时为 TRUE
    tDur : Time;
END_VAR
VAR_OUTPUT
    qDone : Bool;
END_VAR
VAR
    ton1 : TON;
END_VAR
#ton1(IN := #bRun, PT := #tDur);   // IN=FALSE 时自动复位 ET
#qDone := #ton1.Q;

主 FB fbTraffic 声明区:

FUNCTION_BLOCK "fbTraffic"
VAR CONSTANT
    cPhNsG   : Int := 0;
    cPhNsY   : Int := 1;
    cPhAllR1 : Int := 2;
    cPhEwG   : Int := 3;
    cPhEwY   : Int := 4;
    cPhAllR2 : Int := 5;
    cPhCnt   : Int := 6;
END_VAR
VAR_INPUT
    bEnable : Bool;
END_VAR
VAR_OUTPUT
    aLamp : Array[0..5] of Bool;   // nsG nsY nsR ewG ewY ewR
    iPhase : Int;
    bFault : Bool;                 // 互斥自检失败
END_VAR
VAR
    aDur  : Array[0..5] of Time;
    aTon  : Array[0..5] of "fbPhT";
    bInit : Bool;
END_VAR
VAR_TEMP
    i : Int;
END_VAR

提示:

  • 为什么要包装 FB: S7-1200 的 SCL 里直接声明 TON 数组再用变量索引调用 (形如 #toni), 多数版本不支持或行为不可靠 (社区与西门子论坛共识, 待核实: 以你的版本实测为准)。标准做法: 内含单个 TON 的包装 FB → 包装 FB 的数组 → FOR 索引调用。
  • 每个扫描周期用 FOR 把 6 个定时器全部调用一遍, bRun := (#iPhase = #i): 当前相位计时, 其余自动复位。实例不被调用就冻结 (ET/Q 保持) — 若只调用当前相位的定时器, 离开相位时 ET 不清零, 下次进来立刻触发; 踩一次就懂。
  • 相位推进: IF #aTon[#iPhase].qDone THEN #iPhase := (#iPhase + 1) MOD cPhCnt; END_IF;
  • 输出解算用 CASE 写 (读起来像状态说明书), aLamp 每个元素只在对应相位为 TRUE; 末尾加互斥自检: 每方向三灯之和恒 1, 违反则 bFault — “输出不变式“的雏形。
IF NOT #bInit THEN
    #aDur[cPhNsG] := T#10S;  #aDur[cPhNsY] := T#3S;
    // TODO1: 补齐其余 4 个时长
    #bInit := TRUE;
END_IF;
FOR #i := 0 TO cPhCnt - 1 DO
    #aTon[#i](bRun := (#iPhase = #i) AND #bEnable, tDur := #aDur[#i]);
END_FOR;
// TODO2: 相位推进 (MOD 循环)
// TODO3: CASE #iPhase OF ... 输出解算到 aLamp + 互斥自检

验收标准: 1) 完整周期 28s, 相位顺序符合表; 2) 任意时刻每方向恰有一盏灯 (bFault 恒 FALSE); 3) 相位 2/5 的全红间隔真实存在 (输出锁存或趋势证明); 4) bEnable 断开 → 全灭 (或你定义的失败安全态), 重启后从相位 0 开始。 进阶变体: 行人按钮插入“全红+行人绿“相位; 绿末 3s 闪烁 (复用 fbBlink); 夜间黄闪; 相位时长搬进全局 DB 供 HMI 修改 (与 3.5 联动)。

3.2 通用电机设备 FB: 多实例驱动 3 台 + UDT 接口 (骨架)

目标: 一个 FB 封装电机设备全部行为 (启停/故障锁存与复位/运行时长累计/维护提醒), 接口 UDT 化, 多实例驱动 3 台。 涉及知识点: UDT 定义接口, FB 多实例 (multi-instance), OB 无 static → 包装 FB 分层, 时长累计的正确姿势。 UDT 定义 (PLC data types):

类型字段类型初值含义
udtMotorCfgrMaintHoursReal2000.0维护阈值 [h]
udtMotorCfgtFaultOnTimeT#200MS故障确认延时 (滤毛刺)
udtMotorStabRun / bFault / bMaintDueBoolFALSE运行/故障锁存/待维护
udtMotorStarRunHours / iStartCntReal / Int0累计运行时长 [h] / 启动次数

FB fbMotor 声明区:

FUNCTION_BLOCK "fbMotor"
VAR_INPUT
    bStart : Bool;              // 瞬动
    bStop  : Bool;              // 常闭
    bReset : Bool;              // 故障复位 (瞬动)
    bFault : Bool;              // 过载/接触器故障反馈 (电平)
    cfg    : "udtMotorCfg";     // 参数 (存在实例 DB, HMI 可改)
END_VAR
VAR_OUTPUT
    bRun : Bool;
END_VAR
VAR
    sta       : "udtMotorSta";  // 状态 (HMI 经实例 DB 符号路径访问)
    trigStart : R_TRIG;
    trigReset : R_TRIG;
    tonFault  : TON;
    tonHour   : TON;            // 1s 节拍
END_VAR

提示:

  • OB1/OB30 没有 static 区 (OB 无实例 DB), 不能声明多实例。所以建包装 FB fbPlant, 在其 static 里声明 fbMotor 实例数组, OB1 用单实例 DB 调 fbPlant — TIA 标准分层: OB 是事件入口, FB 是组件, 实例是对象。大结构体参数若要按引用传递 (省拷贝) 可用 VAR_IN_OUT; 本题简化用 VAR_INPUT + static, 想清楚两者代价。
  • 运行时长不要用扫描周期累加 (周期抖动大); 用 TON 1s 节拍: #tonHour(IN := #sta.bRun, PT := T#1S); IF #tonHour.Q THEN #sta.rRunHours := #sta.rRunHours + 1.0/3600.0; END_IF;
  • 故障: bFault 经 tonFault 延时确认后锁存并断开 bRun; bReset 用边沿解锁。想清楚: 为什么故障检测用电平、复位用边沿 (提示: 复位若用电平, 按住不放时故障永远无法再报)。维护: rRunHours > cfg.rMaintHours → bMaintDue 锁存, 手动复位; 复位动作包含 rRunHours 清零 (模拟保养完成)。
  • 与 C 类比: fbMotor 约等于 class (数据与逻辑合一), 多实例约等于对象数组; 但没有继承与虚函数, “接口“靠 UDT 约定。类比失效点: 实例不是运行期 new 出来的堆对象, 是编译期分配在实例 DB 里的连续数据; 调用是同步过程调用, 不存在线程或消息循环。
#trigStart(CLK := #bStart AND NOT #sta.bFault);
#trigReset(CLK := #bReset);
#tonFault(IN := #bFault, PT := #cfg.tFaultOn);
// TODO1: 故障锁存 (tonFault.Q 上升即锁存) 与复位
// TODO2: 启保停 (复用 1.1 思路, 停止条件含故障与 bStop 常闭)
// TODO3: 1s 节拍累计 rRunHours; trigStart.Q 计启动次数
// TODO4: bMaintDue 判定与复位清零
#bRun := #sta.bRun;

包装 FB fbPlant (接口: VAR_INPUT aStart/aStop/aFault : Array[1..3] of Bool 与 bReset : Bool; static aMot : Array[1..3] of “fbMotor”; TEMP i : Int), 代码区:

FOR #i := 1 TO 3 DO
    #aMot[#i](bStart := #aStart[#i], bStop := #aStop[#i],
              bFault := #aFault[#i], bReset := #bReset);
END_FOR;

验收标准: 1) 3 台独立启停互不影响; 2) 任一台 bFault 持续 200ms 后锁存停机, bReset 脉冲解锁且需重新启动; 3) 某台 cfg.rMaintHours 改 0.001 并运行数秒后 bMaintDue 置位, 复位后 rRunHours 清零; 4) 时长精度: 对照 PLCSIM 真实时间跑 1 分钟, 误差 < 1%。 进阶变体: 接触器反馈与指令不一致检测 (启动后 2s 无反馈 → 故障); 双阈值 (磨合期/保修期); 改一个常量把 3 台扩到 10 台 (体会多实例复用价值); 给 fbMotor 写最小单元测试 FB。

3.3 PID_Compact 温控 + 一阶惯性对象仿真联调 (骨架)

目标: 闭环控制自写的一阶惯性 (PT1) 温度对象: 设定 60 度, 观察超调与稳定时间; PID_Compact 与自写 PI 各做一遍并对比。 涉及知识点: PID_Compact 调用约束 (cyclic interrupt OB 固定周期), 被控对象离散化, PLCSIM 对 PID 的限制, PI 参数与抗饱和。 仿真可行性 (重要, 官方手册): PID_Compact V2.x 在一代 S7-1200 上不支持 PLCSIM 仿真, 仅 S7-1500 与 S7-1200 G2 可; 且仿真中自整定可能因采样时间误差中断, 需设 CycleTime.EnEstimation = FALSE、EnMonitoring = FALSE。若你的工程是一代 S7-1200 + 标准 PLCSIM, PID_Compact 路线走不通, 用替代法 (自写 PI) 完成 — 它反而更能讲清 PID 本质。 IO 与变量定义: 建 cyclic interrupt OB (如 OB30, 100ms) 作固定采样节拍。被控对象 FB fbPlantPT1 (完整给出, 属基础设施):

FUNCTION_BLOCK "fbPlantPT1"
VAR_INPUT
    rU       : Real;         // 加热功率 0..100 [%]
    rAmbient : Real := 20.0; // 环境温度
    rGain    : Real := 0.8;  // 稳态增益: u=100% 时温升 80 度
    rTau     : Real := 20.0; // 时间常数 [s]
    rTs      : Real := 0.1;  // 采样周期 [s], 必须等于 OB30 周期
END_VAR
VAR_OUTPUT
    rY : Real;               // 对象输出 (温度)
END_VAR
// 一阶欧拉离散: y += Ts/Tau * (K*u + Ambient - y); 稳态 y = Ambient + K*u
#rY := #rY + (#rTs / #rTau) * (#rGain * #rU + #rAmbient - #rY);
IF #rY < #rAmbient THEN #rY := #rAmbient; END_IF;   // 物理下限

包装 FB fbTempLoop 声明区:

FUNCTION_BLOCK "fbTempLoop"
VAR CONSTANT
    cKp : Real := 2.0;
    cTi : Real := 30.0;    // 积分时间 [s]
END_VAR
VAR_INPUT
    rSp            : Real := 60.0;   // 设定值
    bReset         : Bool;
    bUsePidCompact : Bool := FALSE;  // TRUE=PID_Compact, FALSE=自写 PI
END_VAR
VAR_OUTPUT
    rPv  : Real;
    rOut : Real;            // 0..100
END_VAR
VAR
    pidC  : PID_Compact;    // 多实例; 引脚随版本 (V2.x/V3) 有差异, 以指令帮助为准 (待核实)
    plant : "fbPlantPT1";
    rIntg : Real;           // PI 积分项
END_VAR
VAR_TEMP
    rErr : Real;
END_VAR

自写 PI 参考 (fbTempLoop 代码区核心部分, 可直接抄):

#rErr := #rSp - #plant.rY;
#rIntg := #rIntg + #rErr * (0.1 / #cTi);      // 0.1 = 采样周期
IF #rIntg > 100.0 THEN #rIntg := 100.0; END_IF;
IF #rIntg < 0.0 THEN #rIntg := 0.0; END_IF;
#rOut := #cKp * #rErr + #rIntg;
IF #rOut > 100.0 THEN #rOut := 100.0; END_IF;
IF #rOut < 0.0 THEN #rOut := 0.0; END_IF;
#plant(rU := #rOut);   // TODO: bUsePidCompact 分支下改调 PID_Compact
#rPv := #plant.rY;

PID_Compact 分支 (仅当你的 CPU 可仿真时): #pidC(Setpoint := #rSp, Input := #plant.rY, Reset := #bReset); #rOut := #pidC.Output; 手/自动与 PID 组态在实例 DB 的调试 (Commissioning) 界面完成。OB30 代码区每 100ms 调用一次: "fbTempLoop_DB"(rSp := %MD100, bReset := %M10.1); (plant.rTs 必须与此周期一致)。 验收标准: 1) 自写 PI: rSp=60, 稳态误差趋 0 (积分作用), 无持续振荡; 2) 手动扰动 (plant.rAmbient 从 20 改 10), 恢复过程可观测; 3) Kp 增大 10 倍, 记录振荡加剧趋势; 4) 若走 PID_Compact 路线: 完成一次自整定, 与手调参数对比记录在案。 进阶变体: 对象加纯滞后 (环形缓冲延迟 N 步), 看 PI 明显变难; 完整 anti-windup (输出饱和时冻结/回算积分); 加 D 项与一阶滤波; 把 rPv 写入数组供 HMI 趋势 (与 3.5 联动)。

3.4 Modbus TCP 从站数据暴露 (骨架 + 仿真限制说明)

目标: PLC 作为 Modbus TCP server (从站), 把 16 个 holding register 暴露给 PC 主站: 读计数、写设定值。 涉及知识点: MB_SERVER, TCON_IP_v4 连接描述, 非优化 DB, 字节序, 点表 (tag mapping) 文档。 Modbus 数据模型一句话: holding register 是可读可写的 16 位寄存器 (FC03 读, FC06/FC16 写), 对应 PLC 侧一块非优化 DB 里的 Word 数组。 PLCSIM 限制 (决定你的路线): 标准 PLCSIM 无真实 TCP/IP, Modbus 指令不被仿真 (社区多方一致结论; 官方口径: 开放用户通信仅经 Softbus 在本机内工作)。可行路线: (a) PLCSIM Advanced (真实 TCP/IP 走虚拟网卡; 一代 S7-1200 不在其支持列表, 需 S7-1500 或 S7-1200 G2, 待核实: 以 Advanced 版本支持表为准); (b) 真机; (c) 仿真替代题: 两个 PLCSIM 实例 + TSEND_C/TRCV_C (Softbus 内) 互发数据, 练开放用户通信 API, 达到同样的“数据暴露“目的。 IO 与变量定义 (真机/Advanced 路线): 全局 DB dbModbus, 属性取消 Optimized block access (非优化, 按绝对地址访问):

字段类型初值含义
connTCON_IP_v4ConnectionType=16#0B (TCP), ID=1, ActiveEstablished=FALSE (从站被动), LocalPort=502连接描述
hrArray[0..15] of Word0暴露的 holding registers

包装 FB fbModbusSrv 声明区 + 代码骨架:

FUNCTION_BLOCK "fbModbusSrv"
VAR
    mbSrv   : MB_SERVER;    // 多实例; 参数以你版本的库为准 (待核实)
    bErr    : Bool;
    wStatus : Word;
END_VAR
// MB_SERVER 每个扫描周期无条件调用 (非边沿触发), 连接由它自己维护
#mbSrv(DISCONNECT := FALSE, MB_HOLD_REG := "dbModbus".hr, CONNECT := "dbModbus".conn);
#bErr    := #mbSrv.ERROR;
#wStatus := #mbSrv.STATUS;
// TODO1: 生产数据打包进 hr (计数 Int → Word 直接 MOVE; Real 拆两个 Word, 注意 Modbus 大端字节序, 字节交换用 SWAP)
// TODO2: 主站可写的 hr 元素映射回程序内设定值

真机步骤: 1) 硬件组态给 CPU 设固定 IP (如 192.168.0.1/24), PC 同网段; 2) 建 dbModbus (非优化) 并填 conn 初值 (LocalPort 502); 3) OB1 每周期调用 MB_SERVER; 4) PC 端任一 Modbus 主站工具 (开源 CLI: mbpoll; GUI: Modbus Poll、QModMaster) FC03 读地址 0..15、FC06 写单寄存器; 5) Wireshark 抓端口 502 核对 MBAP 头与寄存器值, 异常 STATUS 记成表。 验收标准 (真机/Advanced): 1) 主站读 hr[0] == PLC 内计数; 2) 主站写 hr[1] → PLC 设定值变化且被程序采用; 3) 拔网线再恢复, 连接自动重建; 4) 至少整理 3 个 STATUS 错误码含义。 仿真替代题验收 (标准 PLCSIM 可做): 两个 PLCSIM 实例各跑一个 CPU 工程: A 每 1s 用 TSEND_C 发 8 字节, B 用 TRCV_C 接收并回显; 演示 REQ 边沿重连一次、断开一次, 记录 DONE/BUSY/ERROR/STATUS 行为。 进阶变体: 暴露 Float (字节序全流程); 多连接 (MB_SERVER 多实例, S7-1200 连接数上限查 CPU 手册, 待核实); 反向用 MB_CLIENT 读真实 Modbus 温湿度传感器; hr 映射写成正式点表文档 (SCADA 集成交付物)。

3.5 KTP HMI: 监控画面 + 配方 (可用仿真)

目标: 给 3.2 的三电机系统配一个 KTP 画面: 总览/操作/趋势三屏 + 报警 + 配方切换三套工艺参数。 涉及知识点: HMI 连接与 HMI tag, 画面元素动态化, 离散量报警, 配方 (recipes), HMI 仿真联动 PLCSIM。 仿真可行性: STEP 7 Basic 自带 WinCC Basic, KTP Basic 面板仿真无需额外许可; PC 运行系统 RT Advanced/Professional 仿真无许可每次限 1 小时 (官方)。Basic 面板支持配方但有限制 (无目录层级、记录数量有限; 第三方资料称约 100 条与 256KB, 待核实, 以 WinCC 手册为准)。 步骤清单:

  1. 项目树添加设备 HMI > KTP700 Basic PN; 在 Connections 里把 HMI 的 Ethernet 接口连到 PLC 的 PROFINET 接口 (仿真时同一 PLCSIM 实例)。
  2. HMI tag 表: 建 HMI 变量, 连接指向该 PLC, 地址用符号路径 (如 fbPlant_DB 的 static: aMot[1].sta.bRun — 优化 DB 必须符号访问)。
  3. 画面 1 总览: 3 个电机图形 (圆/矩形), 填充色绑 bRun/bFault/bMaintDue (绿/红/黄)。
  4. 画面 2 操作: 启动按钮用 Set bit while pressed + 释放复位 组合模拟瞬动 (或在 PLC 侧脉冲化), 停止按钮注意常闭语义; I/O 域显示 rRunHours; 画面上若放急停, 必须标注“非安全功能“。
  5. 画面 3 趋势: 实时曲线绑 rRunHours 或 3.3 的 rPv。报警: PLC 侧离散量报警 (bFault/bMaintDue), HMI 报警视图分报警/警告两类, 支持确认。
  6. 配方: 定义配方 (元素 = 3 台电机 cfg 参数), 建 3 条数据记录; 画面放配方视图, 支持从 PLC 读/写到 PLC。
  7. 仿真: 先启动 PLCSIM, 再选中 HMI 画面点 Simulation — HMI 运行系统本机启动并自动连 PLCSIM。

验收标准: 1) HMI 启停任一电机, PLC 状态变化并反馈画面 (1s 内); 2) 趋势曲线时间轴与数值可读; 3) 报警出现、确认、消失全流程; 4) 切换配方数据记录后 3 台参数整组变化且生效; 5) 停 PLCSIM 再恢复, HMI 断线提示并自动重连。 进阶变体: 用户组与权限 (操作工/工程师); 画面模板 + 传参画面 (每电机一个详情页); 报警文本双语; 把 3.6 产线做成 5 画面小项目 (总览/产线/电机详情/趋势/报警)。

3.6 综合小项目: 小型产线 (两段传送带 + 料仓 + 计数 + 班次统计 + 报警)

目标: 把 0.1~3.5 拼成一个小产线控制程序, 20 分钟能讲清架构。 涉及知识点: 架构分层, 顺序启停状态机, 故障传播, 班次统计 (时间处理), 报警首出, 全局数据组织。 工艺描述: 料仓下料到传送带 A, A 末端转 B, B 出料口装箱; 光电计件; 三班倒 (06/14/22 点切换), 每班统计产量; 任一段故障按工艺方向有序停机; 急停立即全停 (软件急停, 非安全回路)。 文字架构图:

              +--------- fbLine (协调 FB) ---------+
              | 产线状态机: 停止/启动中/运行/停止中/故障/急停 |
              +------------------------------------+
 料仓                传送带 A             传送带 B
 +--------+ 下料阀 +------------+ 光电1 +------------+ 光电2
 | fbSilo |------>| fbConveyor A|----->| fbConveyor B|-----> fbPacker 装箱 (1.3)
 +--------+       +------------+       +------------+         |
            (内含 fbMotor)        (内含 fbMotor)              |
                 |                     |                      |
         dbProd (fbShift 班次统计)  dbAlarm (fbAlarm 报警锁存) ← HMI (3.5) / Modbus (3.4)

分块清单 (先建全部空块再逐个填):

类型职责复用
OB1SCL调 fbLine
OB30 (100ms)cyclic interrupt时基/平滑量 (滤波)2.1
OB100startup初始化 (时长表、数组)3.1
fbLineFB产线状态机、顺序启停、联锁CASE 手法 3.1
fbConveyorFB单段皮带: 电机+堵转检测+下游满停内含 fbMotor (3.2)
fbSiloFB料位换算+缺料滞回报警+下料阀时序2.2
fbPackerFB计件装箱1.3
fbShiftFB班次判定+产量环形归档 (8 班)时间处理
fbAlarmFB报警锁存、首出报警、复位2.2 滞回手法
dbProd / dbAlarm全局 DB产量/报警数据, HMI 与 Modbus 从这里取3.4/3.5

关键 FB 骨架, fbLine 声明区:

FUNCTION_BLOCK "fbLine"
VAR CONSTANT
    cStStop    : Int := 0;
    cStStart   : Int := 1;   // 逆序启动: B 先启, 2s 后 A
    cStRun     : Int := 2;
    cStStopSeq : Int := 3;   // 顺向停止: A 先停, 5s 排空后 B
    cStFault   : Int := 4;
    cStEstop   : Int := 5;
END_VAR
VAR_INPUT
    bStart   : Bool;
    bStop    : Bool;
    bEstopNC : Bool;
END_VAR
VAR_OUTPUT
    aBelt  : Array[1..2] of Bool;
    bValve : Bool;
    iState : Int;
END_VAR
VAR
    tonSeq : TON;          // 顺序节拍
    convA  : "fbConveyor";
    convB  : "fbConveyor";
    // silo/packer/shift/alarm 实例同法声明 (见分块清单)
END_VAR

fbLine 代码区 (骨架) 与 fbShift 声明区:

// TODO1: 急停最高优先 (NOT bEstopNC → cStEstop, 清 aBelt/bValve)
// TODO2: CASE iState OF — cStStart: B 先启, tonSeq 2s 后 A → cStRun;
//        cStStopSeq: A 先停, tonSeq 5s (排空) 后停 B → cStStop
// TODO3: 故障传播: 任一 conv 故障 → cStFault, 上游立即停
// TODO4: 产量: packer.iCount 累加进 dbProd, 班次切换时由 fbShift 归档+清零

fbShift 声明要点: 输出 iShift : Int (0/1/2 班) 与 aHist : Array[0..7] of Int (最近 8 班产量, 环形); static 放 dtNow : DTL、iHourIn : Int (可注入的当前小时, 可测性)、iShiftLast : Int、iIdx : Int、bInit : Bool。

提示: 用 RD_LOC_T(OUT => #dtNow); 取本地时间, 按小时判班 (6..13 → 0 班, 14..21 → 1 班, 其余 2 班); 班次号变化瞬间: aHist[iIdx] := 当前班累计, iIdx := (iIdx+1) MOD 8, 累计清零。两点注意: 首扫用 bInit 防误归档; 判班函数把小时做成输入参数 (iHourIn), 测试注入 5/6/13/14/21/22 边界值 — PLCSIM 时钟跟随 PC 时间, 不注入就得在真实时间上等 8 小时。fbAlarm 核心: Array[0..15] of Bool 报警位 + iFirstOut (首出报警) + 复位语义 (报警位随条件消失自动清, 首出须手动复位)。 验收标准: 1) 顺序启动正确 (B 先 A 后, 间隔 2s); 顺序停止正确 (A 先 B 后, 排空 5s); 2) 中段人为置故障: 上游立即停, 下游排空后停; 3) 急停全停且从置位到输出清除不超过一个扫描周期 (输出锁存观察), 注释写明软件急停边界; 4) 班次切换 (注入小时参数测试) 产量正确归档, 旧班数据可查; 5) 报警首出可追溯, 复位语义正确; 6) HMI 上能开停线、看产量与报警 (与 3.5 联动)。 进阶变体: OEE 三要素最小实现 (时间/性能/质量开动率); 料仓自动补料滞回控制; 用 3.4 把 dbProd 暴露给 SCADA; 班次表做成 HMI 可改参数。

附录 A: 每题完成度自查

  • 验收标准逐条通过, 关键代码注释写的是“为什么“而不是“是什么“; L2/L3 每题都自问一遍: 哪些状态放 static, 哪些放 TEMP, 为什么。
  • 每题完成后独立 commit 一次 (TIA V21 起有原生 Git 集成; V18+ 可用 Version Control Interface, 注意 VCI+Git 存不下硬件组态与库类型), 并能在 5 分钟内讲清这道题的块结构与数据流。

附录 B: 事实出处

  • PLCSIM 标准版 vs Advanced 官方对比 (TCP/IP、Trace、Modbus、实例数): https://docs.tia.siemens.cloud/r/simatic_s7_1500_et_200mp_manual_collection_enus_21/comprehensive-information/cpu-function-manuals/s7-plcsim-advanced/product-overview/s7-plcsim-products
  • PID_Compact V2.x 的 PLCSIM 限制与 EnEstimation/EnMonitoring 设置: https://docs.tia.siemens.cloud/r/simatic_s7_1500_et_200mp_manual_collection_enus_21/comprehensive-information/technology-function-manuals/pid-control/using-pid_compact/pid_compact-as-of-v2/simulating-pid_compact-as-of-v2-with-plcsim
  • 许可 (Trial 21 天、PLCSIM 无需许可、WinCC RT 仿真限 1 小时): https://docs.tia.siemens.cloud/r/en-us/v21/installation/licensing/licensing-step-7-and-wincc
  • TCON_IP_v4 连接描述与 MB_SERVER 的 CONNECT 参数: https://docs.tia.siemens.cloud/r/en-us/v20/modbus-tcp-s7-1200-s7-1500/
  • Basic 面板性能边界 (配方等): https://docs.tia.siemens.cloud/r/en-us/v20/performance-features-basic-panels-panels-comfort-panels-rt-advanced-rt-professional/
  • S7-1200/1500 安全编程指南 (软件急停不等于安全回路的依据): https://assets.new.siemens.com/siemens/assets/api/uuid:1a7ce755-79a1-494e-ae14-5e5f72c29205/safety-programming-guideline-for-simatic-s7-1200-and-1500.pdf

附录 C: 待核实清单汇总

正文标 (待核实) 之处: PLCSIM 单次扫描模式是否随版本提供; NORM_X/SCALE_X 是否不限幅; STRG_VAL 格式位细节; TON 数组变量索引调用在你版本上的实际行为; PID_Compact 具体引脚 (V2.x/V3 差异); Basic 面板配方记录数上限; PLCSIM Advanced 对 S7-1200 G2 的 Modbus 仿真支持。遇到时以你安装版本的指令帮助 (F1) 与官方手册为准。(注: Array[*] 已核实 S7-1200 支持——需优化块 + 固件 >= V4.2, 见 03 章 9.3。)