Modbus 协议完全入门:主从通信、四大寄存器、8 个功能码与报文逐字节拆解
Modbus 协议完全入门:主从通信、四大寄存器、8 个功能码与报文逐字节拆解
Modbus 是工业自动化、物联网网关里出现频率极高的一种通信协议,PLC、变送器、传感器、电表、温控仪表几乎都支持它。本文从"主站/从站"讲起,系统梳理 Modbus 的三种传输模式、四大数据区、8 个常用功能码,并结合真实报文逐字节拆解读线圈(01H)和读保持寄存器(03H)的完整问答过程,最后讲清初学者最容易懵的"寄存器地址与偏移量"以及 CRC 校验。零基础可读,也适合作为工作中的查询手册。
一、Modbus 是什么
Modbus 是一种用于自动化设备之间数据传输的通信协议,它由 Modicon(现施耐德)于 1979 年提出,因为协议简单、公开、免费,成了工业领域事实上的通用语言。
它支持多种物理介质/传输方式,最常见的三种:
- Modbus-RTU:跑在串口(RS-232 / RS-485)上,二进制紧凑编码,使用 CRC 校验,工业现场最常用;
- Modbus-ASCII:同样可跑在串口上,但把数据编码成可见的 ASCII 字符(十六进制字符),使用 LRC 校验,可读性好、效率较低;
- Modbus-TCP:跑在以太网(TCP/IP)上,端口默认 502,在 RTU 数据基础上加了一个 MBAP 报文头,不再需要 CRC(TCP 本身已保证可靠性)。
主站与从站:一主多从、有问才有答
Modbus 是典型的主从(Master/Slave,现也称 Client/Server)协议,核心是"一主多从、主站问询、从站应答":
- 主站(Master / Modbus Poll):发起通信的一方,通常是上位机软件或控制器(可以用软件模拟);
- 从站(Slave / Modbus Slave):响应请求的一方,是各种硬件设备,如变送器、检测器、PLC、电表等。
通信永远由主站发起。例如主站想知道当前温度,就在软件上点击发送一条"问询报文",对应地址的从站收到后,再把温度数据放在"应答报文"里返回。从站不会主动说话。
在 Modbus Poll / Slave 这类调试软件中,常见缩写含义如下:
| 缩写 | 含义 |
|---|---|
| Tx | 发送出去的数据帧(Transmit) |
| Rx | 接收到的数据帧(Receive) |
| Err | 错误的数据帧(校验失败/异常应答) |
| ID | 从站编号(设备地址) |
| F | 功能码(Function) |
| SR | 帧间隔时间(请求间隔) |

二、报文长什么样:RTU 与 ASCII 的帧结构
Modbus 报文(协议数据)整体可以概括为:地址 + 功能码 + 数据 + 校验。RTU 模式和 ASCII 模式的封装略有不同。
ASCII 模式(以冒号 : 开头,以回车换行 CRLF 结尾,中间都是可见字符):
| 起始 | 地址 | 功能代码 | 数据数量 | 数据 1 … 数据 n | LRC 高 | LRC 低 | 回车 | 换行 |
|---|---|---|---|---|---|---|---|---|
: | 2 字符 | 2 字符 | … | … | 2 字符 | 2 字符 | CR | LF |
RTU 模式(纯二进制,靠帧间至少 3.5 个字符时间的空闲来分帧):
| 地址 | 功能代码 | 数据数量 | 数据 1 … 数据 n | CRC 低字节 | CRC 高字节 |
|---|
注意两个细节:
- RTU 的 CRC 是低字节在前、高字节在后(小端顺序),这和寄存器数据"高字节在前"正好相反,初学者很容易看反;
- ASCII 的 LRC 占一个字节,但在报文里用两个十六进制 ASCII 字符表示(高、低各一个字符)。
Modbus-TCP 则在功能码和数据前面加了一个 7 字节的 MBAP 头:
| 事务标识(2) | 协议标识(2,固定 0) | 长度(2) | 单元标识(1,即从站地址) | 功能码 + 数据 |
|---|
TCP 模式没有 CRC,因为 TCP 协议本身已经保证了数据的可靠与有序。

