Modbus
学习Modbus通信协议前先对一些基础知识进行了解
1、什么是协议?
协议就是互相之间的约定,如果不让别人知道那就是暗号。现在就来定义一个新的最简单协议。例如,协议: “A” --“LED灭”、“B” --“报警”、 “C” --“LED亮”,设备接收到“A”控制一个LED灭,设备接收到“B”控制报警,设备接收到“A”控制一个LED亮。那么当收到对应的信息就执行相应的动作,这就是协议。
协议在通信中又分为硬件层协议和软件层协议:
- 硬件层协议解决的是1和0的传输的问题,相当于公路,常见的有485总线、232总线等
- 软件层协议解决在硬件层上有序的发送数据,相当于交通规则,不然就相当于在公路上乱开车,这是不允许的,常见的有TCP/IP、MODBUS等。
2、一帧数据
一帧数据指的是你们通过约定每次发送的数据,一帧数据包含帧头,帧尾和校验码组成 数帧头包括接收方主机物理地址的定位以及其它息。帧数据区含有一个数据体。每帧数据之间存在间隔,间隔时间根据具体的时间来定,modbus是每帧数据之间3.5个字符。
3、校验码
校验码一般放在是由前面的数据通过某种算法得出的,用以检验该组数据的正确性。代码作为数据在向计算机或其它设备进行输入时,容易产生输入错误,为了减少这种输入错误,编码专家发明了各种校验检错方法,并依据这些方法设置了校验码。常用的校验有:累加和校验SUM、字节异或校验XOR、纵向冗余校验LRC、循环冗余校验CRC等。
4、大小端
这是因为在计算机系统中,是以字节为单位的,每个地址单元都对应着一个字节。一个字节为8位(bit)。在C语言中除了8位的char型之外,还有16位的short型,32位的long型(要看具体的编译器)。另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。具体如下:
大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。
小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
Modbus协议简介
前文说到协议就是互相之间的约定,那么这个约定可以是两个人之间的约定,我叫A设备发0x01,使B设备的LED1亮,我A设备发0x02,使B设备的LED2亮,这样规定的协议只能使A设备和B设备之间进行通信,无法做到与其他设备进行通信,没有统一性,那么有那个协议可以使所有人都是用一个通信协议,不同公司的设备都可以互相通信呢?MODBUS就是这样的一个常见的协议。
Modbus协议特点
1、支持多种电气接口,如RS-232、RS-485等,还可以在各种介质上传送,如双绞线、光纤、无线等。
2、单主/多从
3、Modbus通信总是由主设备发起,当从设备没有收到来自主设备的请求时,不会主动发送数据。
4、从设备之间不能相互通信
5、主设备向从设备发送报文有广播和单播两种方式
Modbus一帧数据的基本结构
基本结构:设备码+地址码+功能码+数据区+CRC码
初始结构 = ≥4字节的时间
地址码 = 1 字节
功能码 = 1 字节
数据区 = N 字节
CRC码 = 16位CRC码
结束结构 = ≥4字节的时间
Modbus工作模式
主从模式
1) 主机只能有一个,每个从机必须有一个唯一的地址(0–247);
2) 0地址为广播地址,即主机向0号地址的设备发数据时,也就是要把该数据包发给所有的从设备;
备注:主从模式没有冲突检测,多主模式有(网络通信、CAN总线)
Modbus的传输方式:RTU方式、ASC方式、TCP/IP方式
RTU方式:发送的时候十六进制的方式发送,效率相比于ASC高一倍。
ASC方式:发送的时候拆分为ASCII码的方式发送,效率低,但好调试。
TCP/IP方式:与RTU类似。
RTU方式发送(也叫十六进制方式)
例:要发0x03,就发送二进制0 0 0 0 0 0 1 1;
ASC方式发送(ASCII方式)
例:要发0x03,要先将0x03拆分为十位的‘0’和个位的‘3‘,就是拆成两个字符0和3,0和3有对应的ASCII码为0x30和0x33,依次发出去,即先发送0x30:0011 0000,再发0x33:0011 0011;
Modbus的主机寻址帧格式
1) RTU方式
RTU寻址帧格式
地址码 | 功能码 | 数据1 | 数据2 | ……… | 数据n | CRCL | CRCH |
---|
地址码:地址码为通讯传送的第一个字节。这个字节表明由用户设定地址码的从机将接收由主机发送来的信息。并且每个从机都有具有唯一的地址码,并且响应回送均以各自的地址码开始。主机发送的地址码表明将发送到的从机地址,而从机发送的地址码表明回送的从机地址。
功能码:MODBUS约定了127个功能码,就是主机找从机可能有127个事情要做;
数据码:通过后面附加的数据来说清除,为了实现这个功能码所需要附加的一些信息,在不同的功能码里面这个数据码的解释是不一样的。
校验码:CRCL、CRCH这两个为前面的CRC检验的CRC码;
例:03 01 00 13 00 25 0D F6
03一字节代表发给03号设备,01一字节代表读取线圈寄存器,00 13两字节代表从线圈寄存器的第13位开始读取,00 25二字节代表从00 13位开始读取25个位,0D F6代表CRC校验码
注意点:
程序编写中会有一个问题在RTU方式中没有起始符和结束符,不知道数据长度到底有多长,所以规定发送接收数据寻址帧时,从机以接收数据停止时间达到3.5个字节以上代表寻找帧发送完成,并开始处理。例如:波特率9600bit/s,所以每位数据传输时间t =(1000000us)/9600 =104us
因为1个字节发送需要10位,所以一个字节需要的时间是T=10t=1040us
所以3.5T=3.51040=3645us=4ms,停止时间大于4ms时从机就开始处理数据。
(注意:主机发送时不能停止,并且发送间隔要大于4ms)
2) ASC方式
ASC寻址帧格式
“:“ | 功能码 | 数据1 | 数据2 | ……… | 数据n | 检验码 | 13 | 10 |
---|---|---|---|---|---|---|---|---|
0x3A | 两字节 | 两字节 | 两字节 | ……… | 两字节 | 两字节 | 结束符 | 结束符 |
帧头为字符“:“(不用拆分);
功能码与数据和RTU方式发送的东西一样,但要注意发送是要拆分为2个ASCII码的方式发送;
LRC校验:(地址+功能码+数据1+数据2+……+数据n)之和除以256后取余(这是这个数据就一定小于256),在取反加1,即余数的补码;
13就是C语言中\r回车,这个10就是C语言中的换行
备注:因为有实际的起始符和结束符,所以可以很方便的调试,但在实际中不用。
3) TCP/IP方式
Modbus TCP协议与RTU协议非常类似,主要区别是,在RTU协议上加一个MBAP报文头,同时由于TCP是基于可靠连接的服务,RTU协议中的CRC校验码就不再需要,所以在Modbus TCP协议中是没有CRC校验码,用一句比较通俗的话说就是:Modbus TCP协议就是Modbus RTU协议在前面加上五个0以及一个6,然后去掉两个CRC校验码字节就OK.虽然这句话说得不是特别准确,但是也基本上这就是RTU与TCP之间的区别。
Modbus RTU与Modbus TCP指令对比
MBAP报文头 | 地址码 | 功能码 | 寄存器地址 | 寄存器数量 | CRC校验 | |
---|---|---|---|---|---|---|
Modbus RTU | 无 | 01 | 03 | 01 8E | 00 04 | 25 DE |
Modbus TCP | 00 00 00 00 00 06 00 | 无 | 03 | 01 8E | 00 04 | 无 |
对上表的说明:
由于TCP是基于TCP连接的,不存在所谓的地址码,所以06后面一般都是“00”(当其作为Modbus网关服务器挂接多个RTU设备的时候,数值从01-FF).即“00 03 01 8E 00 04”对应的是RTU中去掉校验码的指令,前面则是五个0以及一个6。其中6表示的是数据长度,即“00 03 01 8E 00 04”有6个字节长度。而当其为操作指令的时候,其指令是“00 00 00 00 00 09 01 10 01 8e 00 01 02 00 00”,其中“00 09”表示后面有9个字节。
Modbus的从设备回应数据包格式
1) 回应的数据包与主机查询的数据包格式是一致的,个别地方做修整;
2) 正常回应时:功能码与主机发送功能码一致;
异常回应时:功能码要在收到的功能码基础上加上128;
(备注:正常回应就是从机控制的传感器检测没问题,异常回应就是检测的传感器有毛病,返回的数据不能用,所以主机检测到返回的功能码(正常的功能码小于128)大于128则代表返回的数据有问题,要做出错误处理)
寄存器与功能码
在对功能码了解前先对Modbus的寄存器进行了解,
这其中有涉及到线圈、离散输入、保持、输入四种寄存器。
- 线圈寄存器:实际上就可以类比为开关量(继电器状态),每一个bit对应一个信号的开关状态。所以一个byte就可以同时控制8路的信号。比如控制外部8路io的高低。 线圈寄存器支持读也支持写,写在功能码里面又分为写单个线圈寄存器和写多个线圈寄存器。对应下面的功能码也就是:0x01 0x05 0x0f
- 离散输入寄存器:如果线圈寄存器理解了这个自然也明白了。离散输入寄存器就相当于线圈寄存器的只读模式,他也是每个bit表示一个开关量,而他的开关量只能读取输入的开关信号,是不能够写的。比如我读取外部按键的按下还是松开。所以功能码也简单就一个读的 0x02
- 保持寄存器:这个寄存器的单位不再是bit而是两个byte,也就是可以存放具体的数据量的,并且是可读写的。一般对应参数设置,比如我我设置时间年月日,不但可以写也可以读出来现在的时间。写也分为单个写和多个写,所以功能码有对应的三个:0x03 0x06 0x10
- 输入寄存器:这个和保持寄存器类似,但是也是只支持读而不能写,一般是读取各种实时数据。一个寄存器也是占据两个byte的空间。类比我我通过读取输入寄存器获取现在的AD采集值。对应的功能码也就一个 **0x04 **
常用的几个功能码如下:
0x01: 读线圈寄存器
0x02: 读离散输入寄存器
0x03: 读保持寄存器
0x04: 读输入寄存器
0x05: 写单个线圈寄存器
0x06: 写单个保持寄存器
0x0f: 写多个线圈寄存器
0x10: 写多个保持寄存器
寄存器地址分配如下:
常用的几个功能码的详细说明
1、01号命令,读可读写数字量寄存器(线圈状态):
计算机发送命令:[设备地址] [命令号01] [起始寄存器地址高8位] [低8位] [读取的寄存器数高8位] [低8位] [CRC校验的低8位] [CRC校验的高8位]
尺子分割线
例:[11][01][00][13][00][25][CRC低][CRC高]
意义如下:
<1>设备地址:在一个485总线上可以挂接多个设备,此处的设备地址表示想和哪一个设备通讯。例子中为想和17号(十进制的17是十六进制的11)通讯。
<2>命令号01:读取数字量的命令号固定为01。
<3>起始地址高8位、低8位:表示想读取的开关量的起始地址(起始地址为0)。比如例子中的起始地址为19。
<4>寄存器数高8位、低8位:表示从起始地址开始读多少个开关量。例子中为37个开关量。
<5>CRC校验:是从开头一直校验到此之前。
设备响应:[设备地址] [命令号01] [返回的字节个数][数据1][数据2]…[数据n] [CRC校验的高8位] [CRC校验的低8位]
尺子分割线
例:[11][01][05][CD][6B][B2][0E][1B] [CRC高] [CRC低]
意义如下:
<1>设备地址和命令号和上面的相同。
<2>返回的字节个数:表示数据的字节个数,也就是数据1,2…n中的n的值。
<3>数据1…n:由于每一个数据是一个8位的数,所以每一个数据表示8个开关量的值,每一位为0表示对应的开关断开,为1表示闭合。比如例子中,表示20号(索引号为19)开关闭合,21号断开,22闭合,23闭合,24断开,25断开,26闭合,27闭合…如果询问的开关量不是8的整倍数,那么最后一个字节的高位部分无意义,置为0。
<4>CRC校验同上。
2、05号命令,写数字量(线圈状态):
计算机发送命令:[设备地址] [命令号05] [需下置的寄存器地址高8位] [低8位] [下置的数据高8位] [低8位] [CRC校验的低8位] [CRC校验的高8位]
尺子分割线
例:[11][05][00][AC][FF][00][CRC高][CRC低]
意义如下:
<1>设备地址和上面的相同。
<2>命令号:写数字量的命令号固定为05。
<3>需下置的寄存器地址高8位,低8位:表明了需要下置的开关的地址。
<4>下置的数据高8位,低8位:表明需要下置的开关量的状态。例子中为把该开关闭合。注意,此处只可以是[FF][00]表示闭合[00][00]表示断开,其他数值非法。
<5>注意此命令一条只能下置一个开关量的状态。
设备响应:如果成功把计算机发送的命令原样返回,否则不响应。
3、03号命令,读可读写模拟量寄存器(保持寄存器):
计算机发送命令:[设备地址] [命令号03] [起始寄存器地址高8位] [低8位] [读取的寄存器数高8位] [低8位] [CRC校验的高8位] [CRC校验的低8位]
尺子分割线
例:[11][03][00][6B][00][03] [CRC高][CRC低]
意义如下:
<1>设备地址和上面的相同。
<2>命令号:读模拟量的命令号固定为03。
<3>起始地址高8位、低8位:表示想读取的模拟量的起始地址(起始地址为0)。比如例子中的起始地址为107。
<4>寄存器数高8位、低8位:表示从起始地址开始读多少个模拟量。例子中为3个模拟量。注意,在返回的信息中一个模拟量需要返回两个字节。
设备响应:[设备地址] [命令号03] [返回的字节个数][数据1][数据2]…[数据n] [CRC校验的高8位] [CRC校验的低8位]
尺子分割线
例:[11][03][06][02][2B][00][00][00][64] [CRC高] [CRC低]
意义如下:
<1>设备地址和命令号和上面的相同。
<2>返回的字节个数:表示数据的字节个数,也就是数据1,2…n中的n的值。例子中返回了3个模拟量的数据,因为一个模拟量需要2个字节所以共6个字节。
<3>数据1…n:其中[数据1][数据2]分别是第1个模拟量的高8位和低8位,[数据3][数据4]是第2个模拟量的高8位和低8位,以此类推。例子中返回的值分别是555,0,100。
<4>CRC校验同上。
4、06号命令,写单个模拟量寄存器(保持寄存器):
计算机发送命令:[设备地址] [命令号06] [需下置的寄存器地址高8位] [低8位] [下置的数据高8位] [低8位] [CRC校验的高8位] [CRC校验的低8位]
尺子分割线
例:[11][06][00][01][00][03] [CRC高] [CRC低]
意义如下:
<1>设备地址和上面的相同。
<2>命令号:写模拟量的命令号固定为06。
<3>需下置的寄存器地址高8位,低8位:表明了需要下置的模拟量寄存器的地址。
<4>下置的数据高8位,低8位:表明需要下置的模拟量数据。比如例子中就把1号寄存器的值设为3。
<5>注意此命令一条只能下置一个模拟量的状态。
设备响应:如果成功把计算机发送的命令原样返回,否则不响应。
5、16号命令,写多个模拟量寄存器(保持寄存器):
计算机发送命令:[设备地址] [命令号16] [需下置的寄存器地址高8位] [低8位] [数据数量高8位] [数据数量低8位] [下置的数据高8位] [低8位][……][……] [CRC校验的高8位] [CRC校验的低8位]
例:[11][16][00][01][00][01][00][05] [CRC高] [CRC低]
意义如下:
<1>设备地址和上面的相同。
<2>命令号:写模拟量的命令号固定为16。
<3>需下置的寄存器地址高8位,低8位:表明了需要下置的模拟量寄存器的地址。
<4>需下置的数据数量高8位,低8位:表明了需要下置的数据数量,这里为1。
<5>下置的数据高8位,低8位:表明需要下置的模拟量数据。比如例子中就把1号寄存器的值设为5。
设备响应:如果成功把计算机返回的如下命令,否则不响应。
设备响应:[设备地址] [命令号16] [需下置的寄存器地址高8位] [低8位] [数据数量高8位] [数据数量低8位] [CRC校验的高8位] [CRC校验的低8位],如上例返回:
[11][16][00][01][00][01] [CRC高] [CRC低]
对Modbus的基本开发流程
可以使用RTU、TCP这两种最常用的模式。