Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气 Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。
- 什么是ModBus
- 相关工具
- ModBus poll
- ModBUs slave
- 传输方式
- TCP
- RTU
- ASCII
- 消息帧
什么是ModBus
Modbus 协议是应用于电子控制器上的一种通用语言。通过此协议,控制器相互之间、控制器经由网络(例如以太网)和其它设备之间可以通信。Modbus 协议定义了一个控制器能认识使用的消息结构,而不管它们是经过何种网络进行通信的。它描述了一控制器请求访问其它设备的过程,如果回应来自其它设备的请求,以及怎样侦测错误并记录。它制定了消息域格局和内容的公共格式。
Modbus比其他通信协议使用的更广泛的主要原因有:
- 公开发表并且无著作权要求
- 易于部署和维护
- 对供应商来说,修改移动本地的比特或字节没有很多限制
ModBus能够在点对点和多点网络上运行,ModBus设备采用主从技术进行通信,其中只有一个设备可以发送请求。其他设备通过向主站提供所请求的数据来响应,或者通过采取查询中请求的操作。从机可以是任何外围设备,比如I/O传感器、阀门、网络驱动器、或者其他测量类型的设备。从站处理信息和使用ModBus将其响应消息发送给主站。
相关工具
- 主站模拟器 ModBus poll
- 从站模拟器 ModBus slave
- 串口工具 Virtual Serial Port Driver
- Java工具库 ModBus4j
- golang工具库 goburrow/modbus
传输方式
ModBus协议分为三种通信方式,分别是:
- 异步串行:
ModBus RTU和ModBus ASCII, 传输介质包括有线RS-232/422/485, 光纤, 无线 - 以太网:
ModBus TCP/IP - 高速令牌传递网络:
ModBus PLUS
下面主要介绍的是
ModBus TCP/IP,ModBus RTU和ModBus ASCII
消息帧
ADU: 应用数据单元
PDU: 协议数据单元
ModBus TCP
Modbus TCP的数据帧可分为两部分:ADU=MBAP+PDU = MBAP + 功能码 + 数据域,MBAP有7byte,功能码有1byte,数据域不确定,由具体功能决定。
MBAP为报文头,长度为7字节,组成如下:
- 事务处理标识 2个字节
- 协议标识 2个字节
- 长度 2个字节
- 单元标识符 1个字节
PDU格式如下:
- 功能码 1个字节
- 数据 (不同功能码,内部结构也不同)
ModBus RTU
ADU整体的结构如下:
- 从站地址 1个字节
- 功能码 1个字节
- 数据 0-253个字节
- 校验段 2个字节
- CRC1 1个字节
- CRC2 1个字节
Modbus RTU的报文格式没有定义帧的起始与结束字符, 因此对于帧识别有时间上的要求: 帧与帧之间的时间间隔要大于3.5个字符(字节)的时间, 否则认定为错误情况; 而帧内部的字符之间的间隔不能大于1.5个字符的时间, 否则, 不认为是同一个数据帧; 如下图所示.
ModBus ASCII
ASCII与RTU的区别是, 它的数据帧有开始和结束的标志位.
在ASCII(AmericanStandard Code for Information Interchange)传输模式下,消息帧以英文冒号(“:”,ASCII3A Hex)开始,以回车和换号(CRLF,ASCII 0D and 0A Hex)符号结束,允许的传输的字符集为十六进制的09和AF;网络中的从设备监视传输通路上是否有英文冒号(“:”),如果有的话,就对消息帧进行解码,查看消息中的地址是否与自己的地址相同,如果相同的话,就接收其中的数据;如果不同的话,则不予理会。
在ASCII模式下,每个8位的字节被拆分成两个ASCII字符进行发送,比如十六进制数0xAF ,会被分解成ASCII字符“A”和“F”进行发送,发送的字符量比RTU增加一倍。ASCII模式的好处是允许两个字符之间间隔的时间长达1s而不引发通信故障,该模式采用纵向冗余校验(Longitudinal Redundancy Check ,LRC)) 的方法来检验错误。
ADU整体的结构如下:
- 起始 1个字节
- 地址 2个字节
- 功能码 2字节
- 数据 0-252*2字节
- LRC 2个字节
- 截止 2个字节
ModBuss数据模型
Modbus中,数据可以分为两大类,分别为位变量(Coil)和整形变量(Register),每一种数据,根据读写方式的不同,又可细分为两种(只读,读写)。
Discretes Input位变量 只读Coils位变量 读写Input Registers16-bit整型 只读Holding Registers16-bit整型 读写
地址范围:
| 设备地址 | ModBus地址 | 描述 | 功能 | R/W |
|---|---|---|---|---|
| 1~10000 | address-1 | Coils(Output) | 0 | R/W |
| 10001~20000 | address-10001 | Discrete Inputs | 01 | R |
| 30001~40000 | address-30001 | Input Registers | 04 | R |
| 40001~50000 | address-40001 | Holding Registers | 03 | R/W |
常见功能码:
| 功能码 | 名称 | 功能 | 对应的地址类型 |
|---|---|---|---|
| 01 | 读线圈状态 | 读位(读N个bit)---读从机线圈寄存器,位操作 | 0x |
| 02 | 读输入离散量 | 读位(读N个bit)---读离散输入寄存器,位操作 | 1x |
| 03 | 读多个寄存器 | 读整型、字符型、状态字、浮点型(读N个words)---读保持寄存器,字节操作 | 4X |
| 04 | 读输入寄存器 | 读整型、状态字、浮点型(读N个words)---读输入寄存器,字节操作 | 3x |
| 05 | 写单个线圈 | 写位(写一个bit)---写线圈寄存器,位操作 | 0x |
| 06 | 写单个保持寄存器 | 写整型、字符型、状态字、浮点型(写一个word)---写保持寄存器,字节操作 | 4x |
| 0F | 写多个线圈 | 写位(写n个bit)---强置一串连续逻辑线圈的通断 | 0x |
举例说明
使用ModBusTCP作为例子, RTU和ASCII格式参考不同
读取数据
线圈读取
1 | 请求: |
0003序号0000协议标识符0006长度(01 01 00 00 00 01)01单元标识符(设备地址)01功能码(线圈)0000从0000开始读0001读取1位
1 | 响应: |
0003序号0000协议标识符0004长度 (01 01 01 01)01单元标识符(设备地址)01功能码(线圈)01长度为101值为01
离散值读取
1 | 请求: |
0001序号0000协议标识符0006长度(01 02 00 00 00 0a)01单元标识符(设备地址)02功能码0000从0000开始读000a读取0x0a(十进制为10)个位
1 | 响应: |
0001序号0000协议标识符0005长度为5(01 02 02 95 02)01单元标识符(设备地址)02功能码02值的长度(95 02)95第一个字节的值 二进制为:1001010102第二个字节的值 二进制为:10
第一个字节:1 0 1 0 1 0 0 1
第二个字节:0 1
保持寄存器读取
1 | 请求: |
0001序号0000协议标识符0006长度(01 03 00 00 00 0a)01单元标识符(设备地址)03功能码0000从0000开始读000a读取0x0a (十进制为10)个字符
1 | 响应: |
0001序号0000协议标识符0017长度(01 03 14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00)01单元标识符(设备地址)03功能码14长度(十进制为20) (00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00)0000 0000 0000 0000 0000 0000 0000 0000 0000 0000具体的数据
10条数据都为 0
如果10条数据为1-10递增, 则表示为
0001 0002 0003 0004 0005 0006 0007 0008 0009 000a
输入寄存器读取
1 | 请求: |
0001序号0000协议标识符0006长度(01 04 00 00 00 0a)01单元标识符(设备地址)04功能码0000从0000开始读000a读取0x0a(十进制为10)个字符
1 | 正确的响应 |
0001序号0000协议标识符0017长度(01 03 14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00)01单元标识符(设备地址)04功能码14长度(十进制为20) (00 01 00 02 00 03 00 04 00 05 00 06 00 07 00 08 00 09 7f ff)0001 0002 0003 0004 0005 0006 0007 0008 0009 7fff具体的数据
前9个为 1 到 9 递增, 最后一个为
32767(十进制), 范围是(-32768到32767)
写数据
单写
写线圈
1 | 请求 |
0008序号0000协议标识符0006长度(01 05 00 00 ff 00)01单元标识符(设备地址)05功能码0000写入0000地址ff00写入开 (0000写入关)
1 | 成功的响应 |
0023序号0000协议标识符0006长度(01 05 00 00 00 00)01单元标识符(设备地址)05功能码0000写入0000地址ff00写入开 (0000写入关)
写保持寄存器
1 | 请求: |
0001序号0000协议标识符0006长度(01 06 0000 0001)01单元标识符(设备地址)06功能码0000写入0000地址0001写入的值
1 | 成功的响应 |
0069序号0000协议标识符0006长度(01 06 00 00 00 68)01单元标识符(设备地址)06功能码0000写入0000地址0068写入值68(十进制:104)
批量写
写线圈
1 | 请求 |
000b序号0000协议标识符0009长度(01 0f 0000 00 09 02 7a 01)01单元标识符0f功能码 150000从地址0000开始0009写入数量02长度(7a 01)7a 01输入这些值 (7a二进制为0111 1010,01二进制为0000 0001)
输入的值为
0 1 0 1 1 1 1 0 1
1 | 正确响应 |
000b序号0000协议标识符0006长度(01 0f 0000 0009)01单元标识符0f功能码150000从地址0000开始0009更新值的个数
写保持寄存器
1 | 请求 |
0006序号0000协议标识符001b长度(十进制为27)01单元标识符10功能码(十进制为:16)0000从地址0000开始更改000a改的个数为a(十进制为:10)14字节长度(十进制为20)0006 0007 0008 0009 000a 000b 000c 000d 000e 8004具体的数据
1 | 正确的响应 |
0006序号0000协议标识符0006长度(01 10 00 00 00 0a)01单元标识符10功能码(十进制为:16)0000从地址0000开始更改000a改的个数为a(十进制为:10)