前面我们已经对Modbus的基本事务作了说明,也据此设计了我们将要实现的主从站的操作流程。这其中与Modbus直接相关的就是Modbus消息帧的生成。Modbus消息帧也是实现Modbus通讯协议的根本。

1Modbus消息帧分析

MODBUS协议在不同的物理链路上的消息帧有一些差异,但我们分析一下就会发现,在这些不同的消息帧中具有一下相同的部分,这对我们实现统一的数据操作非常重要,具体描述如下:

1)、简单协议数据单元

MODBUS协议定义了一个与基础通信层无关的简单协议数据单元(PDU)。简单协议数据单元的结构如下:

PDU是一个与具体的传输网络无关的部分,包含功能码和数据。对于特定总线或网络上的 MODBUS 协议只是在PDU的基础上在应用数据单元(ADU)上引入一些附加域。

数据单元部分的开发是最基本的部分,主要是2各方面的类容:一是生成客户端(主站)访问服务器(从站)的命令部分;二是生成服务器(从站)响应客户端(主站)回复部分。

2)、RTU的应用数据单元

对于在串行链路上运行的Modbus协议,其应用数据单元(ADU)是在PDU的基础上,在前面加上地址域,后面加上数据校验。格式如下图所示:

地址域就是所访问从站的地址,为一个8位无符号数,取值0-255,但0和255有固定含义不能使用。CRC校验采用的是CRC16校验方式。

3)、TCP的应用数据单元

在以太网链路上运行的Modbus协议,其应用数据单元(ADU)是在PDU的基础上添加上MBAP报文头形成的,具体格式如下图:

对于MBAP 报文头,包括下列域:

长度

描述

客户机

服务器

事务元标识符

2 个字节

MODBUS 请求/响应事务处理的识别码

客户机启动

服务器从接收的请求中重新复制

协议标识符

2 个字节

0=MODBUS 协议

客户机启动

服务器从接收的请求中重新复制

长度

2 个字节

以下字节的数量

客户机启动(请求)

服务器(响应)启动

单元标识符

1 个字节

串行链路或其它总线上连接的远程从站的识别码

客户机启动

服务器从接收的请求中重新复制

从上表中可知报文头为 7 个字节长:

事务处理标识符:用于事务处理配对。在响应中,MODBUS 服务器复制请求的事务处理标识符。
协议标识符:用于系统内的多路复用。通过值 0 识别 MODBUS 协议。

长度:长度域是下一个域的字节数,包括单元标识符和数据域。

单元标识符:为了系统内路由,使用这个域。专门用于通过以太网TCP-IP 网络和MODBUS串行链路之间的网关对MODBUS或MODBUS+串行链路从站的通信。说的简单点就是串行链路中的地址域。MODBUS客户机在请求中设置这个域,在响应中服务器必须利用相同的值返回这个域。

2、数据帧的具体组成分析

从以上对简单协议基本数据元、RTU应用数据单元和TCP应用数据单元报文格式的分析,我们发现对于基本数据单元部分已一致的,所以我们可以考虑来分层封装协议操作部分:

最开始实现Modbus基本数据单元,这是数据公用部分与具体的应用无关,只需要封装一次,对于这部分的开发只需要按照Modbus的标准协议来开发就好,本次我们计划实现的功能有8个:

功能码

名称

实现

描述

0x01

读线圈

对可读写型的状态量进行读取

0x02

读离散输入

对只读型的状态量进行读取

0x03

读保持寄存器

对可读写型的寄存器量进行读取

0x04

读输入寄存器

对只读型的寄存器量进行读取

0x05

写单个线圈

对单个的读写型的状态量进行写入

0x06

写单个寄存器

对单个的读写型的寄存器量进行写入

0x0F

写多个线圈

对多个的读写型的状态量进行写入

0x10

写多个寄存器

对多个的读写型的寄存器量进行写入

这8个也是Modbus协议所定义的最主要的功能,现在对这几种功能码的报文格式描述如下:

1)读线圈0x01

读线圈就是都一种可以写的开关量,因为Modbus协议起源于PLC应用,而线圈是对PLC的DO输出的称呼,一般适用于主站对从站下达操作命令。读这种具有读写功能的状态量的数据格式如下:

其下发的命令格式为:域名+功能码+起始地址+数量。

2)读离散输入0x02

读状态输入是读取一种只读开关量信号,对应于PLC中的数字输入量。读取这种只读型开关输入量的格式如下:

其下发的命令格式为:域名+功能码+起始地址+数量。

3)读保持寄存器0x03

保持寄存器就是指可以读写的16位数据,通过单个或多个保持寄存器可以用来表示各种数据,如8位整数、16为整数、32位整数、64位整数以及单双精度浮点数等。读取保持寄存器的报文格式如下:

其下发的命令格式为:域名+功能码+起始地址+数量。

4)读输入寄存器0x04

输入寄存器是一种只读形式的16位数据。通过单个或多个输入寄存器可以表示8位整数、16为整数、32位整数、64位整数以及单双精度浮点数等。读取输入寄存器的报文格式如下:

其下发的命令格式为:域名+功能码+起始地址+数量。

5)写单个线圈0x05

写单个线圈量就是对单个的可读写的开关量进行操作,但是其并非是直接写“0”或者“1”,而是在需要写“1”时发送0xFF00;而在需要写“0”时发送0x0000,其具体的报文格式如下:

其下发的命令格式为:域名+功能码+输出地址+输出值。命令的具体内容与读操作有区别但,格式却是完全一样,在编程时实际读和写可以封装在一起。

6)写单个寄存器0x06

写单个寄存器就是对单个的保持寄存器进行操作,数据的格式依然是一样的,实际应用中只适用于对16位整型数据的操作,对于浮点数等则不可以。

其下发的命令格式为:域名+功能码+输出地址+输出值。命令的具体内容与读操作有区别但,格式却是完全一样,在编程时实际读和写可以封装在一起。

7)写多个线圈0x0F

写多个线圈的操作对象与写单个线圈是完全一样的,不同的是数量和操作值,特别是值,写“1”就是“1”,写“0”就是 “0”,这是与写单个线圈的区别。

其下发的命令格式为:域名+功能码+起始地址+输出数量+字节数+输出值。命令报文与前面的几种读写操作有较大的区别,必须要单独处理。

8)写多个寄存器0x10

写多个寄存器的就是对多个可读写寄存器同时进行操作,数据报文的格式与写多个线圈是一致的。

其下发的命令格式为:域名+功能码+起始地址+输出数量+字节数+输出值。

3、基本数据单元的编程

经过上面的分析,我们发现不论是在什么样的物理链路上实现的应用数据,器基本数据段都是相同的。其实加上域名段的格式也是相同的,所以我们就将

域名+PDU一起作为最基本的数据单元来实现。

对于基本数据单元的实现由分为2种情况:一是作为主站(客户端)时,对从站(服务器)的下发命令;二是作为从站(服务器)时,对主站(客户端)命令的响应。所以我们将这两种情况分别封装为2个基础函数:

(1)、作为RTU主站(TCP客户端)时,生成读写RTU从站(TCP服务器)对象的命令:

uint16_t GenerateReadWriteCommand(ObjAccessInfo objInfo,bool *statusList,uint16_t *registerList,uint8_t *commandBytes)

参数分别是PDU单元的基本信息,写对象的对应数据,以及生成的命令字节。而返回值则是生成的命令的长度。

(2)、作为从站(服务器)时,生成主站读访问的响应。对于响应因为写操作的响应实际上就是复制主站(客户端)的命令的一部分,所以我们实际需要生成的响应是包括0x01、0x02、0x03、0x04功能码的情形。

uint16_t GenerateMasterAccessRespond(uint8_t *receivedMessage,bool *statusList,uint16_t *registerList,uint8_t *respondBytes)

参数分别是接收到的信息,读取的对象的数据,以及返回的响应消息。而返回值则是返回的响应消息的长度。

4RTU应用数据单元的编程

对于RTU应用数据单元来说,其报文格式就是:“域名+PDU+CRC”,而域名+PDU我们在上一节中已经实现了,所以要实现RTU的数据单元实际上我们只需要加上CRC校验就已经完成了。

