ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

喷码机串口通信协议实例:Markem 9410/9450对接PLC与MES

喷码机串口通信协议实例:Markem 9410/9450对接PLC与MES

简介:面向马肯依码士9410/9450喷码设备的串口通讯协议与实例,适合自动化工程师、设备集成人员及需要做二次开发的嵌入式开发者。资源将英文原版协议文档、C#工程源码和可执行程序整合在一个压缩包中,共57个文件,涵盖cs源码、sln/csproj解决方案、exe运行文件、PDF协议说明以及RTF使用指南,整包仅1.76MB,结构清晰、便于按需查看。目前已有1346人学习使用,说明这类设备通讯对接需求在产线集成与维护场景中比较常见。借助源码中的串口参数配置、连接管理和报文收发示例,可快速摸清9410/9450的通讯流程;结合协议文档又能进一步核对指令格式与应答逻辑,减少现场调试时的盲试。配套的必读说明和README也能帮助新手理解工程组织方式,从而更顺畅地移植或改造到自己项目中。

1. 喷码机串口通信,为什么值得折腾一份协议实例

产线上那台喷码机,平时看着就是个黑匣子,按几个键能打印,但想让它和MES、PLC对上话,立刻就没辙了。9410/9450这两个型号我用过不少次,串口通讯说简单不简单,说难也不难——难点在于协议文档要么不全,要么是英文的,更别说想找一个能直接改的通讯实例。多数人卡在第一步:不知道发什么字节过去,喷码机才肯理你。

这份资源解决的就是这个问题。它把9410/9450的串口通讯协议整理成了文档,还配了实例代码。你不需要自己从零去试波特率、摸索帧格式,照着实例改一改,就能实现让电脑或者PLC发一串ASCII码,喷码机按要求打印。适合三类人:搞产线自动化的上位机工程师,刚接手喷码机维护的设备工程师,以及做非标设备想快速集成喷码功能的电气工程师。接下来我从协议基础讲起,再拆解实例代码,最后把串口通讯里我踩过的坑全部摆出来,让你少走弯路。

2. 9410/9450串口协议基础:接口、帧格式与通讯参数

2.1 物理接口与接线:DB9与RS232/RS485

9410和9450机型的串口在后背板,常见的是一个DB9公头,默认RS232接口。如果你的PLC或工控机是RS485接口,需要额外加一个RS232转RS485转换器,或者确认该机型选配了RS485板卡。我见过不少人拿一根普通的USB转RS232线接上去没反应,第一反应怀疑协议不对,其实是针脚定义搞错了。

标准DB9公头定义:2脚是TXD,3脚是RXD,5脚是GND。喷码机这边是DCE设备,所以你的电脑侧要用交叉线:电脑的TXD接喷码机的RXD,电脑的RXD接喷码机的TXD,GND直连。如果手头只有直通线,可以在喷码机一侧做2/3交叉转接。另外有些机型用RJ45口引出串口,我就踩过一次,以为RJ45是网口,查了半天手册才看到引脚定义。接线前一定先看随机说明书里的连接图。

2.2 串口参数:波特率、数据位、校验位

9410/9450出厂串口参数一般是波特率9600,数据位8位,无校验,1位停止位,也就是常说的8N1。但注意,机器菜单里有Serial Port设置,里面能改成19200甚至38400。如果实例代码里用的波特率和机器当前设置不一致,通讯会时好时坏,而且没有报错。

除了波特率,还有校验位可选。多数场景用无校验,但有些定制系统会设成Even(偶校验)。我在一个项目里遇到喷码机返回的帧偶尔校验错位,折腾半天发现是机器被人改成了7位数据位加偶校验,而我的上位机还在用8N1。所以拿到机器后,第一步不是写代码,而是进菜单把串口参数截图记录下来,然后让代码里写死这些参数,不要用程序自动探测。

2.3 帧结构:STX/ETX/校验与常见命令类型

9410/9450的链路层协议采用的是ASCII控制字符封装。一次完整的命令帧格式如下:

