参考资料
各博客网站都是缺章少句,排版混乱,偶有错误,不建议参考。
这里有 modbus 中文网的资料
MODBUS 协议中文版/英文版预览及下载 | Modbus 物联网云平台
更权威的是官方文档
MODBUS Application Protocol 1 1 b
以及官方其他文档,在这里寻找
Modbus 规格和实施指南
当然官方文档我也不是很满意,为了保持他们的远古设计架构,各个帧段的介绍不直观了,同时还有少量笔误。
下面有自己重写的有效信息。
另外有我自己实现的库
协议特性
整体帧结构
| 报文头 | 地址域 | 功能码 | 数据域 | 校验域 |
---|
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 校验👍。