欧姆龙PLC串口通讯实战:FINS协议帧结构与DM区读写
2026/9/20 16:37:49 网站建设 项目流程

简介:面向工业自动化工程师、PLC调试人员及设备维护人员,欧姆龙PLC串口FINS命令协议通讯演示文档聚焦PC与CJ2M系列PLC的串口通信,解决命令帧构造与数据读写难题。文档共1个doc文件,压缩包大小321KB,内容精炼易读。已有155人学习下载。内容从实验配置入手,详细讲解PC直连PLC时的命令帧格式,涵盖单元号、头代码、响应等待时间、ICF、DA2、SA2、SID等关键字段,并给出DM区与W工作区的读写实例,附具体Hex命令和返回码解析。同时总结了存储区代码对照、尽量一次读写连续通道等实用技巧,帮助读者快速掌握FINS协议串口通讯的帧结构、调试方法与排错要点。

1. 欧姆龙 PLC 串口调试的第一关:把 FINS 当成地址而不是协议

接手一台欧姆龙 CP1H,编程线不在手边,只有一根 USB 转串口线和一份写着“串口 FINS 演示”的文档,这种情况在产线调试里很常见。很多人第一反应是按 Modbus 的思路找寄存器偏移量,结果发现 FINS 根本没有偏移量这回事,它自带一套网络号、节点号、单元号组成的四层寻址。FINS 是欧姆龙贯穿以太网和串口的应用层协议,串口上的帧就是把以太网帧剥掉 IP 层之后剩下的那部分。这套方案不需要额外装驱动库,用任何能收发字节的编程语言都能直接读写 PLC 的 DM 区、CIO 区。下面这套做法面向的是设备数据采集、上位机对接和售后诊断,目标是让 PLC 的 D 区像一块可以远程读写的内存卡一样被访问。

2. FINS 串口帧结构与地址模型:先看懂一帧命令

2.1 为什么不直接用 Host Link 或 Modbus

欧姆龙早期串口通讯走的是 Host Link,命令是 ASCII 字符串,比如读写 DM 区用 RD/WR 开头的指令。Host Link 的问题在于它只能跑在串口上,无法平滑迁移到以太网。Modbus 在部分欧姆龙 PLC 上以选件或功能码形式存在,比如 CP1W-MODBUS 单元,但 Modbus 的寄存器模型和欧姆龙内部的内存区格式之间有一层映射开销。

FINS 的优势是命令码统一。读内存区是 0101,写内存区是 0102,无论底下是串口、以太网 FINS/TCP 还是 Controller Link,这条命令的格式都一样。CX-Programmer 和 Sysmac Studio 里把串口通信方式设为 FINS 后,PLC 串口收到的就是这种帧。欧姆龙协议宏功能本质也是把 FINS 命令模板塞给 PLC 串口,让它作为主站去轮询第三方设备,而本文要做的正好相反:上位机作为主站主动发 FINS 帧给 PLC。

所以先把裸帧格式吃透,后面无论用协议宏、C# 库还是 Python 脚本,都只是在拼这一串字节。

2.2 一帧 FINS 命令按字节拆开看

FINS 串口帧由三部分组成:ASCII 开头的FINS四个字节、FINS 报文本体(包括头和命令)、结尾的 FCS 校验字符和结束符。网上很多简化图把FINS头直接省略,然后让人对着文档里的“ICF 0x80”找位置,结果实际发出去了 PLC 完全没反应。完整的一帧长这样:

字段长度(字节)典型值说明
FINS446 49 4E 53ASCII 字符 "FINS",是串口帧的识别头
ICF10x80命令帧为 0x80,响应帧为 0x00
RSV10x00保留字段,固定 0
GCT10x02允许网关转发次数,未使用网关时固定 2
DNA10x00目标网络号,本地网络填 0
DA110x00目标节点号,即 PLC 的节点号
DA210x00目标单元号,CPU 单元填 0
SNA10x00源网络号,上位机在本地网络填 0
SA110x01源节点号,即上位机自己
SA210x00源单元号,上位机填 0
SID10x00服务号,用于匹配请求与响应
命令码20x01010101 读内存区,0102 写内存区
数据N按命令变化地址、长度、写入值等
FCS2例如2A从 ICF 到数据末尾的异或,转大写 ASCII
结束符22A 0D星号*加回车 CR

