1. 先把欧姆龙“通信协议家谱”弄明白,选错方向等于白干
要是你没被欧姆龙PLC的通信协议坑过,你多半会觉得PLC通信就是“串口连上、发指令、收数据”这么简单。我第一次接这个任务时也是这么想的,结果连着三天被一台CP1H晾在一边:报文发了、串口灯也闪了,就是读不到一个有效数据。后来把FINS、Host Link、Modbus这些老哥挨个过了一遍,才算真正搞明白这套逻辑。这篇就把我从零到能稳定通信的完整经历,以及踩过的那些说出来都丢人的坑,原原本本写出来。打算跟欧姆龙PLC做上位机通信、接触摸屏,或者刚开始接触FINS协议的朋友,应该能少走至少一个月的弯路。
欧姆龙在不同年代、不同硬件上跑的通信协议其实有好几套,而且互相不兼容。很多人在网上搜到一段代码就往上怼,结果报文格式对不上,第一反应居然是怀疑PLC坏了。我先给大家把这张“家谱”捋清楚。
1.1 欧姆龙到底有哪几种通信协议
我自己按使用场景把欧姆龙的通信协议分成了四类,分别对应不同的硬件年代和通信介质:
- Host Link(也叫C-mode):串口时代的主力协议,帧结构是ASCII文本,命令是RR、WR、RL、WL、RS、SC这种两个大写字母加参数。老一代C200H、CQM1以及后来的CP1串口都支持。触摸屏、工控机串口、老式上位机组态软件大多走这一套。
- FINS:欧姆龙自家定义的“通用语”,报文是二进制,不是ASCII。它既能跑在串口上,也能封在以太网里走FINS/TCP或FINS/UDP。CP、CJ、CS以及NJ/NX基本都支持,是目前做数据交互最常用的协议。
- Modbus-RTU / Modbus-TCP:新机型往往在PLC侧的系统设定里可以直接把串口或以太网口切换成Modbus从站模式。走标准Modbus协议,好处是上位机生态成熟,什么组态软件都能接。
- EtherNet/IP:面向美系工业以太网生态的协议,NJ/NX支持得比较好,但配置量明显比FINS大,适合有CIP设备互联需求的场景。
我用一个表格把这四类的关键差异列出来,方便大家一眼定位自己该学哪个:
| 协议 | 介质 | 报文形式 | 适合场景 | 上手难度 |
|---|---|---|---|---|
| Host Link | RS-232/485 | ASCII文本帧 | 老设备、触摸屏、串口上位机 | 低 |
| FINS(串口) | RS-232/485 | 二进制帧 | 串口数据交换、自定义上位机 | 中 |
| FINS/TCP、UDP | 以太网 | 二进制帧 | 上位机、MES/SCADA、视觉设备 | 中 |
| Modbus-RTU/TCP | 串口、以太网 | 标准Modbus帧 | 与第三方设备互联、快速对接组态软件 | 低 |
| EtherNet/IP | 以太网 | CIP封装帧 | NJ/NX、罗克韦尔生态互联 | 高 |
1.2 选型判断:什么时候用哪一套
我实际判断的规则很简单:PLC有以太网口,优先用FINS/TCP;只有串口,且触摸屏是老款,用Host Link;要跟非欧姆龙设备做从站对接,用Modbus。
这不是说Host Link不好,而是FINS/TCP在以太网上没有波特率、校验位这些破事,调试起来干净得多。串口通信麻烦就麻烦在参数必须和PLC完全一致,差一个停止位都不理你。我自己早期在串口上浪费的时间,有一半是在对“7位还是8位”“偶校验还是无校验”这样的参数。
还有一个特别容易踩的选择坑:欧姆龙的老设备可能只支持Host Link,而某些新机型串口默认却是FINS模式。你在CX-Programmer里把串口设定改成“Host Link”,上位机却按FINS的二进制帧去发,PLC自然不理你。反过来也一样。所以第一步永远是先确认PLC这套硬件在当前连接方式下,到底激活的是哪个协议,而不是先写代码。
2. FINS报文,逐字节拆开给你看
FINS是欧姆龙所有通信协议里信息量最大、也最值得搞懂的一层。因为只要你理解了FINS的报文结构,后面无论是走串口还是走以太网,无非是外面套不同的“壳”而已。我当时就是被网上那些残缺不全的贴子搞得一头雾水,最后老老实实打开英文手册,一帧一帧对出来的。
2.1 十字节命令头里藏了什么
一帧FINS报文,无论读写,最前面永远是10个字节的命令头。这10个字节不是随便写的,每一个都有具体含义:
| 字节名 | 缩写 | 说明 | 常见取值 |
|---|---|---|---|
| 信息控制域 | ICF | 区分请求/响应、是否需要响应 | 请求0x80,响应0xC0 |
| 保留 | RSV | 固定为0 | 0x00 |
| 网关数 | GCT | 允许跨网的层数,常为2 | 0x02 |
| 目标网络号 | DNA | 目标PLC所在网络,本机为0 | 0x00 |
| 目标节点号 | DA1 | 目标PLC的FINS节点号 | 0~254 |
| 目标单元号 | DA2 | 目标CPU单元,通常0 | 0x00 |
| 源网络号 | SNA | 本机所在网络 | 0x00 |
| 源节点号 | SA1 | 本机FINS节点号 | 1~254 |
| 源单元号 | SA2 | 上位机一般填0 | 0x00 |
| 服务标识符 | SID | 应答时原样返回,用于匹配 | 0x00~0xFF |
我第一次看这个表的时候,最大的疑问是:“目标节点号和源节点号到底填什么?”这个真的很重要。
目标节点号(DA1)填的是PLC侧的FINS节点号,不是IP地址。以太网口PLC通常在设置里既要有IP地址,也要有FINS节点号,默认情况下节点号常常取IP地址的最后一个字节。比如IP是192.168.1.10,节点号就是10。源节点号(SA1)是给上位机自己编的,随便取一个1到254之间、跟局域网内其它FINS节点不冲突的数就行,比如100。当时我犯的错就是把SA1和DA1都填成了0,结果PLC收到报文后既不清楚“谁发给我的”,也不知道“该回给谁”,直接不回包。
还有ICF这个字节。请求帧ICF是0x80,响应帧是0xC0,这个很多人知道;但ICF里有个位表示“要不要响应”,如果设成不要响应,PLC执行完命令后不会回任何东西。调试阶段如果报文格式写错,再把响应关掉,那你就完全抓瞎了。
2.2 存区编号:DM、CIO、WR、HR的对应关系
FINS命令头后面跟着的是命令码和参数,其中最关键的是“存区编号”。欧姆龙把PLC内部的数据区分成CIO、WR、HR、DM、EM等区域,每个区域在FINS协议里都有一个唯一的十六进制区码:
| 数据区 | 字寻址区码 | 位寻址区码 | 典型范围 |
|---|---|---|---|
| CIO区 | 0xB0 | 0x30 | CIO 0~6143 |
| WR区 | 0xB1 | 0x31 | W0~511 |
| HR区 | 0xB2 | 0x32 | H0~1535 |
| DM区 | 0x82 | 0x02 | D0~32767 |
| EM区(Bank0) | 0xA0 | 0x20 | E0_0~E0_32767 |
这里有个特别反直觉的规则:位寻址的区码,不是字寻址区码加一个固定偏移,而是字区码减0x80。比如DM区字寻址是0x82,位寻址就是0x02;CIO字寻址是0xB0,位寻址就是0x30。我第一次写位读取命令时,想当然地把0x82后面加个0x01当位区码,结果返回结束码0x1101(存区编号错误)。后来才明白,区码本身就是区分“按字访问”还是“按位访问”的。
还有存区的地址范围,也要跟具体机型对应。CP1系列和CJ2系列的DM区范围不一样,NJ系列的内存甚至没有传统意义上的“CIO区”这种概念。同一个FINS区码,在不同机型上对应的物理地址范围有差异。写代码时最好把地址范围做成配置文件,换机型只改配置,不要写死在程序里。
2.3 读写命令码和构造一帧读DM报文
FINS命令码是两个字节。我实际用得最多的就这几个:
- 0101:读存储器(memory read)
- 0102:写存储器(memory write)
- 2301:切换PLC为运行模式
- 2302:切换PLC为停止模式
- 0701:读单元信息(检查连接是否正常)
以读DM区为例,完整命令是:“0101 + 区码0x82 + 起始字地址2字节 + 读取字数2字节”。注意起始地址和字数都是大端序,也就是高字节在前。
假设我们要从D100开始读10个字(16个字节),那么FINS主体部分是:
01 01 82 00 64 00 0A其中0x0064就是100的十六进制,0x000A就是10。把前面的10字节命令头拼上去,完整的请求帧就是:
80 00 02 00 00 00 00 64 00 00 01 01 82 00 64 00 0A这里我把DA1填成0、SA1填成0x64,是调试初期的常用做法之一。DA1填0表示“发给本节点”,对于直接连接PLC本体的场景通常能通;但如果经过网关转发或者PLC有多个FINS节点,就一定要填实际目标节点号。我建议做产品时不要偷懒,把DA1、SA1都当成配置项暴露出来。
3. 实操:从一帧报文到稳定读写DM区
理论拆完了,接下来是真正动手的过程。我会把串口FINS和以太网FINS/TCP两条路都走一遍,因为它们的封包方式不一样,而且各有各的坑。
3.1 通信参数与节点号怎么定
先做最基础的三件事:确认PLC型号和支持的协议、确认通信介质、确认PLC侧的参数设置。
我那次用的是CP1H,自带一个RS-232C串口和一个百兆以太网口。我先在CX-Programmer里把串口协议设定为“FINS”,波特率设成9600。注意CX-Programmer连接PLC进行参数写入时,本身也是在占用通信资源的,改完设定后要重新上电或者复位通信参数才生效。这个“改完没重启导致怎么都不通”的情况,我在现场见过不止一次。
然后是节点号。以太网FINS节点号默认通常取IP地址的最后一位,比如IP设192.168.1.10,节点号就是10。如果你在PLC的以太网设置里手动改过节点号,那就以设置里的值为准。上位机的源节点号随便选个不冲突的就行,我一直用100,因为它在报文里是0x64,看起来顺眼。
串口FINS的节点号又有另一套说法:串口FINS报文里的节点号往往跟PLC的“单元号”有换算关系,常见说法是单元号+1。我当时用的单元号是0,所以报文里DA1填的是0。这个细节在手册里有专门的对应表,不同系列可能有差异,千万别拿以太网那套直接套。
3.2 串口FINS带FCS校验的完整帧
串口FINS跟以太网FINS最大的区别是:串口需要在报文尾部追加一个“帧校验序列”(FCS),并且整帧外面要套上Host Link风格的壳,也就是“@单元号 + FINS报文 + FCS + * + CR”。
单元号用两个ASCII十六进制字符表示,默认是00。所以上一节那帧读D100的报文,在串口里实际发送的字节序列是:
@ 00 80 00 02 00 00 00 00 64 00 00 01 01 82 00 64 00 0A 1A * CR这里的1A就是FCS。FCS的计算方法很简单:对“@”和“*”之间的所有字节做异或,结果转成两位大写ASCII十六进制字符。我上面这串报文,把00、80、00、02……一直到最后一个0A全部异或,结果是0x1A,所以填1A。
你可能会问:二进制报文明明可以直接算一个字节校验,为什么要转成ASCII再发?这是因为FINS串口借用了Host Link的帧壳,整个帧在串口上以ASCII文本方式传递,二进制数据也要一个字节一个字节地“翻”成可见字符吗?并不完全是。实际上串口FINS的报文主体仍然按原始字节发送,FCS只是以两个ASCII字符追加。真正让很多人懵圈的是:如果报文里出现了0x0D(回车)、*(0x2A)这类与帧定界符冲突的字节,就会导致接收方提前误判帧结束。所以FINS串口帧对报文的内容有隐性限制,数据里一旦撞上这些特殊字节,就得考虑改用固定长度帧或直接换以太网FINS。
3.3 以太网FINS/TCP的封包差异
以太网的FINS/TCP封装比串口简单,但也多了一层“协议头”。
FINS/TCP固定使用9600端口,TCP连接建立后,每一帧数据的格式是:
FINS ← 4个ASCII字符,固定46 49 4E 53 长度 ← 4字节大端整数,表示后面FINS报文的字节数 FINS报文 ← 命令头10字节 + 命令码 + 参数所以同样读D100的10个字,以太网帧就是:
46 49 4E 53 00 00 00 11 80 00 02 00 00 00 00 64 00 00 01 01 82 00 64 00 0A注意中间那个00 00 00 11,十六进制0x11就是17,正好等于后面FINS报文的字节数(10字节命令头 + 2字节命令码 + 1字节区码 + 2字节地址 + 2字节字数 = 17)。
这是不是意味着不用算FCS了?对,TCP协议本身有校验和与重传机制,FINS/TCP不再额外要求FCS。这个区别让我第一次从串口转到以太网时特别爽,再也不用对着ASCII码表算异或了。
FINS/TCP还有一个“节点地址交换”的标准流程:连上9600端口后,先发一段请求获取节点号的报文,PLC分配一个临时节点号给上位机。很多现成库和组态软件都自动做了这一步。不过我在实际测试中发现,部分型号PLC直接发数据帧也能处理,只是不建议这么干。真到了PLC侧设了连接限制或者多上位机并发的场景,不规范握手的隐患就会暴露出来。
3.4 收到的响应怎么解析(含结束码表)
读完请求发出去,PLC会回来一帧类似的帧。串口响应是“@单元号 + 响应FINS报文 + FCS + * + CR”,以太网响应是“FINS + 长度 + 响应FINS报文”。
响应FINS报文结构是:
10字节命令头(ICF变为0xC0,DA1/SA1互换) 命令码(2字节,跟请求一样) 结束码(2字节) 读出的数据结束码是最先要看的东西。0x0000表示正常,其余都是错误。我整理了一份平时最常遇到的结束码表:
| 结束码 | 含义 | 我踩到过的情况 |
|---|---|---|
| 0000 | 正常 | 无 |
| 0101 | 目标节点不存在或未连上 | 节点号填错 |
| 0103 | 目标单元不存在 | DA2填了非0值 |
| 0401 | 命令格式错误 | 参数个数不对 |
| 1001 | 命令过长 | 一次读取字数太大 |
| 1101 | 存区编号错误 | 位寻址区码算错 |
| 1102 | 起始地址超范围 | DM地址超过机型上限 |
| 1103 | 地址+字数超范围 | 读取末尾越界 |
| 1104 | 数据不对 | 写入了非法格式数据 |
| 2001 | 无访问权限 | PLC受保护或未处于允许通信模式 |
解析响应的另一个坑是:PLC返回的数据也是大端序。读D100开始的10个字,响应里的数据是20个字节,按每2个字节一组,高字节在前。比如D100里面的值是0x1234,响应里就是12 34。可你要是用C语言的指针直接把字节数组强转成uint16数组,在小端CPU上读出来就是0x3412,整个数据全反了。
我当时写了一个验证脚本模拟FINS/TCP读DM区,核心逻辑很简单:
import socket, struct def build_read_dm(start, count, dest_node=0, src_node=100): # FINS 命令头:ICF, RSV, GCT, DNA, DA1, DA2, SNA, SA1, SA2, SID fins_header = bytes([0x80, 0x00, 0x02, 0x00, dest_node, 0x00, 0x00, src_node, 0x00, 0x00]) # 命令码 0101 + DM区 0x82 + 起始地址 + 字数 cmd = bytes([0x01, 0x01, 0x82]) + struct.pack('>H', start) + struct.pack('>H', count) fins = fins_header + cmd return b'FINS' + struct.pack('>I', len(fins)) + fins def read_dm(ip, start, count): with socket.create_connection((ip, 9600), timeout=3) as s: s.sendall(build_read_dm(start, count)) resp = s.recv(2048) body = resp[8:] # 跳过 "FINS" + 4字节长度 end_code = struct.unpack('>H', body[12:14])[0] if end_code != 0: raise RuntimeError(f'结束码: 0x{end_code:04X}') raw = body[14:] return [struct.unpack('>H', raw[i:i+2])[0] for i in range(0, 2*count, 2)]用这个脚本我很快就读到了D区的数据。第一次看到返回的数据和PLC里显示的值完全一致时,那种“通了”的感觉特别好。但如果你直接用上面这段代码连真实PLC,大概率还会遇到一两个坑,后面第5章会陆续讲。
3.4.1 写操作与位操作的几个细节
写操作用命令码0102,帧结构和读操作类似,但要在参数后面追加要写入的数据。写10个字,数据就是20个字节,同样大端序。这里有一个很隐蔽的坑:写操作的数据长度必须正好等于字数×2,不能多发,也不能少发。多一个字节,PLC直接回0401命令格式错误;少一个字节,PLC可能把后面的数据当成下一帧,导致通信错乱。
位操作则是把区码换成位寻址区码,然后在“起始字地址”后面追加一个“位号”字节,取值范围0x00到0x0F,代表16位中的哪一位。这个位号不是0到15的十进制数直接转十六进制这么简单吗?其实就这么简单,但好多人会忘记在地址后面加这个额外字节,导致整个报文长度少一位,永远收到1101或0401。
4. Host Link(C-mode):老设备的保底方案
如果你对接的是老触摸屏,或者PLC只有串口、且串口被设成了Host Link模式,那就跑不了FINS,必须用Host Link协议。它比FINS更老,帧结构也更“啰嗦”,全是ASCII字符,但反过来调试时肉眼就能看清报文内容。
4.1 Host Link帧格式与FCS算法
Host Link的标准帧格式是:
@ 单元号 命令 参数 FCS * CR@表示帧起始,单元号是2个ASCII十六进制字符,默认00。命令是两个大写字母,比如RR、WR、RS、SC。参数是命令附带的内容。帧尾的FCS是两位ASCII十六进制字符,算法和串口FINS完全一致:把@和*之间的所有字符按ASCII码异或,结果转成两位大写十六进制。*和CR表示帧结束。
我之前见过一个相当经典的初学者错误:只发了@00RR...没发后面的*CR。由于串口是流式传输,PLC根本不知道这帧有没有结束,它会在超时后报错或者直接丢弃。还有些人把*当成可选,只在有FCS时才发。其实*CR是帧定界符,不是校验的装饰品,一定要每个帧都带。
4.2 常用命令与新旧机型的差异
Host Link常用命令我在实际项目里用到过这些:
- RR:读字数据(I/O区)
- WR:写字数据
- RL:读位状态
- WL:写位状态
- RS:读取PLC运行状态(运行/监视/编程模式)
- SC:切换PLC运行状态
这里有个非常坑的点:Host Link的命令集在不同系列之间并不完全统一。比如有的老C系列里读DM区是RD、写DM区是WD,新一代CP1手册里却把它并进了RR/WR里,靠参数里的存区标识区分。你如果在网上搜到一段针对C200H的Host Link示例,直接拿去连CP1H,大概率命令不被识别。
我的经验是:用Host Link之前,先去欧姆龙官网下载对应机型的手册,翻到“C-mode命令表”那一章,把你需要用到的命令原样抄下来。别看网上那些截屏,很多是错的,或者被人省掉了关键参数位。抄完命令表之后,最好是先用串口调试助手手工发一两条,确认PLC返回了预期响应,再开始写上位机代码。
4.3 串口参数那些默认值要看PLC不看网文
每次有人跟我说“欧姆龙串口默认不是9600吗,为什么连不上”,我都会反问一句:“数据位、校验位、停止位呢?”串口通信是整个链路里最容易因为小参数不一致而全盘失败的地方。
老一代C系列PLC比较常见的默认组合是9600波特率、7个数据位、偶校验、2个停止位。新一代CP1在CX-Programmer里默认可能又是9600、8位、偶校验、1个停止位。这不是因为欧姆龙改规矩了,而是不同CPU单元出厂设定不同。更麻烦的是,有些PLC的串口参数被改过之后,断电再上电还会恢复成设定值,而你上位机这边仍然用初始默认值去连,两边就一直对不上。
我后来养成一个习惯:买来PLC先不写任何业务代码,第一件事就是确认串口参数并把它们记录下来,写进项目文档。这个习惯帮我省了无数次现场排查的时间。
5. 常见问题与排查技巧实录
写到最后这章,我把实际调试中遇到的高频问题整理成了速查手册。这里面的每一条,都是我用加班换来的。
5.1 三层排查法:物理、链路、应用
报文发出去没回应,不要急着改代码。我用的排查逻辑分三层:
第一层是物理层。串口线是不是交叉线?触摸屏那种交叉串口线有时候跟DB9公母头混在一起,插上就错。以太网线通不通,可以先用ping验证。我之前遇到过PLC侧网口指示灯亮,但交换机端口被划了VLAN导致数据过不去的诡异问题,ping一下立刻暴露。
第二层是链路层。串口参数对不对、单元号对不对、FCS算得对不对。这一层可以用串口助手“自发自收”验证:把串口助手收到的数据原样回发,看PLC是否有反应。以太网则可以用Wireshark抓包,看PLC是否收到了帧、有没有回RST。FINS/TCP发出去之后PLC完全不回,多半是节点号或端口不对。
第三层才是应用层。协议命令字对不对、地址范围对不对、数据格式对不对。这一层要用结束码来判断,PLC很少“理都不理”,只要帧能进来,它至少会回一个错误码。怕就怕前面两层出错,PLC压根没收到帧,你还在应用层瞎调。
5.2 常见FINS结束码速查
前面3.4节已经放了一张结束码表,这里补充几个我亲自记过的情境:
- 0103:我曾在DA2这个字节里填了1,本意是想访问串口选件板,结果PLC回了0103目标单元不存在。后来查手册才发现,CP1H的CPU单元号是0,选件板的单元号要按硬件拨码开关计算,不能拍脑袋填1。
- 1102/1103:DM区读超出范围。有一次我读取D32767之后的地址,PLC毫不客气地回了1103。欧姆龙的DM区不是无限大的,开始做之前查一下机型的最大地址,比在代码里加边界判断更靠谱——因为加边界判断你也要提前知道边界值。
- 2001:PLC把访问权限设成了“禁止上位机写入”的时候,读操作正常,写操作直接吃2001。这种问题查代码查不出来,要去PLC设置里看“保护”相关项,或者把PLC切到编程模式后再写一次试试。
5.3 数据字节序“全反了”的真相
这个坑我第3章提过一次,但因为它实在是太常见,必须单独拎出来讲。
PLC内部一个16位寄存器存的是0x1234,FINS响应里返回的字节顺序是12 34,大端序。上位机如果是C#、Java这种天生大端处理的还算好,要是用C语言指针强转,或者用某些32位MCU直接把字节流按小端解释,出来就是0x3412。
我当时跟视觉检测设备联调就吃过这个亏:D区里的产品数量明明显示50,上位机读出来变成12800,两个数都是“对的”,但一对业务逻辑就全乱。后来我把所有通信解析模块的字节序统一改成“先收高字节、后收低字节”,并且在结构体定义里明确标注,再也没出过这个问题。
建议统一在通信层就把大端转成主机字节序,不要在业务代码里到处手工调换高低位,否则换一个平台就要改一遍。
5.4 调试工具与实测心得
我调试欧姆龙通信常用的三样工具:
- 串口调试助手:我用它验证串口FINS和Host Link的帧格式、FCS计算是否正确。先把要发的帧用十六进制编辑好,手动发送,看PLC的返回。这一步能把通信层的问题和业务代码完全隔离。
- Wireshark:抓FINS/TCP包的首选。过滤器写
tcp.port == 9600就能把欧姆龙通信流量筛出来。我靠它确认过帧封包长度、DA1/SA1是否填对,以及PLC回包是否被防火墙拦掉。 - CX-Programmer的“内存监视”功能:写程序前先在PLC软件里直接改D区、W区的值,确认目标地址真实存在,再用上位机去读。这能帮你区分“地址不存在”和“通信没通”两个问题。
实测心得方面,最推荐的做法是:先把一个地址的“读”跑通,再扩展成批量读,最后才做“写”。写操作危险在于你可能会把运行中的PLC内存改乱,我在测试机上把某个W区的输出位写成了1,结果对应的执行机构当场动作,把旁边同事吓了一跳。所以正式调试时,测试地址一定选不会触发实际输出的区域,比如DM区里专门留出的测试地址,或者干脆在PLC程序里加一个“调试允许”的总开关。
6. 这趟折腾最后沉淀下来的几件事
这篇写到这里,其实已经把我从“对着欧姆龙PLC干瞪眼”到“能稳定读写数据”的完整路径讲完了。最后再说几句掏心窝的话。
第一件事:协议文档永远比经验靠谱。我刚开始总觉得一个通信协议嘛,网上搜个示例就够了。事实证明,欧姆龙不同机型、不同固件版本的报文细节差异很大,唯一下得准的结论是“以手册为准”。每对接一个型号的PLC,我都去官网翻一下对应的通信手册,尤其是结尾附的命令表和错误码表,这两章能解决我九成的问题。
第二件事:调试时给自己留一条“看得见”的路。无论是串口助手、Wireshark还是PLC内存监视,一定要保证你在任何环节都能看到“报文到底长什么样、PLC到底回了什么”。很多人通信调不通就反复改代码,越改越乱,就是因为他们在对着空气编程。把链路每一层的收发数据都打出来,问题通常一眼就能定位。
第三件事:通信层和业务层要分开。我后来把所有欧姆龙通信封装成了一个独立模块,对外只提供“读DM区”“写W区”这类接口,内部处理协议帧、节点号、字节序、超时重试。好处是换PLC型号、换协议方式时,业务代码一行都不用改,只改模块内部的实现。这套做法在后续好几个项目里都让我省了大把时间。
欧姆龙PLC的通信协议说白了并不复杂,复杂的是你永远不知道手册里哪一行小字才是关键。希望这篇踩坑实录能帮你少走一段弯路,真到了现场出问题,大概也能像我一样,淡定地把串口助手打开,一帧一帧对下去。通信这种东西,只要链路是通的,数据早晚能上来,心态不崩,事情就赢了一半。