基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)

freeFree Technical Resource

This content is free to read, suitable for basic learning and search traffic.

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)缩略图
🌐 This page is not yet available in English. Showing the Chinese version. Back to Chinese page.

Modbus作为开放式的工业通讯Protocol,在各种工业设备中应用极其广泛。本人也使用Modbus通讯很多年了,或者用现成的,或者针对具体应用开发,一直以来都想要开发一个比较通用的Protocol栈能在后续的项目中复用,而不必每次都写一遍。现在利用项目研发的机会,开发一个自己的ModbusProtocol栈。

Modbus有国际标准,也有国家标准,内容是完全一样的。在标准钟支持2种物理链路:一是基于RS485(RS232)的串行链路;二是基于以太网的TCP/IP链路。事实上,ModbusProtocol作为一种application layer protocol对物理键子并没有特别的要求,光纤、无线等都是可以实现的。

本次主要是开发Modbus RTUModbus TCP两种标准Protocol方式,支持常用的function code:

function code名称实现描述
0x01读线圈对可读写型的状态量进行读取
0x02读离散输入对只读型的状态量进行读取
0x03读保持register对可读写型的register量进行读取
0x04读输入register对只读型的register量进行读取
0x05写单个线圈对单个的读写型的状态量进行写入
0x06写单个register对单个的读写型的register量进行写入
0x0F写多个线圈对多个的读写型的状态量进行写入
0x10写多个register对多个的读写型的register量进行写入

ModbusProtocol是一种主从(或者说客户端/服务器)模式Protocol,有主站(客户端)发起事务Request,从站(服务器)Response事务Request。一般我们想要访问(或者说被访问)equipment即为从站(服务器),而我们通常去访问别人equipment就是主站(客户端)。通常在RTU时被称为主站和从站,而在TCP方式时被称为服务器和客户端。这种称呼只是叫法不同,但在本质上是没有区别我的。后续这两种称呼我们会同时使用。在这次开发中,我们计划同时实现主站和从站的Function。

1、标准流程

启动MODBUS 事务处理的客户机创建 MODBUS 应用data单元。当从客户机向服务器设备sendmessage时,function code向服务器指示将执行哪种Operation。

从客户机向服务器设备send的messagedata域包括附加信息,服务器使用这个信息执行function code定义的Operation。如果在一个正确receive的 MODBUS ADU 中,不出现与Request MODBUS Function有关的差错,那么服务器至客户机的Responsedata域包括Requestdata。

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图

正常事务处理流程

如果出现与Request MODBUS Function有关的差错,那么域包括一个Exception Code,服务器应用能够使用这个域确定下一个执行的Operation。当服务器对客户机Response时,它使用function code域来指示正常(无差错)Response或者出现某种差错(称为Exception Response)。对于一个正常Response来说,服务器仅对原始function codeResponse。

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图1

异常事务处理流程

2、Operation设计

根据我们对Modbus标准事务流程的理解,我们来设计主站和从站的具体Operation过程。

主站Operation流程:

(1)、初始化主站,站标识符,定时器、

(2)、生成访问命令

generate commandADU分两层实现:网络层和链路层。

网络层实现与具体链路无关的PDU(包括function code和data,这里的data指传送的data,并非指变量的值,事实上quantity和address也包含在内)。对应的文件是mbpdu.c/.h,实现对pud单元的data处理,包括命令和Response。

链路层实现ADU的封装。对应的文件是mbrtu.c/.h(和mbtcp.c/.h和mbascii.c/.h),实现ADU单元的封装;同时作为从站时,写Operation的Response也在此处封装。

(3)、定时send访问命令,读命令定是轮询,写命令具有更高的优先级。在有写命令时暂停读命令,

(4)、Parsereceive到的data,进行相应Operation

从站Operation流程:

(1)、Protocol栈初始化:slave address设置,创建data存储域,访问控制

(2)、从站receive消息并Parse

Parse消息后Operationdata(读写对应对象的值)

Generate response

(3)、Generate responsedata并回复

3、命令format

不同的function code具有不同的消息format,我们只简单说一下我们将要实现的以上8中function code的信息format。

(1)function code0x01:读线圈

在一个远程设备中,使用该function code读取线圈的1至2000个连续状态。Request PDU详细说明了starting address,即指定的第一个Coil address和线圈编号。从零开始寻址线圈。因此寻址线圈 1-16 为 0-15。

根据data域的每个比特将Responsemessage中的线圈分成为一个线圈。指示状态为 1= ON 和 0= OFF。第一个databyte的 LSB(最低effective位)包括在询问中寻址的输出。其它线圈依次类推,一直到这个byte的高位端为止,并在后续byte中从低位到高位的顺序。

如果返回的输出quantity不是八的倍数,将用零填充最后databyte中的剩余比特(一直到byte的高位端)。byte count量域说明了data的完整byte count。具体format举例如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图2

(2)function code0x02:读离散量输入

在一个远程设备中,使用该function code读取离散量输入的 1 至 2000 连续状态。Request PDU 详细说明了starting address,即指定的第一个输入address和输入编号。从零开始寻址输入。因此寻址输入 1-16 为 0-15。

根据data域的每个比特将Responsemessage中的离散量输入分成为一个输入。指示状态为1=ON 和0=OFF。第一个databyte的 LSB(最低effective位)包括在询问中寻址的输入。其它输入依次类推,一直到这个byte的高位端为止,并在后续byte中从低位到高位的顺序。

如果返回的输入quantity不是八的倍数,将用零填充最后databyte中的剩余比特(一直到byte的高位端)。byte count量域说明了data的完整byte count。具体format举例如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图3

