Modbus协议思想
Modbus是一个请求/应答协议,通过功能码操作寄存器进行数据传递。
要点:
- 请求/应答的工作模式:
- 该协议的编程模型为轮询形式。
- 链路上不会有突发的数据传输,所有的数据传输都在发起方那边控制。
- 不需要全双工的通信模式,半双工的就可以了。在某些特定场合下,可以使用一根串口线完成通信。
- 功能码:
- 提供了读写寄存器的方法
- 提供了一些其他操作,以及可以自定义的一些功能。
- 寄存器:
- 虚拟寄存器,只是表示功能,不一定在设备上具有实际的存储空间。
- 一共有四类,每类有不同的地址空间,不过很多应用直接都放在一个地址空间里面了。
- 每类的地址范围为2字节,即最大65536个寄存器。
报文格式
-
ADU:应用数据单元
-
PDU:协议数据单元
<-------------ADU----------------> | 地址域 | 功能码 | 数据 | 差错校验 | <------PDU---->
在串口通信的时候,报文头的判断是依据串口线上无数据的时间判断的。
数据长度
ADU长度:256字节。
PDU长度:
- ModbusRTU:253字节
- ModbusTCP:249字节
PDU类型:
- 请求PDU:功能码 + 请求数据
- 响应PDU:功能码 + 响应数据
- 异常响应PDU:(功能码 + 0x80) + 异常码
这里请求PDU和响应PDU的功能码是一样的。这种做法按照modbus规范来说是ok的,因为是请求应答机制的通讯,数据线上的数据都是主机控制的。
但如果自己使用的话,可以魔改一下,把响应功能码也改成特殊的,这样的话我们在链路上抓包的时候可以很容易区分包的类型。
如果后面想要扩展成多主机的话,响应功能码改一下就很有必要了,因为对某一个设备来说,它需要区分当前收到的包是自己发出去的请求的响应还是其他主机对自己的请求。当然这种扩展已经不属于modbus规范了。
数据编码:
大端模式
数据模型(寄存器类型)
比特类型(bit类型):
- 离散量输入
- 离散量读写(线圈)
寄存器类型(16bit类型):
- 只读寄存器(输入寄存器)
- 读写寄存器(保持寄存器)
建议实际使用只用寄存器类型就可以了。每个寄存器是2个字节的大小。注意是大端编码的。
上面寄存器列表里面括号里面是协议规范的名字,这些是因为Modbus源于PLC领域,所有当初有些命名就这么定义,现在看其实没必要。
功能码定义
这里只说几个关键的。
0x04 读单个或多个输入寄存器
-请求
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 功能码 | 0x04 | |
| 1,2 | UINT16 | 起始地址 | [0x0000,0xffff] | 输入寄存器的起始地址 |
| 3,4 | UINT16 | 输入寄存器数量 | [1,125] | 读取寄存器数量N |
-应答
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 功能码 | 0x04 | |
| 1 | UINT8 | 剩余字节数n | 2*N | 表示接下来字段的长度 |
| 2,2+n | UINT16[N] | 输入寄存器 |
-错误响应
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 错误响应功能码 | 0x84 | 0x80+0x04 |
| 1 | UINT8 | 错误码 | 1,2,3,4 |
0x03 读单个或多个读写寄存器
-请求
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 功能码 | 0x03 | |
| 1,2 | UINT16 | 起始地址 | [0x0000,0xffff] | 寄存器的起始地址 |
| 3,4 | UINT16 | 寄存器数量 | [1,125] | 读取寄存器数量N |
-响应
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 功能码 | 0x03 | |
| 1 | UINT8 | 剩余字节数n | 2*N | 表示接下来字段的长度 |
| 2,2+n | UINT16[N] | 寄存器值 |
-错误响应
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 错误响应功能码 | 0x83 | 0x80+0x03 |
| 1 | UINT8 | 错误码 | 1,2,3,4 |
仔细看会发现,功能码3,4的报文除了功能码之外完全一致。它们从功能上来说是完全一样的,只是读取的寄存器的地址不同。这样做的原因是因为modbus标准把输入寄存器和保持寄存器放在了不同的地址空间上面。比如说输入寄存器和保持寄存器都有一个地址为0的,那么读取指令就需要区分读取的是输入寄存器还是保持寄存器。
个人认为这并不是一个好做法,最好是按照功能把协议进行封装,比如说读取的功能就只有一个,然后功能码里面加额外一个字段去标识读取的寄存器类型。
或者采用全局的地址空间,这样读取就都是一致的了。
0x06 写单个读写寄存器
-请求
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 功能码 | 0x06 | |
| 1,2 | UINT16 | 寄存器地址 | [0x0000,0xffff] | |
| 3,4 | UINT16 | 寄存器值 | [0x0000,0xffff] |
-响应
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 功能码 | 0x06 | |
| 1,2 | UINT16 | 寄存器地址 | [0x0000,0xffff] | |
| 3,4 | UINT16 | 寄存器值 | [0x0000,0xffff] |
-错误响应
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 错误响应功能码 | 0x86 | |
| 1 | UINT8 | 错误码 | 1,2,3,4 |
这里可以发现请求和响应的格式是完全一样的,都是寄存器地址+值的格式。这样的好处是发出写请求之后不需要保留写的寄存器地址。
0x10 写多个读写寄存器
-请求
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 功能码 | 0x10 | |
| 1,2 | UINT16 | 起始地址 | [0x0000,0xffff] | |
| 3,4 | UINT16 | 寄存器数量N | [1,123] | |
| 5 | UINT8 | 剩余字节数n | 2*N | |
| 6,6+n | UINT16[n] | 寄存器值 |
-响应
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 功能码 | 0x10 | |
| 1,2 | UINT16 | 起始地址 | [0x0000,0xffff] | |
| 3,4 | UINT16 | 寄存器数量N | [1,123] | 写入的寄存器数量 |
-错误响应
| 字节序 | 类型 | 字段 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UINT8 | 错误响应功能码 | 0x90 | |
| 1 | UINT8 | 错误码 | 1,2,3,4 |
这里可为了减少带宽,所以响应里面没有放入寄存器的值。
异常码
| 代码 | 名称 | 含义 |
|---|---|---|
| 01 | 非法功能 | |
| 02 | 非法数据地址 | |
| 03 | 非法数据值 | |
| 04 | 从站设备故障 | |
| 05 | 确认 | |
| 06 | 从属设备忙 | |
| 08 | 存储奇偶性差错 | |
| 0A | 不可用网关路径 | |
| 0B | 网关目标设备响应失败 |
本文来自博客园,作者:耿畅,转载请注明原文链接:https://www.cnblogs.com/ccnote/p/16200516.html

浙公网安备 33010602011771号