简介:这份PDF资料面向工业自动化、嵌入式开发及现场调试人员,系统梳理Modbus协议的功能码体系与报文实例,帮助读者理解主从设备间的数据交互逻辑。内容覆盖01至19号常用功能码,逐一说明读取线圈、输入状态、保持寄存器、输入寄存器以及强置线圈、预置寄存器等操作的作用与差异,并给出读可读写数字量、读只可读数字量、写数字量、读可读写模拟量、写单个模拟量等典型场景的完整命令帧与响应帧示例,逐字段解析设备地址、命令号、起始地址、寄存器数量与CRC校验的构造方式。资源包为1个PDF文件,约10KB,轻量便携,适合随时查阅。目前已有2326人学习下载,可作为Modbus通信入门与报文排错的速查手册。
1. Modbus 通讯功能码:从一份 PDF 到能跑通的读写链路
很多人第一次接触 Modbus,是从一份叫《Modbus通讯功能码及实例.pdf》的文档开始的。打开一看,功能码 01、02、03、04、05、06、0F、10 排成一列,每个后面跟着请求帧和响应帧的十六进制示例,看着挺清楚,合上文档却不知道从哪下手。问题不在文档,在于它只告诉你「帧长什么样」,没告诉你「这些帧在真实设备上怎么发出去、怎么收回来、错了怎么查」。
Modbus 通讯功能码本质是一套约定:主站用哪个数字告诉从站「我要读你的线圈」还是「我要写你的保持寄存器」,从站按约定回什么。它跑在 Modbus RTU(串口)或 Modbus TCP(以太网)之上,前者靠 CRC 校验,后者靠 MBAP 头。搞懂功能码,等于拿到了读写 PLC、传感器、数控机床运行状态数据的钥匙。这篇面向的是要动手接线、写代码、调通一条 Modbus 链路的工程师,不是背协议概念的考生。
2. 功能码到底在读写什么:四类数据区与请求帧结构
2.1 线圈、离散输入、保持寄存器、输入寄存器不是一回事
Modbus 把从站的数据分成四个区,功能码就是针对这四个区的读写动作。搞混这四个区,是新手翻车的头号原因。
| 数据区 | 读写属性 | 位/字 | 常用功能码 | 典型用途 |
|---|---|---|---|---|
| 线圈 Coil | 读写 | 位 | 01 读、05 写单个、0F 写多个 | 继电器输出、启停控制 |
| 离散输入 Discrete Input | 只读 | 位 | 02 | 限位开关、按钮状态 |
| 保持寄存器 Holding Register | 读写 | 字 | 03 读、06 写单个、10 写多个 | 参数设定值、PLC 寄存器 |
| 输入寄存器 Input Register | 只读 | 字 | 04 | 温度、电流等采集量 |
线圈和离散输入都是「位」,区别在读写权限;保持寄存器和输入寄存器都是「字」(16 位),区别同样在权限。热搜里常出现的「modbus 线圈 寄存器 的区别」,答案就在这张表里:线圈是位、可写,寄存器是字、保持寄存器可写而输入寄存器只读。
2.2 请求帧的字段拆解:从站地址、功能码、起始地址、数量、CRC
以 Modbus RTU 读保持寄存器(功能码 03)为例,主站发出的请求帧固定是 8 个字节:
[从站地址 1B][功能码 1B][起始地址高 1B][起始地址低 1B][数量高 1B][数量低 1B][CRC低 1B][CRC高 1B]假设从站地址 1,读起始地址 0 开始的 2 个保持寄存器,帧就是:
01 03 00 00 00 02 C4 0B逐字段看:01是从站地址,03是功能码,00 00是起始地址 0,00 02是读 2 个寄存器,C4 0B是 CRC16 校验(注意低字节在前)。响应帧则是:
01 03 04 XX XX YY YY CRC04是字节数(2 个寄存器 = 4 字节),后面 4 字节是数据,最后 2 字节 CRC。看懂这个结构,任何功能码的帧你都能自己推。
2.3 功能码 01/02/03/04/05/06/0F/10 的适用边界
- 01、02 读位,响应里用字节数 + 位打包,读 9 个线圈会返回 2 字节,第 2 字节高位补 0。
- 03、04 读字,响应里字节数 = 寄存器数 × 2。
- 05 写单个线圈,数据字段固定
FF 00表示 ON,00 00表示 OFF,写别的值从站会拒绝。 - 06 写单个保持寄存器,直接把 2 字节值放进数据字段。
- 0F 写多个线圈、10 写多个保持寄存器,请求里带字节数,响应里回显起始地址和数量。
选功能码的原则很简单:能读单个就别读一片,能写单个就别写一片,减少一次交互的数据量,链路更稳。但批量采集场景下,10 和 0F 能显著减少轮询次数,该用还得用。
3. 用 Python 跑通第一条 Modbus RTU 读写链路
3.1 环境准备与最小可跑代码
Python 里最省事的是pymodbus,串口依赖pyserial。先装:
pip install pymodbus pyserial下面是一段读保持寄存器 + 写单个线圈的最小代码,串口参数按你的设备改:
from pymodbus.client import ModbusSerialClient import time # 串口参数:波特率、数据位、校验、停止位必须和从站一致 client = ModbusSerialClient( port='COM3', # Linux 下是 /dev/ttyUSB0 baudrate=9600, bytesize=8, parity='N', # 无校验 stopbits=1, timeout=1 # 超时 1 秒,太短会误判掉线 ) if not client.connect(): print("串口打开失败,检查端口号和占用情况") exit() # 读从站 1 的保持寄存器,起始地址 0,读 2 个 rr = client.read_holding_registers(address=0, count=2, slave=1) if not rr.isError(): print("寄存器值:", rr.registers) else: print("读失败:", rr) # 写从站 1 的线圈 0 为 ON wr = client.write_coil(address=0, value=True, slave=1) print("写线圈结果:", wr) client.close()逻辑说明:ModbusSerialClient负责串口收发和 CRC 计算,你不用自己拼帧。read_holding_registers对应功能码 03,write_coil对应功能码 05。slave参数就是从站地址,必须和从站实际设置一致,否则从站不响应,表现为超时。
参数说明:baudrate常见 9600 或 19200,parity常见N(无)、E(偶)、O(奇),这三个参数和stopbits只要有一个和从站不一致,通信就全乱。timeout建议 1 秒,串口链路本身慢,设 0.1 秒容易误报。
3.2 用 Modbus Poll / Modbus Slave 先验证再写代码
血泪经验:不要一上来就写代码调。先用 Modbus Slave 模拟一个从站,用 Modbus Poll 当主站,把功能码、地址、数量点一遍,确认帧能通,再换成自己的代码。这样能把「协议问题」和「代码问题」分开。
Modbus Slave 里设置从站地址、功能码区域(比如 Holding Registers)、起始地址,然后在对应格子里填值。Modbus Poll 里设同样的从站地址、功能码 03、起始地址 0、数量 2,点连接。如果 Poll 里能看到 Slave 里填的值,说明链路和参数都对。这时候再跑上面的 Python 代码,成功率会高很多。
热搜里「modbus slave 密钥」「modbus poll 密钥」这类词,本质是找工具授权,但验证链路用试用版或免费替代工具(比如diagslave、modbuspal)完全够用,不必卡在授权上。
3.3 地址映射:为什么你读的 40001 和代码里的 0 对不上
这是最高频的翻车点。设备手册上写「温度在 40001」,你代码里写address=40001,结果报非法数据地址。原因是 Modbus 有两种地址表示法:
- 协议地址(PDU 地址):从 0 开始,代码里用的就是这个。
- 数据模型地址(PLC 地址):40001 表示保持寄存器区第 1 个,对应协议地址 0。
换算规则:40001 → 0,40002 → 1,30001 → 0(输入寄存器),00001 → 0(线圈),10001 → 0(离散输入)。所以手册写 40001,代码里写address=0;手册写 40100,代码里写address=99。
不同设备的 Modbus 地址映射表不一定一样,有的厂商直接给协议地址,有的给 PLC 地址,还有的偏移量自己定。拿到手册先确认它用的是哪种,别想当然。
4. Modbus TCP 与 RTU 的差异:功能码一样,封装不一样
4.1 MBAP 头:TCP 比 RTU 多出来的 7 个字节
功能码在 TCP 和 RTU 里完全一样,03 还是 03,10 还是 10。区别在帧的封装。Modbus TCP 在 PDU 前面加了 7 字节的 MBAP 头:
[事务标识 2B][协议标识 2B][长度 2B][单元标识 1B][功能码...][数据...]事务标识用来匹配请求和响应,协议标识固定 0,长度表示后面还有多少字节,单元标识在 TCP 里通常当从站地址用(网关场景下有意义)。因为 TCP 本身有校验,Modbus TCP 不再带 CRC。
用 Python 跑 TCP 只需换客户端类:
from pymodbus.client import ModbusTcpClient # TCP 默认端口 502 client = ModbusTcpClient(host='192.168.1.10', port=502, timeout=1) client.connect() rr = client.read_holding_registers(address=0, count=2, slave=1) print(rr.registers if not rr.isError() else rr) client.close()逻辑说明:ModbusTcpClient自动处理 MBAP 头,slave参数对应单元标识。TCP 链路比串口快得多,timeout可以设小一点,但网络抖动时仍建议留 1 秒。
4.2 端口、单元标识与网关场景
Modbus TCP 默认端口 502,但很多设备或网关会改成别的端口,连不上先确认端口。单元标识在直连设备时经常被忽略(填 0 或 1 都行),但在 Modbus RTU 转 TCP 的网关后面,单元标识就是真实从站的地址,填错就读不到。
热搜里「fx5u modbus tcp 主站功能」「汇川 easy 怎么做 modbus 从站」这类问题,本质都是设备侧的角色配置:谁当主站、谁当从站、地址怎么映射。三菱 FX5U 做 TCP 主站时,用内置以太网口,配置里指定对方 IP 和端口,功能码用 03/10 读写;汇川 Easy 系列做从站时,要在软件里使能从站功能并设定从站地址和寄存器映射表。这些配置各厂商界面不同,但底层功能码和帧结构不变。
4.3 用 Wireshark 抓 Modbus TCP 报文定位问题
TCP 的好处是可以抓包。Wireshark 里过滤modbus或tcp.port == 502,能看到完整的请求和响应。如果请求发出去了但没响应,看是不是从站地址错了;如果响应是异常帧(功能码最高位置 1,比如 03 变成 83),看异常码:
- 01 非法功能:从站不支持这个功能码。
- 02 非法数据地址:地址超出从站范围。
- 03 非法数据值:写入的值不合法。
- 04 从站设备故障:从站内部错误。
异常码是排查的黑匣子,比盲猜快得多。RTU 场景下没有 Wireshark 这么方便,但可以用串口助手看原始字节,或者用 Modbus Poll 的报文监视窗口。
5. 避坑与排查:功能码调不通时先看这几条
5.1 现象:请求发出去,从站完全不响应
原因:从站地址不对、串口参数不一致、接线 A/B 反了、从站没上电或没使能。RTU 里 A/B 线接反是最隐蔽的,因为有时候反了也能通,但误码率高。
解决:先用 Modbus Poll 扫从站地址(很多工具支持地址扫描),确认地址;核对波特率、校验、停止位;用万用表量 A/B 线电压,或者直接对调试试;确认从站设备处于运行状态。
5.2 现象:响应是异常帧,异常码 02
原因:读写的地址超出从站实际支持的范围。常见于把 PLC 地址 40001 直接填进代码,或者从站只映射了 0 到 99 而你要读 200。
解决:查设备手册的地址映射表,确认协议地址范围;用 Modbus Poll 从地址 0 开始小批量试读,逐步加数量,找到边界。
5.3 现象:读回来的寄存器值明显不对,比如温度是 65000
原因:字节序或字序问题。Modbus 寄存器是 16 位,但 32 位数据(浮点、长整型)要占两个寄存器,高字低字谁在前,不同厂商不一样。还有的厂商在一个寄存器内高低字节也反。
解决:先读单个寄存器确认原始值,再判断是字节交换还是字交换。pymodbus里可以用payload手动处理,或者用struct按厂商文档解。常见组合有 ABCD、CDAB、BADC、DCBA 四种,试一遍就知道。
5.4 现象:写单个线圈用 05,写 0x0100 被拒绝
原因:功能码 05 的数据字段只认FF 00(ON)和00 00(OFF),写其他值从站返回异常码 03。
解决:写线圈用布尔值,不要自己拼十六进制。pymodbus的write_coil(value=True/False)会自动处理。
5.5 现象:轮询几十个寄存器时偶尔超时
原因:串口链路带宽有限,一次读太多寄存器,响应帧变长,加上从站处理时间,容易超时。或者轮询间隔太短,从站还没处理完下一个请求就来了。
解决:把大批量读拆成多次小批量,每次读 10 到 20 个寄存器;轮询间隔留足,RTU 下建议至少 100 毫秒;timeout适当放大到 2 秒。TCP 下可以并发,但要注意从站是否支持多连接。
6. 进阶:用功能码 10 批量写 + 校验码在线计算自检
批量写保持寄存器(功能码 10)是参数下发场景的常用手段。下面这段代码一次写 4 个寄存器:
from pymodbus.client import ModbusSerialClient client = ModbusSerialClient(port='COM3', baudrate=9600, parity='N', stopbits=1, timeout=1) client.connect() # 从地址 10 开始写 4 个寄存器,值分别是 100,200,300,400 values = [100, 200, 300, 400] wr = client.write_registers(address=10, values=values, slave=1) print("批量写结果:", wr) client.close()逻辑说明:write_registers对应功能码 10,请求帧里会带起始地址、寄存器数量、字节数和数据,响应帧回显起始地址和数量。参数上,values列表长度就是寄存器数量,地址是起始协议地址。注意一次写太多寄存器可能超过从站缓冲区,一般不超过 123 个。
调试时如果手头没有工具,可以用在线 CRC 计算器自检。把请求帧去掉 CRC 的字节输进去,算出 CRC 和实际抓到的对比。比如01 03 00 00 00 02的 CRC 应该是C4 0B(低字节在前)。对不上说明帧拼错了,或者字节序理解反了。
我自己的习惯是:每调一个新设备,先用 Modbus Poll 把要用的功能码全点一遍,把请求帧抄下来,再用代码复现同样的帧。这样代码跑不通时,能确定是代码问题而不是协议理解问题。功能码这东西,看十遍 PDF 不如亲手发一帧、收一帧。希望帮到你。
本文还有配套的精品资源,点击获取