注意这里从头到尾没有长度字段,接收端是靠超时和结束符*\r判断一帧是否收完的。这和 Modbus RTU 靠字节间隙区分帧不同,写接收循环时要小心。

2.3 SA1/DA1 到底该怎么填

有人在欧姆龙手册里看到“SA1”会以为是什么特殊地址,其实它只是源节点号。FINS 寻址由四部分组成:网络号、节点号、单元号,外加内存区地址。DNA/DA1/DA2 描述“我要找的设备在哪”,SNA/SA1/SA2 描述“我是谁”。

上位机没有网络接口时,SA1 通常填 1,DA1 填 PLC 的节点号。部分 PLC 支持 DA1 填 0 表示“本机”,但在多节点环境下这种写法很容易把帧发到错误设备。稳妥做法是在 CX-Programmer 里连上 PLC,查看“节点号”设置,把这个值填进 DA1。SID 随意,但建议每次发送递增,因为响应帧会把 SID 原样返回,上位机可以拿它匹配请求,防止串口上多条命令对不上号。

用 Python 定义一个 FINS 命令头

下面这个函数生成 FINS 报文头,不含FINS四个 ASCII 字符,因为 FCS 校验不覆盖它们:

def fins_header(sid: int = 0x00, da1: int = 0x00, sa1: int = 0x01) -> bytes: # 目标: 本地网络0, PLC节点号da1, CPU单元0 # 源: 本地网络0, 上位机节点sa1, 单元0 return bytes([ 0x80, 0x00, 0x02, # ICF, RSV, GCT 0x00, da1, 0x00, # DNA, DA1, DA2 0x00, sa1, 0x00, # SNA, SA1, SA2 sid # SID ])

ICF 是 0x80 表示这是一条命令帧,PLC 回给上位机的响应帧里 ICF 会是 0x00。如果上位机发出去的帧 ICF 填错方向,PLC 会直接丢弃,而且不会给任何错误码,排查时会表现为“发出去没反应”。

3. FCS 校验与完整帧拼装:串口上多出来的两个字符

3.1 FCS 是一次纵异或,不是 CRC16

FINS 串口帧的校验字段叫 FCS,全称 Frame Check Sequence,算法非常简单:把从 ICF 开始到数据区最后一个字节为止的所有字节做异或,得到一个 8 位结果,再把这个结果转成两个大写 ASCII 十六进制字符放在帧尾。例如异或结果是0x2A,就写入2A这两个 ASCII 字符。

很多照着 Modbus 习惯来的人会在这里踩坑:FINS 不用 CRC16,用 CRC 算法算出来的校验位 PLC 完全不认。XOR 结果只有 8 位,转成 ASCII 后占用两个字节,这也就是为什么最后结束符前面永远多出两个看起来像“数据”的字符。另外FINS四个字母不参与校验,异或的起点是 ICF。

3.2 用 Python 写一个完整拼帧函数

先写 FCS 计算,再写帧拼装:

def _fcs(data: bytes) -> str: xor_sum = 0 for b in data: xor_sum ^= b return f"{xor_sum:02X}" def build_fins_frame(body: bytes) -> bytes: # body 是从 ICF 开始的 FINS 报文本体,不含 "FINS" 头 frame_prefix = b"FINS" fcs = _fcs(body) # 结束符是 "*" + CR,注意没有 LF return frame_prefix + body + fcs.encode("ascii") + b"*\r"