字节含义常见值
STX帧起始0x02
ADDR设备地址(可选)0x30或0x31
CMD命令字'P'、'S'、'Q'等
DATA数据区ASCII字符串
ETX帧结束0x03
CHK校验字节LRC或BCC

我拿到的这份资源里,命令字用得比较多的是这几个:

  • P:打印文本,后面跟要打印的内容。
  • S:设置参数,比如日期格式、计数器值。
  • Q:查询状态,返回喷码机当前运行状态。
  • C:清除或重置缓冲区。

校验方式一般是BCC(异或校验),范围从STX之后到ETX之前,不含STX和ETX。也有用LRC(纵向冗余校验)的,具体看协议文档哪一版。这里有个关键点:很多国产自定义协议喜欢用\r\n结尾,但9410/9450用的是控制字符,如果把它当文本处理,会漏掉很多细节。

3. 通讯实例拆解:从发送一条打印任务到接收应答

3.1 实例包里的文件与使用前提

这个资源解压后,一般能看到协议说明文档(PDF或Word)和一个上位机示例工程,可能是C#或者VB写的。我用的是Python重新实现了一遍,因为产线工控机上装Python环境更方便,而且pyserial库用起来简洁。你手里拿到示例工程后,先确认里面的串口号、波特率和机器设置一致。

先用串口调试助手做最小验证:打开COM口,发送一条查询状态帧,看有没有返回。查询状态帧通常很简单,比如发送\x02Q\x03,但有些固件要求带设备地址和校验。我建议第一次测试直接看实例代码里的初始函数,里面一般有完整帧构造方法。不要自己猜帧格式,拿现成的改最稳。

3.2 初始化串口并发送文本内容

下面是一段Python示例,演示构造一条打印命令并发送到喷码机。假设实例包里的协议是STX包裹的ASCII命令,要求发送P加上文本内容,帧尾带异或校验字节。

import serial import time def xor_checksum(data: bytes) -> int: """计算BCC异或校验,输入不含STX/ETX""" chk = 0 for b in data: chk ^= b return chk def build_print_frame(text: str) -> bytes: cmd = b'P' + text.encode('ascii') frame = b'\x02' + cmd + b'\x03' chk = xor_checksum(cmd) # 有的协议要求校验也要转成ASCII十六进制字符 frame += b'%02X' % chk return frame ser = serial.Serial( port='COM3', baudrate=9600, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=1 ) frame = build_print_frame('LOT2025-001') ser.write(frame) time.sleep(0.2) resp = ser.read(64) print('response:', resp)

这段代码先定义了异或校验函数,build_print_frame把命令字P和打印内容拼起来,并加上STX和ETX,最后把校验字节以两个ASCII十六进制字符追加在帧尾。很多机器要求校验码必须是大写,所以用了%02X格式化。串口初始化用了常见的9600 8N1,根据机器实际设置调整。发送后延时200ms是给喷码机一个处理时间,读取返回。

这里要特别说明:打印内容里的中文必须转成喷码机支持的编码,常见的9410/9450输入法是ASCII和西里尔字符集,中文要先用机器自带的编辑器定义好用户字段,然后通过字段号来调用,而不是直接发中文。直接发UTF-8字节基本会乱码。

3.3 解析喷码机应答、状态码与常见返回

喷码机收到命令后,正常会返回一个应答帧,通常是ACK(0x06)或者是带数据的状态帧。我遇到过三种情况:

  • 返回ACK:命令被接受,正在执行。
  • 返回NAK(0x15):命令格式错误,检查帧结构。
  • 返回一段ASCII内容和状态码:查询类命令的返回。

响应数据里经常包含一个两位状态码,比如00表示正常,01表示喷头未就绪,02表示缺墨。完整的状态码表在协议文档附录里,建议打印一份贴在电柜里。这里的血泪经验是:不要用固定长度截取响应,一定要先读到ETX,再判断中间内容。因为喷码机的返回里可能夹杂有中文警告信息,长度不是固定的。

