news 2026/11/6 3:02:04

欧姆龙PLC通信协议深度解析:HostLink与FINS实战排坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
欧姆龙PLC通信协议深度解析:HostLink与FINS实战排坑指南

如果你在网上搜过“欧姆龙PLC通信协议”,大概和我当初一样:先被FINS、HostLink这些名词绕晕,又被一串十六进制报文折磨到怀疑人生。那是一个客户现场的采集项目,PLC用CP1H,我需要在工控机上通过它读取几十路温度、压力和流量数据,顺带下发几条工艺参数。起初我以为这跟给PLC加个Modbus从站差不多,结果从串口参数对不上、帧校验算错,到FINS地址越界、以太网节点号不通,前后折腾了两三个礼拜。这篇文章把我踩过的坑和最终搞明白的协议逻辑都整理出来,给打算用欧姆龙PLC做上位机通信的朋友一个绕坑指南。

1. 事情起因:一个“连个数据都读不出来”的调试现场

项目本身不复杂:一条旧产线改造,PLC早就存在,型号CP1H,现场还有几台欧姆龙温控器和变频器,生产数据全部存在PLC的D区里。我的任务是用一台Windows工控机,把这些数据实时读上来,做一些简单的统计和报警。刚开始接设备时,我手里只有一根USB转485线,PLC的串口通过一个DB9头引到配电柜面板上,看起来就是标准的串口通信。

我的第一反应是:串口通信嘛,跟Modbus RTU差不多,发命令、收数据、解析就完了。于是我在工控机上用了串口调试助手,按Modbus RTU的格式去尝试,结果不是没响应就是满屏乱码。反复试了几次,我开始意识到,欧姆龙压根不是直接用Modbus,它有自己的一套通信体系。

这个认知上的转变特别重要。很多第一次接触欧姆龙的人都会摔在这里——你以为PLC通信就是“串口+寄存器读写”,但欧姆龙把协议分成了两层:一层是链路层的传输方式,分串口、RS-485、以太网;另一层是应用层的命令格式,分HostLink、FINS、EtherNet/IP。位置不一样,选项不一样,帧格式完全不一样。

那段时间我天天在查资料,网上关于欧姆龙通信的碎片信息很多,但很少有文章把“该用哪个协议、帧长什么样、地址怎么换算”讲清楚。更麻烦的是,很多示例代码用的是老型号C200H、CQM1,跟CP1H的设置细节有差异。我后来能走出来,完全是靠把官方手册里关于通信命令的部分反复啃了几遍,加上一个一个坑慢慢填。

如果你现在也卡在这个阶段,我的建议是先别急着写代码,把下面这个协议选择的问题想清楚,能省你三倍时间。

2. 先理清协议版本:HostLink、FINS和一串缩写到底指什么

2.1 HostLink:串口时代的主从协议

HostLink是欧姆龙在上位机通过串口访问PLC时最常用的协议,本质上是一个主从式协议:上位机做主站,主动发一条命令帧;PLC做从站,处理完后返回一条响应帧。整个过程很像Modbus RTU,但又完全不同。

HostLink的帧结构是ASCII码形式的,命令以“@”字符开头,后面跟着单元号、命令码、参数、校验码和结束符。比如上位机读PLC的DM区1个字,发送的内容从“@”开始,最后以回车符CR结束。这个协议简单、直观,适合点对点串口通信,数据量不大时非常稳定。

使用HostLink时,上位机设置的串口参数通常要和PLC侧保持一致,否则PLC连响应都不会给。这里有个欧姆龙PLC默认参数的坑:很多老款串口默认是9600波特率、7位数据位、偶校验、2位停止位,不是我们常见的8位无校验1位停止。我第一次用8N1去连,收到的全是乱码,搞了一天才发现是参数不匹配。

2.2 FINS:跨网络的应用层协议