_fcs把异或结果格式化成两位大写十六进制,build_fins_frame负责拼装。需要注意fcs.encode("ascii")会把2A这样的两个字符变成两个字节,而不是一个字节。结束符用的是*\r,也就是 ASCII 码0x2A 0x0D如果误用了\r\n,PLC 会把\n当成下一帧的第一个字节,导致后续帧全部错位。

3.3 完整拼一帧读 D100 的命令

把第 2.3 节的fins_headerbuild_fins_frame组合起来,就得到一条可发送的完整读命令:

def make_read_dm(addr: int, count: int = 1, sid: int = 0x01) -> bytes: header = fins_header(sid=sid) # 0101 = 读内存区命令 # 0x89 = DM区, 字单位; addr 转大端双字节; count 转大端双字节 body = header + bytes([0x01, 0x01]) + bytes([0x89]) + addr.to_bytes(2, "big") + count.to_bytes(2, "big") return build_fins_frame(body)

命令码 0101 后面跟三项:内存区码0x89表示 DM 区字单位,起始地址 D100 转成大端00 64,读取字数0001表示读 1 个字。这里最容易错的是地址字节序,D100 要写成00 64,如果按小端写成64 00,PLC 实际访问的是 D25600,数值能读回来但是错的。

发送端还需要处理一个细节:FINS 串口是严格的一问一答模型。上位机发完一帧必须等响应回来才能发下一帧,一帧没完整收完就发下一帧,PLC 会把两帧数据混在一起解析,表现为响应帧超时。

4. 读写 DM 区与响应码排查

4.1 用 pyserial 发送读命令并解析响应

下面用一个最小脚本把第 3 节的拼帧函数跑通:

import serial from time import sleep ser = serial.Serial( port="COM3", baudrate=9600, bytesize=8, parity=serial.PARITY_EVEN, stopbits=1, timeout=0.5 ) frame = make_read_dm(100, count=2, sid=0x01) ser.write(frame) sleep(0.1) # 等 PLC 处理,不能为零 resp = ser.read(256) ser.close() print(resp.hex(" "))

串口参数 9600 8 E 1 是欧姆龙 PLC 串口的默认配置,具体要跟 PLC 侧“外围端口”设置一致。timeout 设 0.5 秒,覆盖了 PLC 一个扫描周期内的响应时间。sleep(0.1)不是万能的,在 Windows 上如果串口驱动的缓存延迟较大,应改为循环读取直到收到*\r

响应帧的解析逻辑如下:跳过最前面的FINS四个 ASCII 字节,接着是 10 字节 FINS 头,再往后是命令码01 01,然后是 2 字节响应码:

def parse_read_dm_response(resp: bytes): assert resp[:4] == b"FINS", "帧头错误" cmd = resp[14:16] end_code = resp[16:18] if end_code != b"\x00\x00": print(f"错误响应码: {end_code.hex()}") return None values = [] data = resp[18:] for i in range(0, len(data), 2): values.append(int.from_bytes(data[i:i+2], "big")) return values

resp[14:16]是响应帧里的命令码,位置计算方式是 4 字节FINS+ 10 字节 FINS 头 = 14 字节偏移。如果响应码不是00 00,后面跟的就不是读取数据而是错误码。注意:即使是错误响应,帧头和结束符也是完整的。

4.2 写 D100 的 0102 命令

写内存区命令是 0102,数据区比读命令多了一项:要写入的值。下面写 2 个字到 D100:

def make_write_dm(addr: int, values: list, sid: int = 0x02) -> bytes: header = fins_header(sid=sid) body = header + bytes([0x01, 0x02]) body += bytes([0x89]) body += addr.to_bytes(2, "big") body += len(values).to_bytes(2, "big") for v in values: body += v.to_bytes(2, "big") return build_fins_frame(body)

values 里的每个元素会被转成 2 字节大端。DM 区是 16 位存储单位,写入值超过 0xFFFF 会触发错误,写入负数要用补码形式,例如 -1 写成 0xFFFF。

4.3 响应码对照表

