☰
Modbus TCP通讯测试全流程:报文解析、工具选型与排障实践
2026/10/30 15:45:35 网站建设 项目流程

简介:一套围绕汇川AM402 PLC与C# Winform上位机开展Modbus TCP通讯的实战工程资源,适合PLC工程师、上位机开发人员及工业通信初学者参考。压缩包内含完整测试项目,覆盖从Socket非阻塞连接、Modbus功能码封装(如读离散输入、写单个寄存器)到异常重试与数据解析显示的联调链路;文件共47个,以C#源文件(15个cs)为核心,配以工程配置、解决方案、编译生成的exe/pdb以及AM402 PLC测试程序等,整体约922KB,便于快速导入Visual Studio查看和运行。已有2256人浏览学习。其中通信类封装与PLC端测试程序可直接作为联调起点,能节省自行搭建环境和排查报文格式的时间。

1. 拿到 InproModebusTCP通讯测试 这类压缩包时,先想清楚要解决什么问题

拿到一个叫InproModebusTCP通讯测试.rar的压缩包,在工控现场太常见了:甲方、同事或设备厂商丢过来,里面可能是测试脚本,也可能是通讯测试的说明和记录,潜台词都差不多——帮我把 Modbus TCP 通讯验证一下。这事看着简单,实际上一大半时间耗在“连不上”和“数据不对”上。把这套测试做扎实,不只是确认一次能不能通,而是把测试沉淀成可复现、可留痕的过程,后面验收和排障都能直接拿数据说话。下面这套做法,适合做 PLC 调试、上位机开发和设备联调的工程师照着复现。

2. 通讯测试前先看懂:Modbus TCP 报文与主从站时序

Modbus TCP 测试翻车的根源,多数不是协议复杂,而是把串口 Modbus 的习惯带了过来。串口要算 CRC、管波特率;TCP 版就是标准 Modbus 报文用 TCP 封装,固定端口 502,链路交给 TCP 保证,没有 CRC。换来的代价是参数错得隐蔽:Unit ID、功能码、寄存器地址、字节序,任何一个对不上,返回的要么是异常码,要么是永远“通了但不对”。好在 Modbus TCP 的报文非常短,手工拆一组请求响应就能看清黑匣子里发生什么。这一章会从参数、报文到代码,一步步把它摊开。

2.1 从站地址、功能码、寄存器地址:三个一错就翻车的参数

Modbus TCP 里“从站地址”不叫地址,叫 Unit ID。串口 Modbus 里这个字节必须和设备拨码一致,TCP 版里它已经被 IP 取代了定位作用,但很多设备仍然会校验它。常见的坑是:主站发 Unit ID=0,有的设备会直接丢弃请求;保守做法是填 1,或者填设备手册指定的值,抓包时注意看它有没有原样回显。

功能码决定了“这次请求要干什么”。通讯测试里最常用的是 0x03 读保持寄存器、0x04 读输入寄存器、0x05 写单个线圈、0x10 写多个寄存器。把功能码搞错,从站会回异常码 0x01(非法功能码),而不是你想读的数据。高手的习惯是先定功能码,再定地址区域,两者是绑定的:保持寄存器用 0x03,输入寄存器用 0x04,不要把设备文档里的“寄存器地址 40001”直接当成 0x03 的起始地址来填。

寄存器地址也要小心“差一个”的问题。Modbus 协议里的地址是从 0 开始计数的,比如第一个保持寄存器是 0x0000;但很多上位机、触摸屏和文档会用 PLC 式地址,第一个保持寄存器写成 40001。于是你在工具里填 40001 还是 0?答案是看工具怎么解释:Modbus Poll 里填 0 对应协议地址 0x0000,也就是文档里的 40001。地址差一、差十这种问题,比协议本身难查得多,尤其在寄存器数量多的时候,表现为读回来整体偏移一位。

2.2 拆一个读保持寄存器请求:从 6 个字节看主从站对话

先看请求侧。下面是一次读保持寄存器的完整请求,共 12 个字节:

字段字节数示例值说明
事务处理标识符20x0001用于匹配请求和响应
协议标识符20x0000Modbus 协议固定为 0
长度字段20x0006从单元标识符到报文结束共 6 字节
单元标识符10x01从站地址 / Unit ID
功能码10x03读保持寄存器
起始地址20x0000从 0 号寄存器开始读
寄存器数量20x000A连读 10 个寄存器

请求的报文是00 01 00 00 00 06 01 03 00 00 00 0A。长度字段 0x0006 是后面01 03 00 00 00 0A这 6 个字节的计数,不含自己的长度,也不含事务标识符和协议标识符。很多手拼报文的人在这里算错,导致从站直接断连。