FINS(Factory Interface Network Service)是欧姆龙推出的通用通信协议,它比HostLink更进一步:HostLink只解决串口链路上的读写,FINS则把命令层设计成和物理链路无关。同样一条FINS读写命令,既可以通过串口发,也可以通过以太网UDP、TCP发,还可以通过Controller Link网络转发到其他PLC。

FINS帧以二进制为主,不像HostLink那样以ASCII明文呈现。它的头部有10个字节,包含目标网络号、目标节点号、目标单元号、源节点号、源单元号等信息,后面才是具体的命令代码和参数。比如读数据区的命令码是0101,写数据区的命令码是0102。

因为FINS自带寻址信息,它天然支持跨网络访问。比如你从以太网发一条FINS命令给PLC,PLC还能通过Controller Link把这条命令转发给另一台PLC,这就是网关转发。对于多台PLC联网的项目,FINS几乎是必选。

2.3 选型建议:什么情况用什么协议

我在项目里总结出的选择标准是这样的:

使用场景推荐协议原因
单台PLC、串口直连、数据量不大HostLink帧结构简单,串口调试方便,适合快速实现
多台PLC、需要跨网络转发FINS(经以太网)自带网络寻址,支持网关路由
上位机走以太网、数据量中等FINS over UDP/TCP速度快,能同时访问多个节点
需要标准工业以太网设备互操作EtherNet/IP面向设备互联,主要和PLC与PLC、PLC与IO模块之间用

我当时选型不够理性,一开始想图省事用串口HostLink,后面因为现场有另一台PLC也要读,又要改走以太网FINS。结果就是同一个项目里把两种协议都踩了一遍,才有了后面这一堆坑。

2.4 必须先建立的观念:协议地址和PLC地址不是一回事

不管用HostLink还是FINS,读写命令里都含有一个“地址”概念。这里特别容易出问题:你在梯形图里用的D100、CIO 0.00、W0.01,和协议帧里的地址并不是直接相等的。协议帧里的地址通常是一个编码后的结构,比如FINS的起始地址由“内存区代码”和“偏移地址”两部分组成,不同区的区码不一样,偏移地址的单位也可能是字或位。

这个观念不建立起来,后面基本就是瞎试。我第一次直接按D100=0x0064去构造FINS地址,结果返回错误码,折腾了半天才发现少了一个关键部分。

3. 第一个大坑:串口HostLink的参数、帧与FCS校验

3.1 串口参数:默认值不是你以为的9600/8/N/1

先说最表层的坑。用HostLink时,上位机软件侧的串口参数必须跟PLC的通信设置一致。CP1H的串口设置不是都能在软件里随便改的,有一部分参数在PLC的系统设置里指定,改完之后还要把设置“传送到PLC”,并且断电重新上电,设置才会真正生效。

这个“断电上电”是最容易忽略的。我在CX-Programmer里改了PLC的串口通信设置为波特率9600、数据位7、偶校验、停止位2,然后直接点在线连接,发现还是不通信。后来看手册才知道,串口通信设置属于“启动时生效”的参数,必须断电上电一次。把PLC断电重启之后,通信马上正常了。

参数常见默认值我第一次试的错值
波特率96009600
数据位78
校验位偶校验无
停止位21

上位机侧还要注意一点:不要开启任何“自动添加回车换行”的功能。很多串口调试工具默认在帧尾自动添加CRLF,这会让HostLink帧尾部多一个0x0A,PLC端有时会忽略,有时直接把整帧当错帧。建议设置成“按原始字节发送”,结束符只加一个CR(0x0D)。

3.2 帧格式的基本骨架:@带头、中间是ASCII、结尾是CR

HostLink读DM区的命令帧格式大致是这样的:

@ 单元号 命令码 起始地址 数据字数 FCS CR

举一个实际读DM0000一个字、单元号为0的请求:

@00RD0000000100CR