响应帧里紧跟命令码后面的两个字节是 End Code,它直接告诉上位机 PLC 为什么没执行成功:

End Code含义常见原因
00 00正常完成
01 01本地节点错误FINS 头字段格式错误,ICF 或 GCT 填错
01 03目标节点不存在DA1 节点号与 PLC 设置不一致
01 04目标单元不存在DA2 单元号填错
11 01内存区不存在内存区码填错,例如 0x89 写成 0x88
11 03地址或数据非法地址超范围、写入值超限、向只读区写入
20 03接收数据错误FCS 校验失败、帧格式错误、结束符缺失

实际调试中 20 03 出现频率最高,原因多半是异或范围算错,把FINS四个字节也算进去了。11 03 则多在读写超过 D 区最大地址范围时出现,CP1H 的 D 区范围是 D0 到 D32767,超出就会报这个码。

串口调试助手在这种场景下非常好用。把make_read_dm(100)生成的那一帧十六进制字符串直接粘贴到 XCOM 或其它串口助手的发送框里,点发送,看返回的十六进制是否以FINS开头。先用工具手动发一帧,再用程序发一帧,能快速定位问题到底出在拼帧还是串口参数上。如果对帧内容没把握,串口监听工具可以把上位机和 PLC 之间的原始字节流完整抓下来,对比发送和接收两边的 FCS 是否一致。

5. 没有 PLC 时怎么验证:搭一个回环从站

5.1 用 Python 模拟 FINS 串口从站

手边没有 PLC 又想验证拼帧逻辑,可以用另一块 USB 转串口共地连接,跑一个最简单的回环从站:把收到的每一帧原样发回去。这样第 4 节的读命令脚本发出去后,收回来的是同一帧,能验证的是帧头、FCS、结束符有没有拼错。代码如下:

import serial s = serial.Serial("COM4", 9600, parity=serial.PARITY_EVEN, timeout=1) while True: chunk = s.read(1) if not chunk: continue buf = bytearray(chunk) while not buf.endswith(b"*\r"): b = s.read(1) if not b: break buf.extend(b) s.write(buf) # 原样回显 print(buf.hex(" "))

这个脚本不解析任何字段,只是一个“帧完整性的回声”。如果上位机程序收到的回显帧和发出去的字节完全一致,说明 FCS 计算和帧尾拼装没毛病;如果收到的和发出去的不一样,先查结束符和 FCS 的大小写。回环通过之后,再把DA1改成实际 PLC 节点号,直接连真机。

5.2 三个最容易忽略的坑

第一,串口参数不匹配比协议错更隐蔽。欧姆龙默认 8 位数据位、偶校验、1 位停止位,但有的老型号出厂设置是 7 位数据位。连不上时先看 CX-Programmer 的 PLC 设置页,核对“外围端口”里的波特率和校验位。USB 转串口用的是 CH340 驱动时,还要确认设备管理器里枚举出的 COM 口号,有的板子会随机占用高位 COM 端口,代码里写死 COM3 就会报端口不存在或串口烧写失败。

第二,仿真软件不跑串口协议栈。欧姆龙 CX-Simulator 或 Sysmac Studio 的仿真模式只能验证梯形图逻辑,FINS 串口帧收发依赖 PLC 固件里的串口驱动,仿真环境里完全没有这条通路。想调试通讯,要么等真机,要么用回环从站方案。

第三,协议宏和上位机直发 FINS 是两套思路。协议宏是把 FINS 帧模板烧录到 PLC 串口上,让 PLC 作为主站主动轮询外部仪表;上位机直发则是把 PLC 当从站,由上位机发起每次读写。两者不能混着用,否则 PLC 串口自己发了帧又回你的帧,总线上会同时出现主站和从站。把一帧验证过的 FINS 命令保存在串口调试助手的快捷发送列表里,换 PLC 型号时只改 DA1 和地址,比每次重新拼帧可靠得多。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询