在电气自动化系统中,PLC 与 MES(制造执行系统)之间的数据交互常依赖条码扫描结果作为工序触发或物料追溯的关键输入。条码本身通常只包含基础编码(如流水号、批次号),但 MES 接口往往要求结构化字符串——例如带前缀的唯一标识符、含时间戳的事件报文、或符合特定协议格式的 JSON 片段。此时,单纯读取条码原始值无法满足通信需求,必须在 PLC 内完成实时、确定性、无外部依赖的字符串拼接。本文以西门子 S7-1200/1500 系列 PLC 的 Structured Text(ST)编程语言为载体,详解如何使用标准函数 CONCAT 安全、高效地构建符合 MES 要求的条码报文。
一、为什么必须在 PLC 层拼接?常见误区澄清
许多工程师试图将拼接逻辑后移至上位机或 MES 侧,理由是“PLC 只该做控制”。但这在工业现场极易引发三类硬伤:
- 时序断裂:条码扫描触发动作(如启动装配工位)与 MES 报文发出之间若存在网络延迟或上位机调度抖动,会导致事件与数据不匹配,追溯链断裂;
- 单点故障:一旦上位机宕机或通信中断,PLC 无法生成有效报文,MES 收不到任何事件,产线状态在系统中“消失”;
- 协议耦合:MES 接口升级(如字段增减、分隔符变更)若需修改上位机代码,每次变更都需停机部署,违背柔性产线原则。
正确做法是:PLC 负责生成语义完整的报文字符串,仅通过标准化协议(如 OPC UA PubSub、MQTT、S7 通信)将最终字符串“原样投递”给 MES。拼接逻辑固化在 PLC 中,与通信通道解耦,既保证实时性,又提升系统鲁棒性。
二、ST 字符串基础:STRING 与 ARRAY[254] OF CHAR 的本质区别
在 ST 中,STRING 并非“万能字符串类型”,而是一个带长度前缀的结构体。声明 VAR str : STRING[32]; 实际等价于:
TYPE STRING_32 :
STRUCT
len : USINT; // 当前实际长度(字节数)
buf : ARRAY[0..31] OF CHAR; // 固定缓冲区,索引 0~31
END_STRUCT
END_TYPE
关键事实:
len字段由系统自动维护,不可手动赋值(如str.len := 5;是非法操作);- 所有字符串函数(包括
CONCAT)均通过读取len决定有效内容,并在拼接后自动更新len; STRING[32]的最大可存字符数为 32,但实际可用长度 = 32 − 1 = 31(因第 0 字节被len占用);
对比 ARRAY[254] OF CHAR:
- 是纯字符数组,无长度信息,需手动追踪末尾
\0或计数; - 不支持
CONCAT等字符串函数,必须用循环逐字复制; - 在通信报文中易因未置零导致脏数据(如前次残留字符混入本次报文)。
因此,所有涉及拼接的变量必须声明为 STRING[N] 类型,严禁用 ARRAY 替代。
三、CONCAT 函数语法与安全边界(核心规则)
CONCAT 是 IEC 61131-3 标准函数,签名如下:
FUNCTION CONCAT : STRING
VAR_INPUT
IN1 : STRING;
IN2 : STRING;
END_VAR
其行为严格遵循以下三条铁律:
-
输出长度 = MIN( IN1.len + IN2.len , N−1 )
若拼接结果超出目标STRING[N]容量,则静默截断,不报错、不警告。例如:s1 := 'ABC'; // len = 3 s2 := 'DEFGHIJK'; // len = 8 result := CONCAT(s1, s2); // 若 result 声明为 STRING[10],则 result = 'ABCDEFGHI'(len=9),'K' 被丢弃 -
空字符串参与拼接完全合法
CONCAT('A', '')返回'A';CONCAT('', '')返回''(len=0)。无需预先判断是否为空。 -
输入字符串若超长,自动截断至声明长度
若s1声明为STRING[5]但实际存了 8 字符(因历史错误写入),CONCAT仅读取其len字段指示的前len个字符,不会越界读取。
✅ 正确用法:始终将
CONCAT结果赋值给一个明确声明长度的STRING[N]变量,并确保N大于等于所有可能输入长度之和 + 安全余量(建议 +5)。
四、实战:构建 MES 条码报文的 4 种典型场景
场景 1:基础条码补全(前缀 + 扫描值)
MES 要求条码格式为 PRD-20240520-00123,其中 PRD- 为固定前缀,20240520 为当日日期(由 PLC 提供),00123 为扫描值。
// 声明变量
sPrefix : STRING[8] := 'PRD-'; // 长度预留足够
sDate : STRING[8]; // 存储 '20240520'
sScan : STRING[16]; // 扫描值(如 '00123')
sResult : STRING[32]; // 输出:足够容纳 'PRD-20240520-00123'(16字符)
// 获取当前日期(示例:使用 TON 计时器+RTC 读取,此处简化为常量)
sDate := '20240520';
// 拼接:CONCAT(CONCAT(sPrefix, sDate), sScan) → 避免嵌套过深,分步更清晰
sTemp := CONCAT(sPrefix, sDate); // 'PRD-20240520'
sResult := CONCAT(sTemp, '-'); // 'PRD-20240520-'
sResult := CONCAT(sResult, sScan); // 'PRD-20240520-00123'
⚠️ 注意:
CONCAT不支持多参数,必须链式调用。CONCAT(a,b,c)是非法语法。
场景 2:动态字段拼接(含设备ID与工序号)
MES 报文需为 JSON 片段:{"barcode":"00123","station":"ASM-01","step":2,"ts":"20240520142305"}。各字段来自不同来源:
| 字段 | 来源 | 类型 | 声明示例 |
|---|---|---|---|
barcode |
条码扫描器 | STRING[16] |
sBarcode |
station |
设备配置常量 | STRING[10] |
'ASM-01' |
step |
INT 变量 | 需转字符串 | iStep → INT_TO_STRING(iStep) |
ts |
RTC 时间 | STRING[14] |
sTimestamp(格式:YYYYMMDDHHMMSS) |
// 声明必要转换函数(系统自带)
sStepStr : STRING[4]; // INT 最大 9999,4字符足够
sStepStr := INT_TO_STRING(iStep); // iStep=2 → '2'
// 构建键值对(注意引号与冒号需显式拼入)
sKey1 := '"barcode":"'; // len=11
sKey2 := '","station":"'; // len=14
sKey3 := '","step":'; // len=9
sKey4 := ',"ts":"'; // len=8
sEnd := '"}'; // len=2
// 组装(总长估算:11+16+14+10+9+4+8+14+2 = 88 → STRING[128] 安全)
sJSON := CONCAT(sKey1, sBarcode);
sJSON := CONCAT(sJSON, sKey2);
sJSON := CONCAT(sJSON, sStation);
sJSON := CONCAT(sJSON, sKey3);
sJSON := CONCAT(sJSON, sStepStr);
sJSON := CONCAT(sJSON, sKey4);
sJSON := CONCAT(sJSON, sTimestamp);
sJSON := CONCAT(sJSON, sEnd);
✅ 验证技巧:在 TIA Portal 中监控
sJSON.len,确认其等于预期字符数(如本例应为 88),避免隐式截断。
场景 3:容错拼接(处理扫描失败或空值)
实际产线中,条码可能污损、扫描器临时故障,导致 sBarcode 为空(len=0)或含非法字符(如控制符)。直接拼接会生成无效报文。
步骤:
- 判空保护:用
LEN(sBarcode) = 0检测; - 清洗非法字符:遍历
sBarcode.buf,替换非数字/字母字符为'X'; - 填充默认值:空时用
'ERR-' + 时间戳替代。
// 清洗函数(内联逻辑,不封装)
i := 0;
WHILE i < LEN(sBarcode) DO
IF (sBarcode.buf[i] < '0') OR (sBarcode.buf[i] > '9') AND
(sBarcode.buf[i] < 'A') OR (sBarcode.buf[i] > 'Z') AND
(sBarcode.buf[i] < 'a') OR (sBarcode.buf[i] > 'z') THEN
sBarcode.buf[i] := 'X';
END_IF;
i := i + 1;
END_WHILE;
// 判空并替换
IF LEN(sBarcode) = 0 THEN
sBarcode := CONCAT('ERR-', sTimestamp); // 'ERR-20240520142305'
END_IF;
⚠️ 关键:
LEN()函数返回sBarcode.len,是唯一可靠的长度依据,禁止用SIZEOF(sBarcode.buf)(返回固定32,非实际长度)。
场景 4:循环批量拼接(同一工位多件扫码)
某工序需连续扫描 5 件,MES 要求单次报文提交全部条码,格式:ITEMS:00123,00124,00125,00126,00127。
// 声明数组与索引
aBarcodes : ARRAY[0..4] OF STRING[16]; // 5个条码
iIndex : INT := 0; // 当前写入位置
sBatch : STRING[256]; // 批量结果(256足够:5*16 + 4*1 + 6 = 89)
// 扫描到一件时执行(伪代码)
IF bScanTrigger THEN
aBarcodes[iIndex] := sScanResult;
iIndex := iIndex + 1;
IF iIndex >= 5 THEN
// 开始拼接
sBatch := 'ITEMS:'; // 初始化
FOR i := 0 TO 4 DO
IF i > 0 THEN
sBatch := CONCAT(sBatch, ','); // 追加逗号
END_IF;
sBatch := CONCAT(sBatch, aBarcodes[i]);
END_FOR;
// 重置索引
iIndex := 0;
END_IF;
END_IF;
✅ 优势:避免重复
CONCAT调用开销(每次调用有微小 CPU 开销),循环内仅 9 次CONCAT(1次初始化 + 4次逗号 + 5次条码),远少于“每扫一件就拼一次”的方案。
五、性能与内存关键点(避坑指南)
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 拼接后字符串变短 | CONCAT 输入之一被意外截断(如 STRING[10] 存了 12 字符,len 字段被破坏) |
在拼接前用 LEN() 检查输入长度,异常时强制重置:s.x.len := 0; |
| PLC 扫描周期跳变 | 链式 CONCAT 调用过多(>20层),单次执行超时 |
将长拼接拆分为 2~3 步,中间变量声明为 STRING[64] 等大容量类型,减少调用次数 |
| 通信报文含乱码 | STRING 变量未初始化,buf 区域残留旧数据 |
在 FB 初始化段执行 sResult := '';(空字符串赋值会清空 len 并置 buf[0] 为 \0) |
| 无法调试拼接结果 | STRING 在监控表中显示为 ...(省略) |
在 TIA Portal 中右键变量 → “更改显示格式” → 选择 “String (ASCII)” |
六、与 MES 通信的最终交付物
拼接完成的 sResult(或 sJSON)不应直接用于通信指令,而应作为数据源接入标准通信模块:
- OPC UA Client:将
sResult映射为OPC_UA_CLIENT.Variable的Value输入; - MQTT Publisher:赋值给
MQTT_PUB.Payload(需确保Payload类型为STRING); - S7 通信:通过
TSEND_C发送前,用STRING_TO_ARRAY转为ARRAY[254] OF BYTE,并设置LEN参数为LEN(sResult)。
✅ 核心原则:PLC 只负责“造好字符串”,不负责“发字符串”。通信协议栈与字符串生成彻底分离。
七、附录:常用辅助函数速查表
| 功能 | 函数名 | 示例 | 注意事项 |
|---|---|---|---|
| 字符串长度 | LEN(s) |
LEN('ABC') = 3 |
返回 USINT,非 INT |
| 整数转字符串 | INT_TO_STRING(i) |
INT_TO_STRING(123) = '123' |
结果不含前导零 |
| 字符串转整数 | STRING_TO_INT(s) |
STRING_TO_INT('123') = 123 |
失败时返回 0,无异常机制 |
| 截取子串 | MID(s, pos, len) |
MID('ABCDEF', 2, 3) = 'BCD' |
pos 从 1 开始计数 |
| 查找字符 | FIND(s, char) |
FIND('ABC', 'B') = 2 |
未找到返回 0 |
// 安全截取最后4位(防越界)
sLast4 := MID(sBarcode, MAX(1, LEN(sBarcode)-3), 4);
暂无评论,快来抢沙发吧!