前言 ??在学习modbus协议之前,读者最好能够具备有串行通信的基础知识。串行通信是指数据以串行的方式传输的一种通信,即仅使用一条数据线,将数据一位一位地依次传输,每一位数据占据一个固定的时间长度。这样,其只需要少数几条线就可以实现设备间的数据交互。像UART、IIC、SPI等都是使用串行通信方式。
??从我们最熟悉的单片的的UART开始说起,我们知道,两个单片机之间通过串口电路,并且遵循已经制定好的“串口通信协议”,就可以实现数据交互。但是这种单片机之间直接通过导线相连的方式并没那么可靠,因为导线中的电平容易收到外界的干扰,可能会导致信息传输错误。为了解决这个问题,就出现了RS-485传输标准。如图是RS-485电路的示意图,MCU_1发送端产生的TTL电平经过电平转换电路之后,变成AB两根导线之间的差分信号。当MCU给转换器输入低电平时,转换器会使得B的电压比A的电压高,反之,A的电压比B的电压高。之后,再通过一个电平转换电路,将差分信号转换成MUC_2能够识别的TTL信号。
文章图片
??RS-485传输方式具有如下特性:
??1)差分传输增加噪声抗扰度,减少噪声辐射;
??2)长距离链路,最长可达4000英尺(约1219米);
??3)数据速率高达10Mbps(40英寸内,约12.2米);
??4)同一总线可以连接多个驱动器和接收器;
??5)宽共模范围允许驱动器和接收器之间存在地电位差异,允许最大共模电压-7-12V ?
? ?我们由此可以看出,RS-485电路是单片机串口的拓展,可以提高通信电路的抗干扰能力,增加传输距离。
??到此,已经解决了设备之间的数据传输问题,但是显然还有一些问题没有考虑,第一,如果通信网络上有多个设备,发送设备要如何指定哪一个设备要接收这条消息呢?第二,接收设备接收到一条消息之后,它要如何解析这条消息呢?要想解决这些问题,设备厂家们可以定义一种在UART协议之上的“上层协议”来约定一些规则。但是如果每个厂家都设计了不同的协议,那么不同厂家的设备之间有存在通信的问题。由此,Modbus协议的意义就显现出来了。
1 Modbus协议概要 ???Modbus通信协议由Modicon公司(现在的施耐德电气Schneider Electric)于1979年为可编程逻辑控制(即PLC)通信而发表。目前,Modbus已经成为工业领域通信协议的业界标准,并且现在是工业电子设备之间常用的连接方式。Modbus作为目前工业领域应用最广泛的协议,与其他通信协议相比,有以下特点:
???1. Modbus协议标准开放、公开发表且无版权要求。
???2.Modbus协议支持多种电气接口,包括RS232、RS485、TCP/IP等,还可以在各种介质上传输,如双绞线、光纤、红外、无线等。
???3.Modbus协议消息帧格式简单、紧凑、通俗易懂。用户理解和使用简单,厂商容易开发和集成,方便形成工业控制网络。
???此协议定义了一个控制器能够认识的消息结构,而不管它们是经过何种网络进行通信的;而且描述了控制器请求访问其他设备的过程,如何应答来自其他设备的请求,以及怎样侦测错误并记录;并制定了统一的消息域结构和内容。
???Modbus协议是一种单主/多从的通信协议。在同一时间,总线上只能有一个主设备,但可以有一个或者多个从设备(最多247个)。Modbus通信总是由主设备发起,从设备没有收到来自主设备的请求时,不会主动发送数据。从设备之间不能相互通信。
???主设备可以采用两种方式向从设备发送modbus请求:
- 单播模式:主设备仅仅寻址单个从设备,从设备接收并处理完请求后,向主设备返回一个响应报文。
- 广播模式:主设备向总线上所有从设备发送请求,而从设备收到请求指令后,仅进行相关的事物处理而不返回应答。因此,广播模式下,请求指令只能是modbus标准功能中的写指令,而不是读指令。地址0被保留作为广播地址。
Data Unit)模型。
文章图片
????????????????图2-1 通用消息帧格式
??Modbus协议包含ASCII、RTU、TCP三种传输模式。当使用串口传输数据时可以选择ASCII或RTU模式。对于同一网络,所有设备必须保持统一的模式。不同模式中的消息帧格式有所不同。
2.1 Modbus ASCII消息帧 ???在ASCII模式中,消息中每个字节都将作为两个ASCII字符发送,如0x53要分成0x35(5的ASCII码)和0x33(3的ASCII码)两个字节发送,因此传输效率较低。
???在该模式中,消息帧以冒号(:)字符(ASCII码为0x3A)开始,以回车换行符(ASCII码为0x0A,0x0D)结束。消息帧的其他字段可以使用的字符是十六进制的0…9,A…F。处于网络上的设备不断侦测“:”字符,当有一个冒号收到时,每个设备进入解码阶段,并解码下一个字段(地址域),判断消息是不是发给自己的。消息帧的字符间发送时间间隔不能超过1秒,否则接收设备认为是发生传输错误。
??一个典型的ASCII消息帧格式如表所示:
??????????表2-1 Modbus ASCII消息帧格式
文章图片
2.2 Modbus RTU消息帧 ???在RTU模式中,消息帧不存在起始字符和结束字符,而是以T3.5作为两帧数据的分隔标志。在实际使用中,网络中的设备不断侦测字符间的停顿时间间隔,判断消息帧的起始点。当收到第一个字节(地址域)时,判断消息是不是发给自己的。在最后一个字符传输结束之后,一个至少T3.5的停顿代表消息的结束,而一个新的消息可以在此停顿后开始。另外,在一帧数据中,如果两个字符之间的空闲间隔大于T1.5,那么认为报文不完整,该报文将被丢弃。
???T3.5即3.5个字符时间,在串行通信中,1个字符通常包括1位起始位、8位数据位、1位奇偶校验位、1位停止位。这样,一个字符就包括11位,那么3.5个字符就是38.5位。
???在串行通信中,波特率代表每秒传输二进制位的个数,如波特率为9600bps代表每秒传输9600bit。那么传输38.5bit的时间为
? (38.5/9600)*1000ms = 4.0104167ms
???在波特率为9600bps的情况下,ModbusRTU要求“前一帧数据的结束”和“后一帧数据的开始”的时间间隔大于4.0104167ms。
文章图片
???????????2-2 Modbus RTU相邻帧间隔
文章图片
??????????图2-3 Modbus RTU消息帧格式
???为了实现RTU通信中的时间间隔管理,定时器将引起大量的中断处理,在较高波特率的情况下,这将导致CPU的沉重负担。为此,协议规定当波特率大于19200bps的情况下,时间间隔使用固定值,建议T1.5为750us,T3.5为1750us。
2.3 地址域 ???地址码是信息帧的第一个字节(8位),从0到255。每个从机都必须有唯一的地址。地址的作用有3个:第一,当主机下发指令时,可以有针对性的指定哪个从机接受指令;第二,从机回复消息时,可以让主机知道该信息来自于何处;第三,从机回复消息时,其他从机不会误认为是主机下发的请求。
???如果地址为0,则认为是一个广播命令,就是所有从机要接收并处理主机发来的信息。
文章图片
??????????表2-2 Modbus寻址范围
2.4 功能码域 ???功能码域由一个字节构成,其取值范围为1到127。从设备根据功能码执行相应的动作,执行完成后,正常情况下,从机回应的功能码与主机下发的功能码一样。如果出现异常,则在返回消息帧中将功能码最高位置1(129~255代表异常码)。据此,主设备可以获取从设备的执行情况。
???另外,对于主设备发送的功能码,从设备根据具体配置来决定是否支持此功能码,如果不支持,则返回异常响应。
???Modbus协议规定了三类功能码,分别是:
- 公共功能码
??1)被明确定义的功能码;
??2)保证唯一性;
??3)由Modbus协会确认,并提供公开文档;
??4)可进行一致性测试;
??5)包括协议定义的功能码和保留将来使用的功能码
- 用户自定义的功能码
??1)有两个用户自定义的功能码区域,分别是65~72和100~110;
??2)用户自定义,不保证唯一性。
- 保留功能码
??Modbus部分功能码如表所示:
??????????表2-3 Modbus部分功能码
代码 | 中文名称 | 寄存器PLC地址 | 位/字操作 | 操作数量 |
---|---|---|---|---|
01 | 读线圈状态 | 00001-09999 | 位操作 | 单个或多个 |
02 | 读离散输入状态 | 10001-19999 | 位操作 | 单个或多个 |
03 | 读保持寄存器 | 40001-49999 | 字操作 | 单个或多个 |
04 | 读输入寄存器 | 30001-39999 | 字操作 | 单个或多个 |
05 | 写单个线圈 | 00001-09999 | 位操作 | 单个 |
06 | 写单个保持寄存器 | 40001-49999 | 字操作 | 单个 |
15 | 写多个线圈 | 00001-09999 | 位操作 | 多个 |
16 | 写多个保持寄存器 | 40001-49999 | 字操作 | 多个 |
???功能码可以分为位操作和字操作两类。位操作的最小单位为BIT,字操作的最小单位为两个字节。
?
?1)位操作指令:读线圈状态01H,读(离散)输入状态02H,写单个线圈06H和写多个线圈0FH。
???2)字操作指令:读保持寄存器03H,写单个寄存器06H,写多个保持寄存器10H。
2.4.2 寄存器地址分配
???????表2-4 MODBUS寄存器地址分配
寄存器PLC地址 | 寄存器协议地址 | 适用功能 | 寄存器种类 | 读写状态 |
---|---|---|---|---|
00001-09999 | 0000H-FFFFH | 01H05H0FH | 线圈状态 | 可读可写 |
10001-19999 | 0000H-FFFFH | 02H | 离散输入状态 | 可读 |
30001-39999 | 0000H-FFFFH | 04H | 输入寄存器 | 可读 |
40001-49999 | 0000H-FFFFH | 03H06H0FH | 保持寄存器 | 可读可写 |
???????????表2-5 MODBUS寄存器种类说明
寄存器种类 | 说明 | PLC类比 | 举例说明 |
---|---|---|---|
线圈状态 | 输出端口。可设定端口的输出状态,也可以读取该位的输出状态。可分为两种不同的执行状态,例如保持型或边沿触发型。 | DO数字量输出 | 电磁阀输出,MOSFET输出,LED显示等。 |
离散输入状态 | 输入端口。通过外部设定改变输入状态,可读但不可写。 | DI数字量输入 | 拨码开关,接近开关等。 |
保持寄存器 | 输出参数或保持参数,控制器运行时被设定的某些参数。可读可写。 | AO模拟量输出 | 模拟量输出设定值,PID运行参数,变量阀输出大小,传感器报警上限下限。 |
输入寄存器 | 输入参数。控制器运行时从外部设备获得的参数。可读但不可写。 | AI模拟量输入 | 模拟量输入 |
???PLC地址可以理解为协议地址的变种,在触摸屏和PLC编程中应用较为广泛。
??
1)寄存器PLC地址
??寄存器PLC地址指存放于控制器中的地址,这些控制器可以是PLC,也可以是触摸屏,或是文本显示器。PLC地址一般采用10进制描述,共有5位,其中第一位代码寄存器类型。第一位数字和寄存器类型的对应关系如表2-4所示。PLC地址例如40001、30002等。
???2)寄存器协议地址
???寄存器协议地址指通信时使用的寄存器地址,例如PLC地址40001对应寻址地址0x0000,40002对应寻址地址0x0001,寄存器寻址地址一般使用16进制描述。再如,PLC寄存器地址40003对应协议地址0002,PLC寄存器地址30003对应协议地址0002,虽然两个PLC寄存器寄存器通信时使用相同的地址,但是需要使用不同的功能码访问,所以访问时不存在冲突。
2.4.5 数据域
???数据域与功能码紧密相关,存放功能码需要操作的具体数据,数据域以字节为单位,长度不是固定的,对于有些功能码,数据域可以为空。
2.4.6差错校验
??校验码由发送设备计算,放置于发送消息帧的尾部。接受消息的设备再重新计算接收到的信息的校验码,比较计算得到的校验码是否与接收到的相符,如果不相符,则表明出错。它用于保证主机或从机对传送过程中出错的信息起不了作用,增加了系统的安全与效率。
???在串行通信中,根据传输模式(RTU或ASCII)的不同,采用了不同的校验方法。
???在ASCII模式中,校验域由两个字符组成,其值基于对全部报文内容执行纵向冗余校验(LRC)计算的结果而来,计算对象不包括冒号和回车换行符。
???在RTU模式中,校验域由16bit即两个字节构成,其值基于对全部报文内容执行循环冗余校验(CRC)计算的结果而来,计算对象包括校验域之前所有字节。
???产生CRC-16码的步骤如下:
??1)预置一个16位的寄存器为全1(即十六进制FFFFH),称此寄存器为CRC寄存器;
???2)把第一个8位数据与CRC寄存器的低8位相异或,结果放回CRC寄存器;
【modbus协议学习笔记】???3)把16位CRC寄存器右移一位,用0添补最高位,检测移出位:
???4)如果移出位为0,则重复第3步骤(再次移出);如果移出位为1,则CRC寄存器与多项式A001H相异或,结果放回CRC寄存器;
???5)重复第3、4步骤,直至移出8位;
???6)将下一个8位数据与CRC寄存器低8位相异或,结果放回CRC寄存器,重复第2、3、4、5步骤;
???7)消息帧中所有字节按照上述步骤计算完成后,当放置CRC值于报文时,必须将得到的16位CRC寄存器高、低字节进行交换。
???8)最后得到的CRC寄存器内容即为产生的CRC校验码。
3 Modbus功能码详解 ???有了以上理论基础之后,下面针对各个功能码进行详细分析。
3.1 读取输出线圈 ???发送报文格式如下:
从站地址 | 功能码 | 起始(高) | 起始(低) | 数量(高) | 数量(低) | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x01 | 0x00 | 0x13 | 0x00 | 0x1B | XXXX |
??发送报文含义:读取1号从站输出线圈,起始地址为0x13=19,对应寄存器PLC地址为00020,线圈数量为0x1B=27,即读取1号从站输出线圈,地址从00020-00046,共27个线圈的状态值。
???返回报文格式如下:
从站地址 | 功能码 | 字节计数 | 字节1 | 字节2 | 字节3 | 字节4 | 校验 |
---|---|---|---|---|---|---|---|
0x01 | 0x01 | 0x04 | 0xCD | 0x6B | 0xB2 | 0x05 | XXXX |
?????CD=1100 1101 对应 00020-00027 ????6B=01101011 对应 00028-00035
?????B2=1011 0010 对应 00036-00043 ???? 05=00000101 对应 00044-00046
3.2 读取输入线圈 ??发送报文格式如下:
从站地址 | 功能码 | 起始(高) | 起始(低) | 数量(高) | 数量(低) | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x02 | 0x00 | 0xC4 | 0x00 | 0x1D | XXXX |
??返回报文格式如下:
从站地址 | 功能码 | 字节计数 | 字节1 | 字节2 | 字节3 | 字节4 | 校验 |
---|---|---|---|---|---|---|---|
0x01 | 0x02 | 0x04 | 0xCD | 0x6B | 0xB2 | 0x05 | XXXX |
?????CD=1100 1101 对应 10197-10204????6B=01101011 对应 10205-10212
?????B2=1011 0010 对应 10213-10220 ????05=00000101 对应 10221-10225
3.3 读取保持寄存器 ??发送报文格式如下:
从站地址 | 功能码 | 起始(高) | 起始(低) | 数量(高) | 数量(低) | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x03 | 0x00 | 0x6B | 0x00 | 0x02 | XXXX |
? 返回报文格式如下:
从站地址 | 功能码 | 字节计数 | 1高 | 1低 | 2高 | 2低 | 校验 |
---|---|---|---|---|---|---|---|
0x01 | 0x03 | 0x04 | 0x02 | 0x2B | 0x01 | 0x06 | XXXX |
3.4 读取输入寄存器 ??发送报文格式如下:
从站地址 | 功能码 | 起始(高) | 起始(低) | 数量(高) | 数量(低) | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x04 | 0x00 | 0x6B | 0x00 | 0x02 | XXXX |
???返回报文格式如下:
从站地址 | 功能码 | 字节计数 | 1高 | 1低 | 2高 | 2低 | 校验 |
---|---|---|---|---|---|---|---|
0x01 | 0x04 | 0x04 | 0x02 | 0x2B | 0x01 | 0x06 | XXXX |
3.5 写单个线圈 ??
发送报文格式如下:
从站地址 | 功能码 | 线圈(高) | 线圈(低) | 断通标志 | 断通标志 | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x05 | 0x00 | 0xAC | 0xFF | 0x00 | XXXX |
???返回报文格式如下:
从站地址 | 功能码 | 线圈(高) | 线圈(低) | 断通标志 | 断通标志 | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x05 | 0x00 | 0xAC | 0xFF | 0x00 | XXXX |
3.6 写单个保持寄存器 ??发送报文格式如下:
从站地址 | 功能码 | 寄存器高 | 寄存器低 | 写入值高 | 写入值低 | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x06 | 0x00 | 0x87 | 0x03 | 0x9E | XXXX |
???返回报文格式如下:
从站地址 | 功能码 | 寄存器高 | 寄存器低 | 写入值高 | 写入值低 | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x06 | 0x00 | 0x87 | 0x03 | 0x9E | XXXX |
3.7 写多个线圈 ???发送报文格式如下:
从站地址 | 功能码 | 起始高 | 起始低 | 数量高 | 数量低 | 字节数 | 字节 | 校验 |
---|---|---|---|---|---|---|---|---|
0x01 | 0x0F | 0x00 | 0x13 | 0x00 | 0x0A | 0x02 | 0xCD00 | XXXX |
0000。
??返回报文格式如下:
从站地址 | 功能码 | 起始高 | 起始低 | 数量高 | 数量低 | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x0F | 0x00 | 0x13 | 0x00 | 0x0A | XXXX |
3.8 写多个保持寄存器 ???发送报文格式如下:
从站地址 | 功能码 | 起始高 | 起始低 | 数量高 | 数量低 | 字节数 | 字节 | 校验 |
---|---|---|---|---|---|---|---|---|
0x01 | 0x10 | 0x00 | 0x87 | 0x00 | 0x02 | 0x04 | 0x01050A10 | XXXX |
??返回报文格式如下:
从站地址 | 功能码 | 起始高 | 起始低 | 数量高 | 数量低 | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x10 | 0x00 | 0x87 | 0x00 | 0x02 | XXXX |
??存器地址为0x0087=135,起始地址为40136,寄存器数量为0x02=2,结束地址为40137,写入值为0x0105和0x0A10,即预置1号从站寄存器40136=0x0105,40137=0x0A10。
???返回报文格式如下:
从站地址 | 功能码 | 起始高 | 起始低 | 数量高 | 数量低 | 校验 |
---|---|---|---|---|---|---|
0x01 | 0x10 | 0x00 | 0x87 | 0x00 | 0x02 | XXXX |