Modbus RTU 是一种基于串口通信(RS-232/RS-485)的主从式通信协议。主站(Master)发起请求报文,从站(Slave)响应报文。通信采用二进制帧结构,带 CRC16 校验,传输效率高,广泛应用于工业自动化领域。

Modbus协议采用主从(以太网的客户端-服务器)架构实现的请求-响应协议。其中有1个中央的主设备(询问器或主机Master)和理论最多 247 个从设备(响应器或从机Slave)连接到同一个网络。
- Modbus阐明了协议类型(协议层),而RS485定义了协议的信号电平(物理层)。
- 注意:以下示例中,CRC16校验码 是乱写的。如需手动校验 CRC16,可以参考这里 CRC在线计算 ,算法选择
CRC-16-MODBUS
。
功能码
常见功能码示例:
0x01
: 读线圈状态 (Read Coils)0x02
: 读离散输入状态 (Read Discrete Inputs)0x03
: 读保持寄存器 (Read Holding Registers)0x04
: 读输入寄存器 (Read Input Registers)0x05
: 写单个线圈 (Write Single Coil)0x06
: 写单个寄存器 (Write Single Register)0x0F
: 写多个线圈 (Write Multiple Coils)0x10
: 写多个寄存器 (Write Multiple Registers)
异常码
当原始请求功能码 + 0x80(即最高位置 1),即为异常功能码。
异常码是单字节值(范围 0x01
–0x0B
),定义了具体的错误类型。以下是标准异常码及含义:
异常码 | 名称 | 原因说明 | 常见场景 |
---|---|---|---|
0x01 | 非法功能码(Illegal Function) | 从站不支持请求的功能码。 | 主站请求了设备未实现的功能(如旧设备不支持 0x18)。 |
0x02 | 非法数据地址(Illegal Data Address) | 请求的地址超出设备支持范围。 | 读取地址 0x0500 ,但设备仅支持 0x0000 –0x04FF 。 |
0x03 | 非法数据值(Illegal Data Value) | 请求中的数据值无效(如写入值超出范围)。 | 写入线圈时发送 0x02 (非 0x00/0xFF),或写入寄存器值超出设备量程。 |
0x04 | 服务器设备故障(Server Device Failure) | 从站内部错误(如硬件故障、内存损坏)。 | 设备过热、固件崩溃或硬件异常。 |
0x05 | 确认(Acknowledge) | 从站已接收请求,但需较长时间处理(建议主站稍后重试)。 | 设备正在执行耗时操作(如电机启动)。 |
0x06 | 服务器设备忙(Server Device Busy) | 从站正处理其他请求,无法立即响应。 | 高并发请求时设备资源不足。 |
0x07 | 负数确认(Negative Acknowledge) | 从站无法执行请求(如未配置网关路由)。 | 网关设备无法转发请求到目标子设备。 |
0x08 | 内存奇偶校验错误(Memory Parity Error) | 从机检测到内存错误(较少见)。 | 设备内存硬件故障。 |
0x0A | 网关路径不可用(Gateway Path Unavailable) | 网关无法建立到目标设备的连接(TCP/RTU 转换失败)。 | 网关配置错误或目标设备离线。 |
0x0B | 网关目标设备无响应(Gateway Target Device Failed to Respond) | 网关已发送请求,但目标设备未响应。 | 目标设备故障或通信中断。 |
注:异常码
0x09
和0x0C
–0xFF
为保留码,标准未定义。
从机地址
从机地址占一个字节,同一个 RS485 总线上的设备从机地址必须唯一。
- 当从机地址为 0 的时候,视为广播消息。
- 可用地址 1 ~ 247
- 保留地址 248 ~ 255
单播和广播
支持单播和广播两种模式。
单播消息
- 主节点向从节点发送明确的命令并处理响应。
- 主节点同一时间只能启动一个Modbus事务。一个MODBUS事务包括2条消息:主站的请求和从站的回复。
- 从节点不会主动发起通讯。
- 从节点之间永远不会相互通信。
广播消息
- 从机地址为 0。
- 无响应(No Response)广播消息不要求任何从站返回响应。主站发送后立即结束本次通信,不会等待从站应答(即使从站成功执行指令)。
- 广播仅支持写入类指令,禁止使用读取类功能码(否则会导致网络冲突或无意义行为)。支持的功能码包括:0x05、0x06、0x0F、0x10.
- 常用语:同步控制、系统初始化、时间同步等。
寄存器地址
modbus
实际上并非使用PLC
地址作为读写地址,而是使用modbus
地址。由于modbus
地址是从0开始的,其换算规则如下:
地址类型 | PLC地址范围 | modbus地址换算 |
---|---|---|
线圈地址范围 | 1 ~ 10000 | = PLC地址-1 |
离散量地址范围 | 10001 ~ 20000 | = PLC地址-10001 |
输入寄存器地址范围 | 30001 ~ 40000 | = PLC地址-30001 |
保持寄存器地址范围 | 40001 ~ 50000 | = PLC地址-40001 |
比如某个属性的地址为411234
,则实际上modbus
读写的地址为411234-410001=1233
。
CRC校验
- 计算 modbusRTU 的 CRC16 校验码的代码
function modbusCRC16(buffer) {
let crc = 0xFFFF;
for (let b of buffer) {
crc ^= b;
for (let i = 0; i < 8; i++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc & 0xFFFF;
}
// 示例
const message = [0x01, 0x03, 0x00, 0x00, 0x00, 0x0A];
const crc = modbusCRC16(message);
console.log("CRC16:", crc.toString(16).toUpperCase());
线圈
- 一个线圈对应一个继电器(因为继电器里面有电磁线圈)
- 每一个线圈有独立的 2 字节地址
- 默认读线圈是读取多个
- 支持单个线圈写入和多个线圈写入
特性 | 单个线圈写入 (0x05) | 多个线圈写入 (0x0F) |
---|---|---|
操作粒度 | 1 个线圈 | 1~1968 个连续线圈 |
数据格式 | 直接值(0xFF00/0x0000) | 位打包(每字节 8 个线圈) |
请求长度 | 固定 6 字节 | 可变(最小 8 字节) |
效率 | 低(逐个操作) | 高(批量操作) |
适用场景 | 单点控制(如启动/停止电机) | 批量配置(如初始化设备状态) |
数据类型
参考 Modbus Poll,常见数据格式:
类型 | 长度 | 说明 |
---|---|---|
Coils | 1 bit | 开关量输出 |
Discrete Inputs | 1 bit | 开关量输入 |
Short | 16-bit 有符号整数 | |
Unsigned Short | 16-bit 无符号整数 | |
Long | 32-bit 有符号整数(2 寄存器) | |
Unsigned Long | 32-bit 无符号整数(2 寄存器) | |
Float | 32-bit IEEE754 浮点数(2 寄存器) | |
Short BCD | 16-bit BCD 编码 | |
Long BCD | 32-bit BCD 编码(2 寄存器) |
Modbus 字节序
MODBUS 使用“大端”表示法来处理地址和数据项。这意味着当传输大于单字节的数值时,最重要的字节首先发送。例如:
例如,一个 16 位( (2 Byte ,也叫做 1 个 Word))的寄存器的值 = 0x1234,首先发送的字节是0x12,然后是0x34。
Big-endian(大端) 是计算机科学中用于描述多字节数据存储方式的一个术语。在计算机存储数据时,数据可以以不同的字节顺序存储。
Big-endian 指的是在多字节数据中,最"重要"的字节(通常是最高位字节)存储在最低的内存地址处(地址总是从左到右递增),而剩余的字节按照重要性递减的顺序存储在更高的内存地址处。这种存储方式类似于书写和阅读时的从左到右顺序。
与之相反的是 "Little-endian"(小端),它将最低位字节存储在最低的内存地址处(左侧),而最高位字节存储在最高的内存地址处。
例如:寄存器值 0x1234 在报文中表示为:12 34
虽然标准规定寄存器是 高字节在前 (AB),但是 部分设备/厂家可能会用 Little-Endian (BA) 来存储或解析 16bit 数据。
由于 32-bit 数据需要两个寄存器表示,不同设备采用不同的字节序(ByteOrder) 和 WordOrder。常见的字节序组合如下:
数据类型 | 寄存器个数 | 字节数 | 位数 | 支持字节序 |
---|---|---|---|---|
16 位整数 | 1 | 2 | 16 | AB/BA |
16 位无符号整数 | 1 | 2 | 16 | AB/BA |
32 位整数 | 2 | 4 | 32 | ABCD/BADC/CDAB/DCBA |
32 位无符号整数 | 2 | 4 | 32 | ABCD/BADC/CDAB/DCBA |
32 位浮点数 | 2 | 4 | 32 | ABCD/BADC/CDAB/DCBA |
以 16-bit 数据 0x1234 , 32-bit 数据 0x12345678 为例,
模式 | 存储顺序 (寄存器1 寄存器2) |
---|---|
AB | 12 34 |
BA | 34 12 |
ABCD | 12 34 56 78 |
CDAB | 56 78 12 34 |
BADC | 34 12 78 56 |
DCBA | 78 56 34 12 |
四种数据表
四种基本的数据表,分别是离散量(Discrete Inputs)、线圈(Coils)、输入寄存器(Input Registers)和保持寄存器(Holding Registers)。
数据表类型 | 值类型 | 访问类型 | 备注 |
---|---|---|---|
离散量(Discrete Inputs) | 位(bit) | 只读 | 道常由输入/输出(I/O)系统提供,表示开关状态的输入信号。 |
线圈(Coils) | 位(bit) | 读写 | 可以被应用程序修改,表示开关状态的输出信号。 |
输入寄存器(Input Registers) | 16位字 | 只读 | 通道常由I/O系统提供,表示模拟量输入的数据或其他只读信息。 |
保持寄存器(Holding Registers) | 16位字 | 读写 | 可以被应用程序修改,用于存储数据或配置参数。 |
功能码 0x01 — 读线圈状态
示例:
-
请求:01 01 [ 00 13 ] [ 00 0A ] 0E 84
(读从站 0x01,起始地址 0x0013,读 0x000A=10 个线圈)
-
响应:01 01 05 [ 4D 01 ] 45 E6
(返回 10 个线圈状态,所以返回数据需要 2个字节)
关于地址格式:
Modbus 线圈地址从 0 开始(如 0 表示第一个线圈)。
注意:部分设备文档使用 1 起始地址(即地址 1 对应协议中的 0),需根据设备手册转换。
关于位序(Bit Order):
协议规定:Modbus 采用 小端序(Little-Endian) 的位排列方式。
字节内位序:在响应数据中,一个字节包含 8 个线圈状态,最低位(LSB, Bit 0)对应起始地址最小的线圈。
示例:读取地址 0 到 7 的线圈,返回字节 0xCD(二进制 11001101):
- `Bit 0`(LSB)→ 线圈 `0`
- `Bit 1` → 线圈 `1`
- ...
- `Bit 7`(MSB)→ 线圈 `7`
所以,对于此次读取的返回值:
- 4D(二进制 01001101):线圈 0-7 状态。线圈 0 是 on
- 01(二进制 00000001):线圈 8-15 状态(实际只用到 8-9)。
后面的离线输入的位序和线圈是一样的。
请求报文
从机地址 | 功能码 | 起始地址高 | 起始地址低 | 线圈数量高 | 线圈数量低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|
01 | 01 | 00 | 13 | 00 | 25 | 0E | 84 |
响应报文
从机地址 | 功能码 | 字节数 | 5字节数据 | CRC低 | CRC高 |
---|---|---|---|---|---|
01 | 01 | 05 | CD 6B B2 0E 1B | 45 | E6 |
功能码 0x02 — 读离散输入
此功能码是读取离散输入状态(即只读的开关量信号,如传感器状态、限位开关等)。每个位(bit)表示一个输入点的状态(1=ON,0=OFF)
地址与线圈的区别:
- 读离散输入(0x02):只读信号(如传感器输入)。
- 读线圈(0x01):可读写信号(如继电器输出)。
报文结构如下:
- 地址范围 1-65536
- 起始地址 2字节 要读取的第一个离散输入地址(多数设备从 地址 0 开始计数,但部分设备手册可能使用 地址 1)。
- 读取数量 2字节 要读取的离散输入数量(1~2000)
请求报文
从机地址 | 功能码 | 起始地址高 | 起始地址低 | 读取数量高 | 读取数量低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|
01 | 02 | 00 | C4 | 00 | 16 | B9 | B8 |
响应报文
- 字节数:返回数据的字节数,每个字节包含 8 个输入状态(高位在前)(N = ceil(读取数量/8))
从机地址 | 功能码 | 字节数 | 数据字节1 | 数据字节2 | CRC低 | CRC高 |
---|---|---|---|---|---|---|
01 | 02 | 02 | AC | DB | 45 | 6E |
功能码 0x03 — 读保持寄存器
请求示例
-
请求:01 03 00 6B 00 03 76 87
(从站 0x01,起始地址 0x006B,读 3 个寄存器)
响应示例
-
响应:01 03 06 02 2B 00 00 00 64 85 DB
(返回 3 个寄存器:0x022B, 0x0000, 0x0064)
请求报文
从机地址 | 功能码 | 起始地址高 | 起始地址低 | 寄存器数量高 | 寄存器数量低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|
01 | 03 | 00 | 6B | 00 | 03 | 76 | 87 |
响应报文
从机地址 | 功能码 | 字节数 | 数据高1 | 数据低1 | 数据高2 | 数据低2 | 数据高3 | 数据低3 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|---|---|---|
01 | 03 | 06 | 02 | 2B | 00 | 00 | 00 | 64 | 85 | DB |
功能码 0x04 — 读输入寄存器
请求报文
从机地址 | 功能码 | 起始地址高 | 起始地址低 | 寄存器数量高 | 寄存器数量低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|
01 | 04 | 00 | 08 | 00 | 01 | B2 | 0B |
响应报文
从机地址 | 功能码 | 字节数 | 数据高 | 数据低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|
01 | 04 | 02 | 00 | 0A | F8 | F4 |
功能码 0x05 — 写单个线圈
请求示例
-
请求:01 05 00 AC FF 00 4E 8B
(写线圈 0x00AC 为 ON)
响应示例
-
响应:01 05 00 AC FF 00 4E 8B
(回显请求报文)
请求报文
从机地址 | 功能码 | 线圈地址高 | 线圈地址低 | 数据高 | 数据低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|
01 | 05 | 00 | AC | FF | 00 | 4E | 8B |
响应报文(回显请求)
从机地址 | 功能码 | 线圈地址高 | 线圈地址低 | 数据高 | 数据低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|
01 | 05 | 00 | AC | FF | 00 | 4E | 8B |
功能码 0x06 — 写单个寄存器
请求报文
从机地址 | 功能码 | 寄存器地址高 | 寄存器地址低 | 数据高 | 数据低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|
01 | 06 | 00 | 01 | 00 | 03 | 9A | 9B |
响应报文(回显请求)
从机地址 | 功能码 | 寄存器地址高 | 寄存器地址低 | 数据高 | 数据低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|
01 | 06 | 00 | 01 | 00 | 03 | 9A | 9B |
功能码 0x0F — 写多个线圈
请求:01 0F 00 13 00 0A 02 CD 01 [CRC]
解析:起始地址=0x0013,数量=10,字节数=2,状态字节=CD 01 = 0b1100 1101 0000 0001(低位在前)
请求报文
从机地址 | 功能码 | 起始地址高 | 起始地址低 | 线圈数量高 | 线圈数量低 | 字节数 | 数据字节1 | … | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|---|---|---|
01 | 0F | 00 | 13 | 00 | 0A | 02 | CD | 01 | 4F | 36 |
响应报文
从机地址 | 功能码 | 起始地址高 | 起始地址低 | 线圈数量高 | 线圈数量低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|
01 | 0F | 00 | 13 | 00 | 0A | B0 | 45 |
功能码 0x10 — 写多个寄存器
请求示例
-
请求:01 10 00 01 00 02 04 00 0A 01 02 C6 F0
(写寄存器 0x0001 和 0x0002,值分别为 0x000A 和 0x0102)
响应示例
-
响应:01 10 00 01 00 02 71 CB
(确认写入 2 个寄存器)
请求报文
从机地址 | 功能码 | 起始地址高 | 起始地址低 | 寄存器数量高 | 寄存器数量低 | 字节数 | 数据1高 | 数据1低 | 数据2高 | 数据2低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
01 | 10 | 00 | 01 | 00 | 02 | 04 | 00 | 0A | 01 | 02 | C6 | F0 |
响应报文
从机地址 | 功能码 | 起始地址高 | 起始地址低 | 寄存器数量高 | 寄存器数量低 | CRC低 | CRC高 |
---|---|---|---|---|---|---|---|
01 | 10 | 00 | 01 | 00 | 02 | 71 | CB |
Modbus RTU 与 Modbus TCP 对比
项目 | Modbus RTU | Modbus TCP |
---|---|---|
传输介质 | 串口 (RS-232/485) | TCP/IP 网络 |
报文格式 | 二进制帧,带 CRC | 无 CRC,增加 MBAP 头 |
地址 | 从站地址 1 字节 | 单独 TCP 连接区分 |
应用场景 | 工业现场设备 | 工业以太网、远程监控 |
帧边界 | 靠定时间隔(3.5 字符间隔) | TCP 数据包边界 |
速度 | 较低 (9600/115200bps) | 较高 (100Mbps+) |
两者在 功能码与数据模型 上完全一致,区别主要在 传输层与报文封装。
以读保持寄存器请求为例对比介绍
modbusTCP消息结构如下:
事务ID | 协议ID | 长度 | 单元ID | PDU |
---|---|---|---|---|
Transaction ID 2 字节 | Protocol ID 2 字节 | Length 2 字节 | Unit ID 1 字节 | 协议数据 |
用于请求/响应匹配 | 固定为 0x00 0x00 |
单元标识符 + PDU | 从机地址,网关中用于路由 | 变长 |
事务ID | 协议ID | 长度 | 单元ID | 功能码 | 起始地址 | 寄存器数量 |
---|---|---|---|---|---|---|
00 01 | 00 00 | 00 06 | 01 | 03 | 00 6B | 00 03 |
转换成 modbusRTU 的消息:
从站地址 | PDU | CRC 校验 |
---|---|---|
1 字节 | 变长数据, 功能码 + 数据(与 TCP 的 PDU 相同) | CRC16 校验 |
01 | 03 00 6B 00 03 | XX XX |
modbusTCP 的消息结构中:
- 事务ID:用于请求/响应匹配(类似 TCP 的序列号),服务器响应时需原样返回。一般是设备自动生成。
- PDU:和 modbusRTU 的功能码和数据部分一样。示例 PDU 是 功能码
0x03
(读保持寄存器),起始地址0x006B
,寄存器数量0x0003
。