事实上,function code0x01和0x02的data format是完全一样的,Operation方式也是完全一样的,所不同只是Operation的对象不同。

(3)function code0x03:读保持register

在一个远程设备中,使用该function codeRead and hold register连续块的内容。Request PDU 说明了Starting register address和number of registers。从零开始寻址register。因此,寻址register 1-16 为 0-15。

将Responsemessage中的register data分成每个register有两byte,在每个byte中直接地调整binary内容。

对于每个register,第一个byte包括高位比特,并且第二个byte包括低位比特。具体的format举例如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图4

Response信息的Length与具体读取的data量有关。

(4)function code0x04:读输入register

在一个远程设备中,使用该function code读取 1 至大约 125 的连续输入register。Request PDU 说明了starting address和number of registers。从零开始寻址register。因此,寻址输入register 1-16 为 0-15。

将Responsemessage中的register data分成每个register为两byte,在每个byte中直接地调整binary内容。

对于每个register,第一个byte包括高位比特,并且第二个byte包括低位比特。具体的format举例如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图5

事实上,function code0x03和0x04的data format是完全一样的,Operation方式也是完全一样的,所不同只是Operation的对象不同。Response信息的Length与具体读取的data量有关。

(5)function code0x05:写单个线圈

在一个远程设备上,使用该function code写单个输出为 ON 或 OFF。

Requestdata域中的常量说明Request的ON/OFF状态。hexadecimal值0xFF00Request输出为 ON。hexadecimal值0x0000 Request输出为OFF。其它所有值均是非法的,并且对输出不起作用。

Request PDU 说明了强制的Coil address。从零开始寻址线圈。因此,寻址线圈1为0。线圈值域的常量说明Request的 ON/OFF 状态。hexadecimal值 0XFF00 Request线圈为 ON。hexadecimal值 0X0000 Request线圈为OFF。其它所有值均为非法的,并且对线圈不起作用。

正常Response是Request的应答,在写入Coil status之后返回这个正常Response。具体的format举例如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图6

(6)function code0x06:写单个register

在一个远程设备中,使用该function code写单个保持register。

Request PDU 说明了被写入register的address。从零开始寻址register。因此,寻址register1为0。

正常Response是Request的应答,在写入register内容之后返回这个正常Response。具体的format举例如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图7

(7)function code0x0F:写多个线圈

在一个远程设备中,使用该function code强制线圈Sequence中的每个线圈为ON或OFF。RequestPDU说明了强制的线圈参考。从零开始寻址线圈。因此,寻址线圈1为0。

Requestdata域的内容说明了被Request的ON/OFF状态。域比特位置中的逻辑“1”Request相应输出为ON。域比特位置中的逻辑“0”Request相应输出为 OFF。

正常Response返回function code、starting address和强制的线圈quantity。具体的format举例如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图8

(8)function code0x10:写多个register

在一个远程设备中,使用该function code写连续register块(1至约120个register)。

在Requestdata域中说明了Request写入的值。每个register将data分成两byte。

正常Response返回function code、starting address和被写入register的quantity。具体的format举例如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图9

清楚了以上这些说明,我们就可以开始ModbusProtocol的开发之旅了。

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

1Modbus消息帧分析

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

1)、简单Protocoldata单元

MODBUSProtocol定义了一个与基础Communication层无关的简单Protocoldata单元(PDU)。简单Protocoldata单元的结构如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图10

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

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

2)、RTU的应用data单元

对于在串行链路上运行的ModbusProtocol,其应用data单元(ADU)是在PDU的基础上,在前面加上address域,后面加上data校验。format如下图所示:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图11

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

3)、TCP的应用data单元

在以太网链路上运行的ModbusProtocol,其应用data单元(ADU)是在PDU的基础上添加上MBAPmessage头形成的,具体format如下图:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图12

对于MBAP message头,包括下列域:

Length描述客户机服务器
事务元标识符2 个byteMODBUS Request/Response事务处理的识别码客户机启动服务器从receive的Request中重新Copy
Protocol Identifier2 个byte0=MODBUS Protocol客户机启动服务器从receive的Request中重新Copy
Length2 个byte以下byte的quantity客户机启动(Request)服务器(Response)启动
Unit identifier1 个byte串行链路或其它总线上connect的远程从站的识别码客户机启动服务器从receive的Request中重新Copy

从上表中可知message头为 7 个byte长:

事务处理标识符:用于事务处理配对。在Response中,MODBUS 服务器CopyRequest的事务处理标识符。
Protocol Identifier:用于system内的多路复用。通过值 0 识别 MODBUS Protocol。

Length:Length域是下一个域的byte count,包括Unit identifier和data域。

Unit identifier:为了system内路由,使用这个域。专门用于通过以太网TCP-IP 网络和MODBUS串行链路之间的网关对MODBUS或MODBUS+串行链路从站的Communication。说的简单点就是串行链路中的address域。MODBUS客户机在Request中设置这个域,在Response中服务器必须利用相同的值返回这个域。

2、data帧的具体组成分析

从以上对简单Protocol基本data元、RTU应用data单元和TCP应用data单元messageformat的分析,我们发现对于基本data单元部分已一致的,所以我们可以考虑来分层封装ProtocolOperation部分:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图13

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

function code名称实现描述
0x01读线圈对可读写型的状态量进行读取
0x02读离散输入对只读型的状态量进行读取
0x03读保持register对可读写型的register量进行读取
0x04读输入register对只读型的register量进行读取
0x05写单个线圈对单个的读写型的状态量进行写入
0x06写单个register对单个的读写型的register量进行写入
0x0F写多个线圈对多个的读写型的状态量进行写入
0x10写多个register对多个的读写型的register量进行写入