三、核心中的核心:四大数据区(寄存器模型)
Modbus 把设备里的数据抽象成四类,每一类都有自己的编号区间和读写属性。搞懂这张表,Modbus 就懂了一半。
| 数据区 | 俗称 | PLC(逻辑)地址 | 协议地址 | 位/字 | 读/写 | 典型用途 |
|---|---|---|---|---|---|---|
| 线圈 Coil | 0 区(0xxxx) | 00001–09999 | 0000H–FFFFH | 位(bit) | 可读可写 | 控制设备的开/关,0=OFF,1=ON |
| 离散输入 Discrete Input | 1 区(1xxxx) | 10001–19999 | 0000H–FFFFH | 位(bit) | 只读 | 设备状态检测:急停按钮、行程开关位置 |
| 输入寄存器 Input Register | 3 区(3xxxx) | 30001–39999 | 0000H–FFFFH | 字(16bit) | 只读 | 数据采集:温度、电压、电流、转速 |
| 保持寄存器 Holding Register | 4 区(4xxxx) | 40001–49999 | 0000H–FFFFH | 字(16bit) | 可读可写 | 参数设置:运行速度、调频、定时、阈值、通信地址 |
一个非常实用的记忆口诀:
- 开关量(0/1):要"控制"就找线圈(可写);只能"看"状态就找离散输入(只读);
- 数值量(16 位字):设备采集上来、只读的是输入寄存器(温度/电压等);需要设置、可读可写的是保持寄存器(如机器转速、阈值)。
名词说明:Holding Register 标准译名为"保持寄存器"(部分资料/口语也写作"保存寄存器",指同一个东西)。"寄存器"就是从站设备中存放数据的一块区域,例如地址 40001、30002。
四、8 个常用功能码
对四大数据区的"读/写、单个/多个"操作,组合起来就是 Modbus 最常用的 8 个功能码。
| 功能码 | 描述 | PLC 地址 | 位/字操作 | 操作数量 |
|---|---|---|---|---|
| 01H | 读线圈寄存器 | 00001–09999 | 位 | 单个或多个 |
| 02H | 读离散输入寄存器 | 10001–19999 | 位 | 单个或多个 |
| 03H | 读保持寄存器 | 40001–49999 | 字 | 单个或多个 |
| 04H | 读输入寄存器 | 30001–39999 | 字 | 单个或多个 |
| 05H | 写单个线圈寄存器 | 00001–09999 | 位 | 单个 |
| 06H | 写单个保持寄存器 | 40001–49999 | 字 | 单个 |
| 0FH(15) | 写多个线圈寄存器 | 00001–09999 | 位 | 多个 |
| 10H(16) | 写多个保持寄存器 | 40001–49999 | 字 | 多个 |
规律非常好记:
- 01/02/03/04 都是"读",依次对应线圈、离散输入、保持寄存器、输入寄存器;
- 05/06 是"写单个",分别写单个线圈、单个保持寄存器;
- 15(0FH)/16(10H) 是"写多个",分别写多个线圈、多个保持寄存器;
- 位操作(线圈/离散输入)的最小单位是 bit;字操作(寄存器)的最小单位是 16 bit(2 个字节)。
五、报文实例精讲(逐字节拆解)
理论看完,下面用两个真实例子把"问询—应答"完整走一遍,这是理解 Modbus 最快的方式。
5.1 实例一:03H 读保持寄存器(最常用)
需求:读取 11H(17)号从站的保持寄存器,起始协议地址为 006BH(=107),读取 2 个寄存器。
主站询问报文:
| 从站地址 | 功能码 | 起始寄存器(高位) | 起始寄存器(低位) | 寄存器数量(高位) | 寄存器数量(低位) | CRC |
|---|---|---|---|---|---|---|
| 0x11 | 0x03 | 0x00 | 0x6B | 0x00 | 0x02 | XXXX |
从站应答报文:
| 从站地址 | 功能码 | 字节计数 | 40108(高位) | 40108(低位) | 40109(高位) | 40109(低位) | CRC |
|---|---|---|---|---|---|---|---|
| 0x11 | 0x03 | 0x04 | 0x02 | 0x2B | 0x01 | 0x06 | XXXX |
逐字节解读:
- 请求里
00 6B= 107,是协议地址(从 0 开始的偏移);00 02表示读 2 个保持寄存器; - 应答里
04是字节计数:2 个寄存器 × 每个 2 字节 = 4 字节; - 数据按"高字节在前"排列为
02 2B 01 06,所以:- 40108 寄存器的值 = 0x022B
- 40109 寄存器的值 = 0x0106
5.2 重点:寄存器地址与偏移量(Offset)的换算
这是初学者最容易懵的地方,Modbus Poll 软件里显示的 00000、00107 并不是物理地址,而是从 0 开始的协议地址(偏移量),必须结合功能码换算成大家熟悉的 5 位 PLC 地址:
- 功能码 03/06/16 操作保持寄存器(4 区),软件里偏移 00000 对应 40001;
- 偏移
00107(=107)对应 40001 + 107 = 40108; - 读 2 个,就是 40108 和 40109(40108 + 2 − 1)。
通用换算关系:
PLC(逻辑)地址 = 区起始地址 + 协议偏移量
例:保持寄存器 40001 + 107 = 40108
协议偏移量 = PLC 地址 − 区起始地址
例:40108 − 40001 = 107 = 0x006B
各区起始地址:线圈 00001、离散输入 10001、输入寄存器 30001、保持寄存器 40001。
5.3 再看一条真实问询帧
笔记中抓到的一条 RTU 帧:
Tx:064 - 01 03 00 00 00 0A C5 CD
| 字段 | 值 | 含义 |
|---|---|---|
| 064 | — | 这是第 64 次问询(软件计数,不是报文内容) |
| 01 | 从站地址 | 问询 1 号从站 |
| 03 | 功能码 | 读保持寄存器 |
| 00 00 | 起始地址 | 从偏移 0(即 40001)开始 |
| 00 0A | 寄存器数量 | 读 10 个(0x0A = 10) |
| C5 CD | CRC | 冗余校验,检测报文在传输中是否出错 |
5.4 实例二:01H 读线圈(按位返回)
需求:从站地址为 01H,读取线圈的起始地址为 0017H,读取 38(十进制,即 26H) 个线圈。
【插图 4:功能码 01H 读线圈的发送与响应帧】(对应图片
消息格式1.png,发布时在此上传)
主站询问报文:
| 从站地址 | 功能码 | 起始地址高位 | 起始地址低位 | 数量高位 | 数量低位 | CRC 高位 | CRC 低位 |
|---|---|---|---|---|---|---|---|
| 01 | 01 | 00 | 17 | 00 | 26 | 0D | D4 |
从站应答报文:
| 从站地址 | 功能码 | 返回字节数 | 数据 1 | 数据 2 | 数据 3 | 数据 4 | 数据 5 | CRC 高位 | CRC 低位 |
|---|---|---|---|---|---|---|---|---|---|
| 01 | 01 | 05 | CD | 6B | B2 | 0E | 1B | 44 | EA |
解读要点:
- 线圈是**按位(bit)**返回的:每个 bit 对应一个线圈的状态,1 代表 ON,0 代表 OFF;
- 一个字节有 8 个 bit,所以 38 个线圈需要 ⌈38 / 8⌉ = 5 个字节,这就是返回字节数
05的由来; - 当线圈数量不是 8 的倍数时,最后一个字节多余的高位补 0;
- 字节内线圈的排列从**最低位(LSB)**开始对应第一个线圈。例如数据第一字节
CDH = 1100 1101,就依次表示了前 8 个线圈的通断状态。
六、CRC / LRC 校验是干什么的
报文最后的 CRC / LRC 是校验码,用来检测数据在串口传输过程中是否因为干扰而出错:
- RTU 用 CRC-16(循环冗余校验):发送方对"地址 + 功能码 + 数据"按固定多项式(Modbus 用 0xA001)算出 2 字节 CRC,附在帧尾(低字节在前);接收方用同样算法重算并比对,不一致就丢弃该帧(软件里表现为 Err / 超时)。
- ASCII 用 LRC(纵向冗余校验):对前面各字节求和取反加一,结果 1 字节,用两个 ASCII 字符表示。
实际开发中一般不需要手写 CRC 算法——串口调试助手会自动算,NModbus4 等库也会自动处理。理解"它用来保证帧没传错、且 RTU 低字节在前"即可,需要手写时再查标准算法表。
七、动手实践:模拟软件与 C# 代码
7.1 没有硬件也能学:Modbus Poll + Modbus Slave
学习和调试阶段不需要真实设备:
- Modbus Slave:模拟从站,配置好从站 ID、寄存器数据,相当于一台虚拟仪表;
- Modbus Poll:模拟主站,选择功能码、起始地址、数量后发送请求并显示应答;
- 串口模式下可用虚拟串口软件(如 VSPD)创建一对互联的 COM 口,分别给 Poll 和 Slave 使用;TCP 模式下直接连
127.0.0.1:502。
在 Modbus Poll 中:
- Alias(列名):给某个寄存器地址自定义一个名字(如"温度设定"),方便辨认;
- 地址栏的
00000是偏移量,配合功能码决定从哪个寄存器开始(见 5.2)。
7.2 C# 中使用 NModbus4
在 C# / WinForms 项目里操作 Modbus,常用开源库 NModbus4(NuGet 直接搜索安装),它把组帧、CRC、解析都封装好了。读保持寄存器的核心代码大致如下(具体 API 随库版本略有差异):
串口 RTU 模式:
using (SerialPort port = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One))
{
port.Open();
var adapter = new SerialPortAdapter(port);
// 创建 RTU 主站
IModbusSerialMaster master = ModbusSerialMaster.CreateRtu(adapter);
// 读 1 号从站、从地址 0 开始的 10 个保持寄存器(对应 40001~40010)
ushort[] values = master.ReadHoldingRegisters(
slaveAddress: 1, startAddress: 0, numberOfInputs: 10);
Console.WriteLine(string.Join(", ", values));
}
TCP 模式:
using (TcpClient client = new TcpClient("127.0.0.1", 502))
{
// 创建 TCP 主站
var master = ModbusIpMaster.CreateIp(client);
// 读 1 号从站、地址 107 开始的 2 个保持寄存器(对应 40108、40109)
ushort[] values = master.ReadHoldingRegisters(1, 107, 2);
}
写操作对应的方法是 WriteSingleRegister / WriteMultipleRegisters / WriteSingleCoil / WriteMultipleCoils,命名与功能码一一对应。
八、常见疑问与易错点
- 软件里的地址 00000 和手册里的 40001 对不上?
一个是从 0 开始的协议偏移,一个是从 1 开始的 PLC 逻辑地址,相差一个区起始值(保持寄存器相差 40001)。换算见 5.2。 - 读出来是乱码 / 一直超时?
先查串口四参数(波特率、数据位、校验位、停止位)是否一致、从站 ID 是否正确、RS-485 的 A/B 是否接反;TCP 则查 IP 和端口 502。 - CRC 到底谁在前?
RTU 帧尾 CRC 低字节在前、高字节在后;寄存器数值则是高字节在前,方向相反。 - 线圈为什么几个状态被打包成一个字节?
线圈是位操作,8 个线圈塞在 1 个字节里返回,从最低位开始对应,不足 8 个时高位补 0。 - 保持寄存器和输入寄存器怎么选?
要下发/修改参数用保持寄存器(4 区,可读写);设备采集上来、只读的测量值用输入寄存器(3 区,只读)。 - 一个寄存器能表示多大的数?
标准寄存器是 16 位无符号数(0~65535);表示负数、32 位整数或浮点数时,需要设备手册约定字节序并把相邻 2 个寄存器拼起来解析。
九、小结
- Modbus 是主从式、问询—应答协议,主站发起、从站回答,支持 RTU / ASCII / TCP 三种传输;
- 数据分四类:线圈(0 区,位,读写)、离散输入(1 区,位,只读)、输入寄存器(3 区,字,只读)、保持寄存器(4 区,字,读写);
- 8 个功能码覆盖"读/写 × 单个/多个":01–04 读、05/06 写单个、15/16 写多个;
- 报文 = 地址 + 功能码 + 数据 + 校验,RTU 用 CRC(低字节在前),ASCII 用 LRC,TCP 用 MBAP 头、无校验;
- 软件显示的是从 0 开始的偏移地址,要加上区起始值才是 40001 这类 PLC 地址;
- C# 开发直接用 NModbus4,调试用 Modbus Poll / Slave 即可在单机完成全流程验证。
理解了"四大数据区 + 8 个功能码 + 一条报文的逐字节含义",再去对接任何 Modbus 设备,基本都能照着手册快速调通。
更多推荐


所有评论(0)