对于RTU数据单元的实现由分为2种情况:一是作为主站时,对从站的下发命令;二是作为从站时,对主站命令的响应。所以我们将这两种情况分别封装为2个基础函数:

(1)、作为RTU主站时,生成读写RTU从站对象的命令:

/*生成读写从站数据对象的命令,命令长度包括2个校验字节*/

uint16_t SyntheticReadWriteSlaveCommand(ObjAccessInfo slaveInfo,bool *statusList,uint16_t *registerList,uint8_t *commandBytes)

参数分别是从站基本信息,下发的数据列表,以及最终生成的命令数组。返回值是是命令的长度。

(2)、作为从站时,生成主站读访问的响应:

/*生成从站应答主站的响应*/

uint16_t SyntheticSlaveAccessRespond(uint8_t *receivedMessage,bool *statusList,uint16_t *registerList,uint8_t *respondBytes)

参数分别是接收到的信息,返回的数据列表,生成的响应信息列表。返回值是响应信息列表的长度。

5TCP应用数据单元的编程

而对于TCP应用数据单元来说,与RTU类式,起报文格式是:“MBAP头+PDU”,而PDU单元就是前面定义的,所以只需要加上MBAP头部就可以了,事实上MBAP头部的实现格式是固定的。

对于TCP应用数据单元的实现同样分为2中情况:一是作为客户端时,对服务器的下发命令;二是作为服务器时,对客户端命令的响应。所以我们将这两种情况分别封装为2个基础函数:

(1)、作为TCP客户端时,生成读写TCP服务器对象的命令:

/*生成读写服务器对象的命令*/

uint16_t SyntheticReadWriteTCPServerCommand(ObjAccessInfo objInfo,bool *statusList,uint16_t *registerList,uint8_t *commandBytes)

(2)、作为(服务器时,生成客户端读写访问的响应:

/*合成对服务器访问的响应,返回值为命令长度*/

uint16_t SyntheticServerAccessRespond(uint8_t *receivedMessage,bool *statusList,uint16_t *registerList,uint8_t *respondBytes)

6、结束语

其实到这里我们对Modbus基本协议已经基本实现,甚至使用这些基本操作也能实现Modbus的通讯。事实上很多人在应用写的Modbus通讯协议比这还要简单,也能实现部分的Modbus通讯功能。当然这不是我们的目标,否则就不需要专门开发库了,我们要进一步封装,让其更通用也更易用才是我们需要的。

最新文章

  1. 《JavaScript高级程序设计》读书笔记--前言
  2. python第二天 - 异常处理
  3. 关于gcd的几个问题
  4. linux-ubuntu常用命令
  5. [shell基础]——split命令
  6. android自动化环境搭建
  7. OpenCV中的常用函数
  8. Unity3d 使用DX11的曲面细分
  9. Babel6.x 转换ES6
  10. 关于IT实例教程
  11. 在 root 下执行 Oracle 程序时找不到 libclntsh.so.11.1 错误的解决办法。
  12. 在CentOS上安装owncloud企业私有云过程
  13. Python学习(四十一)—— Djago进阶
  14. piggy.lzo
  15. Failed to set session cookie. Maybe you are using HTTP instead of HTTPS to access phpMyAdmin.
  16. MD5加密及Hash长度拓展攻击【通俗易懂】
  17. 【Ansible 文档】【译文】网络支持
  18. P1525 关押罪犯 题解
  19. 20145328 《Java程序设计》第4周学习总结
  20. libvirt- Virsh 所有命令详单

热门文章

  1. 【bzoj 3779】重组病毒
  2. PHP调用API接口实现天气查询功能
  3. .net 使用oracle 的存储过程有返回值也有数据集(游标)
  4. GIT原理【摘】
  5. luogu P4931 情侣?给我烧了!
  6. MacOS下IntelliJ IDEA关联JDK1.8源码
  7. Centos 03 基础命令
  8. (7)Java数据结构--集合map,set,list详解
  9. openstack Q版部署-----界面horizon安装(9)
  10. android checkBox setTextColor无效