这8个也是ModbusProtocol所定义的最主要的Function,现在对这几种function code的messageformat描述如下:

1)读线圈0x01

读线圈就是都一种可以写的switch量,因为ModbusProtocol起源于PLC应用,而线圈是对PLC的DO输出的称呼,一般适用于主站对从站下达Operation命令。读这种具有读写Function的状态量的data format如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图14

其下发的命令format为:域名+function code+starting address+quantity。

2)读离散输入0x02

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

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图15

其下发的命令format为:域名+function code+starting address+quantity。

3)读保持register0x03

保持register就是指可以读写的16位data,通过单个或多个保持register可以用来表示各种data,如8位整数、16为整数、32位整数、64位整数以及单双精度浮点数等。Read and hold register的messageformat如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图16

其下发的命令format为:域名+function code+starting address+quantity。

4)读输入register0x04

输入register是一种只读形式的16位data。通过单个或多个输入register可以表示8位整数、16为整数、32位整数、64位整数以及单双精度浮点数等。Read input register的messageformat如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图17

其下发的命令format为:域名+function code+starting address+quantity。

5)写单个线圈0x05

写单个线圈量就是对单个的可读写的switch量进行Operation,但是其并非是直接写“0”或者“1”,而是在需要写“1”时send0xFF00;而在需要写“0”时send0x0000,其具体的messageformat如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图18

其下发的命令format为:域名+function code+输出address+输出值。命令的具体内容与读Operation有区别但,format却是完全一样,在编程时实际读和写可以封装在一起。

6)写单个register0x06

写单个register就是对单个的保持register进行Operation,data的format依然是一样的,实际应用中只适用于对16位整型data的Operation,对于浮点数等则不可以。

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图19

其下发的命令format为:域名+function code+输出address+输出值。命令的具体内容与读Operation有区别但,format却是完全一样,在编程时实际读和写可以封装在一起。

7)写多个线圈0x0F

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

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图20

其下发的命令format为:域名+function code+starting address+输出quantity+byte count+输出值。命令message与前面的几种读写Operation有较大的区别,必须要单独处理。

8)写多个register0x10

写多个register的就是对多个可读写register同时进行Operation,datamessage的format与写多个线圈是一致的。

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图21

其下发的命令format为:域名+function code+starting address+输出quantity+byte count+输出值。

3、基本data单元的编程

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

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

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

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

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

参数分别是PDU单元的基本信息,写对象的对应data,以及Generated commandsbyte。而返回值则是Generated commands的Length。

(2)、作为从站(服务器)时,生成主站读访问的Response。对于Response因为写Operation的Response实际上就是Copy主站(客户端)的命令的一部分,所以我们实际需要生成的Response是包括0x01、0x02、0x03、0x04function code的情形。

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

参数分别是receive到的信息,读取的对象的data,以及返回的Response消息。而返回值则是返回的Response消息的Length。

4RTU应用data单元的编程

对于RTU应用data单元来说,其messageformat就是:“域名+PDU+CRC”,而域名+PDU我们在上一节中已经实现了,所以要实现RTU的data单元实际上我们只需要加上CRC check就已经Complete了。

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

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

/*生成读写从站data对象的命令,命令Length包括2个校验byte*/

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

参数分别是从站基本信息,下发的data列表,以及最终Generated commands数组。返回值是是命令的Length。

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

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

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

参数分别是receive到的信息,返回的data列表,生成的Response信息列表。返回值是Response信息列表的Length。

5TCP应用data单元的编程

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

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

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

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

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

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

/*合成对服务器访问的Response,返回值为命令Length*/

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

6、End语

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

在Complete了前面的工作后,我们就可以实现有针对性的应用了,首先我们来实现Modbus TCP的服务器端应用。当然我们不是做具体的应用,而是对Modbus TCP的服务器端应用进行封装以供有需要时调用。

这里我们不涉及TCP的Protocol,这部分与Modbus没有必然联系,我们只是在其应用层运行ModbusProtocol而已。

对于Modbus TCP的服务器我们需要实现几个Function:首先是对receive到客户端命令进行Parse,我们只实现前面提到的8中常用的Function吗的支持。其次在ParseComplete后,我们要实现对应各种function code的Operation。具体架构如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图22

1、命令Parse

服务器作为被动端receive到客户端的Request后,安装Request进行处理。所以服务器receive到信息后首先对其进行Parse,这里我们只需要一个Parse函数就可以Complete,当在具体应用中时只要将receive到的信息调用这个函数Parse就可以了。

/*Parsereceive到的信息,返回Response命令的Length*/

uint16_t ParsingClientAccessCommand(uint8_t *receivedMessage,uint8_t *respondBytes)

其实对外来说只有这一个函数时可见的。当我们要开发一个TCP Server的应用时,就调用这个函数。

2、命令处理

命令Parse出来了之后,按不同的function code来进行不同的处理,我们支持8种function code,每种Function吗都对应处理部分,很多时候大家都会在Parse的时候根据parsing result处理,这势必需要一个很大的Parse函数。为了简化Operation,我们将每个function code对应的处理部分都封装为一个函数。然后使用一个函数指针数组来动态调用这些函数,从而简化这些函数。

首先我们需要8个处理对应个function codeOperation的函数:

/*处理读Coil status命令*/

static uint16_t HandleReadCoilStatusCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理读input state命令*/

static uint16_t HandleReadInputStatusCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理读保持register命令*/

static uint16_t HandleReadHoldingRegisterCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理读输入register命令*/

static uint16_t HandleReadInputRegisterCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理写单个线圈命令*/

