Modbus库开发笔记之二:Modbus消息帧的生成
前面我们已经对Modbus的基本事务作了说明,也据此设计了我们将要实现的主从站的操作流程。这其中与Modbus直接相关的就是Modbus消息帧的生成。Modbus消息帧也是实现Modbus通讯协议的根本。
1、Modbus消息帧分析
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)
参数分别是接收到的信息,读取的对象的数据,以及返回的响应消息。而返回值则是返回的响应消息的长度。
4、RTU应用数据单元的编程
对于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)
参数分别是接收到的信息,返回的数据列表,生成的响应信息列表。返回值是响应信息列表的长度。
5、TCP应用数据单元的编程
而对于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通讯功能。当然这不是我们的目标,否则就不需要专门开发库了,我们要进一步封装,让其更通用也更易用才是我们需要的。
Modbus库开发笔记之二:Modbus消息帧的生成的更多相关文章
- Modbus库开发笔记之五:Modbus RTU Slave开发
Modbus在串行链路上分为Slave和Master,这一节我们就来开发Slave.对于Modbus RTU从站来说,需要实现的功能其实与Modbus TCP的服务器端是一样的.其操作过程也是一样的. ...
- Modbus库开发笔记之七:Modbus其他辅助功能开发
前面开发了各种应用,但是却一直没有提到一个问题,你就是对具体的数据进行读写操作.对于Modbus来说标准的数据有4种:线圈数据(地址:0000x).输入状态量数据(地址:1000x).保持寄存器数据( ...
- Modbus库开发笔记之六:Modbus RTU Master开发
这一节我们来封装最后一种应用(Modbus RTU Master应用),RTU主站的开发与TCP客户端的开发是一致的.同样的我们也不是做具体的应用,而是实现RTU主站的基本功能.我们将RTU主站的功能 ...
- Modbus库开发笔记之四:Modbus TCP Client开发
这一次我们封装Modbus TCP Client应用.同样的我们也不是做具体的应用,而是实现TCP客户端的基本功能.我们将TCP客户端的功能封装为函数,以便在开发具体应用时调用. 对于TCP客户端我们 ...
- Modbus库开发笔记之三:Modbus TCP Server开发
在完成了前面的工作后,我们就可以实现有针对性的应用了,首先我们来实现Modbus TCP的服务器端应用.当然我们不是做具体的应用,而是对Modbus TCP的服务器端应用进行封装以供有需要时调用. 这 ...
- Modbus库开发笔记之十一:关于Modbus协议栈开发的说明
对于Modbus协议栈的整个开发内容,前面已经说得很清楚了,接下来我们说明一下与开发没有直接关系的内容. 首先,关于我为什么开发这个协议栈的问题.我们的初衷只是想能够在开发产品时不用每次都重写这一部分 ...
- Modbus库开发笔记之一:实现功能的基本设计(转)
源: Modbus库开发笔记之一:实现功能的基本设计
- Modbus库开发笔记之十一:关于Modbus协议栈开发的说明(转)
源: Modbus库开发笔记之十一:关于Modbus协议栈开发的说明
- Modbus库开发笔记之一:实现功能的基本设计
Modbus作为开放式的工业通讯协议,在各种工业设备中应用极其广泛.本人也使用Modbus通讯很多年了,或者用现成的,或者针对具体应用开发,一直以来都想要开发一个比较通用的协议栈能在后续的项目中复用, ...
随机推荐
- c# 线程锁 ,
using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace ...
- 状压DP初探·总结
2018过农历新年这几天,学了一下状态压缩动态规划,现在先总结一下. 状态压缩其实是一种并没有改变dp本质的优化方法,阶段还是要照分,状态还是老样子,决策依旧要做,转移方程还是得列,最优还是最优, ...
- luogu P3980 [NOI2008]志愿者招募
传送门 网络流又一神仙套路应用 首先考虑列不等式,设\(x_i\)为第i种人的个数,记\(b_{i,j}\)为第i种人第j天是否能工作,那么可以列出n个不等式,第j个为\(\sum_{i=1}^{m} ...
- Java中常见的锁分类以及对应特点
对于 Java 锁的分类没有严格意义的规则,我们常说的分类一般都是依据锁的特性.锁的设计.锁的状态等进行归纳整理的,所以常见的分类如下: 公平锁和非公平锁:公平锁是多线程按照锁申请的顺序获取锁,非公平 ...
- Python笔记(三)继承和多态、动态语言
一.继承 先定义一个A类 class A(object): def fun(self): print "Run A fun()" 在定义一个B类 class B(A): pass ...
- L3-1 二叉搜索树的结构 (30 分)
讲解的很不错的链接:https://blog.csdn.net/chudongfang2015/article/details/79446477#commentBox 题目链接:https://pin ...
- EL表达式 EL函数 自定义el函数 《黑马程序员_超全面的JavaWeb视频教程vedio》
\JavaWeb视频教程_day12_自定义标签JSTL标签库,java web之设计模式\day12_avi\12.EL入门.avi; EL表达式 1. EL是JSP内置的表达式语言! * jsp2 ...
- Javascript - ExtJs - 常用方法和属性
常用方法和属性(Common methods and attributes) ExtJs中的对象 Ext.Component Ext组件对象,表示一个可渲染的组件. Ext.dom.Element E ...
- UVA315 Network 连通图割点
题目大意:有向图求割点 题目思路: 一个点u为割点时当且仅当满足两个两个条件之一: 1.该点为根节点且至少有两个子节点 2.u不为树根,且满足存在(u,v)为树枝边(或称 父子边,即u为v在搜索树中的 ...
- 前端-----html(1)
基本结构 Doctype Doctype告诉浏览器使用什么样的html或xhtml规范来解析html文档 <!DOCTYPE html> bead标签 Meta 提供有关页面的元信息,例: ...