Modbus作为开放式的工业通讯协议,在各种工业设备中应用极其广泛。本人也使用Modbus通讯很多年了,或者用现成的,或者针对具体应用开发,一直以来都想要开发一个比较通用的协议栈能在后续的项目中复用,而不必每次都写一遍。现在利用项目研发的机会,开发一个自己的Modbus协议栈。
Modbus有国际标准,也有国家标准,内容是完全一样的。在标准钟支持2种物理链路:一是基于RS485(RS232)的串行链路;二是基于以太网的TCP/IP链路。事实上,Modbus协议作为一种应用层协议对物理键子并没有特别的要求,光纤、无线等都是可以实现的。
本次主要是开发Modbus RTU和Modbus TCP两种标准协议方式,支持常用的기능 코드:
| 기능 코드 | 名称 | 实现 | 描述 |
| 0x01 | 读线圈 | 是 | 对可读写型的状态量进行读取 |
| 0x02 | 读离散输入 | 是 | 对只读型的状态量进行读取 |
| 0x03 | 读保持寄存器 | 是 | 对可读写型的寄存器量进行读取 |
| 0x04 | 读输入寄存器 | 是 | 对只读型的寄存器量进行读取 |
| 0x05 | 写单个线圈 | 是 | 对单个的读写型的状态量进行写入 |
| 0x06 | 写单个寄存器 | 是 | 对单个的读写型的寄存器量进行写入 |
| 0x0F | 写多个线圈 | 是 | 对多个的读写型的状态量进行写入 |
| 0x10 | 写多个寄存器 | 是 | 对多个的读写型的寄存器量进行写入 |
Modbus协议是一种主从(或者说客户端/服务器)模式协议,有主站(客户端)发起事务请求,从站(服务器)响应事务请求。一般我们想要访问(或者说被访问)的设备即为从站(服务器),而我们通常去访问别人的设备就是主站(客户端)。通常在RTU时被称为主站和从站,而在TCP方式时被称为服务器和客户端。这种称呼只是叫法不同,但在本质上是没有区别我的。后续这两种称呼我们会同时使用。在这次开发中,我们计划同时实现主站和从站的功能。
1、标准流程
启动MODBUS 事务处理的客户机创建 MODBUS 应用데이터单元。当从客户机向服务器设备发送报文时,기능 코드向服务器指示将执行哪种操作。
从客户机向服务器设备发送的报文데이터域包括附加信息,服务器使用这个信息执行기능 코드定义的操作。如果在一个正确接收的 MODBUS ADU 中,不出现与请求 MODBUS 功能有关的差错,那么服务器至客户机的响应데이터域包括请求데이터。

正常事务处理流程
如果出现与请求 MODBUS 功能有关的差错,那么域包括一个异常码,服务器应用能够使用这个域确定下一个执行的操作。当服务器对客户机响应时,它使用기능 코드域来指示正常(无差错)响应或者出现某种差错(称为异常响应)。对于一个正常响应来说,服务器仅对原始기능 코드响应。

异常事务处理流程
2、操作设计
根据我们对Modbus标准事务流程的理解,我们来设计主站和从站的具体操作过程。
主站操作流程:
(1)、初始化主站,站标识符,定时器、
(2)、生成访问命令
生成命令ADU分两层实现:网络层和链路层。
网络层实现与具体链路无关的PDU(包括기능 코드和데이터,这里的데이터指传送的데이터,并非指变量的值,事实上数量和주소也包含在内)。对应的文件是mbpdu.c/.h,实现对pud单元的데이터处理,包括命令和响应。
链路层实现ADU的封装。对应的文件是mbrtu.c/.h(和mbtcp.c/.h和mbascii.c/.h),实现ADU单元的封装;同时作为从站时,写操作的响应也在此处封装。
(3)、定时发送访问命令,读命令定是轮询,写命令具有更高的优先级。在有写命令时暂停读命令,
(4)、解析接收到的데이터,进行相应操作
从站操作流程:
(1)、协议栈初始化:从站주소设置,创建데이터存储域,访问控制
(2)、从站接收消息并解析
解析消息后操作데이터(读写对应对象的值)
生成响应
(3)、生成响应데이터并回复
3、命令格式
不同的기능 코드具有不同的消息格式,我们只简单说一下我们将要实现的以上8中기능 코드的信息格式。
(1)기능 코드0x01:读线圈
在一个远程设备中,使用该기능 코드读取线圈的1至2000个连续状态。请求 PDU详细说明了시작주소,即指定的第一个线圈주소和线圈编号。从零开始寻址线圈。因此寻址线圈 1-16 为 0-15。
根据데이터域的每个比特将响应报文中的线圈分成为一个线圈。指示状态为 1= ON 和 0= OFF。第一个데이터바이트的 LSB(最低有效位)包括在询问中寻址的输出。其它线圈依次类推,一直到这个바이트的高位端为止,并在后续바이트中从低位到高位的顺序。
如果返回的输出数量不是八的倍数,将用零填充最后데이터바이트中的剩余比特(一直到바이트的高位端)。바이트数量域说明了데이터的完整바이트数。具体格式举例如下:

(2)기능 코드0x02:读离散量输入
在一个远程设备中,使用该기능 코드读取离散量输入的 1 至 2000 连续状态。请求 PDU 详细说明了시작주소,即指定的第一个输入주소和输入编号。从零开始寻址输入。因此寻址输入 1-16 为 0-15。
根据데이터域的每个比特将响应报文中的离散量输入分成为一个输入。指示状态为1=ON 和0=OFF。第一个데이터바이트的 LSB(最低有效位)包括在询问中寻址的输入。其它输入依次类推,一直到这个바이트的高位端为止,并在后续바이트中从低位到高位的顺序。
如果返回的输入数量不是八的倍数,将用零填充最后데이터바이트中的剩余比特(一直到바이트的高位端)。바이트数量域说明了데이터的完整바이트数。具体格式举例如下:

事实上,기능 코드0x01和0x02的데이터格式是完全一样的,操作方式也是完全一样的,所不同只是操作的对象不同。
(3)기능 코드0x03:读保持寄存器
在一个远程设备中,使用该기능 코드读取保持寄存器连续块的内容。请求 PDU 说明了시작寄存器주소和寄存器数量。从零开始寻址寄存器。因此,寻址寄存器 1-16 为 0-15。
将响应报文中的寄存器데이터分成每个寄存器有两바이트,在每个바이트中直接地调整二进制内容。
对于每个寄存器,第一个바이트包括高位比特,并且第二个바이트包括低位比特。具体的格式举例如下:

响应信息的长度与具体读取的데이터量有关。
(4)기능 코드0x04:读输入寄存器
在一个远程设备中,使用该기능 코드读取 1 至大约 125 的连续输入寄存器。请求 PDU 说明了시작주소和寄存器数量。从零开始寻址寄存器。因此,寻址输入寄存器 1-16 为 0-15。
将响应报文中的寄存器데이터分成每个寄存器为两바이트,在每个바이트中直接地调整二进制内容。
对于每个寄存器,第一个바이트包括高位比特,并且第二个바이트包括低位比特。具体的格式举例如下:

事实上,기능 코드0x03和0x04的데이터格式是完全一样的,操作方式也是完全一样的,所不同只是操作的对象不同。响应信息的长度与具体读取的데이터量有关。
(5)기능 코드0x05:写单个线圈
在一个远程设备上,使用该기능 코드写单个输出为 ON 或 OFF。
请求데이터域中的常量说明请求的ON/OFF状态。十六进制值0xFF00请求输出为 ON。十六进制值0x0000 请求输出为OFF。其它所有值均是非法的,并且对输出不起作用。
请求 PDU 说明了强制的线圈주소。从零开始寻址线圈。因此,寻址线圈1为0。线圈值域的常量说明请求的 ON/OFF 状态。十六进制值 0XFF00 请求线圈为 ON。十六进制值 0X0000 请求线圈为OFF。其它所有值均为非法的,并且对线圈不起作用。
正常响应是请求的应答,在写入线圈状态之后返回这个正常响应。具体的格式举例如下:

(6)기능 코드0x06:写单个寄存器
在一个远程设备中,使用该기능 코드写单个保持寄存器。
请求 PDU 说明了被写入寄存器的주소。从零开始寻址寄存器。因此,寻址寄存器1为0。
正常响应是请求的应答,在写入寄存器内容之后返回这个正常响应。具体的格式举例如下:

(7)기능 코드0x0F:写多个线圈
在一个远程设备中,使用该기능 코드强制线圈序列中的每个线圈为ON或OFF。请求PDU说明了强制的线圈参考。从零开始寻址线圈。因此,寻址线圈1为0。
请求데이터域的内容说明了被请求的ON/OFF状态。域比特位置中的逻辑“1”请求相应输出为ON。域比特位置中的逻辑“0”请求相应输出为 OFF。
正常响应返回기능 코드、시작주소和强制的线圈数量。具体的格式举例如下:

(8)기능 코드0x10:写多个寄存器
在一个远程设备中,使用该기능 코드写连续寄存器块(1至约120个寄存器)。
在请求데이터域中说明了请求写入的值。每个寄存器将데이터分成两바이트。
正常响应返回기능 코드、시작주소和被写入寄存器的数量。具体的格式举例如下:

清楚了以上这些说明,我们就可以开始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 TCP的服务器端应用。当然我们不是做具体的应用,而是对Modbus TCP的服务器端应用进行封装以供有需要时调用。
这里我们不涉及TCP的协议,这部分与Modbus没有必然联系,我们只是在其应用层运行Modbus协议而已。
对于Modbus TCP的服务器我们需要实现几个功能:首先是对接收到客户端命令进行解析,我们只实现前面提到的8中常用的功能吗的支持。其次在解析完成后,我们要实现对应各种기능 코드的操作。具体架构如下:

1、命令解析
服务器作为被动端接收到客户端的请求后,安装请求进行处理。所以服务器接收到信息后首先对其进行解析,这里我们只需要一个解析函数就可以完成,当在具体应用中时只要将接收到的信息调用这个函数解析就可以了。
/*解析接收到的信息,返回响应命令的长度*/
uint16_t 분석ClientAccessCommand(uint8_t *receivedMessage,uint8_t *respondBytes)
其实对外来说只有这一个函数时可见的。当我们要开发一个TCP Server的应用时,就调用这个函数。
2、命令处理
命令解析出来了之后,按不同的기능 코드来进行不同的处理,我们支持8种기능 코드,每种功能吗都对应处理部分,很多时候大家都会在解析的时候根据解析结果处理,这势必需要一个很大的解析函数。为了简化操作,我们将每个기능 코드对应的处理部分都封装为一个函数。然后使用一个函数指针数组来动态调用这些函数,从而简化这些函数。
首先我们需要8个处理对应个기능 코드操作的函数:
/*处理读线圈状态命令*/
static uint16_t HandleReadCoilStatusCommand(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理读输入状态命令*/
static uint16_t HandleReadInputStatusCommand(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理读保持寄存器命令*/
static uint16_t HandleReadHolding회원가입Command(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理读输入寄存器命令*/
static uint16_t HandleReadInput회원가입Command(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理写单个线圈命令*/
static uint16_t HandleWriteSingleCoilCommand(uint16_t coil주소,uint16_t coilValue,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理写单个寄存器命令*/
static uint16_t HandleWriteSingle회원가입Command(uint16_t register주소,uint16_t registerValue,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理写多个线圈状态*/
static uint16_t HandleWriteMultipleCoilCommand(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理写多个寄存器状态*/
static uint16_t HandleWriteMultiple회원가입Command(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
然后我们定义一个函数指针数组来调用这下函数:
uint16_t (*HandleClientCommand[])(uint16_t,uint16_t,uint8_t *,uint8_t *)={HandleReadCoilStatusCommand,
HandleReadInputStatusCommand,
HandleReadHolding회원가입Command,
HandleReadInput회원가입Command,
HandleWriteSingleCoilCommand,
HandleWriteSingle회원가입Command,
HandleWriteMultipleCoilCommand,
HandleWriteMultiple회원가입Command};
3、响应的生成
对于响应命令的生成其实在第二篇中已经说过了,需要说明的是各种写데이터的具体数值以及获得读데이터的各种具体数值我们将在单独的文件中去实现,因为这部分TCP和RTU是相同的,我们将在后续的篇章中说明。
这一次我们封装Modbus TCP Client应用。同样的我们也不是做具体的应用,而是实现TCP客户端的基本功能。我们将TCP客户端的功能封装为函数,以便在开发具体应用时调用。
对于TCP客户端我们主要实现的功能有两个:其一是生成访问TCP服务器的命令,总共支持8中기능 코드。其二是对TCP服务器端返回的信息进行解析并根据结果进行各种操作,同样也是支持8中功能吗的操作。具体软件访问结构如下:

1、访问命令的生成
客户端作为主动交互端,需要向服务器发各种操作请求命令。所以对于TCP客户端来说,首先要生成访问服务器的命令。生成命令只需要按标准的协议格式来生成即可,目前我们只支持前面提到的8个기능 코드。
/*生成访问服务器的命令*/
uint16_t CreateAccessServerCommand(ObjAccessInfo objInfo,void *데이터List,uint8_t *commandBytes)
这样在开发具体的客户端应用时只需要调用这个函数来生成访问服务器的命令就可以了。
2、响应信息的解析
如前一节所述,服务器收到命令后,根据命令进行处理并生成响应信息返回给客户端。客户端接到命令后首先要对响应信息进行解析,解析的过程其实与服务器端是一致的。所不同的是,不需要再根据解析结果生成响应信息了。
/*解析收到的服务器相应信息*/
void 분석ServerRespondMessage(uint8_t *recievedMessage)
这样在开发客户端应用时,我们调用这一函数来解析响应信息就可以了。
3、响应处理
对于解析出来的信息,我们需要根据情况实现操作,比如修改变量的值等,应为主要支持的操作码是8个,理论上对应的每种기능 코드都会有不同的操作,但事实上,由于写操作命令已经不需要做任何操作了,所以对应的操作实际上只有读操作的4种기능 코드。
/*处理读从站状态量返回信息,读线圈状态位0x01기능 코드*/
static void HandleReadCoilStatusRespond(uint8_t *receivedMessage,uint16_t 시작주소,uint16_t 수량)
/*处理读从站状态量返回信息,读输入状态位0x02기능 코드*/
static void HandleReadInputStatusRespond(uint8_t *receivedMessage,uint16_t 시작주소,uint16_t 수량)
/*处理读从站寄存器值的返回信息,读保持寄存器0x03기능 코드)*/
static void HandleReadHolding회원가입Respond(uint8_t *receivedMessage,uint16_t 시작주소,uint16_t 수량)
/*处理读从站寄存器值的返回信息,读输入寄存器0x04기능 코드*/
static void HandleReadInput회원가입Respond(uint8_t *receivedMessage,uint16_t 시작주소,uint16_t 수량)
同样的,我们也定义一个函数指针数组来实现这些函数的调用:
void (*HandleServerRespond[])(uint8_t *,uint16_t,uint16_t)={HandleReadCoilStatusRespond, HandleReadInputStatusRespond, HandleReadHolding회원가입Respond, HandleReadInput회원가입Respond};
如此,TCP客户端的封装就完成了,当然具体的데이터处理部分需要在开发具体应用是才能确定。
Modbus在串行链路上分为Slave和Master,这一节我们就来开发Slave。对于Modbus RTU从站来说,需要实现的功能其实与Modbus TCP的服务器端是一样的。其操作过程也是一样的。首先接收到主站的访问命令,对该命令报文进行解析,这里我们也只是实现前面提到的8种기능 코드。其次我们根据解析的结果进行对应的操作,具体的软件访问结构如下:

从上图中不难发现实际是3步:
第1步、接到命令后先解析。解析的方式与前面几节中的类式。
第2步、根据解析结果进行操作。包括更具命令修改或者获取变量的值。
第3步、生成响应并返回给主机。
1、解析主机命令
从机在接收到主机的命令报文后,对其进行解析,我们暂且支持上图所示的8种기능 코드。
/*解析接收到的信息,并返回合成的回复信息和信息的바이트长度,通过回调函数*/
uint16_t 분석MasterAccessCommand(uint8_t *receivedMessage,uint8_t *respondBytes,uint16_t rxLength)
该函数接收接受到的报文,并生成响应信息报文,返回值是响应报文的长度。在开发应用是将接收到的报文发送个该函数,并将生成的响应信息返回个客户端就可以了。
2、结果操作
解析之后无论是读命令还是写命令都需要进行相应的操作。我们根据不同的기능 코드封装不同的操作:
/*处理读线圈状态命令*/
static uint16_t HandleReadCoilStatusCommand(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理读输入状态命令*/
static uint16_t HandleReadInputStatusCommand(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理读保持寄存器命令*/
static uint16_t HandleReadHolding회원가입Command(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理读输入寄存器命令*/
static uint16_t HandleReadInput회원가입Command(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理写单个线圈命令*/
static uint16_t HandleWriteSingleCoilCommand(uint16_t coil주소,uint16_t coilValue,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理写单个寄存器命令*/
static uint16_t HandleWriteSingle회원가입Command(uint16_t register주소,uint16_t registerValue,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理写多个线圈状态*/
static uint16_t HandleWriteMultipleCoilCommand(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
/*处理写多个寄存器状态*/
static uint16_t HandleWriteMultiple회원가입Command(uint16_t 시작주소,uint16_t 수량,uint8_t *receivedMessage,uint8_t *respondBytes)
同样我们也是定义一个函数指针数组来实现这8个函数的调用:
uint16_t (*HandleMasterCommand[])(uint16_t,uint16_t,uint8_t *,uint8_t *)={HandleReadCoilStatusCommand,
HandleReadInputStatusCommand,
HandleReadHolding회원가입Command,
HandleReadInput회원가입Command,
HandleWriteSingleCoilCommand,
HandleWriteSingle회원가입Command,
HandleWriteMultipleCoilCommand,
HandleWriteMultiple회원가입Command};
3、生成从机响应
处理完还需要生成从机的相应信息,无论是读操作命令还是写操作命令,我们均在对应的기능 코드处理时生成响应报文。这么一来在开发应用时,只需要调用解析函데이터可以实现全部的功能了。
这一节我们来封装最后一种应用(Modbus RTU Master应用),RTU主站的开发与TCP客户端的开发是一致的。同样的我们也不是做具体的应用,而是实现RTU主站的基本功能。我们将RTU主站的功能封装为函数,以便在开发具体应用时调用。
对于RTU主站我们主要实现的功能有两个:其一是生成访问RTU从站的命令,总共支持8中기능 코드。其二是对RTU从站端返回的信息进行解析并根据结果进行各种操作,同样也是支持8中功能吗的操作。具体软件访问结构如下:

1、访问命令的生成
客户端作为主动交互端,需要向服务器发各种操作请求命令。所以对于RTU主站来说,首先要生成访问服务器的命令。生成命令只需要按标准的协议格式来生成即可,目前我们只支持前面提到的8个기능 코드。
/*生成访问从站的命令*/
uint16_t CreateAccessSlaveCommand(ObjAccessInfo objInfo,void *데이터List,uint8_t *commandBytes)
这样在开发具体的客户端应用时只需要调用这个函数来生成访问服务器的命令就可以了。
2、响应信息的解析
如前一节所述,服务器收到命令后,根据命令进行处理并生成响应信息返回给客户端。客户端接到命令后首先要对响应信息进行解析,解析的过程其实与服务器端是一致的。所不同的是,不需要再根据解析结果生成响应信息了。
/*解析收到的从站相应信息*/
void 분석SlaveRespondMessage(uint8_t *recievedMessage)
这样在开发客户端应用时,我们调用这一函数来解析响应信息就可以了。
3、响应处理
对于解析出来的信息,我们需要根据情况实现操作,比如修改变量的值等,应为主要支持的操作码是8个,理论上对应的每种기능 코드都会有不同的操作,但事实上,由于写操作命令已经不需要做任何操作了,所以对应的操作实际上只有读操作的4种기능 코드。
/*处理读从站状态量返回信息,读线圈状态位0x01기능 코드*/
static void HandleReadCoilStatusRespond(uint8_t *receivedMessage,uint16_t 시작주소,uint16_t 수량)
/*处理读从站状态量返回信息,读输入状态位0x02기능 코드*/
static void HandleReadInputStatusRespond(uint8_t *receivedMessage,uint16_t 시작주소,uint16_t 수량)
/*处理读从站寄存器值的返回信息,读保持寄存器0x03기능 코드)*/
static void HandleReadHolding회원가입Respond(uint8_t *receivedMessage,uint16_t 시작주소,uint16_t 수량)
/*处理读从站寄存器值的返回信息,读输入寄存器0x04기능 코드*/
static void HandleReadInput회원가입Respond(uint8_t *receivedMessage,uint16_t 시작주소,uint16_t 수량)
同样的,我们也定义一个函数指针数组来实现这些函数的调用:
void (*HandleServerRespond[])(uint8_t *,uint16_t,uint16_t)={HandleReadCoilStatusRespond, HandleReadInputStatusRespond, HandleReadHolding회원가입Respond, HandleReadInput회원가입Respond};
到这里,RTU主站的封装就完成了,当然具体的데이터处理部分需要在开发具体应用是才能确定。
前面开发了各种应用,但是却一直没有提到一个问题,你就是对具体的데이터进行读写操作。对于Modbus来说标准的데이터有4种:线圈데이터(주소:0000x)、输入状态量데이터(주소:1000x)、保持寄存器데이터(주소:4000x)和输入寄存器데이터(주소:3000x)。我们通讯的目的就是为了对这些데이터进行操作,可是我们前面的封装中并没有提到데이터处理。事实上,也没办法考虑这一点,因为具体的应用데이터千差万别,是没办法封装的。那我们怎么解决这一问题呢?接下来我们将解决这一类问题。
1、데이터处理函数的封装
我们考虑到,不论是在RTU主站、RTU从站、TCP客户端、还是在TCP服务器对데이터的处理本质上是一样的,只要具体应用的데이터结构确定后处理方法也就确定了。鉴于此,我们采用的方法是定义弱化类型的函数。如下:
/*获取想要读取的Coil量的值*/
__weak void GetCoilStatus(uint16_t 시작주소,uint16_t 수량,bool *statusList)
{
//如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}
/*获取想要读取的InputStatus量的值*/
__weak void GetInputStatus(uint16_t 시작주소,uint16_t 수량,bool *statusValue)
{
//如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}
/*获取想要读取的保持寄存器的值*/
__weak void GetHolding회원가입(uint16_t 시작주소,uint16_t 수량,uint16_t *registerValue)
{
//如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}
/*获取想要读取的输入寄存器的值*/
__weak void GetInput회원가입(uint16_t 시작주소,uint16_t 수량,uint16_t *registerValue)
{
//如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}
/*设置单个线圈的值*/
__weak void SetSingleCoil(uint16_t coil주소,bool coilValue)
{
//如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}
/*设置单个寄存器的值*/
__weak void SetSingle회원가입(uint16_t register주소,uint16_t registerValue)
{
//如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}
/*设置多个线圈的值*/
__weak void SetMultipleCoil(uint16_t 시작주소,uint16_t 수량,bool *statusValue)
{
//如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}
/*设置多个寄存器的值*/
__weak void SetMultiple회원가입(uint16_t 시작주소,uint16_t 수량,uint16_t *registerValue)
{
//如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}
/*更新读回来的线圈状态*/
__weak void UpdateCoilStatus(uint16_t 시작주소,uint16_t 수량,bool *stateValue)
{
//在客户端(主站)应用中实现
}
/*更新读回来的输入状态值*/
__weak void UpdateInputStatus(uint16_t 시작주소,uint16_t 수량,bool *stateValue)
{
//在客户端(主站)应用中实现
}
/*更新读回来的线圈状态*/
__weak void UpdateHolding회원가입(uint16_t 시작주소,uint16_t 수량,uint16_t *registerValue)
{
//在客户端(主站)应用中实现
}
/*更新读回来的线圈状态*/
__weak void UpdateInputResgister(uint16_t 시작주소,uint16_t 수량,uint16_t *registerValue)
{
//在客户端(主站)应用中实现
}
在开发具体应用时,我们只需要在应用中实现对应的函数就可以使功能完整,至于具体的데이터如何处理,就要看具体应用中的데이터格式了。当然这些函数并非都需要实现,只需要根据自己的需要实现就可以了。
2、关于大小端的问题
提到데이터通讯,有一个问题是没有办法回避的,那就是大小端的问题。对于Modbus协议来说,采用的是大端模式,就是低位주소存高位바이트데이터,高位주소存低位바이트데이터。
在进行多바이트데이터通讯时,大小端的问题就明显了,比如一个浮点数在不同的系统中存储的顺序是有差别的,你读上来或者写下去的데이터就会出现错误的解析。所以我们在处理데이터报文时是必须考虑这一点的。
谈到Modbus通讯自然免不了循环冗余校验(CRC),特别是在标准的串行RTU链路上是必不可少的。不仅如此在其他开发中,也经常要用到CRC 算法对各种데이터进行校验。这样一来,我们就需要研究一下这个循环冗余校验(CRC)算法。
1、CRC简述
循环冗余检查(CRC)是一种데이터传输检错功能,对데이터进行多项式计算,并将得到的结果附在帧的后面,接收设备也执行类似的算法,以保证데이터传输的正确性和完整性。
CRC 검증的基本思想是利用线性编码理论,在发送端根据要传送的k位二进制码序列,以一定的规则产生一个校验用的监督码(既CRC码)r位,并附在信息后边,构成一个新的二进制码序列数共(k+r)位,最后发送出去。在接收端,则根据信息码和CRC码之间所遵循的规则进行检验,以确定传送中是否出错。
CRC的本质是模2除法的余数,采用的除数不同,CRC的类型也就不一样。通常,CRC的除数用生成多项式来表示。最常用的CRC码的生成多项式有下面几种:

不一样的生成多项式,所以得到的结果自然也是不一样的。事实上在Modbus通讯中采用的是CRC-16的方式。
2、算法分析
CRC 검증码的编码方法是用待发送的二进制데이터t(x)除以生成多项式g(x),将最后的余数作为CRC 검증码。其实现步骤如下:
设待发送的데이터块是m位的二进制多项式t(x),生成多项式为r阶的g(x)。在데이터块的末尾添加r个0,데이터块的长度增加到m+r位,对应的二进制多项式为 。用生成多项式g(x)去除 ,求得余数为阶数为r-1的二进制多项式y(x)。此二进制多项式y(x)就是t(x)经过生成多项式g(x)编码的CRC 검증码。用 以模2的方式减去y(x),得到二进制多项式 。 就是包含了CRC 검증码的待发送문자串。
从CRC的编码规则可以看出,CRC编码实际上是将代发送的m位二进制多项式t(x)转换成了可以被g(x)除尽的m+r位二进制多项式,所以解码时可以用接收到的데이터去除g(x),如果余数位零,则表示传输过程没有错误;如果余数不为零,则在传输过程中肯定存在错误。许多CRC的硬件解码电路就是按这种方式进行检错的。同时可以看作是由t(x)和CRC 검증码的组合,所以解码时将接收到的二进制데이터去掉尾部的r位데이터,得到的就是原始데이터。
实际上,真正的CRC 计算通常与上面描述的还有些不同。这是因为这种最基本的CRC除法存在一个很明显的缺陷,就是데이터流的开头添加一些0并不影响最后校验的结果。为了弥补这一缺陷所以引入了两个概念:一个是“余数初始值”,另一个是“结果异或值”。所谓 “余数初始值”就是在计算CRC值前,为存储变量所赋的初值。对应的“结果异或值”就是在计算完成后,将变量值与这个值作最后的异或运算而得到校验结果。
| 名称 | 校验和位宽 | 生成多项式 | 除数(多项式) | 余数初始值 | 结果异或值 |
| CRC-4 | 4 | x4+x+1 | 3 | ||
| CRC-8 | 8 | x8+x5+x4+1 | 0x31 | ||
| CRC-8 | 8 | x8+x2+x1+1 | 0x07 | ||
| CRC-8 | 8 | x8+x6+x4+x3+x2+x1 | 0x5E | ||
| CRC-12 | 12 | x12+x11+x3+x+1 | 80F | ||
| CRC-16 | 16 | x16+x15+x2+1 | 0x8005 | 0x0000 | 0x0000 |
| CRC-CCITT | 16 | x16+x12+x5+1 | 0x1021 | 0xFFFF | 0x0000 |
| CRC-32 | 32 | x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x1+1 | 0x04C11DB7 | 0xFFFFFFFF | 0xFFFFFFFF |
| CRC-32c | 32 | x32+x28+x27+...+x8+x6+1 | 1EDC6F41 |
说到这里我们已经可以描述一下这个算法的实现过程:
第1步:定义CRC存储变量,并给其赋值为“余数初始值”。
第2步:将데이터的第一个8-bit문자与CRC存储变量进行异或,并把结果存入CRC存储变量。
第3步:CRC存储变量向右移一位,MSB补零,移出并检查LSB。
第4步:如果LSB为0,重复第三步;若LSB为1,CRC寄存器与0x31相异或。
第5步:重复第3与第4步直到8次移位全部完成。此时一个8-bit데이터处理完毕。
第6步:重复第2至第5步直到所有데이터全部处理完成。
第7步:最终CRC存储变量的内容与“结果异或值”进行或非操作后即为CRC值。
3、代码实现
有了前面的准备实际上我们要实现CRC 검증的代码已经很简单了,实现这一过程有各种方法我们说常用的2种:一是直接计算法,就是按照前面的步骤计算出来;二是驱动表法,就是将一些데이터储存起来直接获取计算。因为在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,데이터,val;
crc16 = Initial_Value;
for(int i=0;i<length;i++)
{
if((i % 8) == 0)
{
데이터 = (*buf++)<<8;
}
val = crc16 ^ 데이터;
crc16 = crc16<<1;
데이터 = 데이터 <<1;
if(val&0x8000)
{
crc16 = crc16 ^ POLY16;
}
}
return crc16;
}
(2)驱动表法
对于直接计算法,虽然简单直接,但有时候效率却是个问题,所以在Modbus通讯中我们通常采用驱动表法来实现:
//CRC_16高8位데이터区
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低位바이트值表
const uint8_t auchCRCLo[] = {//CRC_16低8位데이터区
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
};
/*函数功能:CRC 검증码生成
输入参数:puchMsgg是要进行CRC 검증的消息,us데이터Len是消息中바이트数
函数输出:计算出来的CRC 검증码
GenerateCRC16CheckCode查表计算函数*/
static uint16_t GenerateCRC16CheckCode(uint8_t *puckMsg,uint8_t us데이터Len)
{
uint8_t uchCRCHi = 0xFF ; //高CRC바이트初始化
uint8_t uchCRCLo = 0xFF ; //低CRC 바이트初始化
uint32_t uIndex ; //CRC循环中的索引
//传输消息缓冲区
while (us데이터Len--)
{
//计算CRC
uIndex = uchCRCLo ^ *puckMsg++ ;
uchCRCLo = uchCRCHi ^ auchCRCHi[uIndex] ;
uchCRCHi = auchCRCLo[uIndex] ;
}
//返回结果,高位在前
return (uchCRCLo << 8 |uchCRCHi) ;
}
4、종료语
CRC的应用非常广泛,特别是在做通讯时更是经常见到,所以掌握它是非常有必要的,至少会使用它。我们在开发Modbus库函数的过程中,对它也不过是有了一些比较粗浅的理解,在此记述以求共进。
前面我们已经完成了Modbus协议栈的开发,但这不是我们的目的。我们开发它的目的当然是要使用它来解决我们的实际问题。接下来我们就使用刚开发的Modbus协议栈开发一个Modbus TCP Server应用。
开发Modbus TCP Server首先需要有TCP Server的支持以及网络的配置等,但这些与Modbus本身没有什么关系,我们再次不作讨论。我们规定网络和TCP Server已经配置妥当。接下来我们讨论Modbus TCP Server的实现过程。
根据前面对协议栈的封装,我们需要引用Modbus TCP Server的相关封装。在接收到命令后调用解析函数进行解析,解析函数的原型:
uint16_t 분석ClientAccessCommand(uint8_t *receivedMessage,uint8_t *respondBytes);
该函数作为函数指针传递给TCP Server,并回调解析接收到的信息列表。
除此之外,我们要根据具体的需要实现8个回调函数,以完成真正的对Modbus对象的操作,这8个函数的原型如下:
/*获取想要读取的Coil量的值*/
void GetCoilStatus(uint16_t 시작주소,uint16_t 수량,bool *statusList);
/*获取想要读取的InputStatus量的值*/
void GetInputStatus(uint16_t 시작주소,uint16_t 수량,bool *statusValue);
/*获取想要读取的保持寄存器的值*/
void GetHolding회원가입(uint16_t 시작주소,uint16_t 수량,uint16_t *registerValue);
/*获取想要读取的输入寄存器的值*/
void GetInput회원가입(uint16_t 시작주소,uint16_t 수량,uint16_t *registerValue);
/*设置单个线圈的值*/
void SetSingleCoil(uint16_t coil주소,bool coilValue);
/*设置单个寄存器的值*/
void SetSingle회원가입(uint16_t register주소,uint16_t registerValue);
/*设置多个线圈的值*/
void SetMultipleCoil(uint16_t 시작주소,uint16_t 수량,bool *statusValue);
/*设置多个寄存器的值*/
void SetMultiple회원가입(uint16_t 시작주소,uint16_t 수량,uint16_t *registerValue);
这8个函数根据应用的具体需求来实现데이터对象的操作,实现几个如何实现根据各自的应用需求和데이터结构来确定。
上一节我们使用协议占开发了一个Modbus TCP Server应用。接下来我们使用协议栈在开发一个基于串行链路的Mosbus RTU Slave应用。
根据前面对协议栈的封装,我们需要引用Modbus TCP Server的相关封装。在接收到命令后调用解析函数进行解析,解析函数的原型:
분석MasterAccessCommand(uint8_t *receivedMesasage,uint8_t *respondBytes,uint16_t rxLength);
RTU Slave使用串口中断接收信息,接收到信息后使用上述函数解析信息,根据接收的信息命令完成操作。
当然,除了调用解析函数外,我们要根据具体的需要实现8个回调函数,以完成真正的对Modbus对象的操作,这8个函数的原型如下:
/*获取想要读取的Coil量的值*/
void GetCoilStatus(uint16_t 시작주소,uint16_t 수량,bool *statusList);
/*获取想要读取的InputStatus量的值*/
void GetInputStatus(uint16_t 시작주소,uint16_t 수량,bool *statusValue);
/*获取想要读取的保持寄存器的值*/
void GetHolding회원가입(uint16_t 시작주소,uint16_t 수량,uint16_t *registerValue);
/*获取想要读取的输入寄存器的值*/
void GetInput회원가입(uint16_t 시작주소,uint16_t 수량,uint16_t *registerValue);
/*设置单个线圈的值*/
void SetSingleCoil(uint16_t coil주소,bool coilValue);
/*设置单个寄存器的值*/
void SetSingle회원가입(uint16_t register주소,uint16_t registerValue);
/*设置多个线圈的值*/
void SetMultipleCoil(uint16_t 시작주소,uint16_t 수량,bool *statusValue);
/*设置多个寄存器的值*/
void SetMultiple회원가입(uint16_t 시작주소,uint16_t 수량,uint16_t *registerValue);
这8个函数根据应用的具体需求来实现데이터对象的操作,实现几个如何实现根据各自的应用需求和데이터结构来确定。
当然,并非必须使用中断接收信息,也可以使用查询等方式,但是使用中断是一个比较好的方法,因为主站命令的发送一般并无规律,中断方式既可保证信息及时响应,也无须耗费太多的从站资源,而有更多资源去处理其他的任务。
发表回复