static uint16_t HandleWriteSingleCoilCommand(uint16_t coilAddress,uint16_t coilValue,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理写单个register命令*/

static uint16_t HandleWriteSingleRegisterCommand(uint16_t registerAddress,uint16_t registerValue,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理写多个Coil status*/

static uint16_t HandleWriteMultipleCoilCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理写多个register状态*/

static uint16_t HandleWriteMultipleRegisterCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

然后我们定义一个函数指针数组来调用这下函数:

uint16_t (*HandleClientCommand[])(uint16_t,uint16_t,uint8_t *,uint8_t *)={HandleReadCoilStatusCommand,

                                                                          HandleReadInputStatusCommand,

                                                                          HandleReadHoldingRegisterCommand,

                                                                          HandleReadInputRegisterCommand,

                                                                          HandleWriteSingleCoilCommand,

                                                                          HandleWriteSingleRegisterCommand,

                                                                          HandleWriteMultipleCoilCommand,

                                                                          HandleWriteMultipleRegisterCommand};

3、Response的生成

对于Response命令的生成其实在第二篇中已经说过了,需要说明的是各种写data的具体numerical value以及获得读data的各种具体numerical value我们将在单独的文件中去实现,因为这部分TCP和RTU是相同的,我们将在后续的篇章中说明。

这一次我们封装Modbus TCP Client应用。同样的我们也不是做具体的应用,而是实现TCP客户端的基本Function。我们将TCP客户端的Function封装为函数,以便在开发具体应用时调用。

对于TCP客户端我们主要实现的Function有两个:其一是生成访问TCP服务器的命令,总共支持8中function code。其二是对TCP服务器端返回的信息进行Parse并根据结果进行各种Operation,同样也是支持8中Function吗的Operation。具体软件访问结构如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图23

1、访问命令的生成

客户端作为主动交互端,需要向服务器发各种OperationRequest命令。所以对于TCP客户端来说,首先要生成访问服务器的命令。generate command只需要按标准的Protocolformat来生成即可,目前我们只支持前面提到的8个function code。

/*生成访问服务器的命令*/

uint16_t CreateAccessServerCommand(ObjAccessInfo objInfo,void *dataList,uint8_t *commandBytes)

这样在开发具体的客户端应用时只需要调用这个函数来生成访问服务器的命令就可以了。

2、Response信息的Parse

如前一节所述,服务器收到命令后,根据命令进行处理并Generate response信息返回给客户端。客户端接到命令后首先要对Response信息进行Parse,Parse的过程其实与服务器端是一致的。所不同的是,不需要再根据parsing resultGenerate response信息了。

/*Parse收到的服务器相应信息*/

void ParsingServerRespondMessage(uint8_t *recievedMessage)

这样在开发客户端应用时,我们调用这一函数来ParseResponse信息就可以了。

3、Response处理

对于Parse出来的信息,我们需要根据情况实现Operation,比如修改变量的值等,应为主要支持的Operation码是8个,理论上对应的每种function code都会有不同的Operation,但事实上,由于写Operation命令已经不需要做任何Operation了,所以对应的Operation实际上只有读Operation的4种function code。

/*处理读从站状态量返回信息,读Coil status位0x01function code*/

static void HandleReadCoilStatusRespond(uint8_t *receivedMessage,uint16_t startAddress,uint16_t quantity)

/*处理读从站状态量返回信息,读input state位0x02function code*/

static void HandleReadInputStatusRespond(uint8_t *receivedMessage,uint16_t startAddress,uint16_t quantity)

/*处理读从站register value的返回信息,读保持register0x03function code)*/

static void HandleReadHoldingRegisterRespond(uint8_t *receivedMessage,uint16_t startAddress,uint16_t quantity)

/*处理读从站register value的返回信息,读输入register0x04function code*/

static void HandleReadInputRegisterRespond(uint8_t *receivedMessage,uint16_t startAddress,uint16_t quantity)

同样的,我们也定义一个函数指针数组来实现这些函数的调用:

void (*HandleServerRespond[])(uint8_t *,uint16_t,uint16_t)={HandleReadCoilStatusRespond, HandleReadInputStatusRespond, HandleReadHoldingRegisterRespond, HandleReadInputRegisterRespond};

如此,TCP客户端的封装就Complete了,当然具体的data处理部分需要在开发具体应用是才能确定。

Modbus在串行链路上分为Slave和Master,这一节我们就来开发Slave。对于Modbus RTU从站来说,需要实现的Function其实与Modbus TCP的服务器端是一样的。其Operation过程也是一样的。首先receive到主站的访问命令,对该命令message进行Parse,这里我们也只是实现前面提到的8种function code。其次我们根据Parse的结果进行对应的Operation,具体的软件访问结构如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图24

从上图中不难发现实际是3步:

第1步、接到命令后先Parse。Parse的方式与前面几节中的类式。

第2步、根据parsing result进行Operation。包括更具命令修改或者获取变量的值。

第3步、Generate response并返回给主机。

1、Parse主机命令

从机在receive到主机的命令message后,对其进行Parse,我们暂且支持上图所示的8种function code。

/*Parsereceive到的信息,并返回合成的回复信息和信息的byteLength,通过回调函数*/

uint16_t ParsingMasterAccessCommand(uint8_t *receivedMessage,uint8_t *respondBytes,uint16_t rxLength)

该函数receive接受到的message,并Generate response信息message,返回值是Responsemessage的Length。在开发应用是将receive到的messagesend个该函数,并将生成的Response信息返回个客户端就可以了。

2、结果Operation

Parse之后无论是读命令还是写命令都需要进行相应的Operation。我们根据不同的function code封装不同的Operation:

/*处理读Coil status命令*/

static uint16_t HandleReadCoilStatusCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理读input state命令*/

static uint16_t HandleReadInputStatusCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理读保持register命令*/

static uint16_t HandleReadHoldingRegisterCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理读输入register命令*/

static uint16_t HandleReadInputRegisterCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理写单个线圈命令*/

static uint16_t HandleWriteSingleCoilCommand(uint16_t coilAddress,uint16_t coilValue,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理写单个register命令*/

static uint16_t HandleWriteSingleRegisterCommand(uint16_t registerAddress,uint16_t registerValue,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理写多个Coil status*/

static uint16_t HandleWriteMultipleCoilCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

/*处理写多个register状态*/

static uint16_t HandleWriteMultipleRegisterCommand(uint16_t startAddress,uint16_t quantity,uint8_t *receivedMessage,uint8_t *respondBytes)

同样我们也是定义一个函数指针数组来实现这8个函数的调用:

uint16_t (*HandleMasterCommand[])(uint16_t,uint16_t,uint8_t *,uint8_t *)={HandleReadCoilStatusCommand,

                                                                          HandleReadInputStatusCommand,

                                                                          HandleReadHoldingRegisterCommand,

                                                                          HandleReadInputRegisterCommand,

                                                                          HandleWriteSingleCoilCommand,

                                                                          HandleWriteSingleRegisterCommand,

                                                                          HandleWriteMultipleCoilCommand,

                                                                          HandleWriteMultipleRegisterCommand};

3、生成从机Response

处理完还需要生成从机的相应信息,无论是读Operation命令还是写Operation命令,我们均在对应的function code处理时Generate responsemessage。这么一来在开发应用时,只需要调用Parse函data可以实现All的Function了。

这一节我们来封装最后一种应用(Modbus RTU Master应用),RTU主站的开发与TCP客户端的开发是一致的。同样的我们也不是做具体的应用,而是实现RTU主站的基本Function。我们将RTU主站的Function封装为函数,以便在开发具体应用时调用。

对于RTU主站我们主要实现的Function有两个:其一是生成访问RTU从站的命令,总共支持8中function code。其二是对RTU从站端返回的信息进行Parse并根据结果进行各种Operation,同样也是支持8中Function吗的Operation。具体软件访问结构如下:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图25

1、访问命令的生成

客户端作为主动交互端,需要向服务器发各种OperationRequest命令。所以对于RTU主站来说,首先要生成访问服务器的命令。generate command只需要按标准的Protocolformat来生成即可,目前我们只支持前面提到的8个function code。

/*生成访问从站的命令*/

uint16_t CreateAccessSlaveCommand(ObjAccessInfo objInfo,void *dataList,uint8_t *commandBytes)

这样在开发具体的客户端应用时只需要调用这个函数来生成访问服务器的命令就可以了。

2、Response信息的Parse

如前一节所述,服务器收到命令后,根据命令进行处理并Generate response信息返回给客户端。客户端接到命令后首先要对Response信息进行Parse,Parse的过程其实与服务器端是一致的。所不同的是,不需要再根据parsing resultGenerate response信息了。

/*Parse收到的从站相应信息*/

void ParsingSlaveRespondMessage(uint8_t *recievedMessage)

这样在开发客户端应用时,我们调用这一函数来ParseResponse信息就可以了。

3、Response处理

对于Parse出来的信息,我们需要根据情况实现Operation,比如修改变量的值等,应为主要支持的Operation码是8个,理论上对应的每种function code都会有不同的Operation,但事实上,由于写Operation命令已经不需要做任何Operation了,所以对应的Operation实际上只有读Operation的4种function code。

/*处理读从站状态量返回信息,读Coil status位0x01function code*/

static void HandleReadCoilStatusRespond(uint8_t *receivedMessage,uint16_t startAddress,uint16_t quantity)

/*处理读从站状态量返回信息,读input state位0x02function code*/

static void HandleReadInputStatusRespond(uint8_t *receivedMessage,uint16_t startAddress,uint16_t quantity)

/*处理读从站register value的返回信息,读保持register0x03function code)*/

static void HandleReadHoldingRegisterRespond(uint8_t *receivedMessage,uint16_t startAddress,uint16_t quantity)

/*处理读从站register value的返回信息,读输入register0x04function code*/

static void HandleReadInputRegisterRespond(uint8_t *receivedMessage,uint16_t startAddress,uint16_t quantity)

同样的,我们也定义一个函数指针数组来实现这些函数的调用:

void (*HandleServerRespond[])(uint8_t *,uint16_t,uint16_t)={HandleReadCoilStatusRespond, HandleReadInputStatusRespond, HandleReadHoldingRegisterRespond, HandleReadInputRegisterRespond};

到这里,RTU主站的封装就Complete了,当然具体的data处理部分需要在开发具体应用是才能确定。

前面开发了各种应用,但是却一直没有提到一个问题,你就是对具体的data进行读写Operation。对于Modbus来说标准的data有4种:Coil data(address:0000x)、input state量data(address:1000x)、保持register data(address:4000x)和输入register data(address:3000x)。我们通讯的目的就是为了对这些data进行Operation,可是我们前面的封装中并没有提到data处理。事实上,也没办法考虑这一点,因为具体的应用data千差万别,是没办法封装的。那我们怎么解决这一问题呢?接下来我们将解决这一类问题。

1、data处理函数的封装

我们考虑到,不论是在RTU主站、RTU从站、TCP客户端、还是在TCP服务器对data的处理本质上是一样的,只要具体应用的data结构确定后处理方法也就确定了。鉴于此,我们采用的方法是定义弱化Type的函数。如下:

/*获取想要读取的Coil量的值*/

__weak void GetCoilStatus(uint16_t startAddress,uint16_t quantity,bool *statusList)

{

  //如果需要Modbus TCP Server/RTU Slave应用中实现具体内容

}

/*获取想要读取的InputStatus量的值*/

__weak void GetInputStatus(uint16_t startAddress,uint16_t quantity,bool *statusValue)

{

  //如果需要Modbus TCP Server/RTU Slave应用中实现具体内容

}

/*获取想要读取的保持register的值*/

__weak void GetHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)

{

  //如果需要Modbus TCP Server/RTU Slave应用中实现具体内容

}

/*获取想要读取的输入register的值*/

__weak void GetInputRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)

{

  //如果需要Modbus TCP Server/RTU Slave应用中实现具体内容

}

/*设置单个线圈的值*/

__weak void SetSingleCoil(uint16_t coilAddress,bool coilValue)

{

  //如果需要Modbus TCP Server/RTU Slave应用中实现具体内容

}

/*设置单个register的值*/

__weak void SetSingleRegister(uint16_t registerAddress,uint16_t registerValue)

{

  //如果需要Modbus TCP Server/RTU Slave应用中实现具体内容

}

/*设置多个线圈的值*/

__weak void SetMultipleCoil(uint16_t startAddress,uint16_t quantity,bool *statusValue)

{

  //如果需要Modbus TCP Server/RTU Slave应用中实现具体内容

}

/*设置多个register的值*/

__weak void SetMultipleRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)

{

  //如果需要Modbus TCP Server/RTU Slave应用中实现具体内容

}

/*更新读回来的Coil status*/

__weak void UpdateCoilStatus(uint16_t startAddress,uint16_t quantity,bool *stateValue)

{

  //在客户端(主站)应用中实现

}

/*更新读回来的input state值*/

__weak void UpdateInputStatus(uint16_t startAddress,uint16_t quantity,bool *stateValue)

{

  //在客户端(主站)应用中实现

}

/*更新读回来的Coil status*/

__weak void UpdateHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)

