Modbus 协议基础
写在前面
作者对 Modbus 并没有很深入的使用,只是工作中用到了 Modbus 的某些方面, 以下仅是作者在学习及实际使用 Modbus 后对它的一个理解与总结,如有不对的地方可与作者讨论
我们平常说的 Modbus 协议 是一种规定,它其实有多种实现,目前常见的标准实现主要有以下几种
- Modbus RTU: 基于 RS-485/RS-232 的串行通信,使用 CRC-16 校验确保数据完整性
- Modbus TCP: 基于 TCP/IP 以太网通信,依赖 TCP 协议自带的校验确保数据完整性
- Modbus ASCII: 也是基于串行通信,但是数据使用 ASCII 编码,以字符串形式呈现,传输效率低
还有 Modbus over TLS 及一些第三方实现的协议,市面上不为常见,这里也就不谈了
尽管 Modbus 协议 有多种实现,但它们的核心基本是一致的,我们只要了解了一个其它的也很容易就懂了
本文中会先讲协议通用部分,后面会针对不同协议做说明
通信架构
Modbus 采用主从模式通信架构(即一对多),由一个主机控制通信流程,多个从机只能被动响应
- 主机(Master): 唯一拥有通信发起权,主动发送请求帧(如读取数据、写入命令)
- 从机(Slave): 被动响应,仅在收到匹配自身地址的请求时回复,无法主动上报数据