def read_response(ser: serial.Serial) -> bytes: """读取直到ETX,最多读100字节""" buf = b'' while len(buf) < 100: ch = ser.read(1) if not ch: break buf += ch if ch == b'\x03': break return buf

这个读取函数每次读一个字节,遇到帧结束符ETX就退出。这样即使响应长度不固定,也能完整拿到一帧。如果超时没有收到ETX,说明通讯链路有问题,后面的超时重试机制也需要围绕这个函数来写。

4. 参数配置与协议细节:日期、计数器与数据库字段

4.1 打印信息中的日期/时间变量怎么传

9410/9450支持在打印内容里插入日期时间变量,比如<DDMMYY>、<HHMMSS>。这些变量在机器内部会自动替换为当前时间。上位机发送打印文本时,可以直接把这类占位符放进文本数据区。但要注意,喷码机里的日期格式和时间源需要提前设置好,否则发送<DDMMYY>可能显示成DDMMYY而不是实际日期。

我在一个包装线项目里就吃过亏:代码发送的打印内容里有<DDMMYY>,喷码机喷出来的却是字面量。后来检查发现机器菜单里“Date Format”选项被设成了MMDDYY,而且“Variables”功能没启用。正确做法是在机器上先把变量定义好,上位机只管发占位符,不要自己拼时间字符串。这样时间源也可以统一由喷码机维护,避免上位机和机器时钟不同步。

4.2 计数器递增、清零与预置

产线上经常要打连续流水号,喷码机内部有计数器功能。上位机可以通过协议命令去设置计数器的初值、步长和递增方向。常见命令是S加上计数器编号,比如S C1=100表示把1号计数器预置为100。具体用哪个命令字,实例包里的协议文档都会写。

一个容易翻车的地方:计数器的递增是在喷码机内部完成的,不是在每次上位机发送打印命令后立刻递增,而是等喷码机实际喷完一组产品后。有些机型有“重复打印”设置,如果开启,同一计数器值会被打印多次,导致上位机统计的数量和实际产品数量对不上。所以你需要确认“Auto Increment”选项是在“After Print”还是“After Job”触发。

4.3 与PLC联动时的握手机制

如果用PLC直接控制9410/9450,通常需要做硬件握手。除了串口的RTS/CTS,喷码机还支持通过帧内特定字符实现软件握手。我常见的一种做法是:PLC发送打印任务帧,喷码机返回ACK后,PLC再发送下一个任务。这个流程没问题,但要注意喷码机缓冲区是有限的。

实例包里如果带了PLC侧代码,一般会封装一个“发送并等待ACK”的函数。你要在PLC的程序里加一个超时跳转,不能死等。因为喷码机在清洗喷头或者保养时,可能几十秒不响应。我的做法是设置500ms超时,超时后重发一次,重发三次仍无响应就报警。这个重发逻辑在上位机同样适用。

5. 串口通讯避坑:常见问题与排查记录

5.1 发送后无应答:原因可能不是协议

现象:上位机发命令后喷码机没反应,串口调试助手发送同样字节也没反应。

原因排查:先查接线,用万用表量串口2脚和3脚的电压,正常空闲时TXD应该是负电压。然后看串口参数是不是和机器一致。这两步都没问题,最后才看协议帧格式。我遇到过机器地址被设成01,而帧里没带地址,导致机器直接丢弃。

解决:在实例代码中增加设备地址字段,确认和机器菜单里的“Station Address”一致。另外,有些机器需要先发送??之类的握手命令来唤醒串口,看协议文档里的“Wake-up”说明。

5.2 中文乱码:编码格式不匹配

现象:发送文本含中文时,喷码机打出来的是乱码或者问号。

原因:9410/9450默认字符集不包含中文,或者上位机发送的是UTF-8,机器期望的是GB2312或万国码字符集。