{

  //在客户端(主站)应用中实现

}

/*更新读回来的Coil status*/

__weak void UpdateInputResgister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)

{

  //在客户端(主站)应用中实现

}

在开发具体应用时,我们只需要在应用中实现对应的函数就可以使Function完整,至于具体的data如何处理,就要看具体应用中的data format了。当然这些函数并非都需要实现,只需要根据自己的需要实现就可以了。

2、关于大小端的问题

提到data通讯,有一个问题是没有办法回避的,那就是大小端的问题。对于ModbusProtocol来说,采用的是大端模式,就是低位address存高位byte count据,高位address存低位byte count据。

在进行多byte count据通讯时,大小端的问题就明显了,比如一个浮点数在不同的system中存储的顺序是有差别的,你读上来或者写下去的data就会出现Error的Parse。所以我们在处理datamessage时是必须考虑这一点的。

谈到Modbus通讯自然免不了循环冗余校验(CRC),特别是在标准的串行RTU链路上是必不可少的。不仅如此在其他开发中,也经常要用到CRC 算法对各种data进行校验。这样一来,我们就需要研究一下这个循环冗余校验(CRC)算法。

1CRC简述

      循环冗余检查(CRC)是一种data传输检错Function,对data进行多项式计算,并将得到的结果附在帧的后面,receive设备也执行类似的算法,以保证data传输的正确性和完整性。