响应侧要会看长度和异常码。读 10 个寄存器的正常响应是00 01 00 00 00 17 01 03 14后跟 20 个字节数据。这里 0x14=20 表示后面有 20 字节,0x0017=23 是从单元标识符到报文结束的总字节数。如果返回的功能码变成 0x83,说明请求被拒,后面还会跟一个异常码:0x01 非法功能码,0x02 非法数据地址,0x03 非法数据值,0x04 从站设备故障。看到这些码,别去查网络,回头查参数。

2.3 用 Python 模拟 Modbus TCP 主站:最小代码先跑通一次读取

工具点完一通,最后还是要落一段能重复执行的代码。我一般用 pymodbus 3.x 写最小主站,装好之后代码如下:

from pymodbus.client import ModbusTcpClient # 参数说明: # host - 从站 IP,先确保和主站同网段 # port - Modbus TCP 固定 502,除非设备改过 # timeout - 连接和响应超时,单位秒,初调建议 3~5 秒 client = ModbusTcpClient("192.168.1.10", port=502, timeout=3) if not client.connect(): print("连接失败:检查 IP、端口、网线和防火墙") exit(1) # 读保持寄存器:(起始地址, 数量, slave=Unit ID) # 地址从 0 开始,文档里写 40001 的寄存器这里就是 0 resp = client.read_holding_registers(0x0000, count=10, slave=1) if resp.isError(): print("读返回异常:功能码 0x%02X" % resp.function_code) else: print("寄存器值:", resp.registers) client.close()

这段代码的逻辑很直白:先建立 TCP 连接,再发一次读请求,最后判断返回是否异常。resp.isError()是 pymodbus 里判断异常响应的统一入口,不用自己解析异常码;如果返回异常,function_code里能看到 0x83 这类带错误标志的功能码。

写单个线圈也是同样的套路:

# 写线圈:地址 0,值 True,Unit ID 保持和读请求一致 resp = client.write_coil(0x0000, True, slave=1) # 写多个保持寄存器:地址 0x0100 起,写入三个值 resp = client.write_registers(0x0100, [100, 200, 300], slave=1)

pymodbus 里slave参数对应的就是报文里的 Unit ID,读和写必须一致。如果设备要求 Unit ID 固定为 0xFF,把所有slave都改成 0xFF。这段代码跑通,说明 TCP 链路和协议参数都对了,接下来才有资格谈测试。

3. 把测试跑起来:工具选型、最小步骤与读写轮询脚本

通讯测试最大的浪费,是每次换设备都重新点一遍工具、重新记一遍参数。我一般把它分成三块:先用 PC 工具做连通性验证,再用 PLC 库指令做设备侧验证,最后用脚本做批量轮询和留痕。三块的分工不一样,但测试参数要统一,不然就会出现工具能通、程序不能复现的现象。这一章把三块的选型和落地步骤讲清楚。

3.1 工具怎么选:Modbus Poll、PLC 库指令和自写脚本的分工

工具角色适合场景注意点
Modbus PollPC 作主站最快验证从站能不能通、响应时间多少免费版有连接数限制,注意 Unit ID 和超时设置
Modbus SlavePC 作从站没有真实设备时,模拟从站给上位机调寄存器地址范围要覆盖主站请求
PLC 库指令(如 SMART 200 的 MB_CLIENT / MB_SERVER)设备侧主站或从站验证现场程序与库指令配合库存储区要单独分配,不能和用户 V 区重叠
pymodbus 脚本自定义主站批量轮询、偶发问题复现、留痕版本不同导入路径有差异,以你环境里的版本为准

选型不用贪多。现场排查我通常只开一个 Modbus Poll,确认“从站是好的”,然后立刻切回自己写的脚本做复现。Modbus Poll 能给你一个基线:如果 Poll 能通而脚本不能通,那就是脚本参数写错,别让设备背锅。

3.2 一轮完整通讯测试的最小步骤:先读后写,超时从 1000ms 开始

完整测试我按六步走,每一步都有明确产出。

第一步,把 IP 定死。主站和从站设置到同一网段,从站用固定 IP,不要依赖 DHCP。现场最常见的问题是 IP 冲突和子网掩码不对,ping 不通就直接查这两项。

第二步,确认 TCP 端口能进。用命令验证:

ping 192.168.1.10 telnet 192.168.1.10 502

telnet能出现一个黑窗口,说明 TCP 层已经通了。这时不要急着关窗口,等请求超时断开也算正常——Modbus TCP 没有握手协议,对端不会主动发欢迎语。