拆开看:

  • @:起始字符,固定0x40。
  • 00:单元号,两位十六进制ASCII字符。
  • RD:命令码,读DM区;写DM区对应的命令码是WD。
  • 0000:起始地址,即DM0000。
  • 0001:数据字数,这里读了1个字。
  • 00:FCS校验,这里是示意值。
  • CR:结束符,ASCII的0x0D。

响应帧的结构也类似,PLC返回的内容里会先带回一个两位十六进制的结束码,00表示正常,非00就是各种错误,比如格式错误、校验错误、单元号错误等。这里最容易踩的坑还不是格式,而是那个FCS。

3.3 FCS校验:它不是CRC,是对ASCII码挨个异或

HostLink的FCS(Frame Check Sequence)计算方式非常朴实:从“@”这个字符开始,一直到FCS之前的最后一个字符,把所有字符的ASCII码逐个做异或,取结果的低8位,再转成两位大写十六进制文本填入帧尾。

我用Python写过一个最小实现:

def hostlink_fcs(frame_part: str) -> str: value = 0 for ch in frame_part: value ^= ord(ch) return f"{value:02X}" # 例如:计算 "@00RD00000001" 的FCS fcs = hostlink_fcs("@00RD00000001") print(fcs) # 结果为 67

所以一条完整的读DM0000请求帧应该是:

@00RD0000000167CR

我当时犯的错误是把它当成Modbus的CRC16去算,算出来的值当然完全不对,PLC直接不回包。后来从手册里翻到FCS的计算规则,用上面这个函数一算就通了。

这里还要注意一个小细节:FCS计算的是ASCII字符的异或,不是对帧里的二进制数据算。HostLink帧本身就是以ASCII字符串形式传输的,所以算校验时也是逐字符处理。如果你用C#或LabVIEW的字符串发送函数,一不小心会把“0D67”这样的数据真值直接算进去,导致校验对不上。

3.4 响应帧里的结束码和超时问题

通信通了之后,还会遇到响应正常但数据不对的情况。比如读回的数据总是某个固定值,多半是解析响应的位置写错了。响应帧的格式是:

@ 单元号 命令码 结束码 数据 FCS CR

结束码占两个字符,00表示正常,数据部分才是你真正要的值。我第一次解析时直接从响应帧的第4个字符开始取数据,正好把结束码当成数据,读出来的值自然错得离谱。

另外超时时间也要设置合理。HostLink是PLC收到完整帧后再响应,响应时间受PLC扫描周期影响,扫描周期越长,响应可能越慢。上位机超时设得太短,比如50ms,很容易出现“明明能通却老是报超时”的假象。我后来统一设为500ms,实测稳了很多。

4. 第二个大坑:FINS地址映射,D100并不等于0x0064

4.1 FINS请求帧长什么样

当通信从串口转向以太网时,协议基本就切换到了FINS。FINS帧和HostLink差别很大,它没有“@”这种可读起始符,而是统一的二进制头部和命令区。

一个从以太网UDP发出的FINS读请求大致是这个样子:

ICF RSV GCT DNA DA1 DA2 SNA SA1 SA2 SID 80 00 02 00 10 00 00 00 1A 01

各字节含义:

  • ICF=80:表示这是一条需要响应的请求帧。
  • RSV=00:保留字节,固定为0。
  • GCT=02:网关允许,表示允许通过网关转发。
  • DNA=00:目标网络号,00表示本地网络。
  • DA1=10:目标节点号,即PLC的节点号。
  • DA2=00:目标单元号,00表示CPU单元。
  • SNA=00:源网络号。
  • SA1=1A:源节点号,即上位机的节点号。
  • SA2=00:源单元号。
  • SID=01:服务ID,用于把请求和响应配对。

头部后面跟命令码和具体参数。比如读数据区的命令码是0101,读1个字参数里要写起始地址和字数。

4.2 内存区代码和偏移:不是把D100直接填进去

这就是我前面提到的地址问题。FINS读写命令里的“起始地址”并不是“D100是100就填0064”,而是要把内存区代码和偏移地址组合在一起。

