1. 为什么要在PLC里折腾字符串
工控现场干久了,你会发现一个挺扎心的事实:PLC最擅长的是逻辑控制、数值运算、运动控制,但对字符串的处理能力一直比较拉胯。尤其是从三菱FX3U一路用到FX5U、Q系列的老电气工程师,大部分时间都在跟BOOL、WORD、DINT打交道,字符串顶多用来显示个报警文本。
但设备一旦涉及MES对接、扫码枪上传、工单下发、配方管理,你就躲不开字符串处理。“这台设备的工单号B2407-018,停机原因是AL10.1,当前计件128件”——类似这种信息,要么你花大价钱买上位机或者HMI慢慢拼,要么干脆用PLC自己把字符串拼接出来,直接写进触摸屏或者传给MES。
我做三菱ST语言的项目也有几年了,从早期的简单寄存器加减,到后来的复杂数据结构、字符串处理,踩过的坑不少。这篇文章就把我实际用得最频繁的两个字符串函数——CONCAT和REPLACE,结合工单拼接这个真实场景,做一个详细的对比拆解。顺便把ST里头其它常用的字符串辅助函数也捋一遍,算是给后来人铺条路。
先说个结论:CONCAT负责“把碎片拼成整体”,REPLACE负责“把错误的部分纠正过来”,两者在工单拼接里是黄金搭档。但用不好,一个乱码就够你排查一整天。下面逐个展开。
2. ST语言字符串基础:三菱PLC的字符串变量没那么难用
2.1 字符串变量的定义与存储
三菱ST语言里,字符串变量常用的有两种:固定长度字符串和可变长度字符串,对应到三菱PLC的数据类型就是String(定长)和String(可变)。GX Works3软件的ST编辑器里,声明方式如下:
VAR sWorkNo : STRING(16); // 工单号,最多16个字符 sStepName : STRING(32); // 工序名称,最多32个字符 sResult : STRING(64); // 拼接最终结果 END_VAR说实话,这里最大的坑在于字符串长度。你在声明的时候给了多大空间,这个变量就只能容纳多少个字符。如果拼接后的总长度超过了目标变量的声明长度,超出部分会直接丢弃,不会报错,也不会自动扩展。这种“静默截断”机制坑了多少人,我见过不少新手调试半天,发现字符串尾部莫名其妙“消失”了,最后才想起是长度不够。
从底层存储看,三菱PLC的字符串就是连续字节区域加结束符(类似C语言的\0逻辑),但编译器自动帮你管理终结符。所以,当你声明STRING(16),实际可用的有效字符一般到15或者16个(不同系列有差异),为了稳当,我习惯在声明时就多预留20%的长度。
2.2 ST字符串函数的执行逻辑与注意事项
三菱ST语言里的字符串函数,执行逻辑和高级语言差不多,但是有几个差异点必须注意:
函数调用不像C语言那样返回值赋给变量那么简单,而是直接修改目标变量。举个例子,C语言里你写 a = concat(b, c),三菱ST里通常是这样:
sResult := CONCAT(sWorkNo, sStepName);这条语句等价于C语言的 strcat 逻辑,把sWorkNo和sStepName依次存入sResult。看起来区别不大,但ST语言里所有字符串函数的执行都是在扫描周期里按顺序完成的,如果你在同一个扫描周期内多次调用同一个目标变量,后一次调用会覆盖前一次的结果,这个和高级语言的局部变量作用域完全是两码事。
另外,函数对空字符串的处理也需要留意。三菱ST的CONCAT一旦遇到某个字符串参数为空,行为是跳过该参数继续拼接,而不是报错。这一点用熟了反而是优势——动态拼接工单时,有些字段可能没填写,你可以放心传空字符串,不会中断逻辑。
3. CONCAT的完整拆解:工单拼接这台“流水线”
3.1 CONCAT的基础语法与三菱官方习惯
CONCAT是三菱ST语言里最常用的字符串拼接函数,官方手册上的标准调用格式如下:
sResult := CONCAT(sString1, sString2);这是两个参数的写法,功能和高级语言里的“字符串连接”完全一致,就是把sString1和sString2依次放进sResult,返回合并后的字符串。
但工单拼接往往不止两个部分,我拿最典型的场景举例——把设备号、工单号、报警代码和当前产量合并成一个完整信息串:
sWorkOrder := CONCAT( CONCAT(sDeviceID, ''), // 设备ID CONCAT('工单号:', sOutputOrder), // 工单号 CONCAT('|报警:', sAlarmCode), // 报警代码 CONCAT('|产量:', sCount) // 当前产量 );这一长串嵌套写法,逻辑上没问题,但可读性很差。你三个月后回来看这段代码,大概率要愣一下才能厘清嵌套关系。我个人的习惯是用中间变量分步拼接,虽然代码行数多一点,但排错和后期维护的效率高很多:
sTemp1 := CONCAT(sDeviceID, '工单号:'); sTemp2 := CONCAT(sTemp1, sOutputOrder); sTemp3 := CONCAT(sTemp2, '|报警:'); sTemp4 := CONCAT(sTemp3, sAlarmCode); sResult := CONCAT(sTemp4, sCount);工单拼接现场,表面上是拼字符串,实质是拼数据源类型。这里有个关键点:CONCAT只能接受字符串参数,你没法直接拿INT、DINT这类数值变量去做拼接。比如上面的sCount是INT型,直接写:
sResult := CONCAT('产量:', sCount); // 编译报错三菱ST编译器直接给你标红。解决办法是先做类型转换,用INT_TO_STRING()或者DINT_TO_STRING()把数值变成字符串再拼:
sCountStr := INT_TO_STRING(sCount); sResult := CONCAT('产量:', sCountStr);这个类型转换问题我特意放在前面讲,因为它是新手最常见的一个编译错误,而且提示信息有时候不说人话。
3.2 CONCAT在工单拼接中的经典实战案例
下面给一个可以“抄作业”的完整实例。场景:设备完成一次加工后,需要生成一条工单状态信息,内容包含设备编号、产品型号、加工数量、报警等级,并传给HMI显示或者MES上报。
VAR sDeviceID : STRING(8); // 设备号:T-18 sModel : STRING(16); // 产品型号:PX-200 iQty : INT; // 加工数量 iAlarmLevel : INT; // 报警等级 sQtyStr : STRING(8); // 数量转字符串 sAlarmStr : STRING(4); // 等级转字符串 sMessage : STRING(64); // 最终工单信息 bTrigger : BOOL; // 触发拼接 END_VAR触发控制程序段:
IF bTrigger THEN sQtyStr := INT_TO_STRING(iQty); sAlarmStr := INT_TO_STRING(iAlarmLevel); sMessage := CONCAT('设备', sDeviceID); sMessage := CONCAT(sMessage, ' 产品型号:'); sMessage := CONCAT(sMessage, sModel); sMessage := CONCAT(sMessage, ' 加工数量:'); sMessage := CONCAT(sMessage, sQtyStr); sMessage := CONCAT(sMessage, ' 报警等级:'); sMessage := CONCAT(sMessage, sAlarmStr); sMessage := CONCAT(sMessage, '#'); // 结尾分隔符 END_IF;执行后,如果iQty=128,iAlarmLevel=3,sMessage的值就是:
设备T-18 产品型号:PX-200 加工数量:128 报警等级:3#尾部的“#”是留给上位机做报文解析用的结束符。很多MES报文都要求固定结尾字节,方便解析程序判断一帧数据已经收完。这个习惯我建议你保留。
3.3 CONCAT的隐藏缺点:参数数量上限
三菱ST的CONCAT函数,不同系列、不同版本的手册对这个函数的参数个数定义不太一样。FX5U里很多版本支持最多9个参数,Q系列可能多一些。但如果你的GX Works3软件版本比较老,或者固件版本不支持多参数,那你写CONCAT(s1, s2, s3, s4, s5)会直接提示函数参数个数错误。
所以,我的建议是:
- 如果确定固件支持多参数,可以用:
sResult := CONCAT(s1, s2, s3, s4, s5);简化嵌套。 - 如果不确定,老老实实用嵌套或者中间变量,最多损失一点代码行数,但稳定性是优先的。
实测下来,FX5U在较新的GX Works3版本(1.075J以上)里,CONCAT可以传多个参数,拼接顺序按照参数排列从左到右。旧项目迁移过来的建议统一验证,别想当然。
4. REPLACE的深度解析:纠正与重组的利器
4.1 REPLACE的基本语法与主要用途
REPLACE在三菱ST中的作用是把字符串中某个子串替换成另一个字符串。官方调用格式:
sResult := REPLACE(sSource, sOld, sNew);三个参数分别是:源字符串、要被替换的旧内容、替换后的新内容。
我在工单拼接里用REPLACE较多的是这两种场景:
场景A:设备停机后,产生的工单内容里包含了报警代码,但操作员在HMI上修正了报警原因,需要把原工单文本里的报警类别或者代码换成人工确认后的信息。
场景B:工单号因为条码扫描头误读,某个字符被识别错了(比如把字母“O”录成了数字“0”),用REPLACE把这个错误字符批量纠正过来。
下面给一个实际代码片段,把字符串里的“AL10.1”替换成“AL11.0”:
VAR sSourceStr : STRING(64); // 原始数据 sNewStr : STRING(64); // 替换后数据 END_VAR sSourceStr := '设备02 报警AL10.1 停机'; sNewStr := REPLACE(sSourceStr, 'AL10.1', 'AL11.0');执行后sNewStr的值变为“设备02 报警AL11.0 停机”。
但要提醒你一个容易踩的点:REPLACE在有些三菱系列固件里只替换第一个匹配项,不会替换后面所有匹配项。手册里写的是“将对源字符串中检索到的字符串进行替换”,但实测中FX5U的REPLACE行为是针对首个匹配,还是全部匹配,不同版本有不同的表现。稳妥的办法是确认你的PLC固件手册,必要时用循环扫描替代。
4.2 REPLACE与数据清洗:工单文本的“橡皮擦”
工单系统中最常见的是工单号或者批次号被MES系统推送下来时带了非法空格或特殊字符。比如:
sSourceStr := 'B2407-018 '末尾多了一个空格,你看不到,但一旦把它拼到报警文本里,显示就会多出一个错位空格,上位机解析也会出问题。这种场景用REPLACE把空格替换成空字符串:
sNewStr := REPLACE(sSourceStr, ' ', '');注意第三个参数传空字符串,表示“删除所有空格”。这个操作我几乎在每个项目里都用,堪称“工单清洗三件套”第一名。
还有一招比较偏门但是很实用:用REPLACE做字符串特征的临时替换,配合条件判断,实现类似高级语言里startsWith/endsWith的效果。比如要判断工单号是否以“B”开头:
IF REPLACE(sWorkNo, 'B', '') <> sWorkNo THEN // 说明开头是B END_IF;这段逻辑的本质是:如果替换后字符串变了,说明原字符串里有B;至于是不是开头,还得配合位置判断。不过在三菱ST里确实没有现成的startswith函数,这种变通方法能顶上。
4.3 REPLACE的参数边界与长度陷阱
REPLACE最容易翻车的是长度边界问题。比如sOld的长度比sNew长,替换后总字符串变短了,这通常没问题。但反过来,如果sNew的长度比sOld长,替换后总字符串变长,你必须确保目标变量的声明长度够大,否则尾部会被截断。
我举个实际例子:原字符串“设备01报警AL10.1”,长度是14个字符(包含空格和小数点)。现在想把“AL10.1”替换成“ALARM-1101”,变长了不少,最终结果长度超过目标变量声明,直接给你截掉后面几个字,导致工单信息不完整。
处理方法很简单:计算替换后的预期长度,在声明变量时把余量留足。你可以把中间字符串变量声明为最大可能长度,再赋值给最终工单变量,宁可浪费一点内存,不要因为截断丢了信息。
VAR sTempFull : STRING(128); // 中间长变量 sFinal : STRING(64); // 最终输出变量 END_VAR sTempFull := REPLACE(sSourceStr, 'AL10.1', 'ALARM-1101'); IF LEN(sTempFull) <= 64 THEN sFinal := sTempFull; ELSE // 记录一个出错标志,而不是静默截断 bStrOverflow := TRUE; END_IF;这段代码的意义在于主动检查长度而不是被动接受截断。我把这个习惯称为“字符串安全的最后一道防线”。
5. 工单拼接完整实战:从数据采集到信息生成的ST代码
5.1 工单拼接的数据结构与整体流程设计
工单拼接绝不是一句CONCAT就完事,它背后隐藏着一个数据结构设计的问题。
我以一条完整工单为例:设备编号、工单号、产品型号、工位号、报警代码、操作员编号、加工时间、计件数量。这些数据来源各不相同:
- 设备编号、工位号:PLC内部固定参数,掉电不丢
- 工单号、产品型号:MES下发的报文,可能存在缓存区
- 报警代码:报警功能块输出
- 操作员编号:HMI操作记录或者刷卡器扫码
- 加工时间、计件数量:PLC运行数据
把这些不同类型的数据(字符串、整型、实数、时间类型)统一拼成一个字符串,中间要过“类型转换关”和“分隔符设计关”。
归一化流程可以这样设计:
- 定义各字段的源变量,统一转成字符串;
- 定义拼接中间变量,按照固定顺序逐步CONCAT;
- 所有字段之间用统一分隔符(如“|”或“,”);
- 总拼接结果做长度校验;
- 把结果传给HMI显示或者通过以太网发送到MES。
我用的一张典型数据结构表如下:
| 字段名称 | 数据类型 | 长度 | 示例值 | 备注 |
|---|---|---|---|---|
| 设备编号 | STRING | 8 | T-18 | 固定参数 |
| 工单号 | STRING | 16 | B2407-018 | MES下发 |
| 产品型号 | STRING | 16 | PX-200 | MES下发 |
| 工位号 | STRING | 4 | A03 | 固定参数 |
| 报警代码 | STRING | 8 | AL10.1 | 报警模块 |
| 操作员ID | STRING | 8 | OP01 | HMI输入 |
| 加工时间 | STRING | 20 | 2024-07-18 10:30 | 时间转换 |
| 计件数量 | STRING | 6 | 128 | 整数转换 |
5.2 带校验与截断防护的完整ST函数代码
下面这段代码是我在三菱FX5U项目里实际使用的工单拼接逻辑,做了适当简化,去掉了项目相关的保密字段,保留了核心拼接与防截断框架。
FUNCTION BLOCK FB_WorkOrderBuild VAR // 输入 bBuild : BOOL; // 拼接触发 sDeviceID : STRING(8); // 设备ID sWorkNo : STRING(16); // 工单号 sModel : STRING(16); // 产品型号 sStation : STRING(4); // 工位号 sAlarmCode : STRING(8); // 报警代码 sOperator : STRING(8); // 操作员ID sDateTime : STRING(20); // 时间字符串 iQty : INT; // 计件数量 // 输出 sResultMsg : STRING(128); // 最终工单信息 bOverflow : BOOL; // 长度溢出标志 END_VAR VAR // 中间变量 sQtyStr : STRING(6); sTemp : STRING(128); sTemp2 : STRING(128); sTemp3 : STRING(128); sTemp4 : STRING(128); sTemp5 : STRING(128); END_VAR拼接逻辑段:
// 类型转换 sQtyStr := INT_TO_STRING(iQty); // 分步拼接,便于排错 sTemp := CONCAT('设备:', sDeviceID); sTemp2 := CONCAT(sTemp, ' 工单号:'); sTemp2 := CONCAT(sTemp2, sWorkNo); sTemp2 := CONCAT(sTemp2, ' 型号:'); sTemp2 := CONCAT(sTemp2, sModel); sTemp2 := CONCAT(sTemp2, ' 工位:'); sTemp2 := CONCAT(sTemp2, sStation); sTemp3 := CONCAT(sTemp2, ' 报警:'); sTemp3 := CONCAT(sTemp3, sAlarmCode); sTemp3 := CONCAT(sTemp3, ' 操作员:'); sTemp3 := CONCAT(sTemp3, sOperator); sTemp4 := CONCAT(sTemp3, ' 时间:'); sTemp4 := CONCAT(sTemp4, sDateTime); sTemp4 := CONCAT(sTemp4, ' 数量:'); sTemp5 := CONCAT(sTemp4, sQtyStr); sTemp5 := CONCAT(sTemp5, ';'); // 结束分隔符长度校验段:
// 长度校验:防止静默截断 IF LEN(sTemp5) <= 128 THEN sResultMsg := sTemp5; bOverflow := FALSE; ELSE bOverflow := TRUE; // 截断处理,取前128字符 sResultMsg := LEFT(sTemp5, 128); END_IF;执行效果,当各输入值如下时:
- sDeviceID = 'T-18'
- sWorkNo = 'B2407-018'
- sModel = 'PX-200'
- sStation = 'A03'
- sAlarmCode = 'AL10.1'
- sOperator = 'OP01'
- sDateTime = '2024-07-18 10:30'
- iQty = 128
最终sResultMsg为:
设备:T-18 工单号:B2407-018 型号:PX-200 工位:A03 报警:AL10.1 操作员:OP01 时间:2024-07-18 10:30 数量:128;这条消息可以直接送到HMI的报警信息栏,或者通过以太网模块用ASCII码发送给MES系统。
5.3 为什么分步拼接比写一行嵌套更值得
我见过很多人在ST语言里追求“一行代码搞定,看起来很高级”,比如:
sResult := CONCAT(CONCAT(CONCAT(sA, sB), sC), sD);这种写法在ST语言里最大的问题是:一旦结果和预期不符,你根本不知道是哪一步出了问题。变量监视窗口里只能看到最终结果,看不到中间过程。
用分步拼接,每个中间变量都能在GX Works3的监视窗口里单独查看。哪个字段拼坏了,一目了然。而且ST语言的调试本来就不如C语言方便,能多拆一步就多拆一步,别怕麻烦。
另外,分步拼接配合一个额外的小技巧:在整条消息的首尾都加上标志性字符。开头可以加“>”或“$”,结尾加“;”或“#”。这样上位机解析时能用“找第一个标志字符”和“找最后一个标志字符”的方式快速截取有效内容。
6. 常见问题排查实录:字符串拼接的坑,我替你踩过了
6.1 数值拼接后出现乱码或菱形符号
这是字符串实操里最容易遇到的问题。你把iQty=128直接用CONCAT拼进去,编译都不一定报错(有的旧版本GX Works2会允许隐式转换,但结果完全不对),但运行起来HMI上显示的不是“128”而是一个方块或乱码字符。
根因是PLC内部对数值和字符串的存储格式不一样,INT是二进制补码,字符串是一串ASCII码。你直接把INT当字符串解析,解析出来的是二进制数据对应的ASCII控制字符,自然是乱码。
处理办法:任何数值参与字符串拼接前,一律显式转换。
sQtyStr := INT_TO_STRING(iQty); // 正确 sBad := CONCAT('数量:', iQty); // 错误/未定义行为这里有个进阶提示:如果数值是REAL实数类型,比如重量2.5kg,用REAL_TO_STRING转换后,不同版本的三菱ST可能输出“2.500000E+00”这种科学计数法格式,并不适合直接显示。建议用REAL_TO_STRING之后再用REPLACE做格式修正,把多余的零和小数点尾巴去掉,或者干脆用MUL到INT再显示,省心得多。
6.2 字符串长度不足导致工单信息被“砍头去尾”
这个问题的隐蔽性极强。你声明了一个STRING(64),拼接时没算总长,结果拼接后的字符串长度是70。PLC不会报错,而是把第65个字符开始的内容全部丢弃。
更坑的是,截断是从尾部开始的,也就是说你工单最后的时间和产量信息会直接消失,但前半部分看起来完全正常。这种bug在调试时极难定位。
我建议的做法是:
- 声明字符串变量时按最大可能值预留,宁长勿短;
- 拼接后尽量加一条长度判断逻辑;
- 如果项目没有严格的内存限制,直接用STRING(255)做中间变量,拼完再压缩到短变量。
6.3 REPLACE替换了所有匹配项,跟你预期不符
再回到REPLACE的一个特性差异。假设你的工单文本里有多个“报警”字样:
sStr := '设备20 报警AL01 等待处理,报警AL02 已确认'; sNew := REPLACE(sStr, '报警', 'ALARM');如果当前固件的REPLACE做的是全部替换,结果就是“设备20 ALARMAL01 等待处理,ALARMAL02 已确认”,你会发现连“ALARMAL01”里的ALARM都被替换逻辑处理过,看起来就别扭了。而如果只是首个匹配替换,结果则是“设备20 ALARMAL01 等待处理,报警AL02 已确认”。
这个行为的差异带来的麻烦在于:你在调试环境验证过的逻辑,移植到另一台固件版本的PLC上,结果可能不一样。我的做法是,在程序注释里明确写明需要的REPLACE行为,并且尽量把要替换的字符串做得“独一无二”,比如替换‘报警AL01’而不是‘报警’,缩小匹配范围。
6.4 中文字符串拼接后的另类长度问题
三菱ST在中文系统下的字符串处理有个隐藏属性:一个字算多少长度,不同厂商的PLC处理不一样。三菱PLC里,String长度按照字节数计算,中文UTF-8编码一个字占3个字节(某些版本是内部转码成Unicode,一个字算2个字节),所以你在工单里“设备”这两个字,可能会占用4到6个字符长度。
这个细节直接导致一个问题:你在电脑上用字符串长度计算器算好的工单长度,传到PLC里长度溢出,信息被截断。处理办法是:中文工单场景下,声明长度最好翻倍预留。比如你认为工单最长为30个字符,实际声明STRING(64)比较稳。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 排查与解决办法 |
|---|---|---|
| 拼接结果尾部缺失 | 目标字符串长度不够 | 预留更大声明长度,加LEN检查 |
| 数值拼进去显示乱码 | INT/REAL未转字符串 | 使用INT_TO_STRING等方法显式转换 |
| REPLACE结果与预期不符 | 固件版本替换行为不同 | 缩小匹配子串,避免通配替换 |
| 中文工单截断 | 中文字节数大于字符数 | STRING长度按字节数留足余量 |
| 编译报错CONCAT参数过多 | 函数版本参数个数限制 | 嵌套调用或分步拼接 |
| HMI显示多出空格 | 源数据带了空白字符 | REPLACE(sStr, ' ', '') 清洗 |
| 多个报警拼接出现重复 | 源报警代码没清空 | 拼接前做MOVE空串处理 |
7. 我总结的ST工单拼接处理心得
最后分享几条用了很多项目、翻了很多车之后总结的经验:
第一条,先定分隔符,再谈拼接。工单信息的字段越多,越要先确定分隔符方案。用“|”还是“,”还是空格,取决于上位机解析的配置。经常出现PLC这边用空格分隔,上位机那边用逗号分隔,结果两边团队扯皮半天。项目启动第一周就把通讯协议文本定下来,能省后面两个月的联调时间。
第二条,字符串操作的变量尽量全部独立命名,不要为了省事复用一个中间变量。复用同一个临时字符串变量,在顺序执行中确实没问题,但是在调试时你会非常痛苦——你根本不知道上一步的结果是被哪个逻辑改掉的。独立命名成本很低,排查收益很高。
第三条,保留一个全局的“原始字符串缓冲区”和“最终结果缓冲区”。我习惯在公共数据块里预定义几个大字符串区域专门用于拼接,不跟FB内部变量混在一起。这样即使FB重新初始化,缓冲区的原始数据还在,方便复现问题。
第四条,报警工单拼接中,REPLACE的使用率比CONCAT还高。因为设备运行过程中,报警代码是实时变化的,AL10.1变成AL10.2,你需要即时更新工单上的报警内容。REPLACE比重新走一遍CONCAT更高效,跳过了之前一堆无关字段的重组。实测在FX5U的扫描周期里,这种局部更新能节省不少CPU执行时间,尤其是工单字符串特别长的时候。
以上是我在ST语言工单拼接上的一部分实操积累。字符串看似是个“辅助功能”,但在工单追溯、MES交互、报警展示这些关键事项里,拼不好一条字符串,影响的是整条产线的数据完整性。希望这篇文章能帮你少踩几个坑。