1. 工单拼接场景下的字符串处理需求拆解
1.1 为什么工单系统离不开字符串拼接
做过产线工单系统的人都有一个共识:PLC 侧的数据从来不是给人看的。设备状态、批次号、时间戳、工位编号,这些数据在寄存器里各占各的地址,各用各的数据类型,彼此之间毫无关联。但操作员需要看到的是一行能读懂的工单信息,比如“WO-20240513-A线-03工位-合格”。这中间的转换工作,就是字符串拼接要解决的核心问题。
在三菱 ST(Structured Text)编程环境里,字符串处理不像高级语言那么自由。没有动态内存分配,没有丰富的标准库,字符串长度还要提前声明。很多从 C# 或 Python 转过来做 PLC 的工程师,第一次写工单拼接逻辑时都会踩坑——要么拼接结果被截断,要么数值转字符串后带了一堆空格,要么 REPLACE 函数用错了位置参数导致整个字符串乱掉。
这篇文章围绕 CONCAT 和 REPLACE 这两个最常用的字符串函数展开,把工单拼接场景下的选型逻辑、参数细节、实操步骤和排查经验一次讲透。不管你是刚接触三菱 ST 的新手,还是已经写过一些工单逻辑但总觉得不够优雅的老手,都能从中找到可以直接复用的方案。
1.2 CONCAT 与 REPLACE 的核心差异
先给结论:CONCAT 负责“从无到有”,REPLACE 负责“从有到优”。
CONCAT 的本质是把两个或多个字符串按顺序首尾相接,生成一个新的字符串。它不关心原字符串里有什么内容,只负责把它们拼在一起。在工单场景里,CONCAT 适合做初始拼接——把工单号、产线编号、工位号、状态码这些独立字段拼成一条完整记录。
REPLACE 的本质是在一个已有字符串中,把指定的子串替换成新的内容。它关心的是“找到什么”和“换成什么”。在工单场景里,REPLACE 适合做模板填充或格式修正——比如先定义一个工单模板“WO-XXXX-XX-XX”,然后用实际数据逐段替换占位符。
两者的核心差异可以用一个生活类比来理解:CONCAT 像把几块积木首尾粘起来搭一座桥,REPLACE 像在已经搭好的桥上把某一段换成不同颜色的积木。前者关注组合,后者关注修改。
| 对比维度 | CONCAT | REPLACE |
|---|---|---|
| 核心功能 | 多个字符串顺序拼接 | 替换指定子串 |
| 输入数量 | 2 个及以上字符串 | 源字符串、被替换串、替换串 |
| 结果长度 | 各输入长度之和 | 通常与源字符串等长或按替换内容变化 |
| 典型场景 | 初始工单信息组装 | 模板占位符填充、格式修正 |
| 常见坑点 | 结果超长被截断 | 位置参数理解错误 |
1.3 工单拼接的典型数据流
一条完整的工单信息在 PLC 内部的流转路径通常是这样的:底层设备通过输入模块把状态字写入数据寄存器,这些寄存器里存的是 INT 或 DINT 类型的数值。工单号可能来自上位机下发的字符串数组,产线编号和工位号可能是固定的常量字符串,时间戳需要从时钟寄存器读取后转换。
这些数据源的类型不统一,长度不统一,格式也不统一。CONCAT 和 REPLACE 要做的,就是把这些“散装”数据整合成一条格式规范的工单字符串,再写入 HMI 或上传给 MES 系统。
理解这个数据流很重要,因为它决定了你在哪个环节用 CONCAT、在哪个环节用 REPLACE。我的经验是:先用 CONCAT 把各字段拼成原始串,再用 REPLACE 做格式微调。这个顺序反过来会非常别扭,后面会详细说明原因。
2. CONCAT 函数深度解析与实操要点
2.1 CONCAT 的参数结构与返回值机制
三菱 ST 中 CONCAT 的标准调用形式是CONCAT(IN1, IN2),返回一个字符串。在 GX Works3 环境下,这个函数的参数类型通常是 STRING 或 STRING 数组元素。需要注意的是,三菱的 CONCAT 一次只接受两个输入参数,如果你要拼接四个字段,就需要嵌套调用或者连续调用。
嵌套调用的写法是CONCAT(CONCAT(IN1, IN2), CONCAT(IN3, IN4)),这种写法在逻辑上没问题,但可读性很差,调试时很难一眼看出拼接顺序。连续调用的写法是分多步执行,每一步把结果存入一个临时字符串变量,下一步再拿这个临时变量和下一个字段拼接。我个人的习惯是连续调用,虽然多写几行代码,但后期维护和排查方便得多。
返回值机制方面,CONCAT 返回的是一个新的字符串,不会修改任何输入参数。这一点和 REPLACE 不同,REPLACE 在某些实现中会直接修改源字符串。理解这个差异对内存规划很重要——每次 CONCAT 都会产生一个新的字符串占用,如果在一个扫描周期内做大量拼接操作,需要留意字符串变量的声明长度是否足够。
2.2 字符串长度声明与截断风险
这是 CONCAT 使用中最容易踩的坑,没有之一。三菱 ST 中声明字符串变量时,必须指定最大长度,比如strWorkOrder : STRING[50]。这个 50 是字符数上限,不是字节数。如果你拼接后的结果超过 50 个字符,多出来的部分会被直接截断,而且不会报错。
我见过一个真实的案例:某工位拼接后的工单信息理论长度是 42 个字符,工程师声明了 STRING[40],测试时用短工单号没发现问题,上线后遇到长工单号,结果状态码被截掉了,操作员看到的工单永远显示“合”而不是“合格”。这种问题排查起来非常费时间,因为逻辑本身没错,错在声明长度。
规避方法很简单:在声明字符串变量时,把所有可能拼接的字段最大长度加起来,再留 20% 的余量。比如工单号最长 20 字符、产线编号 5 字符、工位号 5 字符、状态码 5 字符、分隔符 6 字符,合计 41 字符,那声明 STRING[50] 就比较稳妥。
注意:三菱 ST 中字符串长度声明不足导致的截断是静默发生的,不会触发任何错误标志。建议在拼接完成后加一个长度校验逻辑,如果结果长度等于声明上限,就置一个警告位。
2.3 数值转字符串的预处理
CONCAT 只能拼接字符串,不能直接拼接数值。工单号里的序号、时间戳里的时分秒、工位编号,这些在 PLC 里往往是 INT 或 DINT 类型,必须先转成字符串才能参与拼接。
三菱 ST 提供了 INT_TO_STRING 和 DINT_TO_STRING 等转换函数。但这里有一个细节:转换后的字符串默认不带前导零。比如工位号 3 转换后是“3”而不是“03”。如果你的工单格式要求固定两位,就需要手动补零。
补零的常用做法是用 REPLACE 或者条件判断。比如先拼接一个“00”,然后用 REPLACE 把“00”中对应位置替换成实际数字。这个技巧在后面 REPLACE 章节会详细展开。另一种做法是用 RIGHT 函数截取“00”加数字后的右侧两位,但 RIGHT 函数在三菱 ST 中的可用性取决于具体型号,不是所有环境都支持。
我的建议是:在工单拼接逻辑设计阶段,就把所有需要固定位数的字段列出来,统一用 REPLACE 做补零处理,这样格式最可控。
2.4 CONCAT 嵌套调用的可读性优化
前面提到嵌套调用可读性差,这里展开说一下优化方案。假设你要拼接四个字段:工单号、产线号、工位号、状态码。嵌套写法是这样的:
strResult := CONCAT(CONCAT(CONCAT(strWO, strLine), strStation), strStatus);这行代码能跑,但如果你第二天回来看,很难快速确认拼接顺序是不是自己想要的。而且如果中间某个字段需要加分隔符,嵌套层级会更深,代码会变得非常难读。
连续调用的写法:
strTemp1 := CONCAT(strWO, '-'); strTemp2 := CONCAT(strTemp1, strLine); strTemp3 := CONCAT(strTemp2, '-'); strTemp4 := CONCAT(strTemp3, strStation); strResult := CONCAT(strTemp4, strStatus);这种写法代码行数多,但每一步的结果都清晰可见,调试时可以在任意一步设置监视点,查看中间结果。如果拼接逻辑出问题,能快速定位是哪个字段导致的。
折中方案是用一个临时字符串变量反复拼接:
strResult := strWO; strResult := CONCAT(strResult, '-'); strResult := CONCAT(strResult, strLine); strResult := CONCAT(strResult, '-'); strResult := CONCAT(strResult, strStation); strResult := CONCAT(strResult, strStatus);这种写法兼顾了可读性和代码量,是我在工单拼接场景中最常用的方式。唯一需要注意的是,strResult 的声明长度要足够容纳最终结果。
3. REPLACE 函数深度解析与模板填充技巧
3.1 REPLACE 的参数语义与常见误解
三菱 ST 中 REPLACE 的标准调用形式是REPLACE(IN, L, P, S),其中 IN 是源字符串,L 是被替换的子串长度,P 是替换的起始位置,S 是替换后的新内容。返回替换完成后的字符串。
这里最容易误解的是 L 和 P 的配合关系。L 是从 P 位置开始要替换掉的字符数,不是替换后的字符数。举个例子,源字符串是“WO-XXXX-XX”,你想把从第 4 位开始的 4 个 X 替换成“2024”,那么 L=4,P=4,S=“2024”。替换后结果是“WO-2024-XX”。
很多新手会误以为 L 是替换内容的长度,写成 L=4、P=4、S=“2024” 碰巧对了,但如果替换内容长度不同就会出错。比如把“WO-XXXX-XX”中的“XXXX”替换成“202405”,如果还写 L=4,结果会变成“WO-202405-XX”吗?不会。因为 L=4 只替换掉 4 个字符,源字符串第 4 到第 7 位是“XXXX”,替换成“202405”后,结果是“WO-202405-XX”,看起来对了,但实际上第 8 位的“-”被挤到了后面,如果源字符串是“WO-XXXXXX-XX”,L=4 就只能替换掉前 4 个 X,剩下两个 X 还在。
所以正确的做法是:L 必须等于被替换子串的实际长度。如果占位符长度不固定,REPLACE 就不太适合,应该考虑用 CONCAT 重新拼接。
3.2 模板占位符的设计原则
用 REPLACE 做模板填充时,占位符的设计直接决定了替换逻辑的复杂度。我总结了几条原则:
第一,占位符长度要固定。比如用“XXXX”表示四位年份,用“XX”表示两位月份,不要用“X”表示一位、“XXX”表示三位这种变长设计。固定长度让 L 参数可以直接写常量,不需要动态计算。
第二,占位符字符要唯一。不要用“-”或“_”这种在正常数据中也会出现的字符做占位符,否则替换时可能误伤。用“X”或“#”这类在工单数据中不会自然出现的字符比较安全。
第三,占位符位置要可预测。模板设计好后,每个占位符的起始位置应该是固定的。如果模板中有可变长度的字段,要么把它放在最后,要么用固定长度填充。
一个典型的工单模板可以设计成:“WO-XXXX-XX-XX-XX”,分别对应工单年份、产线号、工位号、状态码。每个占位符长度固定,位置固定,替换逻辑就非常清晰。
3.3 多字段替换的执行顺序
当一个模板中有多个占位符需要替换时,执行顺序会影响结果。因为每次 REPLACE 都会改变字符串的长度(如果替换内容长度和被替换长度不同),后续占位符的位置可能会偏移。
最安全的做法是从右往左替换。因为替换右侧内容不会影响左侧占位符的位置。比如模板“WO-XXXX-XX-XX-XX”,先替换最右边的状态码占位符,再替换工位号,再替换产线号,最后替换年份。这样每次替换都不会影响还没替换的占位符位置。
如果从左往右替换,每次替换后都要重新计算后续占位符的位置,逻辑会复杂很多。我试过从左往右替换,代码里全是位置计算,后期改模板时非常痛苦。从那以后,我的工单模板替换逻辑一律从右往左。
提示:如果替换内容长度和被替换占位符长度完全一致,那么替换顺序无所谓。但在实际工单场景中,很难保证所有字段长度都刚好匹配,所以从右往左是更稳妥的默认策略。
3.4 REPLACE 与 CONCAT 的配合使用
单独用 REPLACE 做模板填充有一个前提:模板本身已经是一个完整的字符串。但模板从哪来?通常是在 PLC 初始化时用 CONCAT 拼出来的,或者直接用一个常量字符串赋值。
更常见的做法是混合使用:先用 CONCAT 把不需要格式化的字段拼在一起,再用 REPLACE 处理需要补零或格式修正的字段。比如工单号和时间戳用 CONCAT 直接拼,工位号和状态码需要固定两位,就用 REPLACE 在拼接结果上做修正。
这种混合方式的优势在于:CONCAT 处理简单拼接效率高,REPLACE 处理格式修正针对性强。两者各司其职,逻辑清晰,排查也方便。
4. 工单拼接完整实操流程
4.1 数据准备与变量声明
假设我们要拼接一条工单信息,包含以下字段:工单号(字符串,最长 20 字符)、产线号(整数,1-9)、工位号(整数,1-99)、状态码(整数,0-99)、时间戳(字符串,格式 HH:MM:SS)。
变量声明如下:
strWorkOrder : STRING[20]; (* 工单号 *) iLineNum : INT; (* 产线号 *) iStationNum : INT; (* 工位号 *) iStatus : INT; (* 状态码 *) strTimeStamp : STRING[8]; (* 时间戳 *) strLineStr : STRING[2]; (* 产线号字符串 *) strStationStr : STRING[2]; (* 工位号字符串 *) strStatusStr : STRING[2]; (* 状态码字符串 *) strResult : STRING[60]; (* 最终工单信息 *) strTemplate : STRING[60]; (* 工单模板 *)这里 strResult 声明为 60 字符,是因为工单号 20 + 产线号 2 + 工位号 2 + 状态码 2 + 时间戳 8 + 分隔符 6 + 前缀 4 = 44 字符,留了足够余量。
4.2 数值转字符串与补零处理
产线号、工位号、状态码都是整数,需要转成字符串并补零到固定两位。以工位号为例:
(* 先转成字符串 *) strStationStr := INT_TO_STRING(iStationNum); (* 如果长度不足两位,前面补零 *) IF LEN(strStationStr) < 2 THEN strStationStr := CONCAT('0', strStationStr); END_IF;这段逻辑很直白:转字符串后检查长度,不足两位就在前面拼一个“0”。产线号和状态码同理。这种补零方式比用 REPLACE 更直观,适合字段数量不多的情况。
如果字段很多,可以写一个通用的补零函数块,传入整数和目标长度,返回补零后的字符串。这样主逻辑会更简洁。
4.3 模板定义与 REPLACE 填充
定义工单模板:
strTemplate := 'WO-XXXX-XX-XX-XX-XX:XX:XX';这个模板中,“XXXX”是工单号占位符(实际工单号可能更长,这里只是示意),“XX”分别对应产线号、工位号、状态码、时、分、秒。
从右往左替换:
(* 替换秒 *) strTemplate := REPLACE(strTemplate, 2, 22, RIGHT(strTimeStamp, 2)); (* 替换分 *) strTemplate := REPLACE(strTemplate, 2, 19, MID(strTimeStamp, 4, 2)); (* 替换时 *) strTemplate := REPLACE(strTemplate, 2, 16, LEFT(strTimeStamp, 2)); (* 替换状态码 *) strTemplate := REPLACE(strTemplate, 2, 13, strStatusStr); (* 替换工位号 *) strTemplate := REPLACE(strTemplate, 2, 10, strStationStr); (* 替换产线号 *) strTemplate := REPLACE(strTemplate, 2, 7, strLineStr); (* 替换工单号 *) strTemplate := REPLACE(strTemplate, 4, 4, strWorkOrder);这里的位置参数需要根据模板实际结构调整。上面示例中模板长度和占位符位置是假设的,实际使用时需要先数清楚每个占位符的起始位置。
4.4 拼接结果校验与输出
拼接完成后,建议加一段校验逻辑:
(* 检查结果长度是否异常 *) IF LEN(strResult) >= 60 THEN bWarning := TRUE; (* 结果可能被截断 *) END_IF; (* 检查是否还有未替换的占位符 *) IF FIND(strResult, 'X') > 0 THEN bWarning := TRUE; (* 存在未替换的占位符 *) END_IF;FIND 函数用于查找子串位置,如果结果中还能找到“X”,说明有占位符没被替换掉。这个校验在模板调整后特别有用,能快速发现遗漏。
校验通过后,把 strResult 写入 HMI 显示寄存器或上传给上位机。写入时注意目标寄存器的字符串长度限制,避免在输出环节被二次截断。
5. 常见问题与排查技巧实录
5.1 拼接结果乱码或显示异常
乱码问题通常有三个原因:字符串长度声明不足导致截断、数值转字符串时编码不对、HMI 显示控件的字符集设置不匹配。
排查顺序:先在 PLC 侧监视 strResult 的实际内容,确认 PLC 内部字符串是否正确。如果 PLC 侧正常但 HMI 显示乱码,问题在 HMI 控件设置。如果 PLC 侧就是乱码,检查 INT_TO_STRING 的转换结果是否正常,以及 CONCAT 的输入参数是否都是有效的字符串变量。
我遇到过一次乱码是因为字符串变量声明为 STRING[10],但实际拼接结果有 12 个字符,多出的部分被截断后,最后一个字符的编码不完整,导致 HMI 显示异常。把声明长度改大后问题消失。
5.2 REPLACE 替换位置偏移
位置偏移是 REPLACE 使用中最常见的问题。根本原因通常是替换内容长度和被替换占位符长度不一致,导致后续占位符位置发生变化。
解决方法有两个:一是确保替换内容长度和占位符长度一致,比如占位符是“XX”两位,替换内容也必须是两位,不足补零。二是从右往左替换,这样即使长度变化,也不会影响还没替换的左侧占位符。
如果模板中占位符长度无法统一,建议放弃 REPLACE 方案,改用 CONCAT 逐段拼接。虽然代码多一些,但位置逻辑简单,不容易出错。
5.3 CONCAT 嵌套层级过深导致编译错误
三菱 ST 对嵌套调用的层级有限制,具体层数取决于编译器和 PLC 型号。嵌套过深时编译会报错,提示表达式过于复杂。
解决方法就是前面说的连续调用,把嵌套拆成多步。每步的结果存入临时变量,下一步再拼接。这样既避免了嵌套限制,又提高了可读性。
5.4 工单号含特殊字符导致拼接异常
工单号中如果包含“-”或“_”等分隔符,而模板中也用这些字符做分隔,REPLACE 替换时可能会误匹配。比如工单号是“WO-2024”,模板中也有“WO-XXXX”,替换工单号时可能把模板中的“WO-”也替换掉。
规避方法:模板占位符不要用工单数据中可能出现的字符。用“#”或“@”这类在工单号中不会出现的字符做占位符,替换时就不会误匹配。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 结果被截断 | 字符串声明长度不足 | 监视结果长度是否等于声明上限 | 增大声明长度,留 20% 余量 |
| 显示乱码 | 编码不匹配或截断 | PLC 侧和 HMI 侧分别监视 | 检查字符集设置,确保长度足够 |
| 替换位置偏移 | 替换内容长度不一致 | 逐步监视每次 REPLACE 结果 | 从右往左替换,或统一长度 |
| 编译报错 | 嵌套层级过深 | 查看编译错误信息 | 拆成连续调用 |
| 占位符未替换 | 替换逻辑遗漏 | 用 FIND 检查结果中是否还有占位符 | 补充替换逻辑 |
| 数值转字符串带空格 | 转换函数默认行为 | 监视转换结果 | 用 TRIM 或手动截取 |
6. 性能优化与代码维护建议
6.1 减少不必要的字符串操作
字符串操作在 PLC 中是比较耗资源的,尤其是在高速扫描周期中。每增加一次 CONCAT 或 REPLACE,都会增加扫描周期的负担。
优化原则:能一次拼接完成的,不要分多次。比如四个字段拼接,如果嵌套调用可读性可以接受,就尽量用嵌套而不是连续调用。另外,工单信息如果不需要每个扫描周期都更新,可以加一个触发条件,只在数据变化时执行拼接逻辑。
6.2 字符串变量的复用策略
在工单拼接逻辑中,临时字符串变量可以复用。比如 strTemp1 在第一步拼接后,第二步可以覆盖使用。这样能减少变量声明数量,降低内存占用。
但复用变量会降低代码可读性,因为同一个变量在不同步骤代表不同含义。我的建议是:在逻辑简单、步骤少的场景中复用,在逻辑复杂、步骤多的场景中为每个中间结果声明独立变量,方便调试。
6.3 模板与逻辑分离
把工单模板定义成一个独立的字符串常量或初始化变量,不要硬编码在拼接逻辑中。这样修改模板格式时只需要改一个地方,不需要在大量代码中搜索替换。
如果模板有多种格式(比如不同产线用不同模板),可以用数组或条件选择来管理。模板和拼接逻辑分离后,代码的维护成本会大幅降低。
6.4 版本兼容性注意事项
不同型号的三菱 PLC 对字符串函数的支持程度不同。有些型号支持 FIND、MID、LEFT、RIGHT 等函数,有些型号只支持基础的 CONCAT 和 REPLACE。在编写工单拼接逻辑前,先确认目标 PLC 型号支持的函数列表。
如果目标型号不支持某些函数,可以用基础函数组合实现。比如 LEFT 可以用 MID 替代,RIGHT 可以用 MID 加 LEN 计算位置来替代。虽然代码会复杂一些,但兼容性更好。
我在实际项目中遇到过因为 PLC 型号不支持 FIND 函数,导致占位符校验逻辑无法实现的情况。后来改用逐字符比较的方式替代,虽然效率低一些,但功能不受影响。所以前期确认函数支持列表这一步,能省掉很多后期返工的时间。