内存区代码在欧姆龙手册里专门有一张“Memory Area Codes”的表格。D区、CIO区、W区、H区,甚至同一个区的“按字读”和“按位读”都对应不同的代码。我第一次做FINS读D100时,直接按直觉把地址参数写成0064,结果PLC返回错误码0105一类的地址错误。后来对着手册一查才发现,D100的正确构造应该是“DM区的区码”加上偏移地址100,而地址的单位还得分字和位。

这里有一个很多人反复犯的错:FINS地址里的“偏移地址”通常是以字为单位的,如果你要在D区里找D100,偏移值就是100没错,但前面那个区码必须填对。区码填错了,哪怕偏移正确,PLC也会告诉你地址越界或者区域不存在。

正确的做法是先在官方手册里找到那张表,把自己要访问的区域和访问类型(按字读还是按位读)对应好,再填到命令里。我后来把DM区、CIO区、W区、H区的字访问代码抄在一张便签上贴在显示器旁边,再没出过错。

4.3 字节序:高位字节在前,组合32位数据时更要小心

FINS响应返回的数据,每个字是两个字节。欧姆龙以太网传输默认是大端序,也就是一个字里的高字节先到,低字节后到。例如一个字的值是0x0D67,响应里看到的就是0x0D然后0x67。

用上位机解析时,如果直接按小端序组合,读出来的数和PLC梯形图里监控到的数就会差一大截。我第一次读回来一个温度值,显示起来是几千,实际只有几十度,就是这个原因。

更恶心的坑在32位浮点数据。很多PLC项目里温度、流量是用浮点数存的,占用两个连续的字。把这两个字合起来组成浮点数时,不仅要处理字节序,还要处理字的顺序。欧姆龙的浮点存储顺序和西门子的习惯不一样,跟三菱也不同。

我的经验是:不要猜,直接用梯形图在PLC里写一个已知的浮点常量,比如1234.5,然后看通信读回来的字节序列,对着IEEE 754格式慢慢推。这样一次就能搞清楚这个型号的存储顺序。

4.4 命令参数的“数量”是字数不是字节数

还有一个细节上的坑:FINS读写命令里有一个参数表示数据数量,它默认是“字数”,不是字节数。比如你想读4个字,这个参数就填0004,不是0008。有些人习惯照搬Modbus那边的思路,填成8,结果PLC要么返回错误,要么把后面无关的数据也带回来。

另外,命令参数里要写的是“起始地址+数量”,不是“起始地址+结束地址”。这两个概念完全不同,我第一次写代码时没注意,想当然地填了结束地址,导致读回来的数据条数永远不对。

5. 第三个大坑:以太网连不上,问题出在节点号、端口和TCP连接数

5.1 节点号默认是1,IP末位却是10,连不上

用FINS over UDP时,最有迷惑性的一个坑就是节点号。FINS帧里有两个关键字节叫DA1和SA1,它们分别是目标节点号和源节点号。对欧姆龙以太网单元来说,节点号需要在网络的配置工具里设置,默认值往往是1。

而你在PLC里设的IP地址末位可能是10。从协议角度看,IP地址和FINS节点号是两套寻址体系,但欧姆龙的以太网单元在做FINS响应时,通常要求本机节点号和IP末位对应。如果你的目标是192.168.1.10,但节点号还是1,那上位机发出去的目的节点号就得是1,而不是10,否则PLC不会把它当成是发给自己的。

我当时的排查过程很典型:上位机发FINS请求到PLC的IP,DA1填了10,结果完全没响应;用Wireshark抓包发现PLC没有回包。后来在Network Configurator里把PLC的以太网节点号改成10,重新下载配置,通信立刻就通了。

所以碰到“IP能ping通但FINS无响应”的情况,第一件事就是查节点号是不是跟IP末位一致,别急着怀疑代码。

5.2 UDP和TCP的报文差异:多了一层长度前缀