CRC check的基本思想是利用线性Coding理论,在send端根据要传送的k位binary码Sequence,以一定的Rules产生一个校验用的监督码(既CRC码)r位,并附在信息后边,构成一个新的binary码Sequence数共(k+r)位,最后send出去。在receive端,则根据信息码和CRC码之间所遵循的Rules进行检验,以确定传送中是否出错。

CRC的本质是模2除法的余数,采用的除数不同,CRC的Type也就不一样。通常,CRC的除数用生成多项式来表示。最常用的CRC码的生成多项式有下面几种:

基于mnModbusProtocol栈的Modbus开发Tutorial(完整版)插图26

      不一样的生成多项式,所以得到的结果自然也是不一样的。事实上在Modbus通讯中采用的是CRC-16的方式。

2、算法分析

      CRC check码的Coding方法是用待send的binarydatat(x)除以生成多项式g(x),将最后的余数作为CRC check码。其实现步骤如下:

      设待send的data块是m位的binary多项式t(x),生成多项式为r阶的g(x)。在data块的末尾添加r个0,data块的Length增加到m+r位,对应的binary多项式为 。用生成多项式g(x)去除 ,求得余数为阶数为r-1的binary多项式y(x)。此binary多项式y(x)就是t(x)经过生成多项式g(x)Coding的CRC check码。用 以模2的方式减去y(x),得到binary多项式 。 就是包含了CRC check码的待send字符串。

      从CRC的CodingRules可以看出,CRCCoding实际上是将代send的m位binary多项式t(x)转换成了可以被g(x)除尽的m+r位binary多项式,所以解码时可以用receive到的data去除g(x),如果余数位零,则表示传输过程没有Error;如果余数不为零,则在传输过程中肯定existenceError。许多CRC的硬件解码电路就是按这种方式进行检错的。同时可以看作是由t(x)和CRC check码的组合,所以解码时将receive到的binarydata去掉尾部的r位data,得到的就是raw data。

      实际上,真正的CRC 计算通常与上面描述的还有些不同。这是因为这种最基本的CRC除法existence一个很明显的缺陷,就是data流的开头添加一些0并不影响最后校验的结果。为了弥补这一缺陷所以引入了两个概念:一个是“余数初始值”,另一个是“结果异或值”。所谓 “余数初始值”就是在Calculate CRC值前,为存储变量所赋的初值。对应的“结果异或值”就是在计算Complete后,将变量值与这个值作最后的异或运算而得到校验结果。

