跳到正文
Learn-LAD
PLC AI 实战/重构不改行为:把三段重复逻辑收成一个 FB

重构不改行为:把三段重复逻辑收成一个 FB

把三电机延时启停里的三段重复定时逻辑重构成一个参数化 FB,用同一张场景表在虚拟 PLC 上做回归,逐点对比前后行为。

实战 第 1 课 40 分钟 TiaLink

学完你能:能安全地重构一段现有程序:先存行为基线,再改结构,最后用同一张场景表证明行为没变;并知道编译 0/0 为什么证明不了这件事。

概述

前五课一路把「敢改」拆开了:读懂(L1)→ 知道波及谁(L2)→ 写块到编译 0/0(L3)→ 让程序真跑起来(L4)。这一课补上最后一块:改结构,但不改行为。

本课对象是示例库的三电机延时启停:按一下按钮,20 秒窗口打开,三台电机分别延时 5/10/15 秒启动,窗口一过全停。原程序在同一个网络里把 TON + 线圈 写了三遍,只有 PT 和输出不同——典型的复制粘贴。我们把它收成一个参数化 FB,实例化三次,点表、符号、地址一个都不动。

本课流程已在 TiaLink 1.1.0.0 + TIA Portal V19 + S7-PLCSIM Advanced 6.0 上实测跑通,而且真的翻车了一次:我顺手把 IEC_Timer_0_DB 的调用从 TP 写成 TON,编译照样 0 错误 0 警告,但按按钮后三个电机一个都不启动。改回 TP 后,同一张场景表逐点回归,前后完全一致。

口径:重构的前提是行为基线已经存在。没有基线就动手,等于拿现场当测试环境;安全逻辑、联锁、急停相关块不做「顺手重构」。

你将学会

  • 重构前先存行为基线:同一张场景表,改前跑一遍、改后跑一遍,逐格对比;
  • 把三段重复逻辑收成一个参数化 FB(输入 Enable/Pt,输出 Q),用实例 DB 三次调用;
  • 在 SCL 外部源里写 FB / 实例 DB / OB,并知道 TP 这类系统定时器必须写成实例 DB 的方法;
  • 看懂「编译 0/0 但行为错了」这类事故:TP 与 TON 一字之差,ET 会骗人;
  • 回归时只信输出位与场景表,结构化成员(DB.Ton.ET)读不到就换证据;
  • 交付一份「重构前后对照 + 回归报告」,说明哪些是结构变化、哪些是行为契约。

前置条件

  • Windows 机器,已安装 TIA Portal V19;
  • TiaLink 已启动(不要加 --no-sim)并连接沙盒工程;仿真前确认 http://localhost:8080/api/v1/sim/status 里 available=true;
  • 一台 S7-1500 沙盒(示例源是 S7-1200,但 1200 的 LAD + IEC 定时器可以直接导入 1500 编译并仿真,本课已实测);
  • 一个支持 Streamable HTTP MCP 的客户端(Claude Code、Cursor、VS Code 等),MCP 端点为 http://localhost:8080/mcp;
  • 练习对象:示例库三电机延时启停(点表 5 个标签 + 4 个系统实例 DB + 1 个 OB);
  • 三条纪律:沙盒工程、先基线后改动、导入 ≠ 保存(重构版不保存,除非你确认要替换原程序)。

步骤

真实使用只有一次输入:说清「先跑基线、再重构成一个 FB、用同一张表回归」。建设备、导入、跑场景、写 SCL、替换、再跑一遍,都是 agent 做的。

第一步:配好 TiaLink 与 MCP

在博途那台机器上启动 TiaLink、打开沙盒工程。页首状态条会自动检测;然后在客户端里加一个 Streamable HTTP MCP 服务:

先在本页顶部点「设置」填一次地址(存在本机,示例库页面也用同一个地址),下面的 URL 会跟着更新,点「复制」直接贴进客户端配置:

MCP
{
  "mcpServers": {
    "tialink": {
      "type": "http",
      "url": "http://localhost:8080/mcp",
      "headers": { "Authorization": "Bearer <你的 TiaLink API Token>" }
    }
  }
}