解决:不要直接发中文。在喷码机中用用户字段(User Field)定义好固定中文内容,上位机通过字段号调用。实例包里如果有“字段映射”示例,优先使用。如果必须动态发中文,需要把喷码机菜单里的“Message Mode”设置为相应的多字节语言,并确认协议文档中关于中文字符的转义说明。

5.3 喷码机重启后连接丢失

现象:喷码机断电重启后,上位机重新连接却能打开串口,但发命令无返回。

原因:喷码机串口在启动过程中可能占用一段时间,或者需要重新发送初始化序列才能进入可通信状态。

解决:上位机启动后,先等5~10秒再发送握手帧。更稳妥的办法是监听串口设备在线状态,在喷码机断电恢复后,程序自动重新发送初始化序列。我在代码里加了一个重连检测:每隔1秒发送查询状态命令,连续收到3次NAK就重新初始化串口。

5.4 偶发丢包或乱码:线缆与电磁干扰

现象:通讯大部分时间正常,但喷码机启动喷印时偶发数据错误,返回帧校验不过。

原因:喷码机喷头驱动电路开关时产生电磁干扰,串口线屏蔽层接地不良。

解决:使用带屏蔽的RS232专用线,屏蔽层单端接地。把串口线远离喷头线和电源线,如果走线槽,尽量分开。另一个方法是降低波特率到9600,增加帧间延时。我在一个产线上就是靠给串口线加磁环解决的高频干扰丢包。

5.5 实例代码与当前固件版本不配套

现象:实例包里代码和协议文档都对,但实际机器上跑不通,个别命令返回错误。

原因:喷码机固件升级过,协议指令有微调,比如有的版本命令字区分大小写。

解决:先看实例包里的协议文档版本号,再看机器主菜单“About”里的固件版本。如果文档版本比机器固件老,可以去官网下载对应版本的手册。另外一个笨办法:用串口调试助手逐条测试实例里的命令,把机器实际返回记录下来,和文档中的示例比对,往往能找到差异。

6. 把实例改造成稳定服务:超时重试与日志记录

读到的协议实例通常只能跑通基本收发,离“能稳定用在产线”还差一层封装。我会建议你加三样东西:超时重试、命令队列、操作日志。这里写一个简化版的服务类,你可以参考改造。

import time import logging import serial from collections import deque class MarkemSerialService: def __init__(self, port, baudrate=9600): self.ser = serial.Serial(port, baudrate, timeout=1) self.retry = 3 self.timeout = 0.5 self.logger = logging.getLogger('MarkemSerial') self.queue = deque() def send_command(self, cmd: bytes) -> bool: for attempt in range(self.retry): self.ser.write(cmd) self.ser.flush() resp = self._read_frame() if resp and b'\x06' in resp: # ACK self.logger.info(f'ok: {cmd}') return True else: self.logger.warning(f'retry {attempt + 1}: {cmd}') time.sleep(self.timeout) self.logger.error(f'fail: {cmd}') return False def _read_frame(self): buf = b'' while len(buf) < 100: ch = self.ser.read(1) if not ch: break buf += ch if ch == b'\x03': break return buf def add_task(self, task): self.queue.append(task) def run_once(self): while self.queue: task = self.queue.popleft() self.send_command(task)

这段代码里,send_command会重试3次,每次读取一帧响应,如果收到ACK字节就认为成功。add_task可以把多个打印任务放入队列,run_once按顺序执行。日志记录里能看到每次是成功还是失败,配合时间戳,出了问题能快速定位是哪台机器、哪个任务没发出去。

实际使用中,我还会在发送前检查串口是否打开,在发送后记录打印内容中的计数器和产品编号。从那以后我每次做喷码机通讯改造,都会先读一遍协议文档的“Exceptions”章节,再强制走一遍“发送–应答–重试–日志”的流程,哪怕只是一个简单的查询命令。这套习惯帮我避开了不少后续产线调试时的尴尬状况。这份资源里提供的协议文档和实例就是很好的起点,下载下来,对照自己的机器参数跑一遍,你会发现串口对接没那么玄学。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进