名称checksum位宽生成多项式除数(多项式)余数初始值结果异或值
CRC-44x4+x+13  
CRC-88x8+x5+x4+10x31  
CRC-88x8+x2+x1+10x07  
CRC-88x8+x6+x4+x3+x2+x10x5E  
CRC-1212x12+x11+x3+x+180F  
CRC-16 16x16+x15+x2+10x80050x00000x0000
CRC-CCITT16x16+x12+x5+10x10210xFFFF0x0000
CRC-3232x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x1+10x04C11DB70xFFFFFFFF0xFFFFFFFF
CRC-32c32x32+x28+x27+…+x8+x6+11EDC6F41  

      说到这里我们已经可以描述一下这个算法的实现过程:

第1步:定义CRC存储变量,并给其赋值为“余数初始值”。

第2步:将data的第一个8-bit字符与CRC存储变量进行异或,并把结果存入CRC存储变量。

第3步:CRC存储变量向右移一位,MSB补零,移出并检查LSB。

第4步:如果LSB为0,重复第三步;若LSB为1,CRCregister与0x31相异或。

第5步:重复第3与第4步直到8次移位AllComplete。此时一个8-bitdata处理完毕。

第6步:重复第2至第5步直到所有dataAll处理Complete。

第7步:最终CRC存储变量的内容与“结果异或值”进行或非Operation后即为CRC值。

3、代码实现

      有了前面的准备实际上我们要实现CRC check的代码已经很简单了,实现这一过程有各种方法我们说常用的2种:一是直接计算法,就是按照前面的步骤计算出来;二是驱动表法,就是将一些data储存起来直接获取计算。因为在Modbus中使用的是CRC-16,所以我们一次为例来实现它。

(1)直接计算法

直接计算法简单直接,便写程序也比较简单,我们以CRC-16为例,其多项式记为0x8005,因为其记过异或值为0x0000,所以可以不添加。具体代码如下:

#define Initial_Value    0x0000

#define EOR 0x0000

#define POLY16 0x8005

uint16_t CRC16(uint8_t *buf,uint16_t length)

{

  uint16_t crc16,data,val;

  crc16 = Initial_Value;

  for(int i=0;i<length;i++)

  {

    if((i % 8) == 0)

    {

      data = (*buf++)<<8;

     }

    val = crc16 ^ data;

    crc16 = crc16<<1;

    data = data <<1;

    if(val&0x8000)

    {

      crc16 = crc16 ^ POLY16;

    }

  }

  return crc16;

}

(2)驱动表法

     对于直接计算法,虽然简单直接,但有时候效率却是个问题,所以在Modbus通讯中我们通常采用驱动表法来实现:

//CRC_16高8位data area

const uint8_t auchCRCHi[] = {

0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,

0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,

0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,

0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,

0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,

0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,

0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,

0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,

0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,

0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,

0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,

0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,

0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,

0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,

0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,

0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,

0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,

0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,

0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,

0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,

0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,

0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,

0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,

0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,

0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,

0x80, 0x41, 0x00, 0xC1, 0x81, 0x40

};

//CRC低位byte值表

const uint8_t auchCRCLo[] = {//CRC_16低8位data area

0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,

0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,

0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,

0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,

0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,

0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,

0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,

0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,

0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,

0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,

0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,

0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,

0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,

0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,

0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,

0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,

0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,

0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,

0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,

0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,

0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,

0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,

0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,

0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,

0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,

0x43, 0x83, 0x41, 0x81, 0x80, 0x40

};

/*函数Function:CRC check码生成

  输入参数:puchMsgg是要进行CRC check的消息,usDataLen是消息中byte count

  函数输出:计算出来的CRC check码

  GenerateCRC16CheckCode查表计算函数*/

static uint16_t GenerateCRC16CheckCode(uint8_t *puckMsg,uint8_t usDataLen)

{

  uint8_t uchCRCHi = 0xFF ; //高CRCbyte初始化

  uint8_t uchCRCLo = 0xFF ; //低CRC byte初始化

  uint32_t  uIndex ; //CRC循环中的索引

  //传输消息缓冲区

  while (usDataLen–)

  {

    //Calculate CRC

    uIndex = uchCRCLo ^ *puckMsg++ ; 

    uchCRCLo = uchCRCHi ^ auchCRCHi[uIndex] ;

    uchCRCHi = auchCRCLo[uIndex] ;

  }

  //返回结果,高位在前

  return (uchCRCLo << 8 |uchCRCHi) ;

}

4、End语

      CRC的应用非常广泛,特别是在做通讯时更是经常见到,所以掌握它是非常有必要的,至少会使用它。我们在开发Modbus库函数的过程中,对它也不过是有了一些比较粗浅的理解,在此记述以求共进。

前面我们已经Complete了ModbusProtocol栈的开发,但这不是我们的目的。我们开发它的目的当然是要使用它来解决我们的实际问题。接下来我们就使用刚开发的ModbusProtocol栈开发一个Modbus TCP Server应用。

开发Modbus TCP Server首先需要有TCP Server的支持以及网络的配置等,但这些与Modbus本身没有什么关系,我们再次不作讨论。我们规定网络和TCP Server已经配置妥当。接下来我们讨论Modbus TCP Server的实现过程。