通信过程
主机(Master)发起请求
- 构造请求帧(请求报文),通过串口(Modbus RTU/ASCII)或以太网(Modbus TCP)发送
- 请求帧示例(Modbus RTU 格式)
[01][03][00 01][00 04][2B 14] // Modbus RTU 请求帧格式从机(Slave)响应
- 从机执行接收到的功能码操作并返回数据
- 响应帧示例(Modbus RTU 格式)
[01][03][08][40 27 AE 14 41 C8 0 0][12 71] // Modbus RTU 响应帧格式主机处理响应结果,校验数据有效性(如 Modbus RTU 的 CRC 校验)
是的,通信过程就是这么简单,至于请求/响应帧如何构建我们后面会详细讲到
寄存器
我们想要通过 Modbus 对设备进行操作,无非就是要对设备进行一些设置或者读取一些信息,那么设备内肯定就会有存储这些数据的容器,存储数据的容器就是 寄存器
对于 Modbus 寄存器 而言,很多人在初学时会被这些什么线圈、散离输入等名词搞得云里雾里,其实想的太复杂,你可以简单的理解为是一些不同类型的容器,用来存放设备相关的数据,并提供了读/写的能力
Modbus 中有 4 种不同类型的寄存器,每种寄存器也有多个,我们在请求时会通过 功能码 来区分操作(读/写)哪种类型的寄存器, 通过 寄存器地址 来确定具体操作的是哪些寄存器
线圈寄存器(Coils)- 数据类型: 1 位(bit),存储值
0或1 - 读写属性: 可读可写(功能码:
0x01读、0x05写单个、0x0F写多个) - 地址范围: 00001 ~ 09999
- 用途理解: 控制设备开关量(继电器状态),因为
bit只能表示两种状态(0或1,也相当于OFF或ON),比如用于控制设备开关、指示灯开关等
- 数据类型: 1 位(bit),存储值
离散输入寄存器(Discrete Inputs)- 数据类型: 1 位(bit),存储值
0或1 - 读写属性: 只读(功能码:
0x02读) - 地址范围: 10001 ~ 19999
- 用途理解: 一般用于监测设备外部信号,比如监测设备某个按钮是按下还是松开的状态
- 数据类型: 1 位(bit),存储值
保持寄存器(Holding Registers)- 数据类型: 16 位(两个字节),存储值 0-65535(无符号)或 -32768~32767(有符号)
- 读写属性: 可读可写(功能码:
0x03读、0x06写单个、0x10写多个) - 地址范围: 40001 ~ 49999
- 用途理解: 这类寄存器就可以存放具体的数据量了,一般可用于存放设备可读写的配置、参数等,比如设置设备显示的时间,可以设置当然也能获取
输入寄存器(Input Registers)- 数据类型: 16 位(两个字节),存储值 0-65535(无符号)或 -32768~32767(有符号)
- 读写属性: 只读(功能码:
0x04读) - 地址范围: 30001 ~ 39999
- 用途理解: 与保持寄存器类似,但是只支持读取,一般用于存放设备的实时数据,比如温度传感器的温度值(有些设备可能会使用保持寄存器来存储这类数据)
数据交互
前面 通信架构 中说明了 Modbus 是主从架构,即 一个主机对多个从机,那么主机要怎么知道要对哪个从机进行操作呢?
是的,每个 Modbus 从机都会有一个地址: 从机地址 ,地址范围是 1-247,且不会重复
为什么从机地址是 1-247 ?
- 协议规范限制: Modbus 协议规定了协议帧中的从机地址字段为 8 位(也就是 1 字节),理论范围也就是 0-255 之间,但是这 256 个地址不会都给 Modbus 从机使用
- 0: 广播地址(仅 RTU/ASCII),若请求时指定从机地址是 0,那么这个请求会发送到所有从机,从机收到请求后不会响应。一般会用于给所有设备设置参数
- 1-247: 实际可用的从机地址(最大 247 个设备)
- 248-255: 保留未使用(部分厂商自定义)
- 历史遗留: 早期工业设备规模较小,247 个从机已满足需求,且 RS-485 总线负载能力有限
结合前文所讲,已经满足主从机交互条件,现在你大概也能知到 Modbus 通信需要哪些参数了
从机地址: 明确需要交互的是哪一台从机设备功能码: 明确操作(是读还是写)以及操作的寄存器类型(线圈寄存器、保持寄存器等)数据域: 明确操作的具体是哪些地址的寄存器以及实际的数据
另外除了上面这些参数外,我们还需要保证数据传输的完整性,所以 Modbus 还有 校验码 用来验证本次传输的数据是否完整。 不过这个校验规则并不是所有协议都相同( Modbus RTU 使用 CRC-16、Modbus ASCII 使用 LRC、Modbus TCP 则没有,因为 TCP 协议本身就具有数据可靠性)
提示
Modbus RTU / ASCII / TCP 这几种协议的通信都略有差别,我们后面会将这些协议分别来讲,你要知道的是不论哪种 Modbus 协议的请求/响应帧中都离不开上面这些参数, 交互时我们必须要将这些参数按照你使用的 Modbus 协议规定的帧格式进行构造
Modbus RTU
注意
之前说过 Modbus 不同协议的核心内容是基本是相同的,所以我会重点讲解一下 Modbus RTU 协议,了解了这个协议后其他的协议很容易就懂了
Modbus RTU 是使用最广泛的 Modbus 协议,是 Modbus 协议 的串口通信模式,它基于 RS-485/RS-232 物理层传输, RS-485 尤其常见,因为它支持多点通信、长距离传输(1200 米)、抗干扰能力强,且数据使用二进制编码,效率高、速度快(相对于 Modbus ASCII), 不过对时序要求严格,使用时需要格外注意波特率和帧间间隔(后面会说明)
帧字段说明: Modbus RTU 协议的请求/响应帧均遵循以下结构模板
[从机地址] [功能码] [数据区域] [CRC校验码]
| 字段 | 长度 | 说明 |
|---|---|---|
| 从机地址 | 1 字节 | 目标设备地址(1-247),广播地址为 0 |
| 功能码 | 1 字节 | 指定操作类型(比如 读保持寄存器、写线圈 等) |
| 数据域 | 可变长度 | 包含寄存器地址、数量、实际数据 |
| 校验码 | 2 字节 | CRC-16(循环冗余检验 16 位算法) |
注意:在不同的操作中 数据域 的结构可能会不一样,我们在下面会举例说明
提示
在看下面的例子前,你需要了解:协议帧中的寄存器地址都是 0x0000 - 0xFFFF,与实际寄存器的地址不一样,就比如 保持寄存器是 30001-39999, 这是因为 Modbus 将协议帧中的地址进行了偏移
| 寄存器类型 | Modbus 地址范围 | 协议帧地址范围 | 偏移值 |
|---|---|---|---|
| 线圈 | 00001 ~ 09999 | 0x0000 ~ 0xFFFF | 0 |
| 离散输入 | 10001 ~ 19999 | 0x0000 ~ 0xFFFF | 10000 |
| 输入寄存器 | 30001 ~ 39999 | 0x0000 ~ 0xFFFF | 30000 |
| 保持寄存器 | 40001 ~ 49999 | 0x0000 ~ 0xFFFF | 40000 |
公式:Modbus 地址 = 协议帧地址 + 偏移值 + 1
这里暂时先有个概念就行,看了下面的示例后自然会明白
读取寄存器- 示例:读取保持寄存器(功能码
0x03),读取寄存器地址范围40002-40005 - 请求帧:
[01][03][00 01][00 04][2B 14]从机地址 功能码 起始地址 读取数量 CRC [ 01 ] [ 03 ] [ 00 01 ] [ 00 04 ] [ 2B 14 ] 上面我们说了偏移值,就以这个请求帧来说:帧中的地址是
0x0001,因为功能码是03所以需要加上40000 + 1,由此得出实际寄存器地址为40002 - 响应帧:
[01][03][08][40 27 AE 14 41 C8 0 0][12 71]从机地址 功能码 数据长度 数据 CRC [ 01 ] [ 03 ] [ 08 ] [ 40 27 AE 14 41 C8 0 0 ] [ 12 71 ]
- 示例:读取保持寄存器(功能码
写入寄存器(单个)- 示例:写入单个保持寄存器(功能码
0x06),写入寄存器地址40003 - 请求帧:
[01][06][00 02][12 34][08 18]从机地址 功能码 地址 数据 CRC [ 01 ] [ 06 ] [ 00 02 ] [ 12 34 ] [ 08 18 ] - 响应帧:与请求帧相同
- 示例:写入单个保持寄存器(功能码
写入寄存器(多个)- 示例:写入多个保持寄存器(功能码
0x10),写入寄存器地址范围40001-40002 - 请求帧:
[01][10][00 00][00 02][04][12 34 56 78][2B 26]从机地址 功能码 起始地址 写入数量 数据长度 数据 CRC [ 01 ] [ 10 ] [ 00 00 ] [ 00 02 ] [ 04 ] [ 12 34 56 78 ] [ 2B 26 ] - 响应帧:与请求帧相同
- 示例:写入多个保持寄存器(功能码
提示
由于线圈/散离输入寄存器存储单位是一位(1 bit),所以 8 个寄存器才会占用 1 字节,9 个寄存器占用 2 字节...
例:[01][0F][00 00][00 04][01][05][8E 09] 表示写入线圈寄存器 00001-00004, 数据长度 1,写入数据 05(二进制表示 00000101),低位在前,所以寄存器 1=ON, 2=OFF, 3=ON, 4=OFF
写入单个线圈寄存器时,数据使用固定值表示:FF00(ON),0000(OFF)
例:[01][05][00 02][FF 00][8C 3A] 表示写入 ON 到地址 00003,写单个线圈的情况较少,更推荐直接使用 0x0F
注意
你也可能也注意到了,数据域中的 数据长度 是一个字节表示,也就代表最大只能是 255
其实这在 Modbus 标准文档中也规定了每帧长度不可超过 256 个字节,我们在使用时还要考虑到从机地址、功能码等字段占用的字节数, 所以理论读取时(0x03)最大可读取 125((256-6)/2)个寄存器,理论写入(0x10)最大 123((256-9)/2)个寄存器, 需要读取/写入更多寄存器时,我们需要考虑分批操作
时序 / 帧间隔
我们现在知道了交互中数据传输的格式,现在我们来说说如何区分完整的一帧(一次请求/响应)
我们要知道 Modbus RTU 是基于串口通信(UART)的,而在 UART 中传输的是字符流(依次发送从机地址、功能码...),UART 可不会管你传输的是从机地址还是什么数据,它有数据就发送, 你可以想想,如果连续发送多次请求时,两个 Modbus RTU 帧是不是就连在一起了? 而 Modbus RTU 帧中也没有"起始符"与"结束符"来标识一帧的开始与结束,那么我们要怎么来区分呢? 根据时间间隔 !
串口通信(UART)
如果你不知道串口通信(UART),请看这里,我这里就简单讲一下 UART 的传输机制
我们得知道,数据传输的本质是传输二进制(bit 也就是高电平与低电平),这对人类很不友好,我们如果直接通过二进制来做什么事的话就非常的困难, 所以 UART 给我们封装了更高层次的传输方式(以字节为单位),对于我们来说传输单位就是字节,这很棒!那它是怎么实现的呢?
UART 为了确保每字节的数据能够正常解析,所以制定了以下规则:
[起始位][数据位][校验位(可选)][停止位]
- 起始位(Start Bit): 表示数据开始,固定为
0(低电平) - 数据位(Data Bits): 这个是我们实际的数据,通常是 8 位(1 字节),按 从低位到高位(LSB) 传输
- 校验位(Parity Bit): 这个是可选的,None(无校验)、Odd(奇校验)、Even(偶校验)
- 停止位(Stop Bit): 表示数据结束,固定为
1(高电平)
有多种模式可以选择,Modbus 通常使用 8N1 模式,也就是数据是 8 位(1 字节)且无校验位
例如:8N1 模式
我们发送数据 0x12(二进制:00010010)时,对于我们来说发送的是 8 位(1 字节)数据,但实际发送的是 10 位, UART 给我们的数据包装了一下,加上了起始位与停止位,0 00010010 1,我们将这个数据称为 1 个字符,这样接收方如果也是 8N1 模式的话就能正常解析数据
Modbus RTU 规定:
一帧内的数据传输间隔时间必须 <= 1.5 个字符的时间,两帧之间的间隔必须 ≥ 3.5 个字符的时间
- 帧内间隔 ≤ 1.5 字符时间: 如果同一帧内两个字节之间的传输时间间隔超过 1.5 字符时间,那么会认为帧损坏(CRC 校验也会失败),该帧会被丢弃
- 帧间间隔 ≥ 3.5 字符时间: 如果线路空闲超过 3.5 字符时间,接收方就知道这帧结束了,准备接收下一帧。如果两帧之间小于 3.5 个字符,同样 CRC 校验也会失败
计算公式:1 字符时间 = (1 起始位 + 8 数据位 + 1 停止位 + 校验位) / 波特率
示例(波特率 9600):
Modbus ASCII
提示
因为这个协议在市面上很少见,在这里就简单的说明一下,能有个了解就好
Modbus ASCII 最主要的一点就是:它把所有数据(地址、功能码、数据域、校验码)都转换成 ASCII 字符(0-9, A-F),原本 1 个字节数据它用 2 个 ASCII 字符呈现
可以简单的理解为它把原始数据输出成了 16 进制的字符串显示
比如:寄存器值 0x1234(两个字节 0x12 、0x34)→ 转换为 ASCII 字符串 1234(4 个字符,我们知道 ASCII 编码中,一个字符使用一个字节编码,也就是 4 个字节)
帧字段说明
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧起始符 | 1 字节 | 固定 : ,表示本次请求开始 |
| 从机地址 | 2 ASCII 字符 | 目标设备地址(1-247),广播地址为 0 |
| 功能码 | 2 ASCII 字符 | 指定操作类型(比如 读保持寄存器、写线圈 等) |
| 数据域 | N × 2 ASCII 字符 | 包含寄存器地址、数量、实际数据 |
| 校验码 | 2 ASCII 字符 | LRC(纵向冗余检验) |
| 帧结束符 | 2 字节 | 固定 CR LF(回车+换行),表示本次请求结束 |
从上面的帧字段能看出,和 Modbus RTU 相比,除了主要使用 ASCII 编码外,还多出了两个字段 帧起始符 与 帧结束符,这与 Modbus RTU 的时序不同, Modbus ASCII 使用这两个字段构建一段完整的帧,这就相当于是手动控制一段请求的开始与结束,这就对时序没有要求
其次就是 Modbus ASCII 使用 LRC 进行校验计算,这与 Modbus RTU 的 CRC-16 也不同
Modbus TCP
现在我们来说说 Modbus TCP,故名思意它是基于 TCP/IP 网络的通信协议,默认使用 502 端口, 同样使用二进制编码进行数据传输(与 Modbus RTU 帧结构类似,但是增加了 MBAP 报文头,且去除了 CRC 校验)。 相比串口通信,TCP 速度快、距离远,特别适合 SCADA 系统(数据采集与监控系统)
Modbus TCP 报文格式

Modbus TCP 的报文由 MBAP 头(前 7 字节) + Modbus PDU(协议数据单元) 组成
- MBAP 报文头
- 事务标识(2 字节): request id,用于请求/响应的配对,一般由主机递增生成,从机原样返回
- 协议标识(2 字节): 固定为
0x0000(表示 Modbus 协议) - 字节长度(2 字节): 后续的字节数(从 单元标识 开始计算)
- 单元标识(1 字节): 设备地址,通常为
0xFF(可忽略),因为设备通过 IP 地址直接寻址
- Modbus PDU: 参考 Modbus RTU 帧结构
- 读(请求):
功能码(1 字节)起始地址(2 字节)数量(2 字节) - 读(响应):
功能码(1 字节)数据长度(1 字节)数据 - 写(请求):
功能码(1 字节)起始地址(2 字节)数量(2 字节)数据长度(1 字节)数据 - 写(响应):
功能码(1 字节)起始地址(2 字节)数量(2 字节)
- 读(请求):
提示
Modbus TCP 协议去除了 CRC 校验,因为 TCP 协议本身就是可靠的数据传输
由于是基于 TCP 协议, 所以也没有 Modbus RTU 所有的串行通信的波特率限制以及帧间隔时间
