1. 了解Modbus相关概念
Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气 Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准,并且现在是工业电子设备之间常用的连接方式。
Modbus通信协议具有多个变种,支持串口(主要是RS-485总线)、以太网等多个版本,其中最常用的是Modbus RTU、Modbus TCP和Modbus ASCII三种;但是在我们实际应用中,Modbus ASCII很少用到,RTU最多,TCP次之。
从上面对比来看,ASCII协议相对于其他两个来说拥有开始标记和结束标记,所以ASCII协议的程序在对数据包的处理时能更加方便;Modbus ASCII协议的DATA域传输的都是可见的ASCII字符,因此在调试阶段就显得更加直观,另外它的LRC校验程序也比较容易编写,这些都是Modbus ASCII的优点。
Modbus ASCII的主要缺点是传输效率低,因为它传输的都是可见的ASCII字符,原来用RTU传输的数据每一个字节,用ASCII的话都要把这个字节拆分两个字节,比如RTU传输一个十六进制数0xF9,ASCII就需要传输字符'F'和字符'9',对应的ASCII码0x46和0x39两个字节,这样它的传输的效率肯定就比RTU低。所以一般来说,如果所需要传输的数据量较小可以考虑使用ASCII协议,如果所需传输的数据量比较大,最好能使用RTU或TCP协议。
在工业现场一般都是采用Modbus RTU协议,一般而言,大家说的基于串口通信的Modbus通信协议就是指Modbus RTU通信协议。与Modbus RTU协议相比较,Modbus TCP协议则是在RTU协议上加一个MBAP报文头,并且由于TCP是基于可靠连接的服务,RTU协议中的CRC校验码就不再需要,所以在Modbus TCP协议中是没有CRC校验码的,所以就常用一句比较通俗的话来说:Modbus TCP协议就是Modbus RTU协议在前面加上五个0以及一个6,然后去掉两个CRC校验码,这种说法可能不太准确,但是在实际的应用中就是这样。
2. 协议解读
2.1 数据模型
数据模型是对可访问数据的一种抽象,Modbus协议的数据模型定义了四种可访问的数据,分别是:
离散量输入(Discrete Input);线圈(Coils);输入寄存器(Input registers);保持寄存器(Holding registers);
其中,离散量输入和线圈只支持以位(bit)的方式进行访问,输入寄存器和保持寄存器只支持以字(WORD)的方式进行访问;离散量输入和输入寄存器只支持以只读的方式进行访问,而线圈和保持寄存器既可以读也可以写。
在实际使用时必须将抽象的数据模型映射到真实的物理存储区才能被访问。
Modbus协议允许设备将四种数据分别映射到不同的存储区块中,各个区块之间相互独立,使用不同的功能码可读取到不同的数值,如下图所示:
Modbus协议也允许设备将四种数据映射到同一存储区块中,这样通过不同的功能码读取数据可能会得到相同的数据(比如:输入寄存器和保持寄存器为同一物理区块),如下图所示:
2.2 Modbus数据地址
数据模型中的每一种数据都最多允许有65536个元素(编号1~65536),元素的地址编号从0开始,因此地址的范围为:0~65535。
由于65536是比较大的数值,实际应用一般不需要这么大的存储区,因此PLC厂家普遍采用的是10000以内的地址范围,即:
线圈地址范围:00001~09999
离散量输入地址范围:10001~19999
输入寄存器地址范围:30001~39999
保持寄存器地址范围:40001~49999
2.3 Modbus常用功能码
一下是常用的功能码:
0x01: 读线圈寄存器
0x02: 读离散输入寄存器
0x03: 读保持寄存器
0x04: 读输入寄存器
0x05: 写单个线圈寄存器
0x06: 写单个保持寄存器
0x0f: 写多个线圈寄存器
0x10: 写多个保持寄存器
线圈寄存器:线圈这个词来自PLC,可以理解为开关量(继电器状态),每一个bit对应一个信号的开关状态。所以一个byte就可以同时控制8路的信号。比如控制外部8路io的高低。线圈寄存器支持读也支持写,写在功能码里面又分为写单个线圈寄存器和写多个线圈寄存器。
离散输入寄存器:如果线圈寄存器理解了这个自然也明白了。离散输入寄存器就相当于线圈寄存器的只读模式,他也是每个bit表示一个开关量,而他的开关量只能读取输入的开关信号,是不能够写的。
保持寄存器:这个寄存器的单位不再是bit而是两个byte,也就是可以存放具体的数据量的,并且是可读写的。一般对应参数设置,比如我我设置时间年月日,不但可以写也可以读出来现在的时间。写也分为单个写和多个写。
输入寄存器:这个和保持寄存器类似,但是只支持读而不能写,一般是读取各种实时数据。一个寄存器也是占据两个byte的空间。
功能码:01 (0x01)读线圈
功能:读取远程设备(从站)线圈的1至2000连续状态。
说明:请求 PDU 详细说明了起始地址,即指定的第一个线圈地址和线圈编号。从零开始寻址线圈,因此寻址线圈 1-16 为 0-15;根据数据域的每个比特将响应报文中的线圈分成为一个线圈,指示状态为 1= ON 和 0= OFF。第一个数据字节的 LSB(最低有效位)包括在询问中寻址的输出。其它线圈依次类推,一直到这个字节的高位端为止,并在后续字节中从低位到高位的顺序。如果返回的输出数量不是八的倍数,将用零填充最后数据字节中的剩余比特(一直到字节的高位端)。字节数量域说明了数据的完整字节数。
*N=输出数量/8,如果余数不等于 0,那么 N = N+1
上图是一个请求读离散量输出 20-38 的实例。将输出 27-20 的状态表示为十六进制字节值 CD,或二进制 1100 1101。输出 27 是这个字节的MSB,输出 20 是 LSB。通常,将一个字节内的比特表示为 MSB 位于左侧,LSB 位于右侧。第一字节的输出从左至右为 27 至 20。下一个字节的输出从左到右为 35 至 28。当串行发射比特时,从 LSB 向 MSB 传输:20 . . . 27、28 . . . 35 等等。在最后的数据字节中,将输出状态 38-36 表示为十六进制字节值 05,或二进制 0000 0101。输出38 是左侧第六个比特位置,输出 36 是这个字节的 LSB。用零填充五个剩余高位比特。
功能码:02 (0x02)读离散量输入
这个就不说了,跟上面读线圈类似。
功能码:03 (0x03)读保持寄存器
功能:读取远程设备(从站)保持寄存器连续块的内容。
说明:请求 PDU 说明了起始寄存器地址和寄存器数量。从零开始寻址寄存器。因此,寻址寄存器 1-16 为 0-15。将响应报文中的寄存器数据分成每个寄存器有两字节,在每个字节中直接地调整二进制内容。对于每个寄存器,第一个字节包括高位比特,并且第二个字节包括低位比特。
以上是一个请求读寄存器 108-110 的实例。
将寄存器 108 的内容表示为两个十六进制字节值 02 2B,或十进制 555。将寄存器 109-110 的内容分别表示为十六进制 00 00 和 00 64,或十进制 0 和 100。
功能码:04(0x04)读输入寄存器
功能:读取远程设备(从站)的连续输入寄存器。
说明:请求 PDU 说明了起始地址和寄存器数量。将响应报文中的寄存器数据分成每个寄存器为两字节,在每个字节中直接地调整二进制内容。对于每个寄存器,第一个字节包括高位比特,并且第二个字节包括低位比特。
以上是一个请求读输入寄存器 9 的实例。
将输入寄存器 9 的内容表示为两个十六进制字节值 00 0A,或十进制 10。
功能码:05 (0x05)写单个线圈
功能:针对一个远程设备(从站),使用该功能码写单个输出为 ON 或 OFF。
说明:请求数据域中的常量说明请求的 ON/OFF 状态。十六进制值 FF 00 请求输出为 ON。十六进制值00 00 请求输出为 OFF。其它所有值均是非法的,并且对输出不起作用。
请求 PDU 说明了强制的线圈地址。从零开始寻址线圈。因此,寻址线圈 1 为 0。线圈值域的常量说明请求的 ON/OFF 状态。十六进制值 0XFF00 请求线圈为 ON。十六进制值 0X0000 请求线圈为OFF。其它所有值均为非法的,并且对线圈不起作用。
正常响应是请求的应答,在写入线圈状态之后返回这个正常响应。
以上是一个请求写线圈 173 为 ON 的实例。
功能码:06 (0x06)写单个寄存器
功能:在一个远程设备中,使用该功能码写单个保持寄存器。
说明:请求 PDU 说明了被写入寄存器的地址。从零开始寻址寄存器。因此,寻址寄存器 1 为 0。正常响应是请求的应答,在写入寄存器内容之后返回这个正常响应。
以上是一个请求将十六进制 00 03 写入寄存器 2 的实例。
功能码:15 (0x0F) 写多个线圈
功能:针对一个远程设备(从站),使用该功能码强制线圈序列中的每个线圈为 ON 或 OFF。
说明:请求 PDU 说明了强制的线圈参考。请求数据域的内容说明了被请求的 ON/OFF 状态。域比特位置中的逻辑“1”请求相应输出为ON。域比特位置中的逻辑“0”请求相应输出为 OFF。
正常响应返回功能码、起始地址和强制的线圈数量。
以上是一个请求从线圈 20 开始写入 10 个线圈的实例。
请求的数据内容为两个字节:十六进制 CD 01 (二进制 1100 1101 0000 0001)。使用下列方法,二进制比特对应输出。
比特:1 1 0 0 1 1 0 1 0 0 0 0 0 0 0 1
输出:27 26 25 24 23 22 21 20 – – – – – – 29 28
传输的第一字节(十六进制 CD)寻址为输出 27-20,在这种设置中,最低有效比特寻址为最低输出(20)。
传输的下一字节(十六进制 01)寻址为输出 29-28,在这种设置中,最低有效比特寻址为最低输出(28)。
应该用零填充最后数据字节中的未使用比特。
功能码:16 (0x10) 写多个寄存器
功能:针对一个远程设备(从站),使用该功能码写连续寄存器块。
说明:在请求数据域中说明了请求写入的值。每个寄存器将数据分成两字节。
正常响应返回功能码、起始地址和被写入寄存器的数量。
以上是一个请求将十六进制 00 0A 和 01 02 写入以 2 开始的两个寄存器的实例。
下面附两个读写状态图:
3. 在编程中使用
开发环境是.NET,我们在编写程序之前首先搞清楚你是Master还是Slave,一般情况下Master主站比较多,使用RTU或TCP链接从站,采用主动问询的方式读取数据;如果每次读取的数据间隔比较大,可以断开链接,下次采集的时候再次链接就行,避免资源长期占用。
还有些情况是采用Slave从站的方式,被动接收数据,主站数据刷新会不断给订阅的从站推送数据,数据更新比较实时,但是有些数据长时间不刷新导致数据假死,这个要根据实际应用来判断。
_modbusSlave.ModbusSlaveRequestReceived += requestReceiveHandler;
Modbus相关的DLL文件有Modbus.dll、 nModbus.dll、 nModbus4.dll等,版本不同功能也略有差异,根据自己的需要选择,也可以自己封装一个(如果你封装一个更好用的,记得分享);底层都是一样的,串口或Socket通讯。
另外再说一下Socket发送。Socket在最终发送的时候,是转换成字节流byte[]数组,然后使用Socket发送,比如我们要读取的设备ID是11,开始地址是32300,读取8位,最终在发送的时候byte数组格式大概是[0]=11, [1]=3, [2]=126, [3]=44, [4]=0, [5]=8(当然还要在前面加上Header),全部翻译成16进制就是 0B 03 7E 2C 00 08 ,这样应该就比较清楚了。
以上信息有部分是参考第三方文档和网络上的技术文章,如有侵权请与作者联系。需要Modbus相关资料或技术交流也可与我联系。(微信:xiaoyiyz)
注:感觉对你有帮助请关注并分享给更多人,您的关注和分享是我写文章的动力,感谢支持。 |