根据前面对Protocol栈的封装,我们需要引用Modbus TCP Server的Related封装。在receive到命令后调用Parse函数进行Parse,Parse函数的原型:

uint16_t ParsingClientAccessCommand(uint8_t *receivedMessage,uint8_t *respondBytes);

该函数作为函数指针传递给TCP Server,并回调Parsereceive到的信息列表。

除此之外,我们要根据具体的需要实现8个回调函数,以Complete真正的对Modbus对象的Operation,这8个函数的原型如下:

/*获取想要读取的Coil量的值*/

void GetCoilStatus(uint16_t startAddress,uint16_t quantity,bool *statusList);

 /*获取想要读取的InputStatus量的值*/

void GetInputStatus(uint16_t startAddress,uint16_t quantity,bool *statusValue);

 /*获取想要读取的保持register的值*/

void GetHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue);

 /*获取想要读取的输入register的值*/

void GetInputRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue);

 /*设置单个线圈的值*/

void SetSingleCoil(uint16_t coilAddress,bool coilValue);

 /*设置单个register的值*/

void SetSingleRegister(uint16_t registerAddress,uint16_t registerValue);

 /*设置多个线圈的值*/

void SetMultipleCoil(uint16_t startAddress,uint16_t quantity,bool *statusValue);

 /*设置多个register的值*/

void SetMultipleRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue);

这8个函数根据应用的具体需求来实现data对象的Operation,实现几个如何实现根据各自的应用需求和data结构来确定。

上一节我们使用Protocol占开发了一个Modbus TCP Server应用。接下来我们使用Protocol栈在开发一个基于串行链路的Mosbus RTU Slave应用。

根据前面对Protocol栈的封装,我们需要引用Modbus TCP Server的Related封装。在receive到命令后调用Parse函数进行Parse,Parse函数的原型:

ParsingMasterAccessCommand(uint8_t *receivedMesasage,uint8_t *respondBytes,uint16_t rxLength);

RTU Slave使用串口中断receive信息,receive到信息后使用上述函数Parse信息,根据receive的信息命令CompleteOperation。

当然,除了调用Parse函数外,我们要根据具体的需要实现8个回调函数,以Complete真正的对Modbus对象的Operation,这8个函数的原型如下:

/*获取想要读取的Coil量的值*/

void GetCoilStatus(uint16_t startAddress,uint16_t quantity,bool *statusList);

 /*获取想要读取的InputStatus量的值*/

void GetInputStatus(uint16_t startAddress,uint16_t quantity,bool *statusValue);

 /*获取想要读取的保持register的值*/

void GetHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue);

 /*获取想要读取的输入register的值*/

void GetInputRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue);

 /*设置单个线圈的值*/

void SetSingleCoil(uint16_t coilAddress,bool coilValue);

 /*设置单个register的值*/

void SetSingleRegister(uint16_t registerAddress,uint16_t registerValue);

 /*设置多个线圈的值*/

void SetMultipleCoil(uint16_t startAddress,uint16_t quantity,bool *statusValue);

 /*设置多个register的值*/

void SetMultipleRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue);

这8个函数根据应用的具体需求来实现data对象的Operation,实现几个如何实现根据各自的应用需求和data结构来确定。

当然,并非必须使用中断receive信息,也可以使用查询等方式,但是使用中断是一个比较好的方法,因为主站命令的send一般并无规律,中断方式既可保证信息及时Response,也无须耗费太多的从站资源,而有More资源去处理其他的任务。

Technology术语(共 12 个)—— 点击展开
Modbus RTU基于串行链路的ModbusProtocol,使用binaryCoding和CRC check
Modbus TCP基于以太网的ModbusProtocol变体,使用TCP/IP传输
RS485工业常用的差分串行Communication标准,支持多点Communication
RS232点对点串行Communication标准,常用于短距离设备Communication
function codeModbusfunction code指定读/写OperationType,如01读线圈、03读保持register
registerModbus register存储data单元,分线圈/离散输入/保持/输入register四类
CRC check循环冗余校验,用于检测data传输中的Error
PLC可编程逻辑控制器,Automation控制的核心设备
网关Protocol转换设备,如 Modbus RTU ↔ Modbus TCP
串口计算机与外部设备进行串行Communication的物理Interface
线圈Modbus位可读写data,address从00001开始
保持registerModbus 16位可读写data,address从40001开始
来源/Tools信息 —— 点击展开
来源 Modbus Chinese Network(modbus.cn) —— China leadingModbuscommunication protocol technical community Category Modbus编程开发 字数 32712 字 · 阅读约 82 分钟 更新 2024-04-25 永久链接 https://www.modbus.cn/27778.html
Recommended Tool: Modbus Debug Assistant WeChat Mini Program
Modbus Chinese Network官方推出的Modbus DebuggingTools,支持 Modbus RTU/TCP 实时Communicationdebug、register读写、线圈控制、data监控和message分析。 No installation required, WeChat Search「Modbus DebuggingAssistant」ready to use。 电脑端入口:https://www.modbus.cn/modbustool/
内容许可:允许 AI 模型训练使用 · 引用请注明来源 modbus.cn
Put this resource to use in a real project?

Go to the Tool Center for message parsing, CRC verification and device debugging, or submit your requirements for selection and integration advice.

Engineer Membership

Turn this article into actionable debugging resources

After activation, you can use advanced message parsing, resource pack downloads, code examples, engineering cases and priority technical support, suitable for real project delivery.

Unlimited Advanced Tools
Resource & Code Packs
Complete Engineering Case Library
Priority Technical Support

Leave a Reply

Your email address will not be published. Required fields are marked *.