
1. 为什么绕不开 FINS先从一次产线数据采集说起1.1 一次典型的欧姆龙 PLC 接入场景几个月前帮朋友看一个产线数据采集项目现场用的是某款 CJ 系列 PLC上位机要把一批 D 寄存器里的工艺参数弄到数据库里。朋友一开始想走 Modbus-TCP结果翻了几百页手册发现欧姆龙这边原生支持的以太网协议是 FINS官方也叫 Factory Interface Network Service。如果坚持用 Modbus还得在 PLC 侧额外配置或者加协议转换模块成本和技术风险都不划算。那次排查的结论很简单别绕直接用 FINS。FINS 是欧姆龙自己定义的应用层协议精通它的一个好处是不管你是通过网络FINS/TCP、FINS/UDP还是串口Host Link跟 PLC 通信只要地址和帧格式的规则通了后面的开发大部分都是体力活。这篇文章就是我后来整理出来的完整笔记从帧结构、地址映射、常用指令到排错经验全部按项目实操的角度写。适合谁看一个是搞上位机软件、SCADA、MES 对接的工程师另一个是刚接触欧姆龙 PLC、想搞明白网络通信的新手。读完至少能达到这个程度不依赖库文件自己用 Python 或 C# 拼出一帧 FINS 报文并且知道收到响应后怎么解析。1.2 FINS 解决的三个实际问题FINS 出现在欧姆龙 PLC 体系里其实就是为了解决三件通信上的麻烦事。第一跨网段路由。FINS 协议里专门设计了网络号Network、节点号Node、单元号Unit这三个编址概念。这意味着你可以通过路由一跳一跳地访问不同网段里的 PLC而不需要在上位机上临时加一堆路由表。这一点在设备分散的工厂现场非常实用。第二不同型号之间的兼容。欧姆龙的 PLC 历史很长从老式的 C 系列到后来的 CS/CJ/NJ/NX寄存器叫法一直在变但 FINS 用一套统一的内存区代码把 D、W、H、CIO 这些区域抽象了出来。上位机程序员不用关心底层是哪个型号只要知道当前设备支持哪些内存区就行。第三上位机、HMI、PLC 之间互相通信。FINS 既可以由 PC 发起读写也允许 PLC 主动向上位机发送数据。很多要求实时性比较高的场合比如设备完成动作后立刻上报状态直接让 PLC 通过 FINS 指令发帧给上位机比上位机反复轮询更高效。2. FINS 报文拆开看命令帧里的每个字节都得认识2.1 帧头那 10 个字节到底在干什么FINS 的报文结构并不复杂但第一次接触的人很容易被一堆十六进制字节吓到。其实你只需要把它分成三段来理解FINS 帧头10 字节 命令区2 字节 参数区N 字节。先看帧头部分这是整条报文最固定的部分。以 UDP 通信为例一个请求帧的完整组成如下字节位置名称常见值说明1ICF0x80信息控制字段请求一般为 0x80响应一般为 0xC02RSV0x00保留字节固定填 03GCT0x02网关允许经过的次数固定 24DNA0x00目标网络号通常为 05DA10x01目标节点号比如 PLC 节点号是 16DA20x00目标单元号CPU 单元一般为 07SNA0x00源网络号通常为 08SA10x00源节点号上位机自己的节点号9SA20x00源单元号一般为 010SID0x01服务 ID用于匹配请求和响应为什么要特别强调 ICF因为 ICF 的 bit0 到 bit2 是控制数据响应方向的标志很多初学者拿到手册只知道填 0x80但等到 PLC 返回响应帧时发现第一字节变成了 0xC0就以为报文错了。其实那是正常的响应帧的 ICF 最高位会被置位表示这是一条来自 PLC 的应答。GCT 这个字段是当年为了支持多级网络设计的普通局域网通信填 0x02 就好别自作聪明填 0x80。DA1 和 SA1 是调试中最容易出错的字段尤其要注意PLC 侧设置的节点号必须和 DA1 保持一致上位机发送时用的源节点号要和当地网络配置一致否则 PLC 会直接丢弃这条请求。2.2 命令区和参数区真正干活的 MRC/SRC帧头后面紧跟着两个字节叫 MRC主命令码和 SRC子命令码它们组合在一起决定这条报文要做什么事情。我整理了一份高频命令速查表日常开发基本逃不出这几条命令MRCSRC作用内存区读取0101批量读取字或位数据内存区写入0102批量写入字或位数据内存区强制置位0103强制一个位或字为 1/ON内存区强制复位0104强制一个位或字为 0/OFFCPU 单元数据读取0501读取 CPU 型号、版本等信息时钟读取0701读取 PLC 内部时钟时钟写入0702校准 PLC 时钟MRC 和 SRC 本身不携带地址信息真正的干活参数在后面。比如内存区读取0101的参数部分就是区代码1 字节、起始地址2 字节、读取长度2 字节。也就是说每次必须把“读哪里、从哪开始、读多少”这三件事交代清楚。FINS 还有一个让多数工程师觉得友好的设计报文里没有 CRC 校验码。这在当时是为了降低协议复杂度让 PLC 这种资源紧张的嵌入式设备能快速处理但同时也意味着你在应用层必须自己处理好超时重试。如果用 UDP还得靠 SID 和超时机制来保证可靠性。这点我后面会详细讲。3. 地址映射D、W、H、CIO 可不是拍脑袋写的3.1 内存区代码与位/字访问FINS 协议里内存地址不是直接填“D100”这种字符串而是要转换成字节型的“区代码 地址数值”。区代码是协议里固定的你写上位机时记住最常用的几个就够。对于欧姆龙 CJ/CS/NJ 系列我习惯用这套对应关系PLC 区域字访问区代码位访问区代码典型用途CIO 区0x000x01外部输入输出继电器WR 区内部工作区0x020x03程序内部继电器HR 区保持继电器0x040x05断电保持位DM 区0x820x83数据存储最常用举个例子如果你想读取 D100 开始的 10 个字那么内存区读取命令的参数区域应该这么填区代码 0x82起始地址 0x00 0x64100 的十六进制读取数量 0x00 0x0A10 个字。位访问的地址算法要特别注意。很多第一次写的人会直接把位号当地址填进去比如想读 W10.03地址却填成了 0x0003这绝对错。FINS 的位地址计算方式是字编号乘以 16再加上位偏移。W10.03 对应的位地址是 10 × 16 3 163也就是十六进制的 0x00A3。只要翻过这个错位访问基本就通了。3.2 不同系列 PLC 的前后兼容问题有的老工程师可能印象里还有 IR、SR、TR 这些老式继电器区的叫法那是在 C 系列年代。后来的 CS1/CJ1/CJ2/NJ 系列统一改成了 CIO 区并保留了 W、H、D 这些常用区域FINS 区代码的对应关系也跟着做了调整。从实际项目角度看我建议你不要把区代码背死而是要养成“先看型号手册”的习惯。比如有些特殊模块地址落在 CIO 的扩展区里光用 0x00 字访问还做不对可能需要配合 CIO 区的高位扩展地址。再比如 NJ/NX 系列使用符号寻址更多FINS 直连反而变成了兜底手段。另外一个容易踩的坑是设备内存区的大小限制。不要以为协议支持 65535 个地址就一定能访问到不同型号的 DM 区实际范围可能只有 32768 或更少。如果你批量读取时把地址加超了PLC 会返回一个错误完成码不会默默帮你截断。所以写通用程序时最好把 PLC 型号对应的地址范围做成配置文件不要写死在代码里。4. 最常用的三类指令读、写、强制4.1 内存区读取0101假设目标 PLC 节点号是 1上位机节点号是 0我们要读取 D100 开始的 10 个字。完整请求帧按十六进制写出来就是80 00 02 00 01 00 00 00 00 01 01 01 82 00 64 00 0A前 10 字节是 FINS 帧头第 11、12 字节是 01 01表示内存区读取命令后面从 0x82 开始是参数区。这条报文总共 17 字节。注意起始地址和数量都是两个字节而且必须按高位在前、低位在后的顺序排。D100 0x0064所以地址部分是 00 6410 个字 0x000A所以数量部分是 00 0A。收到响应后帧头 10 字节不变第 11、12 字节还是会原样返回 01 01。第 13、14 字节是完成码0000 代表成功如果出现其他值就要去查手册对应的错误含义。成功时从第 15 字节开始就是纯数据区每两个字节组成一个字。这里有个实用经验不要只看第一个数据的值先把返回的字节数和请求长度对一遍因为有的 PLC 在访问非法地址时会返回正常完成码但数据区塞满 0。4.2 内存区写入0102写入命令和读取命令只有两个地方不同SRC 从 01 变成 02参数区最后多了一段要写入的数据。比如向 D200 写入两个字第一个字是 0x1234第二个字是 0xABCD请求帧是这样的80 00 02 00 01 00 00 00 00 01 01 02 82 00 C8 00 02 12 34 AB CD前面到 00 02 为止和读取是一样的逻辑后面的 12 34 AB CD 就是要写入的数据。写入数据的总字节数必须是偶数因为 FINS 以字为基本单位组织数据。如果你打算写一个 8 位的字节也得凑成一个字再写进去。这里想提醒一件事写入操作属于破坏性操作尤其在生产环境里一定要在调试软件里保留上一次写入值的记录。我见过不少同事在测试时把长度写错一次写出去几十个字把旁边的配方数据全冲掉了。所以写入前建议先发一条读取命令把目标区域原值读回来打印在界面上确认之后再写。4.3 强制 ON/OFF0103/0104强制类指令在调试设备时非常好用比如你想单独测试某个气缸的输出位又不想改梯形图直接对 CIO 区或 W 区的一个位做强制置位就行。强制置位命令是 0103强制复位命令是 0104。参数区和读取命令相似只是最后多了一个 2 字节的写入数据。对于位访问写入数据一般用 0x0000 表示 OFF0xFFFF 表示 ON。对于字访问就把这个 2 字节数据当作要强制的值。举个例子强制 W10.03 为 ON。先按前面的公式算位地址10 乘以 16 加 3 等于 163也就是 0x00A3。请求帧为80 00 02 00 01 00 00 00 00 01 01 03 03 00 A3 FF FF注意这里区代码是 0x03因为 W 区位访问用的是 0x03。很多同学在这一步翻车区代码写成了 0x02那就是在强制作一个字语义完全变了。强制操作是有风险的动作程序里如果同一个位既被强制又被梯形图正常控制运行状态会变得很难排查。所以在现场调试结束后记得把不需要的强制全部复位。另外某些安全回路里的输出点可能被 PLC 的辅助功能锁住强制指令执行成功但外部输出不动这不一定是协议问题要结合 PLC 的程序逻辑和硬件接线来看。5. 用 UDP 把报文真发出去可运行的 Python 示例5.1 为什么建议先从 UDP 起步FINS 在以太网上有两种传输方式UDP 和 TCP。如果只是做上位机数据采集我强烈建议先从 UDP 版本入手。原因很简单UDP 报文就是裸的 FINS 帧十六进制结构和手册一一对应调试时用抓包工具一眼就能看懂而 FINS/TCP 外面还包了一层 TCP 头包含命令代码、错误码、长度字段初学者很容易把这层结构搅混。UDP 的默认端口是 9600PLC 侧配置好 IP 地址和节点号之后用微信小程序、网络调试助手或者 Wireshark 就能直接看到报文。先通过简单工具确认链路通不通再去写正式代码效率高得多。5.2 发送帧与接收响应的完整脚本我贴一段项目里常用的小工具功能是读取 D 区数据。代码做了最小化处理方便你直接抄走改参数。import socket PLC_IP 192.168.0.10 PLC_PORT 9600 PLC_NODE 1 # PLC 侧设置的 FINS 节点号 PC_NODE 0 # 上位机 FINS 节点号 def fins_read_dm(start, count, sid1): # 10 字节 FINS 帧头 header bytes([ 0x80, 0x00, 0x02, 0x00, PLC_NODE, 0x00, 0x00, PC_NODE, 0x00, sid ]) # 内存区读取命令 参数区 cmd bytes([ 0x01, 0x01, # MRC01, SRC01 0x82, # DM 区字访问 (start 8) 0xFF, start 0xFF, (count 8) 0xFF, count 0xFF ]) frame header cmd sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(2) sock.sendto(frame, (PLC_IP, PLC_PORT)) resp, _ sock.recvfrom(1024) sock.close() if len(resp) 15: raise ValueError(响应帧长度不完整) # 响应帧头部10字节 MRC/SRC 2字节 完成码2字节 数据区 command resp[10:12] if command ! b\x01\x01: raise ValueError(f命令码不匹配: {command.hex()}) completion int.from_bytes(resp[12:14], big) if completion ! 0: raise ValueError(fPLC 返回错误完成码: 0x{completion:04X}) data resp[14:] words [] for i in range(0, len(data), 2): if i 1 len(data): words.append(int.from_bytes(data[i:i2], big)) return words if __name__ __main__: result fins_read_dm(start100, count10) for idx, val in enumerate(result): print(fD{100 idx}: 0x{val:04X} ({val}))这段代码重点解释两个细节。第一个是地址拆字节。start100就得用(start 8) 0xFF取高字节再用start 0xFF取低字节。不要直接写出100的 ASCII 或者十进制文本FINS 只认二进制字节。第二个是响应帧的完成码位置。网上很多旧代码把完成码当成第 13、14 字节那是因为他们用的报文结构里把 MRC/SRC 当作命令区后直接跟数据。以我这份精力核对过的结构为准前 10 字节是帧头第 11、12 字节是 MRC/SRC第 13、14 字节才是完成码。不过也提醒一句不同上位机库和网关透传工具可能在帧格式上做了二次封装你要是用第三方组件先抓包确认偏移量。5.3 响应完成码与常见错误码响应帧里的完成码是一个 2 字节大端整数。0000 表示一切正常其他值就需要结合命令上下文判断。我挑几个实际遇到过的完成码含义常见原因0x0000正常无0x1101参数格式错误请求参数区字节数不对0x1102数据越界访问地址超出 PLC 内存区范围0x1103数据长度错误写入长度和后续数据不相符0x2102节点号不存在DA1 指定的节点不存在或离线0x2103目标节点无法响应PLC 在运行程序但对 FINS 处理繁忙写程序时建议把完成码转成字符串输出到日志里而不是只显示一个数字。故障发生时一条清晰的错误提示比抓半天包有用得多。6. FINS/TCP 与串口 Host Link什么时候该换一种走法6.1 UDP 和 TCP 的差异决定使用取舍UDP 做采集足够快但它有一个特点无连接、不保证送达。在局域网里直接和 PLC 通信丢包概率不高但如果你要跨路由转发或者现场网络里有大量的广播风暴UDP 就会变得不稳定。FINS/TCP 的好处是 TCP 自带重传和保序链路断了马上知道更适合长时间运行的关键任务。它的结构大概是前面加一个 FINS/TCP 帧头包含命令代码、错误码、总长度后面才跟标准的 FINS 帧。端口同样是 9600但是 TCP 会先建立连接之后在这个连接上持续收发。取舍很简单数据采集频率高、允许偶尔丢一帧重读的情况优先 UDP。需要上位机持续监控 PLC 状态、断开要快速报警的情况考虑 TCP。跨网段、多级路由的情况UDP 配路由表更灵活但也要评估链路稳定性。我的经验是普通车间内部用 UDP 足够了但生产系统对接 MES 时报文要上一层数据库哪怕丢一帧数据都会造成质量追溯缺口这时我更倾向 FINS/TCP再配合 PLC 侧的“主动发送”或者上位机的高频轮询兜底。6.2 串口 Host Link 的经典帧格式不要以为所有欧姆龙 PLC 都带网口。老设备、小 PLC 往往只有串口。串口上跑的 FINS 变体叫 Host Link帧格式和以太网 FINS 差别很大。Host Link 报文的开头是一个字符然后是两位 PLC 单元号、两位命令码、中间是参数正文最后是两位 FCS 校验码以*和回车的 CR 结束。例如读取 D100 的 Host Link 帧看起来会像0010RD00010000000A00FCS*CR这里的命令码 RD 表示内存读取数据长度和地址都用 ASCII 码表示计算方式是“字地址乘 2 再转 ASCII”。也就是说 D100 实际上要换算成十六进制地址 0064但 Host Link 是按字节地址计算的要乘以 2变为 00C8。很多人第一次从 FINS 切到 Host Link 就挂在这里。如果你现在要开发的产品还需要兼容串口老设备建议把 Host Link 单独封装一层通信驱动别和以太网 FINS 混在一个对象里。两者的帧分隔、校验、地址算法完全不同硬套逻辑会让代码充满 if 分支。本篇文章以 FINS 以太网为核心Host Link 先记住这个坑后面做项目遇到时再去翻具体型号手册。7. 排查与避坑几类让我耗过通宵的问题7.1 收不到响应先从节点号和网络号下手收不到响应是 FINS 调试里最折磨人的问题。我见过太多人上来就抓包、改防火墙折腾半天发现是 PLC 那边的节点号根本没设对。排查顺序应该按这个来确认 PLC 的 IP 地址和上位机在同一网段不能通的情况下后面都白搭。用网络调试助手发一帧最简单的 FINS 报文比如读取 CPU 状态看 PLC 有没有回应。如果没回应去 PLC 参数设置里检查 FINS 节点号。欧姆龙 PLC 的节点号默认可能不是 1有人设了 3、有人设了 11你的 DA1 必须和它一致。检查上位机节点号 SA1。有的 PLC 在设置里开启了节点号检查SA1 是 0 或和别的设备冲突都会被丢弃。最后再检查 Windows 防火墙、路由器端口映射、交换机端口隔离这些问题。我遇到过一个情况PLC 有两个 CPU 模块用软件工具连的是第二个模块但 DA2 还是填 0导致请求全部发到了第一个模块。DA2目标单元号在多 CPU 系统里就等于选择哪个 CPU现场千万别忽略。7.2 多字数据读出来乱码字节序最有嫌疑FINS 协议本身用大端方式传数据也就是高字节在前。很多 PLC 内部的 D 寄存器也是按高字节低字节顺序存储的但等你把数据交给数据库或者上位机显示时还要根据业务值做一次字节交换。比如读回来一个浮点数 0x41200000在欧姆龙 PLC 里可能是 D100 和 D101 两个字存的究竟是 D100 存高 16 位还是低 16 位不同程序写法会产生两种完全不同的结果。有的工程师写 C# 时直接用 BitConverter 转结果发现所有数值都乘了个奇怪的系数就是因为单字序和双字序没对齐。我的建议是在上位机代码里只做一层“数据转换层”把原始字列表转成 float32、int32、BCD 等业务类型。这样 PLC 侧梯形图怎么排布双字上位机只需改一个函数不要在全工程里到处写解析逻辑。7.3 响应超时和 SID 复用SID 是服务 ID用来把请求和响应配成一对。理想情况下你每次发送请求时用递增的 SID收到响应后对一下 SID确保是当前请求的结果。但当通信超时、你再次重发同一条命令时一个很容易犯的错是SID 仍然用之前那个值而 PLC 网络栈里确实缓存着旧响应。结果就是你的程序收到了一个旧的响应误以为这次重发成功了。更麻烦的是有的 PLC 执行写命令后数据已经写进去了但响应因为网络原因没回到上位机你超时后重发一次等于把同一个值又写了一遍问题不大可如果中间现场被人工改过值你的重发就成了破坏性操作。所以重试逻辑一定要带两个条件SID 必须递增重试次数和冷却时间要可配置。我发现工业现场的通信故障常常是瞬时性的几百毫秒级别设置 500ms 到 1 秒的重试间隔通常就够。别一开始就把超时设成 10 秒一旦 PLC 和上位机同时卡住整个调度线程会越堵越深。7.4 抓包工具看一眼胜过十次瞎猜调试 FINS 时我离不开 Wireshark而且最好是直接用 Wireshark 自带的 FINS 过滤条件比如fins或者按 UDP 端口 9600 过滤。抓包的观察重点就这么几个请求帧是不是完整 17 字节、DA1 和 SA1 是不是预期值、响应帧完成码是多少、耗时多少毫秒。很多时候你还没走到代码层面抓包结果已经把问题指出来了。另外有些应用层的框架软件会把 FINS 帧打包成其他格式比如通过网关转发的 Modbus 数据这时候抓包反而会迷惑你要记得区分原始协议和封装协议。8. 最后的经验从看懂到能干活还差什么协议看懂了帧也能拼了但真正到了现场你还会发现协议之外还有很多事项要落实。我在实际项目里体会最深的一点是工具链比协议本身更能决定调试效率。强烈建议你手里常备网络调试助手、Wireshark、一个可以自定义报文的脚本框架。别在公司开发的正式上位机里做实验而是在一个独立测试工具里先把通信验证通再搬进正式工程。另外FINS 报文没有校验码这不代表你可以不做校验。UDP 在局域网里丢帧的概率虽然不高但一旦发生最好的对策是做好请求超时重发和日志记录。日志里把每一帧收发都留底将来出问题能回溯。我们项目里上线初期碰到的很多诡异问题最后都是靠日志里的原始帧数据定位的不是靠猜。如果你正在从串口通信转到以太网通信我自己的经验是先把 UDP 版本跑通再考虑 TCP。FINS 的地址映射、区代码、响应解析这些知识在两种传输方式里完全通用先把一套传输通道吃透后续扩展成本就会低很多。这份笔记写到这里剩下就是你自己拿一台 PLC 或者一个模拟器从 D100 开始读几个字把它跑通后面就顺了。