前阵子接手了一条包装线的上位机改造客户要求上位机直接读欧姆龙CP1H的D区数据。我一开始觉得PLC通信嘛无非是组帧、发送、解析难不到哪去。结果从串口参数到FCS校验再到内存区地址映射我硬是折腾了两个晚上才全部打通。现在回头看欧姆龙的通信协议并不算复杂但坑确实不少很多细节手册里写得很散不自己踩一遍根本记不住。这篇就把我在HostLink和FINS协议上踩过的坑、以及最后彻底搞明白的关键点整理出来给正要和欧姆龙PLC打交道的人一个参考。1. 先别急着撸代码把欧姆龙的通信协议家族认清楚1.1 HostLink串口上位机最常用的老将欧姆龙PLC最经典、也最容易接触到的协议就是HostLink也叫上位机链接协议。它跑在RS-232C或RS-422/485串口上格式是ASCII文本帧帧头是帧尾是星号加回车。CX-Programmer通过串口给PLC下载程序时底层用的就是HostLink协议所以只要是个欧姆龙PLC基本没有不支持HostLink的。HostLink最大的优点是简单没有复杂的包头和数据封装一条命令就是一串字符串口调试助手里都能直接手动发。但简单不等于好使它有两个特别容易栽跟头的点一个是FCS帧校验的计算范围另一个是地址字段到底是十六进制字地址还是BCD地址。这两个点我在后面会单独展开讲。另外要注意HostLink协议里PLC是纯从站上位机必须主动发命令PLC才会响应而且同一时刻只能处理一条命令必须等收到上一条命令的响应后才能发下一条。如果上位机连续发了几条命令而没有等待响应PLC会直接把后面的命令丢掉这一点在写上位机时一定要做“同步请求-响应”的逻辑不能像UDP那样只管发。1.2 FINS贯穿串口和以太网的“正统”协议如果说HostLink是功能机时代的产物那FINS就是体格完整、面向未来的协议。FINS的全称是Factory Interface Network Service欧姆龙自家的工厂接口网络服务。它不绑定传输介质既可以跑在串口上又可以跑在以太网上甚至可以通过Controller Link、DeviceNet这些现场总线转发帧格式完全统一。FINS和HostLink最大的区别在于它多了一个10字节的FINS帧头。这个帧头里包含了源网络号、源节点号、目标网络号、目标节点号这些路由信息所以FINS天然支持多级网络转发。打个比方HostLink像是你站在车间里直接朝对面工位喊一嗓子FINS则是写好了收件人和寄件人的快递单中途有中转站也知道往哪送。正因为有这层路由信息初学FINS的人常常会被一堆编号搞晕网络号是多少、节点号是多少、单元号是多少完全不知道填什么。我后面会专门解释这三个号码的关系。FINS还有一个别的好处——它区分内存区代码读DM区和读CIO区的命令格式里有明确标识不会因为地址撞车而读错区域。1.3 Modbus-RTU跨厂商兼容的“逃生通道”很多新一代欧姆龙PLC比如CP1H-E、NJ/NX系列的部分机型本身就内置了Modbus-RTU从站功能可以通过串口被支持Modbus的主站设备直接读取。如果你的上位机、HMI或者触摸屏本身不支持HostLink和FINS但支持Modbus那用Modbus-RTU去读取欧姆龙PLC是最省事的路。但这里要泼一盆冷水欧姆龙的Modbus地址映射不是你想的那么直观。比如Modbus的保持寄存器地址和PLC里D区地址之间有一个固定的偏移或映射关系每个系列还不完全一样。千万别拿西门子或者三菱的Modbus映射经验直接套到欧姆龙上必须先查你那个具体型号的通信手册里的Modbus地址映射表。我见过有人用Modbus地址40001去读D0结果读回来的是CIO一个莫名其妙的值原因就是地址映射没查手册。所以做方案选型时我的建议是如果只是自家上位机读欧姆龙PLC优先用FINS/TCP或者HostLink这两种协议是欧姆龙的“母语”地址映射最自然如果有第三方设备来读才考虑Modbus-RTU并且一定要把型号手册里的映射表打印出来放桌上。2. HostLink协议拆解帧格式与FCS校验的硬骨头2.1 一条完整命令帧长什么样以CP1H通过RS-232C读取D100这一个字的数据为例。要把这条命令发给PLC完整的帧是这样的00RR0064000103*拆开看是这样几个部分字段内容说明帧起始符 (0x40)固定HostLink每帧必须以开头单元号00两位ASCII范围00-31对应PLC的单元号一般CPU单元是00命令码RR大写RR代表Read读一个或多个字起始地址0064四位十六进制D100对应的字地址是0x0064读取个数0001四位十六进制0001表示读1个字FCS03帧校验两位十六进制ASCII字符终止符* CR星号加回车0x2A 0x0D注意这里的地址字段D100写的是0064不是0100。这是当时折磨我很久的一个坑。HostLink地址字段用的是十六进制字地址D100就是十进制100换算成十六进制就是0064。而很多资料上写“D0100”那是PLC软件里显示的内存区编号不能直接当成协议地址用。你要是按0100去发读到的其实是D256的数据而且可能还在CIO区撞了地址而不自知。还有命令码大小写不能搞错RR是读WR是写FR是错误响应帧里的错误标识这都是固定大写。发帧的时候和终止符都不能省略但是FCS的计算范围恰好又要把和终止符排除在外很多人恰恰在这里算错。2.2 FCS校验的两种计算方式FCS全称Frame Check Sum是HostLink协议里最让人头疼的一步。它的计算规则其实很简单把之后、FCS之前的所有字符逐字节异或得到一个字节的结果然后把结果拆成两位十六进制大写字符作为FCS字段放到帧尾。拿上面那条帧举例要参与计算的是00RR00640001也就是ASCII字节序列0x30 0x30 0x52 0x52 0x30 0x30 0x36 0x34 0x30 0x30 0x30 0x31。把它们逐个异或过程不复杂但手算容易眼花我强烈建议用脚本算。举个Python示例def hostlink_fcs(payload: bytes) - str: fcs 0 for b in payload: fcs ^ b return f{fcs:02X} def build_read_dm(unit: int, addr: int, count: int) - bytes: body f{unit:02d}RR{addr:04X}{count:04X}.encode(ascii) fcs hostlink_fcs(body) frame b body fcs.encode(ascii) b*\r return frame这段代码里build_read_dm(0, 100, 1)生成的就是00RR0064000103*回车。FCS的常见错误有两种一种是把也拿来异或了这样算出来的校验码永远不对另一种是异或结果小于0x10时比如结果是0x03有人直接发ASCII字符0x03不可见字符而正确做法是发两个ASCII字符‘0’和‘3’也就是0x30和0x33。响应帧的FCS计算范围完全一样解析时先提取帧尾倒数第三个字符之前的帧体部分再算一遍FCS做比对。还有个小细节有些PLC对终止符只认CR不认LF有的则要求CRLF才算完整。多数串口调试软件里设置成“仅发CR”就行如果加了LF导致PLC无响应优先怀疑这里。2.3 响应帧格式与错误码识别PLC对一条HostLink命令的响应分两种正常时返回和命令码相同的帧比如读命令返回的就是00RR后跟数据出错时返回的是00FR后跟两位十六进制错误码。很多人写上位机解析时只管“收到RR就解析数据”一旦PLC返回错误码帧头是FR程序就懵了。错误码是个好东西。比如CPU单元号写错时PLC会返回错误码13单元号错误地址超范围时返回错误码0C或19一类。具体错误码含义每个型号手册都有表我这里只提醒一件事上位机程序里要把FR响应当作正常业务分支来处理不能当垃圾包丢掉。你只有把错误码解析出来才能知道自己的帧到底哪里写错了这是最快定位问题的手段。3. FINS协议从串口到以太网一定要啃下这块硬骨头3.1 FINS帧头十个字节一个都不能少FINS协议和HostLink最大的不同就是自带10字节的“邮递面单”——FINS帧头。这10个字节依次是ICF、RSV、GCT、DNA、DA1、DA2、SNA、SA1、SA2、SIDICF命令控制位一般是0x80表示这个帧需要PLC返回响应如果你把这一位设成0那就是“发完不管”PLC不会回复很多初学者在这儿踩坑收不到数据第一反应是网络不通其实是你自己没要回应。RSV保留字节固定0x00。GCT网关计数通常是0x02如果一个FINS帧要通过多级网络转发那里程表会累计但大多数直连场景下固定0x02没问题。DNA和SNA目标网络号和源网络号。直连时都是0x00。DA1和SA1目标节点号和源节点号。这是最容易被搞混的一对后面单独说。DA2和SA2目标单元号和源单元号。单元号指的是PLC CPU单元在机架上的编号单CPU系统就是0x00。SID服务标识相当于一次会话的编号。请求和响应里带同一个SID方便上位机把响应对应到具体的请求上。每发一条命令换一个SID是很好的习惯可以避免并发时串包。你只要把这一屏绕清楚FINS的帧结构就掌握了一半。3.2 内存区代码与起始地址最容易错的两个点FINS命令中读内存区的命令码是0x0101写内存区的命令码是0x0102。命令码后面紧跟着内存区代码、起始地址和长度。内存区代码是按区域区分的区域内存区代码字访问说明CIO0x30输入输出继电器区WR0x31工作区HR0x32保持区AR0x33辅助区DM0x02数据内存区最常用起始地址是2字节大端序的十六进制字地址这一点和HostLink的道理是一样的。读D100这个字起始地址就是0x0064如果要连续读10个字长度字段写0x000A。踩坑点在这里很多人会把内存区代码和地址搞混。比如读CIO区想读CIO0内存区代码是0x30地址是0x0000但有的人把地址写成了0x3000结果PLC直接报地址超范围错误。正确的理解是内存区代码负责“挑选区域”起始地址负责“在这个区域里找位置”两者是独立的两个字段。还有一个坑是区域代码不是万能的。读CIO区的位和读CIO区的字在FINS里用的内存区代码不一样字访问走0x30位访问要另查内存区代码表我用0x30去读位数据就会读回一个按字组织的值。所以明确你要读的是位还是字别混着来。3.3 FINS/TCP连接、节点登记、请求三步走FINS跑在以太网上时使用的TCP端口是9600这一点基本是欧姆龙默认的。但FINS/TCP和HostLink的“一帧一应答”不同它要先建立TCP连接然后做一次节点数据地址登记之后才能发正式的FINS命令。节奏大概是这样上位机主动连接PLC的9600端口。连接建立后上位机发送一条节点登记命令把本机的FINS节点号告诉PLC。这条命令的命令码是0x0001。PLC返回节点登记响应后再发送像0101读内存这样的正式FINS命令。一个TCP连接可以连续收发多条FINS命令不用反复登记但如果断开重连必须重新走一遍节点登记。这里牵扯出最容易搞混的节点号问题。PLC侧在CX-Programmer里配置IP地址时会同时有一个FINS节点号通常默认为IP地址的末位比如IP是192.168.0.10FINS节点号默认就是10十六进制0x0A。上位机侧的节点号要另外设一个和PLC不冲突的值比如0x4064或0x6096不要随便填0。我一开始没登记节点就直接发读命令PLC没任何反应抓包抓了半天才发现少了一步。还有一次是上位机节点号设成了和PLC一样的10结果PLC直接拒绝服务。记住目标节点号填PLC的FINS节点号源节点号填上位机自己的FINS节点号这两者不能相同。4. 实操代码用Python把HostLink和FINS/TCP都打通4.1 HostLink读取D100的完整示例不管用什么语言HostLink通信的套路都是先算FCS再组帧然后串口发送并等待响应。下面是完整的Python示例用pyserial实现import serial import time def hostlink_fcs(payload: bytes) - str: fcs 0 for b in payload: fcs ^ b return f{fcs:02X} def build_read_dm(unit: int, addr: int, count: int) - bytes: body f{unit:02d}RR{addr:04X}{count:04X}.encode(ascii) fcs hostlink_fcs(body) return b body fcs.encode(ascii) b*\r def read_dm(ser: serial.Serial, addr: int, count: int, unit: int 0): cmd build_read_dm(unit, addr, count) ser.write(cmd) resp ser.read_until(b\r, timeout2) if len(resp) 11: raise TimeoutError(PLC无响应检查串口参数、单元号和接线) # 响应格式 unit cmd data fcs * CR head resp[:4] # 00RR data_part resp[4:-3] # 去掉FCS和*CR if data_part.startswith(bFR): err_code data_part[2:4] raise RuntimeError(fPLC返回错误响应: {err_code}) # 每个字占4个ASCII字符Read命令返回的文字数*2字节 if len(resp[4:-3]) count * 4: return resp # 实际解析时再转十进制 return None if __name__ __main__: ser serial.Serial(portCOM3, baudrate9600, bytesize7, parityE, stopbits2, timeout2) cmd build_read_dm(0, 100, 1) ser.write(cmd) resp ser.read_until(b\r) print(发送:, cmd) print(收到:, resp)这里串口参数我用的是7E2也就是7数据位、偶校验、2停止位。重点提醒欧姆龙不同型号、不同介质的串口默认参数可能不一样有的默认8N2。别默认猜先通过CX-Programmer的PLC设置确认当前串口格式再改上位机串口参数。我在这上面浪费了大半个晚上就是因为我按8N1发完全没反应而PLC实际是7E2。解析响应时要特别注意每个字在ASCII帧里占4个字符2字节十六进制比如D100里存的是0x1234响应数据段就是“1234”。如果PLC数据是BCD格式你还要把0x1234按BCD解释成1234这个完全取决于PLC程序里怎么存储的协议层不做转换。4.2 FINS/TCP读取DM区的完整示例FINS/TCP的Python实现比HostLink多一个节点登记的步骤。下面是一个最小可用的例子import socket import struct def fins_header(da1: int, sa1: int, sid: int) - bytes: # ICF0x80, RSV0x00, GCT0x02 # DNA0x00, DA1, DA20x00, SNA0x00, SA1, SA20x00 return bytes([0x80, 0x00, 0x02, 0x00, da1, 0x00, 0x00, sa1, 0x00, sid]) def node_reg_frame(sa1: int) - bytes: # 节点登记命令码0x0001 FINS节点号1字节 0x0000 return b\x00\x01 bytes([sa1]) b\x00\x00 def memory_read_frame(area: int, addr: int, count: int, da1: int, sa1: int, sid: int) - bytes: head fins_header(da1, sa1, sid) body struct.pack(HBBHH, 0x0101, area, addr, count) return head body def read_dm_tcp(plc_ip: str, plc_node: int, pc_node: int, dm_addr: int, count: int): sock socket.create_connection((plc_ip, 9600), timeout3) # 第一步节点登记 sock.sendall(node_reg_frame(pc_node)) node_resp sock.recv(1024) # 第二步发送读命令 sid 0x01 cmd memory_read_frame(0x02, dm_addr, count, plc_node, pc_node, sid) sock.sendall(cmd) resp sock.recv(1024) sock.close() if len(resp) 14: raise RuntimeError(响应帧过短) # 前10字节FINS头然后命令码2字节完成码2字节后面是数据 if resp[12:14] b\x00\x00: return resp[14:] else: err resp[12:14].hex() raise RuntimeError(fFINS错误码: {err}) if __name__ __main__: # PLC IP 192.168.0.10节点号10上位机节点号64 data read_dm_tcp(192.168.0.10, 0x0A, 0x40, 0x0064, 1) print(D100 , data.hex())这段代码的关键点是FINS头里ICF写0x80表示需要响应GCT写0x02。节点登记帧的细节在不同手册里画法有细微差别如果连接后PLC不回任何包优先去对照官方手册的节点登记章节。但整个“三步走”的框架是固定的绝不会错。收到响应后前10字节是FINS头紧接着的2字节是命令码0x0101然后是2字节完成码。完成码是0x0000代表正常其他值就是FINS错误码。数据从第14字节开始每个字2字节大端序。解析成int的时候要自己乘256加低位别直接按本地字节序去读。4.3 CX-Programmer的通信配置核对清单自己写代码之前我强烈建议先用CX-Programmer把PLC在线连上确认三件事CPU单元号、串口通信参数、以太网的IP和FINS节点号。很多人跳过这一步直接写上位机程序结果排查半天发现PLC的串口模式根本不是HostLink而是被人在后台改成Modbus了。用CX-Programmer在线连接时如果走USB线我遇到过驱动不识别的问题。欧姆龙的USB驱动要单独装Windows有时只会识别成“未知USB设备”这个时候去设备管理器里手动更新驱动指定到CX-Programmer安装目录的驱动文件夹里。装好之后设备管理器里才能看到OMRON的USB接口。走以太网时在工程里新建PLC要选对型号然后在“PLC”菜单下选择“在线工作”通信设置里填PLC的IP地址。如果连接失败CX-Programmer会提示“不能与PLC通信”这时候先ping一下PLC的IP通了再看节点号和网络号是否一致。在CX-Programmer的设置里PLC的FINS节点号和IP地址低8位是否一致并不强制但默认是一致的除非有人改过。还有一个容易被忽略的点PLC如果处于运行模式下有的地址区是可以读不可以写的。你在上位机里发写命令如果PLC返回错误码先看看PLC模式是不是允许写这个区。CX-Programmer里能看到当前PLC模式Run/Monitor/PROGRAM三个状态有些写操作在PROGRAM模式反而更宽松。5. 排查实录串口没反应、报错码、数据错乱怎么办5.1 串口没反应的五个排查方向串口发帧后PLC完全没反应这是最崩溃的。按下面这个顺序排查基本能定位90%的问题。一是看串口参数。欧姆龙HostLink默认参数经常被误传不同PLC默认值不同一定要以CX-Programmer里确认到的参数为准尤其是数据位和校验位。用串口调试助手直接发先用回环测试确认串口线没断再确认PLC侧COM口的DIP开关或者软件设置真的在HostLink模式。二是看单元号。帧里的单元号要和PLC的CPU单元号对上单CPU一般是00但是如果机架上有多单元就要写对应的单元号。PLC收到单元号不对的帧不会响应也不会报错基本就是石沉大海。三是看FCS。把帧复制出来自己写个Python脚本重算一遍FCS看看和你发的是不是一致。我见过很多“没反应”其实纯粹是FCS多算了这一个字节一改就好。四是看终止符。* CR这两种字符一个都不能少有的上位机在printf时把回车写成了\nLFPLC就是不认。五是看接线。RS-232C的2、3脚要交叉RS-422/485的A、B端子不能接反。这个听着低级但现场调试时真遇到不少。5.2 响应帧里的错误码怎么解读当你收到PLC的响应但内容是FR开头时千万别慌那说明PLC已经收到你的帧了是命令本身有问题。FR后面的两位就是错误码比如错误码含义常见原因14格式错误帧长度不对、命令码大小写错13单元号错误帧里的单元号和PLC不匹配0C地址超范围地址字段超出该区域实际范围0B写保护对只读区或写保护区执行写操作19数据错误数据长度或参数不合法FINS协议的错误码是2字节十六进制比如0x1101表示命令长度太长0x1103表示命令长度和参数不匹配0x110B表示地址超范围。收到错误码后先看是长度问题还是地址问题不要一头扎进网线排查。我当时排查一个FINS错误码0x110B整整折腾了两小时最后发现D区地址写成了0x1000但那个型号的PLC D区最大只有0x0FFF。地址超范围这个错误码救了我因为一开始根本没看懂错误码含义以为是不会编。5.3 数据总是错乱的隐蔽原因串口通信通了数据也收到了但数值总是对不上。这种情况十有八九是字节序或者进制问题。欧姆龙PLC内存区里存的数据从协议层读出来就是一串字节PLC程序如果写的十进制串口读回来是十六进制的存储格式比如PLC里D100放的是十进制1234用HostLink读到的是“04D2”而不是ASCII的“1234”。你要么在PLC程序里用指令转成BCD要么在上位机里把0x04D2按十进制解释成1234。另一个隐蔽坑是PLC程序里用到的数据格式是单精度浮点数还是整数。一个32位浮点数占两个字如果你只读了一个字解析出来永远是半个数据。遇到D区数据怎么都对不上时先在CX-Programmer的在线内存视图里看这个地址的原始值确认它到底是什么类型、多少位再回头改上位机的读取长度和解析方式。字节序问题也很经典。FINS读回来的一个16位字是高位在前也就是大端序比如0x04D2在字节流里是0x04, 0xD2。如果你按PC本机的小端序去解析就变成0xD204数值差了十万八千里。我在C#里用BitConverter.ToInt16踩过这个坑后来改成手动拼字节才解决。6. 踩坑心得与工程落地建议6.1 通信健壮性比“能通”更重要我见过很多人用串口助手手动发了几个帧能读到数据就算完事了。但工程现场的PLC通信上位机程序要长期稳定运行需要考虑通信超时、响应校验、错误重试和异常恢复。上位机发一条指令后PLC没有响应是常态尤其是有干扰或者PLC负担大的时候。超时时间不要设得太短串口300ms到500ms比较合适以太网1秒左右。超时后要先重发两三次连续失败再报故障不要一失败就报警否则半夜误报能把人折腾死。响应帧一定要做FCS校验不能只检查帧头和帧尾。我有一段时间收到的数据偶尔错乱后来发现是RS-485布线没做终端电阻波形反射导致数据位错误。如果没有FCS校验错数据就会直接进数据库很难发现。6.2 建议把协议文档里的表打印出来现在大家电脑里都存着PDF手册但真到调试时电子文档翻起来效率很低。我的习惯是把每个型号通信手册里的三张表打印出来贴在现场内存区域地址映射表、FINS命令码和区域代码表、错误码表。排查问题时直接抬头看纸比逐个标签页切换快太多。特别是地址映射表每个PLC型号的CIO、WR、HR、DM以及特殊继电器区地址范围都不一样光靠记忆一定会出问题。打印出来还有一个好处写上位机程序时对照着表逐条核对不容易把D地址和CIO地址写串。6.3 如果让我重新来一次我会先做这三件事第一件事先用CX-Programmer在线连上PLC把串口格式、节点号、单元号全部确认并截图存档。这一步把所有“猜”的环节都消掉了。第二件事用脚本或工具先手动发一条最简单的读命令比如读D0逐步看响应确认FCS和地址映射没问题后再扩展到多字读取、写操作、批量循环。第三件事把上位机的通信模块单独封装成一个类或者独立服务输入输出用统一的约定比如地址统一用“D100”这样的字符串底层负责转成不同协议的地址。这样即使后面把HostLink换成FINS/TCP上位机业务层一句都不用改。我现在的项目里通信层就是按这个思路写的后面帮客户兼容了另一台不同协议的PLC只改了底层适配业务逻辑完全没动。欧姆龙PLC通信这件事说难不难说简单也不简单。绝大多数坑都集中在帧格式细节和参数确认上把这两块啃下来剩下的无非是多写几行代码的问题。希望这篇踩坑实录能让你少走点弯路。
阅读完成 · 觉得有帮助?