FINS走UDP和走TCP,报文格式不一样。UDP方式比较简单,直接发FINS帧;TCP方式则要多包一层:FINS协议本身在TCP上传输时,报文前面要加4字节的ASCII标识“FINS”和4字节的长度字段,然后才是FINS头部和命令数据。

如果你照搬UDP代码,把目标IP和端口改成TCP连接后直接发同样的帧,PLC根本不会理你。我第一次从UDP切换到TCP时,就吃了这个亏。查手册才看到FINS/TCP报文的格式,大概是:

'F' 'I' 'N' 'S' 长度(4字节) FINS帧内容

长度字段是从FINS头部开始算起的字节数,不是包含前面“FINS”标识在内的总数。不同手册表述略有差异,但只要按这个思路去抓包对比,很快就能对上。

端口方面,欧姆龙以太网单元的FINS通信默认端口是9600,TCP和UDP都用这个端口。如果是自定义端口,要在以太网单元设置里配置,上位机必须保持一致。

5.3 TCP连接耗尽:每帧新建Socket的代价

FINS over TCP是面向连接的。每次发命令前先建立TCP连接,通信完成后关闭连接,代码写起来简单,但会在防火墙和PLC侧留下大量连接记录。

我曾经写了一个读取函数,每次轮询都新建一个TcpClient,读完就Close。前几次正常,跑了半小时后突然连不上PLC,ping都通,TCP握手却失败。用netstat查看本机端口状态,发现一堆TIME_WAIT状态的连接。再查PLC那侧才发现,以太网单元允许的并发连接数量是有限的,我不断新建连接,把连接槽位占满了。

正确的做法是用长连接:程序启动时建立连接,后续所有FINS命令都复用这一条TCP连接,程序退出时再关闭。如果业务上必须频繁重连,也要控制间隔,不能每秒钟开合一次。欧姆龙以太网单元对连接数量限制比较严格,很多型号只有几个连接槽位,被CX-Programmer在线监视占掉几个后,其他程序基本就挤不进去了。

5.4 CX-Programmer在线监视会挤占通信资源

说到在线监视,这是现场调试时最容易忽略的隐性因素。我用CX-Programmer连上PLC在线监控梯形图,同时跑自己写的上位机程序,结果上位机老是超时。刚开始以为是网络问题,折腾半天才发现,只要把CX-Programmer的在线连接断开,上位机就正常了。

原因很简单:PLC的通信端口和CPU通信服务是共享资源,多个主站同时去访问,PLC会按优先级处理,有些型号甚至不允许外接程序和CX-Programmer同时通过同一个端口通信。所以排查通信问题时,先把CX-Programmer离线,再用自己的程序测,否则会得到一堆假象。

6. 我的排查方法论:从串口助手到抓包的最小复现法

6.1 先跑通官方途径,再谈自己的代码

很多通信问题其实是自己代码的问题,而不是协议的问题。所以我的习惯是:先用官方工具把通信链路打通,再做自己的程序。

  • 串口HostLink:可以用串口调试助手,对照手册发送一条最简单的读DM命令,如果能收到正确响应,说明参数设置和帧结构没问题。
  • 以太网FINS:可以用欧姆龙的Network Configurator或CX-Protocol测试功能,先建立一个标准的FINS读写,确认PLC侧节点的可达性。

官方工具打通之后,你的任务就变成了“用代码模拟官方工具的行为”,而不是“从零摸索协议”。这样可以大幅度缩小排查范围。

6.2 串口监视和Wireshark抓包

排查通信问题,抓包是最好的老师。串口可以用带监视功能的虚拟串口工具或者硬件串口嗅探器,把上位机发给PLC的真实字节流记录下来;以太网直接上Wireshark,过滤条件和端口号一行搞定。

抓包要看的东西:

  • 上位机发出的帧是否完整:起始符、命令码、FCS、结束符有没有。
  • PLC返回的帧是什么内容:结束码是不是00,数据长度对不对。
  • 响应帧的源节点号、目标节点号是否符合预期。

