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 Registers
16-bit整型 只读Holding Registers
16-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
长度为1
01
值为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
第一个字节的值 二进制为:10010101
02
第二个字节的值 二进制为: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
功能码15
0000
从地址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
)