Modbus由MODICON公司于1979年开发,是一种工业现场总线协议标准。1996年施耐德公司推出基于以太网TCP/IP的Modbus协议,ModbusTCP。
Modbus通信的设备分为主站(mater)和从站(slave),主站为主动方,从站为被动方。
通信过程
通信的过程为:
- 主站设备主动向从站设备发送请求
- 从站设备处理主站的请求后,向主站返回结果。
- 如果从站设备处理请求出现异常,则向主站设备返回异常功能码。
数据传输方式
modbus的数据传输被定义为对以下4个存储块的读写:
- 线圈(coils) 操作单位为1位字的开关量,PLC的输出位,在Modbus中可读可写
- 离散量(discreteinputs) 操作单位为1位字的开关量,PLC的输入位,在Modbus中只读
- 输入寄存器(inputregisters) 操作单位为16位字(两个字节)数据,PLC中只能从模拟量输入端改变的寄存器,在Modbus中只读
- 保持寄存器(holdingregisters) 操作单位为16位字(两个字节)数据,PLC中用于输出模拟量信号的寄存器,在Modbus中可读可写
MODBUS-TCP报文结构
modbus-tcp报文结构:
如上图所示:modbus-tcp的报文由MBAP+PDU组成。
MBAP报文头
其中MBAP报文头的组成为:
域 | 长度 | 描述 |
事务元标识符 | 2 个字节 | MODBUS 请求/响应事务处理的识别码,主要用于在主站设备在接收到响应时能知道是哪个请求的响应 |
协议标识符 | 2 个字节 | 对于MODBUS 协议来说,这里恒为0 |
长度 | 2 个字节 | 以下字节的数量,也就是完整报文的字节数减去6 |
单元标识符 | 1 个字节 | 串行链路或其它总线上连接的远程从站的识别码,也就是要访问的从站的标识号,因为只有一个字节,所以一个主站最多只能访问256个从站设备 |
由上表可知,报文头为 7 个字节长。
PDU报文体
PDU的组成为功能码(一个字节)和数据(n个字节)
其中功能码为一个字节,modbus定义的功能码有:
- 01 读线圈(coils)状态,读取单个或多个
- 02 读离散输入(discreteinputs)状态,读取单个或多个
- 03 读保持寄存器(holdingregisters),读取单个或多个
- 04 读输入寄存器(inputregisters),读取单个或多个
- 05 写单个线圈(coils)状态,单个写入
- 06 写单个保持寄存器(holdingregisters),单个写入
- 15 写多个线圈(coils),多个写入
- 16 写多个保持寄存器(holdingregisters),多个写入
另外,当响应报文的功能码最高位为1时(即:(function & 0x80) != 0),表示为异常响应,这时数据为一个字节的异常码,具体的异常码定义有:
- 01 功能码不能被从机识别
- 02 从机的单元标识符不正确
- 03 值不被从机接受
- 04 当从机试图执行请求的操作时,发生了不可恢复的错误。
- 05 从机已接受请求并正在处理,但需要很长时间。返回此响应是为了防止在主机中发生超时错误。主站可以在下一个轮询程序中发出一个完整的消息,以确定处理是否完成。
- 06 从站正在处理长时间命令。Master应该稍后重试。
- 07 从站不能执行程序功能。主站应该向从站请求诊断或错误信息。
- 08 从站在内存中检测到奇偶校验错误。主设备可以重试请求,但从设备上可能需要服务。
- 10 专门用于Modbus网关。表示配置错误的网关。
- 11 专用于Modbus网关的响应。当从站无法响应时发送。
PDU报文详情
1、读线圈
请求报文:
功能码:01,一个字节 | 偏移量offset(读取数据的开始位置),两个字节 | 读取数量,两个字节 |
正常响应报文:
功能码:01,一个字节 | 数据长度(字节数),一个字节 | 线圈状态数据,n个字节(由于网络传输的数据都是以整字节为单位的,所以收到的数据可能比请求中要读的位数要多,这时按位将数据转换为开关量,只需解析请求中读取数量字段设定的位数就可以了) |
异常响应报文:
功能码:129(0x81),一个字节 | 异常码,一个字节 |
2、读离散输入
请求报文:
功能码:02,一个字节 | 偏移量offset(读取数据的开始位置),两个字节 | 读取数量,两个字节 |
正常响应报文:
功能码:02,一个字节 | 数据长度(字节数),一个字节 | 离散输入状态数据,n个字节(由于网络传输的数据都是以整字节为单位的,所以收到的数据可能比请求中要读的位数要多,这时按位将数据转换为开关量,只需解析请求中读取数量字段设定的位数就可以了) |
异常响应报文:
功能码:130(0x82),一个字节 | 异常码,一个字节 |
3、读保持寄存器
请求报文:
功能码:03,一个字节 | 偏移量offset(读取数据的开始位置),两个字节 | 读取数量,两个字节 |
正常响应报文:
功能码:03,一个字节 | 数据长度(字节数,这里应该是请求报文中的读取数量的2倍),一个字节 | 保持寄存器数据,n个字节(数据的字节数应该是请求报文中的读取数量的2倍) |
异常响应报文:
功能码:131(0x83),一个字节 | 异常码,一个字节 |
4、读输入寄存器
请求报文:
功能码:04,一个字节 | 偏移量offset(读取数据的开始位置),两个字节 | 读取数量,两个字节 |
正常响应报文:
功能码:04,一个字节 | 数据长度(字节数,这里应该是请求报文中的读取数量的2倍),一个字节 | 输入寄存器数据,n个字节(数据的字节数应该是请求报文中的读取数量的2倍) |
异常响应报文:
功能码:132(0x84),一个字节 | 异常码,一个字节 |
5、写单个线圈
请求报文:
功能码:05,一个字节 | 偏移量offset(写入数据的开始位置),两个字节 | 要写入的线圈状态值(只关注0和非0),两个字节 |
正常响应报文:
功能码:05,一个字节 | 偏移量offset(写入数据的开始位置),两个字节 | 要写入的线圈状态值(只关注0和非0),两个字节 |
异常响应报文:
功能码:133(0x85),一个字节 | 异常码,一个字节 |
6、写单个保持寄存器
请求报文:
功能码:06,一个字节 | 偏移量offset(写入数据的开始位置),两个字节 | 要写入的保持寄存器数据,两个字节 |
正常响应报文:
功能码:06,一个字节 | 偏移量offset(写入数据的开始位置),两个字节 | 要写入的保持寄存器数据,两个字节 |
异常响应报文:
功能码:134(0x86),一个字节 | 异常码,一个字节 |
7、写多个线圈
请求报文:
功能码:15,一个字节 | 偏移量offset(写入数据的开始位置),两个字节 | 要写入的数量,两个字节 | 数据长度(字节数),一个字节 | 线圈状态数据,n个字节 |
正常响应报文:
功能码:15,一个字节 | 偏移量offset(写入数据的开始位置),两个字节 | 要写入的数量,两个字节 |
异常响应报文:
功能码:143(0x8f),一个字节 | 异常码,一个字节 |
8、写多个保持寄存器
请求报文:
功能码:16,一个字节 | 偏移量offset(写入数据的开始位置),两个字节 | 要写入的数量,两个字节 | 数据长度(字节数),一个字节 | 保持寄存器数据,n个字节 |
正常响应报文:
功能码:16,一个字节 | 偏移量offset(写入数据的开始位置),两个字节 | 要写入的数量,两个字节 |
异常响应报文:
功能码:144(0x90),一个字节 | 异常码,一个字节 |