第三步,读保持寄存器。Modbus Poll 里设置从站 IP、端口 502、Unit ID、功能码 0x03、起始地址 0、数量 10,超时填 1000ms。能读出数值就记下基线,包括响应时间。

第四步,写线圈或写寄存器。先找一个不影响工艺的地址,比如空闲线圈或设备手册明确标注“测试用”的寄存器。写之前记录原值,写后读回确认。这里特别提醒:不要在运行中的设备上随意写,写坏了没有后悔药。

第五步,观察连续轮询。把轮询周期从 1000ms 缩短到 200ms,持续 5 分钟,看有没有超时和异常码。短周期轮询最容易暴露从站处理能力不够的问题。

第六步,整理记录。把 IP、Unit ID、功能码、地址、数量、期望值、实读值、响应时间、错误码写进表格,保留原始报文截图。这个习惯能省掉后面一半的扯皮。

3.3 读写轮询脚本与测试记录模板:结果要能留痕

工具测试只能覆盖“当下这一会儿”,要验证稳定性,得让脚本挂机跑。下面这段脚本会把每一轮的结果写进 CSV,同时统计平均延迟和错误次数:

import time import csv from pymodbus.client import ModbusTcpClient def run_test(host, port, unit, register, count, rounds=100, interval=0.2, timeout=2): # host - 从站 IP # port - 502 # unit - Unit ID,读和写必须一致 # register - 起始寄存器地址,基于 0 # count - 单次读取的寄存器数量 # rounds - 总轮询次数 # interval - 轮询间隔(秒),不要小于从站响应时间 # timeout - 请求超时(秒) client = ModbusTcpClient(host, port=port, timeout=timeout) if not client.connect(): print("连接失败:", host, port) return ok = 0 fail = 0 latencies = [] with open("mb_test_log.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["seq", "timestamp", "status", "latency_ms", "values"]) for i in range(rounds): t0 = time.time() resp = client.read_holding_registers(register, count=count, slave=unit) dt_ms = (time.time() - t0) * 1000 if not resp.isError(): ok += 1 values = " ".join(str(v) for v in resp.registers) writer.writerow([i, time.strftime("%H:%M:%S"), "OK", round(dt_ms, 2), values]) latencies.append(dt_ms) else: fail += 1 writer.writerow([i, time.strftime("%H:%M:%S"), "ERR", round(dt_ms, 2), ""]) time.sleep(interval) client.close() avg = sum(latencies) / len(latencies) if latencies else 0 print(f"完成:OK={ok}, ERR={fail}, 平均延迟={avg:.1f}ms")

逻辑说明:每一轮先记录开始时间,再发请求,用dt_ms计算单次响应耗时;isError()只负责判断协议层是否异常,网络层的超时会抛异常或被 pymodbus 封装成错误响应,所以ERR行要保留原始latency_ms,超时的行通常延迟会等于 timeout 乘 1000。

参数说明:interval别设太短,建议 200ms 起步;从站处理不过来时,缩短interval只会加剧失败,不会提高吞吐。rounds想测长期稳定性就直接改成 86400,配合 200ms 间隔可以连续跑一夜。CSV 里的latency_ms一列就是最直接的稳定性指标,平均延迟正常不代表没有偶发超时,要看的是ERR行的数量和时间分布。

4. 避坑:smart200 的 done 为 0,与通讯测试里另外 4 个高频翻车点

测试做得多了就会发现一个共性:单次操作一切正常,一进循环就出妖。这里面一半是参数问题,一半是时序问题。下面 5 条是我调试现场最常碰到的,按现象、原因、解决的顺序说,看完能少走很多弯路。

4.1 Done 一直为 0:先查触发方式,再查连接建立

现象:西门子 SMART 200 用 MB_CLIENT 指令做 Modbus TCP 主站,EN 给通,REQ 也给信号了,但 Done 位始终为 0,Error 位也不变化。很多人第一反应是“从站没连上”,然后反复检查 IP 和端口,其实一大半情况不是连接问题。

最常见的第一个原因是 REQ 触发了。MB_CLIENT 是边沿触发指令,REQ 必须有一个从 0 到 1 的跳变,不能用常 ON 一直给高电平。如果你用 SM0.0 去驱动 REQ,只有程序第一次扫描能产生上升沿,之后永远没有新的请求,Done 自然一直为 0。解决方法是把 Done 位取反后接回 REQ,让上一条完成后自动发起下一条;或者用一个定时器,每 200ms 给一个脉冲。

