- 工欲善其事,必先利其器。
- Modbus学习必备的三大神器分别是Modbus Poll、Modbus Slave及VSPD,Modbus Poll软件主要用于仿真Modbus主站或Modbus客户端,Modbus Slave软件主要用于仿真Modbus从站或Modbus服务器,而VSPD全称Configure Virtual Serial Port Driver,是用来给电脑创建虚拟串口使用的。
- 首先,要了解Modbus 协议。需要了解什么是通信协议?什么是Modbus 协议 和Modbus 协议是干什么用的?
- 什么是通信协议?
- 通信协议:指双方实体完成通信或服务所必须遵循的规则和约定。意思就是:交流什么、怎样交流及何时交流,都必须遵循某种互相都能接受的规则。
- 目的:为了数据传输。
2.Modbus 协议?
Modbus通信协议:由Modicon公司(现在的施耐德电气Schneider Electric)于1979年为可编程逻辑控制(即PLC)通信而发表。目前,Modbus已经成为工业领域通信协议的业界标准,并且现在是工业电子设备之间常用的连接方式。Modbus作为目前工业领域应用最广泛的协议。
Modbus 协议的作用:工业电子设备之间常用的连接方式。(比如说:PLC)
相对其它通信协议modbus 协议的特点:免费;简单;接口丰富。
- (1)Modbus协议标准开放、公开发表且无版权要求。
(2)Modbus协议支持多种电气接口,包括RS232、RS485、TCP/IP等,还可以在各种介质上传输,如双绞线、光纤、红外、无线等。 - (3)Modbus协议消息帧格式简单、紧凑、通俗易懂。用户理解和使用简单,厂商容易开发和集成,方便形成工业控制网络 Modbus协议是一种应用层报文传输协议,包括ASCII、RTU、TCP三种报文类型,协议本身并没有定义物理层,只是定义了控制器能够认识和使用的消息结构。
(PLC)数据的传输离不开存储和读写操作,为了更好存储不同的数据类型,我们可以理解modbus 会将布尔和非布尔的数据分开存储。
那什么是布尔呢?(0和1/ on & off/false &true)
- 布尔类型只有两个值,false 和 true。
- 通常用来判断条件是否成立。
- C语言语法规定,如果变量值为 0 就是 false,否则为 true,布尔变量只有这两个值。
因此,就有了线圈和寄存器的概念。
如何理解线圈和寄存器?
线圈:从电气角度来看,在电气控制回路中,一般都是靠接触器或中间继电器来实现控制,接触器或中继最终靠的是线圈的得电和失电来控制触点闭合和断开,因此用线圈表示布尔量;
- 寄存器:用来暂时存放参与运算的数据和运算结果,具有接收数据、存放数据和输出数据的功能。
- 而寄存器在计算机中,就是用来存储数据的,因此非布尔的数据放在寄存器里。
回到存储区分类,目的就是: 更好地存储和区分不同的数据类型。
存储类型 & 存储区名称:
Modbus的线圈和寄存器应该也按照只读、读写来进一步细分,因此这就形成了Modbus的存储区,如下表所示:
- 存储区代号:
- 为什么需要存储区代号?上面表格里的存储区名称是一个全称,开发和使用中使用全称会比较麻烦,因此需要给他们取个别名。所以Modbus也要给这些存储区取一个代号,干脆直接用数字吧,于是,就有了下面的规定:
(可以简单理解为:其实就跟我们的姓名和小名一样,姓名是正式场合使用,日常场合,我们一般可以使用小名。)
- 存储区范围:
- 无论是什么存储区,都会有一个范围的限制;Modbus的每个存储区也规定了一个范围,不能无限制使用。
- Modbus规定每个存储区的最大范围是65536
- 我们常说PLC地址,那么这个地址是怎么组成的呢?
- 它是由存储区编号加上一个地址索引组成,我们把这样的PLC地址,理解为绝对地址,后面的地址索引,理解为相对地址。
- 对于Modbus来说,绝对地址和相对地址是怎么样的呢?
- 遵从公式:绝对地址=区号+相对地址
因此,Modbus存储区范围如下图所示:
在实际使用中,一般用不了这么多地址,一般情况下,10000以内就已经足够使用了;因此,为了方便有一种短的地址模型,如下图所示:
(反之其它地址范围剩余部分则为:长地址模型)
协议的目的是为了数据传输,也就是为了读取数据和写入数据,我们已经确定好4个存储区,存储不同的数据类型,那么接下来我们就要对这些存储区进行读写,那么可能会产生很多种不同的行为。于是有了功能码这个概念。
- 功能码:
- 目的:行为代号,区分行为动作
读取和写入是2种行为,存储区有4个,但是我们知道输入线圈和输入寄存器是只读的,因此不能进行写入,除去这2种的话,应该会产生6种不同的行为,如下图所示:
然而,Modbus规约将写入输出线圈和写入保持寄存器这2种行为,又进一步做了细分,包括写入单个和写入多个,因此原来的6种行为就变成了8种行为,同时给每种行为设置一个代号,就形成了下图所示的功能码列表:
(Modbus规约中的功能码其实不止这8个,还有一些功能码是用于诊断或异常码,但是一般很少使用,这8种功能码是最主要的核心功能码。)
- 协议分类:
- Modbus严格来说,是一个标准化的规约,而不是一个具体协议。
- 通信介质: 接口:1)串口 2)以太网;
- Modbus规约上有三种不同的协议,分别是ModbusRTU、ModbusAscII、ModbusTCP
报文格式:
1.ModbusRTU的报文格式: 从站地址(1个字节)+功能码(1个字节)+数据部分(N个字节)+校验、CRC检验(2个字节)
2. ModbusAscii的报文格式:开始字符(:)+从站地址(2个字节)+功能码(2个字节)+数据部分(N个字节)校验、CRC检验(2个字节)+结束字符(CR LF)
3.ModbusTCP的报文格式:事务处理标识符(2个字节)+协议标识符(2个字节)+长度(2个字节)+单元标识符(1个字节)+功能码(1个字节)+数据部分(N个字节)
(Modbus TCP 不使用校验,因为TCP协议是一个面向连接的可靠协议 )
在针对具体报文进行分析之前,先补充几个计算机基础知识:
1. 位(Bit)与 字节(Byte)
- 在计算机中最小的数据单位是Bit(位), 但是一般计算机系统能读取和定位到最小的信息单位是字节(Byte).
(一个字节(Byte)为8个Bit(位),一个英文字母通常占用一个字节,一个汉字通常占用两个字节) - 计算机底层都是二进制代码,在实际应用中,使用经常使用浮点数、整数、字符串;在进行赋值运算或者算术运算时,必须得保证参与运算的数据类型保持一致,如果不一致,就必须进行数据转换。
- 二进制 & 十进制 & 16进制:
- 使用不同的进制方式来表示同个数值(同一个数值的不同表示方式)
那为什么用16进制呢?
- 计算机硬件是0101二进制的,16进制刚好是2的倍数,更容易表达一个命令或者数据。
- 最早规定ASCII字符集采用的就是8Bit(后期扩展了,但是基础单位还是8Bit), 8bit用2个16进制直接就能表达出来,不管阅读还是存储都比其他进制要方便。
- 计算机中CPU运算也是遵照ASCII字符集,以16、32、64的这样的方式在发展,因此数据交换的时候16进制也显得更好。
- 为了统一规范,CPU、内存、硬盘我们看到都是采用的16进制计算。
- 比如:网络编程,数据交换的时候需要对字节进行解析都是一个Byte一个Byte的处理,1个Byte可以用0xFF两个16进制来表达。数据存储,存储到硬件中是0101的方式,存储到系统中的表达方式都是byte方式。一些常用值的定义,比如:我们经常用到的html中color表达,就是用的16进制方式,4个16进制位可以表达好几百万的颜色。(0x 表示16进制)
什么是高八位?什么是低八位?
- 内存里,一个单元是一个字节,也就是8位。
- 如果是16位的指令,就是同时操作连续的2个内存地址,将这连续的2个内存地址当成一个单位,所以就有高8位和低8位之分。
- 由于计算机仅识别二进制描述的数字,所以对一个内存地址,也就是8位二进制,如:0000 0001,0000就是高四位,0001就是低四位。
什么是小端?什么是大端?为什么会出现大小端?
- 小端Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。(低低高高,低地址低字节,高地址高字节)
- 大端Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。(低高高低,低地址高字节,高地址低字节)
- 大小端出现原因:计算机系统是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但对于位数大于8位的处理器,如16位或32位的处理器,由于寄存器宽度大于一个字节,那么必然存在一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式的出现。
低位字节&高位字节:比如:123456 其中的1就是低位字节,6就是高位字节。
举一个例子,在32位数字0x12 34 56 78在内存中的表示形式为:
- 1)大端模式:
- 低地址 -------------------------> 高地址
- 0x12 | 0x34 | 0x56 | 0x78
- 2)小端模式:
- 低地址 -------------------------> 高地址
- 0x78 | 0x56 | 0x34 | 0x12
下面针对具体报文进行分析,Modbus协议在串行链路上的报文格式如下所示:
读取输出线圈:
发送报文格式如下:
发送报文含义:
读取1号从站输出线圈,起始地址为0x13=19,对应地址为00020,线圈数量为0x1B=27,即读取1号从站输出线圈,地址从00020-00046,共27个线圈的状态值。
(协议中的起始地址指的是索引,后面的地址指的是具体地址,对于任意一个存储区,索引都是从0开始的,但是对应的具体地址,与存储区是相关的,比如输出线圈,0对应00001;输入线圈,0对应10001;输入寄存器,0对应30001;保持寄存器,0对应40001。)
返回报文格式如下:
返回1号从站输出线圈00020-00046,共27个线圈的状态值,返回字节数为4个,分别为CD 6B B2 05。
CD=1100 1101 对应 00020-00027
6B=0110 1011 对应 00028-00035
B2=1011 0010 对应 00036-00043
05=0000 0101 对应 00044-00046
读取输入线圈:
发送报文格式如下:
发送报文含义:
读取1号从站输入线圈,起始地址为0xC4=196,对应地址为10197,线圈数量为0x1D=29,即读取1号从站输入线圈,地址从10197-10225,共29个线圈的状态值。返回报文格式如下:
返回报文含义:
返回1号从站输入线圈10197-10225,共29个线圈的状态值,返回字节数为4个,分别为CD 6B B2 05。
CD=1100 1101 对应 10197-10204
6B=0110 1011 对应 10205-10212
B2=1011 0010 对应 10213-10220
05=0000 0101 对应 10221-10225
读取保持寄存器:
发送报文格式如下:
发送报文含义:
读取1号从站保持寄存器,起始地址为0x6B=107,对应地址为40108,寄存器数量为0x02=2,即读取1号从站保持寄存器,地址从40108-40109,共2个寄存器的数值。
返回报文格式如下:
返回报文含义:
返回1号从站保持寄存器40108-40109,共2个寄存器的数值,返回字节数为4个,分别为02 2B 01 06。
40108对应数值为0x022B;
40109对应数值为0x0106;
读取输入寄存器:
发送报文格式如下:
发送报文含义:
读取1号从站输入寄存器,起始地址为0x6B=107,对应地址为30108,寄存器数量为0x02=2,即读取1号从站输入寄存器,地址从30108-30109,共2个寄存器的数值。
返回报文格式如下:
返回报文含义:
返回1号从站输入寄存器30108-30109,共2个寄存器的数值,返回字节数为4个,分别为02 2B 01 06。
30108对应数值为0x022B;
30109对应数值为0x0106;
预置单线圈:
发送报文格式如下:
发送报文含义:
预置1号从站单个线圈的值,线圈地址为0x00AC=172,对应地址为00173,断通标志0xFF00表示置位,0x0000表示复位,即置位1号从站输出线圈00173。
返回报文格式如下:
返回报文含义:预置单输出线圈原报文返回。
预置单寄存器:
发送报文格式如下:
发送报文含义:
预置1号从站单个保持寄存器的值,寄存器地址为0x0087=135,对应地址为40136,写入值为0x039E,即预置1号从站保持寄存器40136值为0x039E。
返回报文格式如下:
返回报文含义:预置单保持寄存器原报文返回。
预置多线圈:
发送报文格式如下:
从站地址 | 功能码 | 寄存器高 | 寄存器低 | 写入值高 | 写入值低 | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x06 | 0x00 | 0x13 | 0x00 | 0x0A |
发送报文含义:
预置1号从站多个线圈的值,线圈地址为0x0013=19,对应地址为00020,线圈数为0x0A=10,写入值为0xCD00,即预置1号从站线圈00020-00027=0xCD=1100 1101,00028-00029=0x00=0000 0000。
返回报文格式如下:
返回报文含义:预置多输出线圈返回报文是在原报文基础上除去字节数及具体字节后返回。
预置多寄存器:
发送报文格式如下:
发送报文含义:
预置1号从站多个寄存器的值,寄存器地址为0x0087=135,起始地址为40136,寄存器数量为0x02=2,结束地址为40137,写入值为0x0105和0x0A10,即预置1号从站寄存器40136=0x0105,40137=0x0A10。
返回报文格式如下:
返回报文含义:预置多保持寄存器返回报文是在原报文基础上除去字节数及具体字节后返回。
Modbus TCP 协议分析:
ModbusTCP与ModbusUDP的报文格式是一样的,它们之间的区别其实就是TCP与UDP的区别。
ModbusTCP与ModbusRtu(ModbusASCII)之间的区别如下图:
从上图可以看出,ModbusTCP在Modbus串行通信的基础上,去除了校验(由于TCP本身就带有校验和)和设备地址(ModbusTCP弱化了设备地址,用IP地址来取代),再加上MBAP报文头(占7 bytes)。
怎么理解MBAP报文头呢?
对比Modbus RTU 和Modbus TCP 的报文格式:
针对具体报文进行分析,Modbus协议在以太网链路上的报文格式如下所示:
读取输出线圈:
发送报文格式如下:
发送报文含义:读取服务器1号从站输出线圈,起始地址为0x13=19,对应地址为00020,线圈数量为0x1B=27,即读取1号从站输出线圈,地址从00020-00046,共27个线圈的状态值。
(这里值得注意一下,协议中的起始地址指的是索引,后面的地址指的是具体地址,对于任意一个存储区,索引都是从0开始的,但是对应的具体地址,与存储区是相关的,比如输出线圈,0对应00001;输入线圈,0对应10001;输入寄存器,0对应30001;保持寄存器,0对应40001。)
返回报文格式如下:
返回报文含义:返回服务器1号从站输出线圈00020-00046,共27个线圈的状态值,返回字节数为4个,分别为CD 6B B2 05。
CD=1100 1101 对应 00020-00027
6B=0110 1011 对应 00028-00035
B2=1011 0010 对应 00036-00043
05=0000 0101 对应 00044-00046
读取输入线圈:
发送报文格式如下:
发送报文含义:读取服务器1号从站输入线圈,起始地址为0xC4=196,对应地址为10197,线圈数量为0x1D=29,即读取1号从站输入线圈,地址从10197-10225,共29个线圈的状态值。
返回报文格式如下:
返回报文含义:返回服务器1号从站输入线圈10197-10225,共29个线圈的状态值,返回字节数为4个,分别为CD 6B B2 05。
CD=1100 1101 对应 10197-10204
6B=0110 1011 对应 10205-10212
B2=1011 0010 对应 10213-10220
05=0000 0101 对应 10221-10225
读取保持寄存器:
发送报文格式如下:
发送报文含义:读取服务器1号从站保持寄存器,起始地址为0x6B=107,对应地址为40108,寄存器数量为0x02=2,即读取1号从站保持寄存器,地址从40108-40109,共2个寄存器的数值。
返回报文格式如下:
返回报文含义:返回服务器1号从站保持寄存器40108-40109,共2个寄存器的数值,返回字节数为4个,分别为02 2B 01 06,40108对应数值为0x022B,40109对应数值为0x0106。
读取输入寄存器:
发送报文格式如下:
发送报文含义:读取服务器1号从站输入寄存器,起始地址为0x6B=107,对应地址为30108,寄存器数量为0x02=2,即读取1号从站保持寄存器,地址从30108-30109,共2个寄存器的数值。
返回报文格式如下:
返回报文含义:返回服务器1号从站输入寄存器30108-30109,共2个寄存器的数值,返回字节数为4个,分别为02 2B 01 06,30108对应数值为0x022B,30109对应数值为0x0106。
预置单线圈:
发送报文格式如下:
发送报文含义:预置服务器1号从站单个线圈的值,线圈地址为0x00AC=172,对应地址为00173,断通标志0xFF00表示置位,0x000表示复位,即置位1号从站输出线圈00173。
返回报文格式如下:
返回报文含义:预置单输出线圈原报文返回。
预置单寄存器:
发送报文格式如下:
发送报文含义:预置服务器1号从站单个保持寄存器的值,寄存器地址为0x0087=135,对应地址为40136,写入值为0x039E,即预置1号从站保持寄存器40136值为0x039E。
返回报文格式如下:
返回报文含义:预置单保持寄存器原报文返回。
预置多线圈:
发送报文格式如下:
发送报文含义:预置服务器1号从站多个线圈的值,线圈地址为0x0013=19,对应地址为00020,线圈数为0x0A=10,写入值为0xCD00,即预置1号从站线圈00020-00027=0xCD=1100 1101,00028-00029=0x00=0000 0000。
返回报文格式如下:
返回报文含义:预置多输出线圈返回报文是在原报文基础上除去字节数及具体字节后返回。
预置多寄存器:
发送报文格式如下:
发送报文含义:预置服务器1号从站多个寄存器的值,寄存器地址为0x0087=135,起始地址为40136,寄存器数量为0x02=2,结束地址为40137,写入值为0xCD00和0x0A10,即预置1号从站寄存器40136=0x0105,40137=0x0A10。
返回报文格式如下:
返回报文含义:预置多保持寄存器返回报文是在原报文基础上除去字节数及具体字节后返回。