参考资料#
各博客网站都是缺章少句,排版混乱,偶有错误,不建议参考。
这里有 modbus 中文网的资料
MODBUS 协议中文版 / 英文版预览及下载 | Modbus 物联网云平台
更权威的是官方文档
MODBUS Application Protocol 1 1 b
以及官方其他文档,在这里寻找
当然官方文档我也不是很满意,为了保持他们的远古设计架构,各个帧段的介绍不直观了,同时还有少量笔误。
下面有自己重写的有效信息。
另外有我自己实现的库
stbanana/modbusX: modbus protocol support (github.com)
协议特性#
整体帧结构#
报文头 | 地址域 | 功能码 | 数据域 | 校验域 | |
---|---|---|---|---|---|
RTU | 1 字节 (从机 ID) | 1 字节 | n 字节不定 | 2 字节 (对其他所有内容 CRC-MB16) | |
TCP | 6 字节 (2 字节事务号 + 2 字节协议标识 - 就是全 0+ 2 字节后续字节总长 - 包括从机 ID) | 1 字节 (从机 ID) | 1 字节 | n 字节不定 |
关于报文头,在官方文档的总体模型中划分到地址域的,但实际上的字节总长又包括从机 ID。大概是早期只有串口协议时,整体模型已经定下了,新加的 modbusTCP 只能为了兼容乱搞,
我这里直接重新归类好了,不标准,但我乐意。
寄存器属性#
RW 属性 | bit 位数 | |
---|---|---|
线圈 (0x01) | 可读写 | 1 位 bit |
离散输入寄存器 (0x02) | 只读 | 1 位 bit |
保持寄存器 (0x03) | 可读写 | 16 位 bit |
输入寄存器 (0x04) | 只读 | 16 位 bit |
整体模型#
使用软件抽象一个内存结构,每个地址存有不同意义的数据,和 FPGA 模拟 SRAM 是一个道理。不过每个寄存器地址不都是 16 位,也可以是 1 位。属性是 RO\RW。
RTU 系列#
(0x01) 读线圈#
主机请求#
地址域 | 功能码 | 起始地址 | 线圈数量 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 (CRC-MB16) |
从机响应#
地址域 | 功能码 | 字节数 | 线圈状态 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 1 字节 (计算线圈状态部分的字节数) | n 字节 | 2 字节 (CRC-MB16) |
示例#
请求读 20~38 地址的线圈数据,总之回复地址是由低到高,最终字节不齐在高位填 0。
[!NOTE]
最终的输出状态 38-36 回复字节,用零填充 5 个剩余 bit(一直到高位端)。
请求 | 响应 | ||
---|---|---|---|
地址域 (从机 ID) | 0x01 | 地址域 (从机 ID) | 0x01 |
功能码 | 0x01 | 功能码 | 0x01 |
起始地址高 8 位 | 0x00 | 字节数 | 0x03 |
起始地址低 8 位 | 0x13 | 输出状态 27-20 | 0xCD |
输出数量高 8 位 | 0x00 | 输出状态 35-28 | 0x6B |
输出数量低 8 位 | 0x13 | 输出状态 38-36 | 0x05 |
校验 CRC 低 8 位 | 0xA9 | 校验 CRC 低 8 位 | 0x42 |
校验 CRC 高 8 位 | 0xC8 | 校验 CRC 高 8 位 | 0x82 |
(0x02) 读离散输入寄存器#
主机请求#
地址域 | 功能码 | 起始地址 | 离散输入数量 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 (CRC-MB16) |
从机响应#
地址域 | 功能码 | 字节数 | 离散输入状态 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 1 字节 (计算离散输入状态部分的字节数) | n 字节 | 2 字节 (CRC-MB16) |
示例#
请求读 197~218 地址的离散输入寄存器数据,总之回复地址是由低到高,最终字节不齐在高位填 0。
[!NOTE]
最终的输入状态 218-213 回复字节,用零填充 2 个剩余 bit(一直到高位端)。
请求 | 响应 | ||
---|---|---|---|
地址域 (从机 ID) | 0x01 | 地址域 (从机 ID) | 0x01 |
功能码 | 0x02 | 功能码 | 0x02 |
起始地址高 8 位 | 0x00 | 字节数 | 0x03 |
起始地址低 8 位 | 0xC4 | 输入状态 204-197 | 0xAC |
输出数量高 8 位 | 0x00 | 输入状态 212-205 | 0xDB |
输出数量低 8 位 | 0x16 | 输入状态 218-213 | 0x35 |
校验 CRC 低 8 位 | 0xB8 | 校验 CRC 低 8 位 | 0x22 |
校验 CRC 高 8 位 | 0x39 | 校验 CRC 高 8 位 | 0x88 |
(0x03) 读保持寄存器#
主机请求#
地址域 | 功能码 | 起始地址 | 保持寄存器数量 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 (CRC-MB16) |
从机响应#
地址域 | 功能码 | 字节数 | 保持寄存器状态 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 1 字节 (计算保持寄存器状态部分的字节数) | n 字节 | 2 字节 (CRC-MB16) |
示例#
请求读 108~110 地址的保持寄存器数据
请求 | 响应 | ||
---|---|---|---|
地址域 (从机 ID) | 0x01 | 地址域 (从机 ID) | 0x01 |
功能码 | 0x03 | 功能码 | 0x03 |
起始地址高 8 位 | 0x00 | 字节数 | 0x06 |
起始地址低 8 位 | 0x6B | 寄存器值高八位(108) | 0x02 |
寄存器数量高 8 位 | 0x00 | 寄存器值低八位(108) | 0x2B |
寄存器数量低 8 位 | 0x03 | 寄存器值高八位(109) | 0x00 |
校验 CRC 低 8 位 | 0x74 | 寄存器值低八位(109) | 0x00 |
校验 CRC 高 8 位 | 0x17 | 寄存器值高八位(110) | 0x00 |
寄存器值低八位(110) | 0x64 | ||
校验 CRC 低 8 位 | 0x05 | ||
校验 CRC 高 8 位 | 0x7A |
(0x04) 读输入寄存器#
主机请求#
地址域 | 功能码 | 起始地址 | 输入寄存器数量 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 (CRC-MB16) |
从机响应#
地址域 | 功能码 | 字节数 | 输入寄存器状态 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 1 字节 (计算输入寄存器状态部分的字节数) | n 字节 | 2 字节 (CRC-MB16) |
示例#
请求读 9~10 地址的保持寄存器数据
请求 | 响应 | ||
---|---|---|---|
地址域 (从机 ID) | 0x01 | 地址域 (从机 ID) | 0x01 |
功能码 | 0x04 | 功能码 | 0x04 |
起始地址高 8 位 | 0x00 | 字节数 | 0x06 |
起始地址低 8 位 | 0x6B | 寄存器值高八位(9) | 0x02 |
寄存器地址高 8 位 | 0x00 | 寄存器值低八位(9) | 0x2B |
寄存器地址低 8 位 | 0x03 | 寄存器值高八位(10) | 0x00 |
校验 CRC 低 8 位 | 0xC1 | 寄存器值低八位(10) | 0x00 |
校验 CRC 高 8 位 | 0xD7 | 校验 CRC 低 8 位 | 0xF3 |
校验 CRC 高 8 位 | 0xF4 |
(0x05) 写单个线圈#
[!NOTE]
写单个线圈,输出值部分仅允许 FF 00 表示 ON、00 00 表示 OFF,其他值都不合法
主机请求#
地址域 | 功能码 | 输出地址 | 输出值 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 (CRC-MB16) |
从机响应#
地址域 | 功能码 | 地址 | 输出值 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 (CRC-MB16) |
示例#
请求写 173 地址的线圈数据为 ON
请求 | 响应 | ||
---|---|---|---|
地址域 (从机 ID) | 0x01 | 地址域 (从机 ID) | 0x01 |
功能码 | 0x05 | 功能码 | 0x05 |
寄存器地址高 8 位 | 0x00 | 寄存器地址高 8 位 | 0x00 |
寄存器地址低 8 位 | 0xAC | 寄存器地址低 8 位 | 0xAC |
寄存器值高 8 位 | 0xFF | 寄存器值高 8 位 | 0xFF |
寄存器值低 8 位 | 0x00 | 寄存器值低 8 位 | 0x00 |
校验 CRC 低 8 位 | 0x4C | 校验 CRC 低 8 位 | 0x4C |
校验 CRC 高 8 位 | 0x1B | 校验 CRC 高 8 位 | 0x1B |
(0x06) 写单个保持寄存器#
主机请求#
地址域 | 功能码 | 保持寄存器地址 | 寄存器值 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 (CRC-MB16) |
从机响应#
地址域 | 功能码 | 保持寄存器地址 | 寄存器值 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 (CRC-MB16) |
示例#
请求写 2 地址的保持寄存器数据为 0x0003
请求 | 响应 | ||
---|---|---|---|
地址域 (从机 ID) | 0x01 | 地址域 (从机 ID) | 0x01 |
功能码 | 0x06 | 功能码 | 0x06 |
寄存器地址高 8 位 | 0x00 | 寄存器地址高 8 位 | 0x00 |
寄存器地址低 8 位 | 0x02 | 寄存器地址低 8 位 | 0x02 |
寄存器值高 8 位 | 0x00 | 寄存器值高 8 位 | 0x00 |
寄存器值低 8 位 | 0x03 | 寄存器值低 8 位 | 0x03 |
校验 CRC 低 8 位 | 0x2C | 校验 CRC 低 8 位 | 0x2C |
校验 CRC 高 8 位 | 0x0B | 校验 CRC 高 8 位 | 0x0B |
(0x0F) 写多个线圈#
主机请求#
地址域 | 功能码 | 起始地址 | 设置数量 | 字节数 | 设置值 | 校验域 |
---|---|---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 1 字节 (计算设置值部分的字节数) | n 字节 | 2 字节 (CRC-MB16) |
从机响应#
地址域 | 功能码 | 起始地址 | 设置数量 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 (CRC-MB16) |
示例#
请求写 20 地址开始的 10 个线圈
[!NOTE]
共需写入 2 字节 (16bit),用零填充 6 个剩余 bit(一直到高位端)。
线圈地址 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | — | — | — | — | — | — | 29 | 28 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
对应值 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
依据规范填充后,实际设置值段应当是 0xCD 0x01
请求 | 响应 | ||
---|---|---|---|
地址域 (从机 ID) | 0x01 | 地址域 (从机 ID) | 0x01 |
功能码 | 0x0F | 功能码 | 0x0F |
起始地址高 8 位 | 0x00 | 起始地址高 8 位 | 0x00 |
起始地址低 8 位 | 0x13 | 起始地址低 8 位 | 0x13 |
设置数量高 8 位 | 0x00 | 设置数量高 8 位 | 0x00 |
设置数量低 8 位 | 0x0A | 设置数量低 8 位 | 0x0A |
字节数 | 0x02 | 校验 CRC 低 8 位 | 0x24 |
设置值 (27~20 地址) | 0xCD | 校验 CRC 高 8 位 | 0x09 |
设置值 (29~28 地址) | 0x01 | ||
校验 CRC 低 8 位 | 0x72 | ||
校验 CRC 高 8 位 | 0xCB |
(0x10) 写多个保持寄存器#
主机请求#
地址域 | 功能码 | 起始地址 | 设置数量 | 字节数 | 设置值 | 校验域 |
---|---|---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 1 字节 (计算设置值部分的字节数) | n 字节 | 2 字节 (CRC-MB16) |
从机响应#
地址域 | 功能码 | 起始地址 | 设置数量 | 校验域 |
---|---|---|---|---|
1 字节 | 1 字节 | 2 字节 | 2 字节 | 2 字节 (CRC-MB16) |
示例#
请求写 34 地址开始的 4 个保持寄存器
请求 | 响应 | ||
---|---|---|---|
地址域 (从机 ID) | 0x01 | 地址域 (从机 ID) | 0x01 |
功能码 | 0x10 | 功能码 | 0x10 |
起始地址高 8 位 | 0x00 | 起始地址高 8 位 | 0x00 |
起始地址低 8 位 | 0x22 | 起始地址低 8 位 | 0x22 |
设置数量高 8 位 | 0x00 | 设置数量高 8 位 | 0x00 |
设置数量低 8 位 | 0x04 | 设置数量低 8 位 | 0x04 |
字节数 | 0x08 | 校验 CRC 低 8 位 | 0x61 |
设置值高 8 位 (34 地址) | 0x00 | 校验 CRC 高 8 位 | 0xC0 |
设置值低 8 位 (34 地址) | 0x40 | ||
设置值高 8 位 (35 地址) | 0x00 | ||
设置值低 8 位 (35 地址) | 0x24 | ||
设置值高 8 位 (36 地址) | 0x00 | ||
设置值低 8 位 (36 地址) | 0x01 | ||
设置值高 8 位 (37 地址) | 0xBF | ||
设置值低 8 位 (37 地址) | 0x52 | ||
校验 CRC 低 8 位 | 0x5F | ||
校验 CRC 高 8 位 | 0xCC |
异常响应帧#
地址域 | 功能码 | 异常码 | 校验域 |
---|---|---|---|
1 字节 | 1 字节 (功能码 + 0x80) | 1 字节 | 2 字节 (CRC-MB16) |
异常码 | 名称 | 含义 |
---|---|---|
0x01 | 非法功能码 | 接收到的请求指令功能码是不可允许的操作。可能是功能码在其中不被支持,也可能是其正处于错误状态中处理请求。 |
0x02 | 非法数据地址 | 询问中接收到的数据地址是不可允许的地址。特别是,起始地址和传输长度的组合是无效的。对于带有 100 个寄存器的控制器来说,带有起始地址 96 和长度 4 的请求会成功,带有起始地址 96 和长度 5 的请求将产生异常码 0x02。 |
0x03 | 非法数据值 | 实际上是数据段非法的意思。例如非法数据段长度,或者写入或者读取的寄存器数量和数据段不匹配。注意的是,这并不代表寄存器被写入一个期望范围以外的值、实际写入失败 (这种情况是 0x04)。 |
0x04 | 从站设备故障 | 当服务器 (或从站),对寄存器执行请求的操作时出现差错,例如寄存器被写入一个期望范围以外的值等。 |
0x05 | 确认 | 其实并非错误,而是收到长耗时指令,表明已收到并开始处理. |
0x06 | 从属设备忙 | 正在处理耗时命令在忙。(当从机空闲后,应当重发引起此错误的请求) |
0x08 | 存储奇偶性差错 | 设法读取记录文件,但是在存储器中发现一个奇偶校验错误。 |
0x0A | 不可用网关路径 | 与网关一起使用,指示网关不能为处理请求分配输入端口至输出端口的内部通信路径。通常意味着网关是错误配置的或过载的。 |
0x0B | 网关目标设备响应失败 | 与网关一起使用,指示没有从目标设备中获得响应。通常意味着设备未在网络中。 |
TCP 系列#
总之 modbusTCP 相比于 RTU 只是包装了前面的报文头,同时去除了 CRC 校验👍。== 因为 TCP 管理层的链路传输已经很稳健了,而串口传输是不稳健的 ==
报文头中的事务号会在主机 (TCP 客户端) 发起请求时不断累加,从机 (TCP 服务器端) 响应请求会回复相同的事务号表示处理的是什么请求。所以 modbusTCP 是天生支持多帧连发的,主机不会因此误解回复👍。
个人是更喜欢这样流畅的通信协议的。|| 信息准确性什么的交给物理链路层就好了,大雾大雾,校验还是有必要的 ||
(0x01) 读线圈#
主机请求#
事务号 | 协议标识 | 字节总长 | 地址域 | 功能码 | 起始地址 | 线圈数量 |
---|---|---|---|---|---|---|
2 字节 | 2 字节 (全 0) | 2 字节 (后续字节总长) | 1 字节 | 1 字节 | 2 字节 | 2 字节 |
从机响应#
事务号 | 协议标识 | 字节总长 | 地址域 | 功能码 | 字节数 | 线圈状态 |
---|---|---|---|---|---|---|
2 字节 | 2 字节 (全 0) | 2 字节 (后续字节总长) | 1 字节 | 1 字节 | 1 字节 (计算线圈状态部分的字节数) | n 字节 |
示例#
请求读 20~38 地址的线圈数据,总之回复地址是由低到高,最终字节不齐在高位填 0。
[!NOTE]
最终的输出状态 38-36 回复字节,用零填充 5 个剩余 bit(一直到高位端)。
请求 | 响应 | ||
---|---|---|---|
事务号高 8 位 | 0x00 | 事务号高 8 位 | 0x00 |
事务号低 8 位 | 0x01 | 事务号低 8 位 | 0x01 |
协议标识 16 位 (全 0) | 0x00 0x00 | 协议标识 16 位 (全 0) | 0x00 0x00 |
字节总长高 8 位 | 0x00 | 字节总长高 8 位 | 0x00 |
字节总长低 8 位 | 0x06 | 字节总长低 8 位 | 0x06 |
地址域 (从机 ID) | 0x01 | 地址域 (从机 ID) | 0x01 |
功能码 | 0x01 | 功能码 | 0x01 |
起始地址高 8 位 | 0x00 | 字节数 | 0x03 |
起始地址低 8 位 | 0x13 | 输出状态 27-20 | 0xCD |
输出数量高 8 位 | 0x00 | 输出状态 35-28 | 0x6B |
输出数量低 8 位 | 0x13 | 输出状态 38-36 | 0x05 |
其他协议不赘述了
其他协议不赘述了
总之 modbusTCP 相比于 RTU 只是包装了前面的报文头,同时去除了 CRC 校验👍。
此文由 Mix Space 同步更新至 xLog
原始链接为 https://www.yono233.cn/posts/shoot/24_7_26_modbus