第二个原因是库存储区分配错位。SMART 200 的 Modbus 库需要单独指定一块 V 区作为库存储区,这个区域不能和用户程序里用到的 V 区重叠。重叠后程序看起来在跑,实际数据互相踩踏,MB_CLIENT 的内部状态机根本走不完。解决方法是重新分配一个足够大的 V 区,常见做法是留出 300 字节,并确认程序其他地方没有使用这段地址。

第三个原因是连接压根没建立。SMART 200 的 MB_SERVER 同一时刻只能处理一个客户端连接。如果你的电脑正开着 Modbus Poll 连着从站,PLC 这边再发请求,只能排队等待,表现也是 Done 始终为 0。解决方法是先断开 PC 工具,再观察 PLC 的 Done 位是否翻转。抓包确认 TCP 握手有没有建立,一眼就能看出是哪一层卡住。

4.2 连接占满、地址差一、字节序反转与异常码:4 个高频问题

现象:Modbus Poll 还开着,另一边 PLC 的 MB_SERVER 或者另一个主站就连接失败;关掉 Poll 立刻恢复。原因就是上面说的连接数被占满。这不是玄学,是很多从站设备同时只允许一个 TCP 连接。解决方式是测完一个工具立刻断开,或者给 PC 工具配置空闲自动断开,不要挂着一晚上不关。

现象:通讯正常,但读回来的数值全是 0,或者整体偏移了一个寄存器。优先怀疑地址基准不一致。工具里填 0 可能对应文档里的 40001,也可能对应 40000,取决于文档用哪种标注方式。解决方法是先用一个已知值的寄存器做基准,比如从站手册里写明了某个寄存器固定为 0xC350,先读到这个值再定位偏移量。

现象:读回来的数值单个看没问题,但组合起来不对,比如一个 32 位浮点数拆成两个寄存器,数值完全对不上。这是字节序问题。Modbus 寄存器本身是大端,但不同设备会把高字低字反过来,甚至字节序也反转。解决方法是先读回一个已知值,比如 0x12345678,看报文里是12 34 56 78还是78 56 34 12,确定设备是哪种字节序后在代码里统一转换。

现象:返回异常码 0x02(非法数据地址)。原因是请求的起始地址加数量超过了从站实际寄存器范围。解决方法是逐步缩小数量,比如从 100 个改成 10 个,定位到具体哪个地址越界;另外检查功能码和地址区域是否匹配,用 0x03 去读输入寄存器也会得到异常码 0x02,因为在这种实现下那个地址不存在。

现象:PC 工具能通,自己写的程序就是连不上。先查三件事:第一,程序是否绑定了错误的网卡;第二,防火墙是否拦截了 502 端口;第三,端口是否被其他程序占用。排查命令很简单:

netstat -ano | findstr 502

如果有进程占着端口,先杀掉它。这类问题最坑的地方在于“工具能通”会误导你,让你一直去查从站配置,实际上问题在自己主机上。

5. 往后推一步:用 Wireshark 把“能连通”升级成“能复现异常”

前面说的都是基本连通测试。想把偶发超时这类问题定位准,我会再加一层:抓包对照。Wireshark 里用tcp.port == 502作为抓包过滤器,跑起来之后再叠加显示过滤器modbus,就能直接看到 Modbus TCP 解析后的字段,不需要自己数十六进制。

抓包之后我习惯看三个时间点。第一个是 TCP 三次握手,没有握手就说明请求根本没发出去,问题在自己主机;有握手但随后立刻 RST,说明对端拒绝了连接,查端口和 Unit ID。第二个是请求与响应的配对,用事务标识符让 Wireshark 自动关联,看响应里的功能码、地址和数据是否与请求一致。第三个是断开连接时有没有 FIN,如果从站不主动断开,主站也没有超时关闭机制,连接就会一直被占住,这就是偶发超时的经典来源。

定位偶发问题还有个技巧:不要只抓“出问题的那一刻”,而是开着抓包跑完整轮询,回头在 Wireshark 里按时间排序,对比延迟突然变大的几秒钟里网络层发生了什么。是 TCP 重传,还是从站迟迟不回响应?前者是链路问题,后者是从站固件或程序卡死了。有过一次血泪经验:Modbus Poll 点着全通,自己程序一跑就偶发超时,抓包才发现有个客户端连接没有释放,把从站的连接数占满了,后续请求全在排队。从那以后我做通讯测试都会顺手看一眼连接建立和断开,不能只看“有没有数据”,连接生命周期本身也是测试对象。希望帮到你。

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

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

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

立即咨询