看到什么:客户端列出 tialink_ 开头的工具,含 tialink_import(SimaticML / SCL)、tialink_compile、tialink_delete、tialink_sim_*。

第二步:你输入的(全部内容)

把下面这段发给 agent(练习对象换成你的工程):

用虚拟 PLC 做一次「重构 + 行为回归」,不要保存工程。

  1. 新建一台 S7-1500(CPU 1516-3 PN/DP),插一块 DI、一块 DQ;导入示例库的「三电机延时启停」,编译到 0 错误 0 警告;
  2. 先跑基线:注册仿真实例、上电、下载、RUN,然后按这张场景表记录实测值(只记实测,不要抄期望):
    • 按钮 1 秒脉冲后 +3 s / +6 s / +11 s / +16 s / +22 s,分别记录 Tag_1、Motor 1..3;
  3. 再重构:把 Main 里三段重复的 TON + 线圈 收成一个参数化 FB(Enable、Pt → Q),用三个实例 DB 调用;点表、符号、地址保持不变;编译到 0/0;
  4. 再跑同一张表,逐格对比基线;
  5. 交付:重构前后对照(结构变化 / 行为契约)+ 回归表 + 一句结论。如果哪一格对不上,先定位原因,不要改场景表迁就结果。

发完这一条,你等着看结果就行。下面是它实际会做的事(本次实测过程)。

第三步:先拿基线,再动结构

重构只改结构:三个重复分支 → 一个 FB 三个实例
本课的重构目标:行为契约不变,只把重复结构收成参数化 FB

先跑基线(改之前必须有一次)。一次按钮后的实测:

时刻 Tag_1 Motor 1 Motor 2 Motor 3 证据
S0 初始 0 0 0 0 —
+3 s 1 0 0 0 20 s 窗口在跑
+6 s 1 1 0 0 IEC_Timer_0_DB_4.ET = 5000 ms(钳在 PT)
+11 s 1 1 1 0 —
+16 s 1 1 1 1 IEC_Timer_0_DB.ET = 19990 ms
+22 s 0 0 0 0 窗口结束,全部复位

行为契约(重构不能碰的东西):① 20 秒窗口由 TP 决定,按一下开一次;② 三个 TON 都挂在 Tag_1 上,5/10/15 秒错峰;③ 窗口一关全部复位;④ 点表符号与地址不变(HMI/图纸都按符号引用)。

第四步:用 SCL 外部源做替换(第一版)

删掉旧 Main(tialink_delete 先 dryRun 再 confirm),再用一份 SCL 外部源生成 FB + 3 个实例 DB + 新 Main。先按最直接的写法生成第一版:

SCL
1FUNCTION_BLOCK "DelayedMotor"2{ S7_Optimized_Access := 'TRUE' }3VERSION : 0.14VAR_INPUT5   Enable : Bool;6   Pt : Time;7END_VAR8VAR_OUTPUT9   Q : Bool;10END_VAR11VAR12   Ton : TON_TIME;13END_VAR14BEGIN15   #Ton(IN := #Enable, PT := #Pt);16   #Q := #Ton.Q;17END_FUNCTION_BLOCK
SCL
1ORGANIZATION_BLOCK "Main"2{ S7_Optimized_Access := 'TRUE' }3VERSION : 0.14BEGIN5   "IEC_Timer_0_DB".TON(IN := "PB Switch", PT := T#20S);6   "Tag_1" := "IEC_Timer_0_DB".Q;78   "DelayedMotor_DB_1"(Enable := "Tag_1", Pt := T#5S);9   "Motor 1" := "DelayedMotor_DB_1".Q;1011   "DelayedMotor_DB_2"(Enable := "Tag_1", Pt := T#10S);12   "Motor 2" := "DelayedMotor_DB_2".Q;1314   "DelayedMotor_DB_3"(Enable := "Tag_1", Pt := T#15S);15   "Motor 3" := "DelayedMotor_DB_3".Q;16END_ORGANIZATION_BLOCK

看到什么:tialink_import(format=scl, generate=true)一次生成 DelayedMotor(FB1)、DelayedMotor_DB_1..3(DB)、Main(OB1);编译 0 错误 0 警告。

第五步:回归——这次真的翻车了

一次重构事故:编译 0/0,行为全错
把 TP 写成 TON:编译通过,但按按钮后三个电机一个都不动

第一版里,IEC_Timer_0_DB 的调用写成了 TON(原程序是 TP)。编译 0/0,但同一张场景表实测全灭:

时刻 Tag_1 M1 M2 M3 对比基线
S0 0 0 0 0 ✓
+3 s 0 0 0 0 ✗ 基线是 1 / 0 / 0 / 0
+6 s 0 0 0 0 ✗ 基线是 1 / 1 / 0 / 0
+11 s 0 0 0 0 ✗ 基线是 1 / 1 / 1 / 0
+16 s 0 0 0 0 ✗ 基线是 1 / 1 / 1 / 1
+22 s 0 0 0 0 ✓

按住按钮时 IEC_Timer_0_DB.IN = true、ET 会往上走(实测 2118 ms),但 Q 始终是 false——因为 TON 要求 IN 连续保持 20 秒才置位;而按钮只是一个 1 秒脉冲。原程序的 TP 是「按一下,窗口固定 20 秒」,所以 Tag_1 立刻置位并保持。编译只能证明语法与一致性,证明不了行为没变。

修正只改一行(其余 FB、实例 DB、三个调用都不动):

- "IEC_Timer_0_DB".TON(IN := "PB Switch", PT := T#20S);   // 第一版:错
+ "IEC_Timer_0_DB".TP(IN := "PB Switch", PT := T#20S);    // 修正:沿用原程序的 TP

重新编译 0/0,换一个新实例名(避免 ResetModule),再跑同一张表:

时刻 Tag_1 M1 M2 M3 对比基线
S0 0 0 0 0 ✓
+3 s 1 0 0 0 ✓
+6 s 1 1 0 0 ✓
+11 s 1 1 1 0 ✓
+16 s 1 1 1 1 ✓(IEC_Timer_0_DB.ET = 16798 ms)
+22 s 0 0 0 0 ✓

结构变了(3 段重复 → 1 个 FB + 3 实例),行为契约没变。这才是「重构」的完成定义。

验证清单

  • 改动之前跑过一次场景表,基线有实测值(不是补写的期望值);
  • 重构只动结构:点表标签、符号、地址、OB 编号与原来一致;
  • 编译 0 错误 0 警告(并知道这只证明语法/一致性);
  • 重构后同一张表逐格回归,全部与基线一致;
  • 至少能说出一个「编译查不出、只有回归能查」的差异(本例:TP → TON);
  • 回归证据来自输出位/场景表;读不到的成员(如 DB.Ton.ET)不用来下结论;
  • 清楚当前工程未保存,替换正式程序前经过评审与备份。

常见坑

  • 没有基线就重构:改完只能说「看起来对」;先跑一遍,基线是唯一的判据。
  • 把 TP 写成 TON(或反过来):编译 0/0,行为完全不同;定时器类型是行为契约的一部分。
  • SCL 实例 DB 带属性块:DATA_BLOCK "X" "FB" { S7_Optimized_Access := ... } 会报 语法错误:设定值“{”无效;写成 DATA_BLOCK "X" "FB" 后直接 BEGIN。
  • 定时器调用写法:必须 "IEC_Timer_0_DB".TP(...);"IEC_Timer_0_DB"(...) 和裸 TON(...) 都编译失败(Invalid function name / 形参无效)。
  • 重构顺手改符号:地址/符号一改,HMI、图纸、其他块全跟着断;重构和重命名是两件事,分开做。
  • 结构化成员读不到:"DelayedMotor_DB_1".Ton.ET 返回 DoesNotExist(标签表里能看到,按名读失败);用 Q 和输出位做判据。
  • 复用旧实例名:程序或硬件一变,下载卡 ResetModule was unhandled;换新实例名重新注册(同 L4)。
  • 导入 ≠ 保存:重构版留在内存里;要替换正式程序,先评审、再备份、再显式保存。
  • 边沿与复位语义:把「脉冲窗口」改成「电平使能」、把置位/复位换成普通线圈,都是行为变化;这类改动必须进场景表。

下一步

本课工具

TiaLink

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

打开 TiaLink