我遇到过一种情况:代码里看着发送的字符串是对的,但实际发送的字节流多了几个空格,PLC直接不响应。只有抓包才能发现这种隐藏问题。

6.3 每次只改一个变量,保留帧日志

通信调试最忌讳一次改多个参数。比如波特率改一下、FCS算法改一下、地址改一下,如果出问题,完全不知道是哪一步引起的。我后来养成了一个习惯:每次只改一个变量,改完先抓包确认,再继续下一步。

同时,把发送帧和接收帧按时间记录到日志里,格式固定,比如:

[2024-11-20 10:23:01.512] TX: @00RD0000000167CR [2024-11-20 10:23:01.612] RX: @00RD00000D6767CR

日志里保留完整Hex字节比字符串更容易发现问题。尤其是FCS、结束符这种肉眼看不到的字符,只靠打印字符串根本发现不了。

6.4 排查清单:按优先级逐项检查

我在项目后期整理了一张排查清单,按优先级排序,遇到通信问题先过一遍:

优先级检查项说明
1物理接线和IP地址线缆、接口、IP能否ping通
2串口参数/端口号波特率、数据位、校验、停止位,端口9600
3节点号和单元号FINS目标节点号与IP末位对应
4帧格式和FCS起始符、命令码、长度、校验、结束符
5地址映射和数量内存区代码、偏移、字数
6资源占用CX-Programmer是否在线,TCP连接是否耗尽

按照这个顺序排查,我几乎没有解决不了的问题。很多时候问题不在最后一步,而在前面最简单的配置上。

7. 最后:真正搞明白的那件事

这些坑全部踩完以后,我回头看总结出的核心认知只有一句话:欧姆龙PLC通信的本质,就是“一个带寻址信息的命令-响应机制”。你只要能把三样东西搞清楚,就足够了:帧怎么封装、地址怎么映射、资源怎么占用。

帧封装解决的是“能不能通”的问题,主要包括串口参数、FCS校验、帧头和结束符。地址映射解决的是“读哪里的问题”,包括内存区代码、偏移量、字数、字节序。资源占用解决的是“为什么通了又不通”的问题,包括TCP连接数、CX-Programmer在线占用、扫描周期响应时间。

现在再做欧姆龙项目,我会先花半小时把Communication Command Reference那类手册里的相关页面打印出来,对照着设计帧格式,而不是凭印象写代码。通信这块东西,表面看是代码问题,实际上全是协议规范问题。把手册吃透,比在网上找一百个碎片帖子都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 3:00:43

I2C多主机仲裁与时钟延展:开漏输出下的精妙设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:00:41

车规MCU芯驰E3118 XIP模式下FEE开发实战与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:00:08

MCU晶振电路设计全指南:从负载电容计算到不起振排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 2:57:28

SAP ABAP批量建表实战:SE11数据字典自动化生成

上周又接了个活,对方甩过来一个 Excel,三十多个自定义表,每个表二三十个字段,字段全走域和数据元素,命名规范还卡得挺死。要是老老实实打开 SE11 一个对象一个对象点,域、数据元素、表三层走一遍&#xff0…

作者头像 李华
网站建设 2026/11/6 2:57:14

SQL分组聚合与数组转换实战指南

1. SQL中的分组聚合与数组转换在数据处理工作中,我们经常需要对数据进行分组聚合操作。SQL作为关系型数据库的标准查询语言,提供了强大的GROUP BY功能来实现数据分组。但有时候简单的聚合函数(如COUNT、SUM、AVG等)并不能满足我们…

作者头像 李华
网站建设 2026/11/6 2:54:44

期货当日成交量超过持仓量,是统计错了吗?

某个期货合约的当日成交量是3000手,当前持仓量只有1000手。两个数都以手计,为什么前者能比后者大?是不是把成交重复统计了? 这个疑问把当前持仓量当成了当日成交量的上限。实际上,一项描述一段时间里累计发生的成交&am…

作者头像 李华