博途批量复制设备程序:用点表替代复制粘贴
批量复制的成本不在复制,而在四类差异不同步。把差异收到一份点表里,把手工改动变成数据,200 台设备一分钟生成,编译 0 错误。
一条线上 8 台同样的泵,程序写好了第一台,剩下 7 台怎么办?大多数人会复制粘贴,然后改名字、改地址、改延时——一次两分钟,8 台一个下午,中间漏一处就等着现场调试时发现两台设备抢同一个输入点。
这件事真正贵的地方不是动作多,而是复制粘贴复制的是"一样的东西",而工程里要的恰恰是"不一样的东西":块名、块号、背景 DB 的归属、IO 地址、参数默认值、注释里的台号,这些每一台都必须不同。手工改,是拿人的注意力去保证这五样东西在 200 次复制里一次不错。
更省事的路径是把差异做成数据:一份点表,一段脚本,让"改文本"这件事由程序执行。下面是我在一个 S7-1500 测试工程上把这条路径完整跑通的过程——8 台、200 台、以及把 200 多个对象整体搬到另一个 CPU——连同过程中踩到的 6 个坑,这些坑在官方文档里不会写,报错信息也基本指不到真正的原因。
一、先看清:复制粘贴到底失败在哪里
把一台设备"抄"成 8 台,动手前先列出需要改的东西,一共四类:
| 差异类别 | 具体是什么 | 漏了会怎样 |
|---|---|---|
| 标识 | 块名、块号、背景 DB 名 | 轻则编号混乱,重则编译报"程序中含有地址含混不清的对象" |
| 引用 | 背景 DB 记着自己属于哪个 FB | 编译报实例与接口不匹配,或背景 DB 挂到了别的 FB 上 |
| IO 地址 | 点表里不连续、台与台之间夹着别的信号 | 编译只给一条警告,下载后两台设备抢同一个点 |
| 参数与文本 | 延时、量程、阈值,以及注释里的台号 | 编译 0 错误,现场才发现第 5 台的延时还是模板值 |
第三和第四类是真正的麻烦:它们不会让编译失败。地址落在一个没组态的区间里,编译只丢一句"使用了硬件组态中不存在的输入或输出"——一条警告,混在几十条编译消息里;注释里的台号对不上更是没人管,直到有人夜里拿着图纸逐台核。
顺便算一下手工基线的动作数。一台设备:选中块 → 复制 → 粘贴 → 打开改块名 → 改块号(或让它自动编号)→ 复制背景 DB → 处理"实例与接口不匹配"→ 变量表里加 5 个点 → 回到调用处写 5 行实参 → 检查地址有没有和上一台撞。大约 15~20 次交互,8 台就是 130 次以上,200 台是三千次。每一次都是一次"看错一眼就会错"的机会,而其中至少一半(改号、改地址、对实参)机器做得比人稳。
所以判断一个批量做法好不好,别看它省了多少次点击,看它能不能保证这四类差异每一台都对。
二、先问一句:真的需要复制块吗
动手之前有一件事值得花五分钟确认,因为它能砍掉大部分工作量。
同类设备如果逻辑完全一样、只是点位和参数不同,根本不需要复制 FB:一个 FB + N 个背景 DB + 一个按点表生成调用段的块就够了。改一次 FB,8 台全部生效;也不用维护 8 份几乎相同的代码。
只有下面三种情况才值得复制块本身:
- 代码要分叉:第 3 台带变频器、第 6 台要跟隔壁线联锁,逻辑真的不一样了;
- 跨 CPU / 跨项目复用:设备是别人负责的 CPU,你只想把块搬过去,不想留一个共享依赖;
- 按台拆给不同的人维护:某些项目组的分工就是这样,块必须独立。
对应的两种生成模式:
| 一个 FB + N 个背景 DB | N 个 FB 副本 | |
|---|---|---|
| 每台的差异 | 全部在调用时传入 | 部分固化进块里(默认值、注释) |
| 改模板 | 改一次,全部生效 | 改完要重跑一遍生成 |
| 调试 | 所有台共用一份逻辑,改哪里影响谁很清楚 | 各台可以单独改,也容易改歪 |
| 适合 | 90% 的情况 | 代码真的要分叉时 |
后面两种模式都会跑一遍,数字分开记。
三、差异只应该有一个来源:点表
点表的格式很朴素:CSV,第一行的列名用模板 FB 的接口成员名,每行一台设备。
no,name,Start,StopPermit,Fault,RunDelay,Motor1,Motor2
1,Fan01,%I10.0,%I10.1,%I10.2,T#6S,%Q10.0,%Q10.1
2,Fan02,%I10.3,%I10.4,%I10.5,T#9S,%Q10.2,%Q10.3
3,Fan03,%I11.0,%I11.1,%I11.2,T#12S,%Q11.0,%Q11.1
4,Fan04,%I11.4,%I11.5,%I11.6,T#15S,%Q11.2,%Q11.3
5,Fan05,%I12.1,%I12.2,%I12.3,T#18S,%Q12.0,%Q12.1
只做两件事就够用了:
- 布尔成员(
Bool)那一格填地址,脚本据此建变量表; - 非布尔成员(
Time、Real、String)那一格填字面值,写进副本的默认值或调用实参。
这份样例里的地址是故意不连续的,原因很实际:现场的 IO 点表按图纸排,中间夹着液位开关、接近开关、备用手动按钮。第 5 台从 %I12.1 开始,因为 %I12.0 被别的东西占了。
这正是"循环里 i * 8 算地址"这种做法必须被否掉的理由——算得整齐,装不上现场。地址只能从点表来,一个字都不猜。
脚本读进来之后先做两项检查,都是人工复制最容易犯、编译又最容易放过的错:
- 地址写重(两台设备同一个点)——直接把重复的地址列出来,不往下走;
- 块号撞车(新块抢了别人的号)——报出被占用的号,建议换起始编号。
第二项还有个细节:重跑(覆盖已有同名块)应该被允许,否则改一次模板就得先手工删 8 个块。所以判断穿过的是"名字已存在"这一种情况,只有新块占别人的号才报错。
四、生成过程:导出 → 改文本 → 预检 → 导入 → 编译
博途里的块可以原样导出成 SimaticML——一种 XML,块名、块号、接口、每一条 SCL 语句、每一条注释都在里面,全是文本。这条路子的关键就在"全是文本"上:既然是文本,就可以用脚本改、可以 diff、可以 code review、可以进 git,也可以重跑。
一次批量生成,五个动作:
1. 导出模板 FB(以及它的背景 DB 作为模板)
2. 按点表逐台生成 XML:换名字、换编号、换默认值、换注释台号、平移内部 ID
3. 预检:只回报"将创建 / 将覆盖哪些对象",一个字节都不写进工程
4. 一次请求导入全部同种对象;调用段走 SCL 外部源生成
5. 整机编译,把错误和警告原文打出来
第 3 步是整个流程里最值得保留的一步。预检回答的是"我要覆盖什么",这在已经有一批块的项目里比什么都重要——尤其当你手上的点表是从别人那里接过来的。
第 4 步有个效率上的事实:一个请求可以装很多个对象。同种根对象(比如一堆 FB,或者一堆实例 DB)放进同一个文档,一次提交,比一个个导快一个数量级——真正的开销在工程事务上,不在传输上。
调用段单独说一句,因为它是最容易漏的一块。变量表建好了、块也复制好了,但没人调用它们,编译照样 0 错误——程序只是不干活。所以生成流程的最后一步是把 N 段调用写进同一个块里(SCL 外部源生成一个功能块,一段一台):
"DB_Fan01"(Start := "Fan01_Start",
StopPermit := "Fan01_StopPermit",
Fault := "Fan01_Fault",
RunDelay := T#6S,
Motor1 => "Fan01_Motor1",
Motor2 => "Fan01_Motor2");
200 台就是 200 段这样的代码,一次生成,用时 5 秒。比生成更重要的是:实参来自同一份点表,所以变量名、地址、延时不可能和块里对不上——手工写这段时最常见的错误就是第 7 台接了第 8 台的地址,编译不报,仿真不动。
一次真实运行的输出(8 台,复制 FB 副本模式):
模板 FB_MotorSeq:3 Bool 入 / 2 Bool 出;点表 8 台(Fan01 … Fan08);准备 1325 ms
✓ FB × 8 1920 ms (8 个对象)
✓ 背景 DB × 8 1815 ms (8 个对象)
✓ 变量表 Fans_IO 2594 ms (+40 变量)
✓ 调用段 FB_FanCalls 674 ms
编译 2523 ms:errors=0 warnings=1
三条安全默认值从头到尾没变过:先预检、默认不覆盖同名对象、导入后不自动保存。最后一条尤其重要——工程只在内存里被改,/health 一直显示 dirty=true,什么时候保存、要不要保留,仍然是人的决定。改错了直接不保存退出,什么都没有发生。
五、把这件事交给 agent:一段可直接用的提示词
前面四节都是脚本。但很多人手上的客户端已经接好了 MCP 网关(Claude Code、Cursor、VS Code 都行),第一反应是"直接让 agent 做"。这条路能走,而且很直观——读工程 → 看接口 → 改 XML → 导入 → 编译,全部在一次对话里完成,不装任何东西。
前提是提示词把约束写清楚。放开让它自由发挥,它会把地址排整齐、把块号自作主张重排、把注释的台号漏一半,而且编译照样 0 错误。我用的那段是这样的:
背景:这台机器上装着 TiaLink(默认 http://localhost:8080),博途里开着一个工程,
里面有一个已经调好的模板功能块 FB_MotorSeq(3 个 Bool 输入、1 个 Time 输入、2 个 Bool 输出)。
任务:把 FB_MotorSeq 批量复制成 4 台同类设备,块名 FB_<name>,背景 DB 名 DB_<name>。
每台的差异见点表(CSV,列名 = 接口成员名,Bool 列是地址,其它列是字面值):
no,name,Start,StopPermit,Fault,RunDelay,Motor1,Motor2
1,Valve01,%I30.0,%I30.1,%I30.2,T#4S,%Q30.0,%Q30.1
2,Valve02,%I30.3,%I30.4,%I30.5,T#6S,%Q30.2,%Q30.3
3,Valve03,%I31.0,%I31.1,%I31.2,T#8S,%Q31.0,%Q31.1
4,Valve04,%I31.4,%I31.5,%I31.6,T#10S,%Q31.2,%Q31.3
约束(按顺序执行,任一条做不到就停下来问,不要猜):
1. 先只读:报出当前工程的设备、模板块的接口成员,确认点表列名与接口一一对应;
对不上就停下来说明哪一列没用上。
2. 检查点表:地址有没有重复、块号 300 起有没有被占用。有问题先报,不要继续。
3. 预检一次,把"将创建的对象清单"给我看。等我确认后再真写。
4. 每份副本:块名/块号/标题按台号改;Time 类输入的默认值取点表;
注释里出现台号的地方按点表改,改不了的地方列出来,不要沉默地留着。
5. 背景 DB 必须指向自己那份 FB(不是模板 FB)。
6. 变量表按点表地址原样建,一个地址都不许自己推算。
7. 全程不要保存工程。最后给我三样:编译结果(错误/警告数 + 每条警告原文)、
第 1 台与第 4 台的块导出 diff、你认为需要人工复核的点。
这段提示词我在测试工程里原样跑了一遍(agent 只使用网关的读写接口,不碰脚本),结果:4 个 FB 用时 1.7 s、4 个背景 DB 1.7 s、20 个变量 1.8 s、整机编译 2.0 s,0 错误 1 警告(还是那条 IO 地址不在硬件组态里)。导出 diff 抓到 8 处不同,包括块名、块号、延时默认值、三条注释里的台号——以及那句改不掉的"在 1 号运行"。
换成故意写重的点表再跑一次,它按第 2 条停住了:
错误:点表里地址重复:%Q30.0
每条约束都不是凑数的:
| 约束 | 对应的事实 |
|---|---|
| 只读 + 对列名 | 列名和接口错位时,错位是静默发生的,写进去的是另一个参数 |
| 先查地址重复与块号占用 | 地址重复不会让编译失败,只在下载后表现为两台设备抢一个点 |
| 预检后等确认 | 预检只回报第一个根对象,"看起来只建了一个"是汇报口径,不是事实 |
| 改不了要列出来 | 注释里的半结构化句子没有规则能改全,不要求它就默默留着 |
| DB 指向自己那份 FB | 复制实例 DB 最容易漏的一处,编译错报的却是"实例与接口不匹配" |
| 地址原样 | 让模型"顺手把地址排整齐"是这类任务里最危险的自作主张 |
| 不要保存 + 三样验收物 | 不保存意味着改错了什么都没发生;diff 报告是唯一能证明"每台真的不同"的东西 |
什么时候该从提示词换成脚本:设备数超过 10~20 台,逐块改 XML 的成本按台数线性上涨,脚本不变;要反复重跑(这个月 8 台、下个月 14 台),脚本本来就是幂等的;要进 git、要被 review,脚本加点表是可 review 的文本,对话记录不是。反过来,一次性、探路、需要判断的活留给 agent:摸清接口、发现点表里的坑、判断"这台要不要单独处理"——这些是提示词擅长的,脚本不会。
那段提示词我单独存了一份,方便直接复制:agent 提示词。
六、实测数字:8 台、200 台、208 个对象
同一台机器(TIA V19 + S7-1500 + TiaLink 网关),三组实测:
| 场景 | 生成内容 | 用时 | 编译结果 |
|---|---|---|---|
| 8 台泵,复制副本模式 | 8 个 FB + 8 个背景 DB + 40 个变量 + 1 个调用段 | 约 7 s | 0 错误 1 警告 |
| 200 台,单 FB 多背景 DB | 200 个背景 DB + 1000 个变量 + 200 段调用 | 约 58 s | 0 错误 1 警告 |
| 208 个对象跨 CPU 搬运 | 205 个块 + 2 张变量表(486 KB) | 导出 13.7 s + 导入 59 s | 0 错误 1 警告 |
单次请求能装多少,也测了:20 个 FB 一次 3.9 s,100 个实例 DB 一次 13 s。这个比例说明批量处理是有规模效应的——第 101 个块的成本远低于第一个。
这组数字也顺带回答了一个常见疑问:只有 3 台设备,值不值得写脚本? 不值得。3 台手工改,半小时能搞定,而且你能在改的过程中把模板本身的毛病看清楚。这条路的门槛大约在 6~8 台以上,或者设备数量会反复增减(今天 8 台、下个月加到 14 台),又或者同一套模板要在多个项目里复用——那时"重跑一遍"的价值才真正体现出来:改模板、改点表、重跑,而不是再手工改 14 次。
200 台那组的分解:背景 DB 24.7 s、1000 个变量 27.7 s、200 段调用 5.1 s、编译 10.4 s。变量表反而是最慢的一步,因为它是一次性写 1000 条记录。
跨 CPU 那组更有意思:把整个设备的块导出成一个 zip,再按"变量类型 → 功能块 → 实例 DB → 变量表"的顺序分批灌到另一个 CPU,207 个对象、6 个请求,编译 0 错误。顺序不能乱,原因见下一节的第 5 个坑。
唯一那条警告三组都一样:Inputs or outputs are used that do not exist in the configured hardware。原因是测试工程的 CPU 上没组态 IO 模块,点表里的 %I10.0 这类地址落在空处。这就是上一条"编译通过不等于正确"的活例子——0 错误,但地址确实是虚的。
七、六个坑
以下六个都是实测踩过的,报错原文照抄,方便你搜到这篇时不至于白跑一遍。
1. 多个对象放进同一个文档,内部 ID 必须全局唯一
把 8 份导出结果首尾拼起来直接提交,得到:
Duplicate Simatic ML ID '0' found at line number 430 and line position 2.
This ID was previously used at line number 50 and line position 4.
每一份导出的 XML 内部 ID 都从 0 开始,拼在一起就撞了。做法是按顺序给第 i 份的所有 ID 加一个偏移量。
2. 但 ID 是十六进制,UId 是十进制整数
第一版我把两类 ID 统一按十六进制平移,前 8 台居然全过了(偏移量 0x1000~0x8000 的十六进制写法恰好全是数字),到第 10 台(0xA000)立刻失败:
"UId" 特性无效 - 根据数据类型"…int",值"A003"无效 - 字符串"A003"不是有效的 Int32 值。
ID 是十六进制字符串,UId 是十进制整数——必须分开处理。这个坑的危险之处在于它不会在第一次就暴露,8 台以内一切正常,等你按同样的脚本去做 20 台、50 台时才炸。
3. AutoNumber 为真时,你写的编号会被忽略
导出的模板里 <AutoNumber>true</AutoNumber> 意思是"编号由博途自己分配"。想固定块号,必须把它改成 false,否则 XML 里写的 <Number>200</Number> 完全不起作用,回头核对"第 5 台是不是 204 号"时对不上。
4. 预检只回报第一个对象
一个文档里装了 20 个块,预检结果里只有 1 条记录。一开始我以为是"只识别了一个",其实是汇报口径的问题——预检通过之后一次导入,20 个对象全建好了。记住这一点,别被预检的输出骗着改脚本。
5. 导入顺序 = 依赖顺序
先导实例 DB、后导它的 FB,报错是:
Block 'FB_Blower01' does not exist at the object with UID ''.
这句话里没有一处提到"顺序"。正确的顺序是:变量类型(UDT) → FB / FC / OB → 实例 DB → 变量表。批量搬块时按这个顺序分组发请求,一次就过。
6. 合成实例 DB 时,FB 的 Temp 和 Constant 段必须删掉
当目标 CPU 里没有现成的背景 DB 可以当模板时,只能拿 FB 的接口现场拼一个实例 DB。这时 FB 接口里的 Temp、Constant 段会让导入失败:
DB_P001: The Openness import failed: "Temp"部分对此块无效。
它连着报错两次(先是 Constant,删掉之后再报 Temp),每次只说"某部分对此块无效",不说是哪来的。实例 DB 只接受 Input / Output / InOut / Static 这几段。
八、编译通过之后:四步验收
0 错误只是一个必要条件。批量生成的代码要过下面四关:
1)读编译消息,别只看计数。 整机编译出来的失败块有时只有条数没有文字,这时改用单块编译,博途才能把消息挂到具体对象上。批量生成之后至少跑一次单块编译,确认调用段里引用的每个背景 DB 都真的存在。
2)查地址歧义。 如果两个块抢了同一个块号,编译会报"程序中含有地址含混不清的对象"并逐个列出块名——这是唯一能提前发现"编号错位"的信号,别跳过。
3)导出回读,做 diff。 这一步最能说明问题:把第 1 台和第 5 台的块分别导出来 diff,应该不同的行必须不同,不该不同的行必须一样。实测的两份 diff:
- <StartValue>T#6S</StartValue>
+ <StartValue>T#18S</StartValue>
- <MultiLanguageText Lang="zh-CN">1 号风机备电机延时</MultiLanguageText>
+ <MultiLanguageText Lang="zh-CN">5 号风机备电机延时</MultiLanguageText>
- <Name>FB_Fan01</Name>
+ <Name>FB_Fan05</Name>
- <Number>200</Number>
+ <Number>204</Number>
同时它也验证了地址确实是点表里那些不连续的地址:Fan05_Start = %I12.1、Fan06_Start = %I12.6、Fan08_Fault = %I14.7,一个都没被"算整齐"。
4)仿真里强制值。 编译和 diff 证明"生成对了",仿真证明"逻辑对了"。挑两台在虚拟 PLC 里强制输入,看输出是否按预期走——这一步花的时间比前面所有步骤加起来都值。
具体做法:把两台设备的输入分别置 1,观察输出是不是各自独立——如果第 5 台的输出跟着第 1 台的输入动,说明点表里有地址写重了(脚本本该拦住,但变量表被你手工改过就会绕过去);再把延时参数改大一点,看输出是不是按各自的 RunDelay 起跳,这能一次性验证"参数没串台"。这两条检查加起来不到十分钟,却能挡住批量生成里杀伤力最大的两类错误。
九、工具替你改不到的地方
上面那份 diff 里有一行值得单独看:注释里原本写的是"2 号电机在 1 号运行 RunDelay 之后启动",用替换规则把"1 号电机""2 号电机"改成台号之后,剩下那半句"在 1 号运行"没有被替换——因为它写的是"1 号运行",不带"电机"两个字。
这不是脚本写得不好,而是文本替换天生有上限:模板里的差异一旦散落在自由文本(注释、标题、字符串常量)里,就没有可靠的规则能保证改全。
真正的解法不是把替换规则写得更聪明,而是别把差异写进注释:
- 台号、设备名这类东西,要么进接口(一个
DeviceNo输入),要么进块标题,不要撒在 8 条网络注释里; - 延时、量程这类参数,做成接口成员的默认值,让它出现在 diff 里能被一眼看到;
- 只有确实需要人读的说明文字,才留在注释里,并且接受"复制的注释可能不准"这个事实。
换句话说:能被点表覆盖的差异是可控的,写在注释里的差异是不可控的。这条经验对任何批量生成都成立,不只对这个工具。
剩下的边界也说清楚:
- 只处理 PLC 侧的块、变量类型、变量表和 SCL 外部源。WinCC Unified 的屏幕不能这样复制——那部分不走这条导出通道。
- 不碰安全程序。F-CPU 的安全块有额外的复制规则和校验,必须人工按规范走。
- 不自动保存,也不替你做 review。工具负责"少抄几遍、少错几个地址",工艺责任和上线判断仍然在人这边。
- 生成的代码仍然要走项目自己的验证流程:编译 → 仿真 → 现场联调 → 验收。批量生成只是把"抄"这一步做对,不改变验证要求。
收尾
回到开头那个问题:8 台泵、20 台风机、200 个工位,是不是只能一次一次复制粘贴?
不是。把差异收进一份点表,把"改文本"交给脚本,剩下的判断留给人——一个模板加一份表格,8 台几秒钟,200 台一分钟出头,每一次生成都能预检、能 diff、能重跑。
这套做法的脚本我做成了零依赖的命令行工具,放在 GitHub(含 8 台与 200 台两份点表样例,可直接改成自己的),它通过 TiaLink 的 HTTP 接口驱动本机博途工程:导出模板、按点表生成、预检、导入、编译,全程不保存工程。想先看这条路在别处怎么用,可以接着读《AI 改博途程序:TIA Openness MCP 的写保护与边界》和《梯形图转 SCL 迁移实战指南》——前者讲"写之前怎么保证不写坏",后者讲"哪些转换能自动、哪些必须重构",和这篇是同一类问题的三个切面。
这是「AI × 工业 / PLC」专题的一部分。完整地图见:AI × 工业 / PLC 完全指南