基于mnModbus프로토콜栈的Modbus개발튜토리얼(完整版)

무료무료 기술 자료

이 글은 바로 읽을 수 있으며 기초 학습과 검색 유입에 적합합니다.

基于mnModbus协议栈的Modbus开发教程(完整版)

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 功能有关的差错,那么服务器至客户机的响应데이터域包括请求데이터。

基于mnModbus协议栈的Modbus开发教程(完整版)插图

正常事务处理流程

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

基于mnModbus协议栈的Modbus开发教程(完整版)插图1

异常事务处理流程

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(最低有效位)包括在询问中寻址的출력。其它코일依次类推,一直到这个바이트的高位端为止,并在后续바이트中从低位到高位的顺序。

如果返回的출력数量不是八的倍数,将用零填充最后데이터바이트中的剩余比特(一直到바이트的高位端)。바이트数量域설명了데이터的完整바이트数。具体형식举例如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图2

(2)기능 코드0x02:读离散量입력

在一个远程设备中,使用该기능 코드읽기离散量입력的 1 至 2000 连续状态。请求 PDU 详细설명了시작주소,即指定的第一个입력주소和입력번호。从零开始寻址입력。因此寻址입력 1-16 为 0-15。

根据데이터域的每个比特将响应프레임中的离散量입력分成为一个입력。指示状态为1=ON 和0=OFF。第一个데이터바이트的 LSB(最低有效位)包括在询问中寻址的입력。其它입력依次类推,一直到这个바이트的高位端为止,并在后续바이트中从低位到高位的顺序。

如果返回的입력数量不是八的倍数,将用零填充最后데이터바이트中的剩余比特(一直到바이트的高位端)。바이트数量域설명了데이터的完整바이트数。具体형식举例如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图3

事实上,기능 코드0x01和0x02的데이터형식是完全一样的,操作방식也是完全一样的,所不同只是操作的对象不同。

(3)기능 코드0x03:读유지 레지스터

在一个远程设备中,使用该기능 코드유지 레지스터 읽기 (FC03)连续块的콘텐츠。请求 PDU 설명了시작레지스터주소和레지스터 수량。从零开始寻址레지스터。因此,寻址레지스터 1-16 为 0-15。

将响应프레임中的레지스터데이터分成每个레지스터有两바이트,在每个바이트中直接地调整2진수콘텐츠。

对于每个레지스터,第一个바이트包括高位比特,并且第二个바이트包括低位比特。具体的형식举例如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图4

响应信息的길이与具体읽기的데이터量有关。

(4)기능 코드0x04:读입력 레지스터

在一个远程设备中,使用该기능 코드읽기 1 至大약 125 的连续입력 레지스터。请求 PDU 설명了시작주소和레지스터 수량。从零开始寻址레지스터。因此,寻址입력 레지스터 1-16 为 0-15。

将响应프레임中的레지스터데이터分成每个레지스터为两바이트,在每个바이트中直接地调整2진수콘텐츠。

对于每个레지스터,第一个바이트包括高位比特,并且第二个바이트包括低位比特。具体的형식举例如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图5

事实上,기능 코드0x03和0x04的데이터형식是完全一样的,操作방식也是完全一样的,所不同只是操作的对象不同。响应信息的길이与具体읽기的데이터量有关。

(5)기능 코드0x05:写单个코일

在一个远程设备上,使用该기능 코드写单个출력为 ON 或 OFF。

请求데이터域中的常量설명请求的ON/OFF状态。16진수值0xFF00请求출력为 ON。16진수值0x0000 请求출력为OFF。其它所有值均是非法的,并且对출력不起作用。

请求 PDU 설명了强制的코일주소。从零开始寻址코일。因此,寻址코일1为0。코일值域的常量설명请求的 ON/OFF 状态。16진수值 0XFF00 请求코일为 ON。16진수值 0X0000 请求코일为OFF。其它所有值均为非法的,并且对코일不起作用。

正常响应是请求的应答,在쓰기코일 상태之后返回这个正常响应。具体的형식举例如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图6

(6)기능 코드0x06:写单个레지스터

在一个远程设备中,使用该기능 코드写单个유지 레지스터。

请求 PDU 설명了被쓰기레지스터的주소。从零开始寻址레지스터。因此,寻址레지스터1为0。

正常响应是请求的应答,在쓰기레지스터콘텐츠之后返回这个正常响应。具体的형식举例如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图7

(7)기능 코드0x0F:写多个코일

在一个远程设备中,使用该기능 코드强制코일시퀀스中的每个코일为ON或OFF。请求PDU설명了强制的코일参考。从零开始寻址코일。因此,寻址코일1为0。

请求데이터域的콘텐츠설명了被请求的ON/OFF状态。域比特位置中的逻辑“1”请求相应출력为ON。域比特位置中的逻辑“0”请求相应출력为 OFF。

正常响应返回기능 코드、시작주소和强制的코일 수량。具体的형식举例如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图8

(8)기능 코드0x10:写多个레지스터

在一个远程设备中,使用该기능 코드写连续레지스터块(1至约120个레지스터)。

在请求데이터域中설명了请求쓰기的值。每个레지스터将데이터分成两바이트。

正常响应返回기능 코드、시작주소和被쓰기레지스터的数量。具体的형식举例如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图9

清楚了以上这些설명,我们就可以开始Modbus프로토콜的개발之旅了。

前面我们已经对Modbus的基本事务作了설명,也据此设计了我们将要实现的主슬레이브的操作流程。这其中与Modbus直接相关的就是Modbus消息帧的生成。Modbus消息帧也是实现Modbus통신 프로토콜的根本。

1、Modbus消息帧分析

MODBUS프로토콜在不同的物理链路上的消息帧有一些差异,但我们分析一下就会发现,在这些不同的消息帧中具有一下相同的部分,这对我们实现统一的데이터操作非常重要,具体설명如下:

(1)、简单프로토콜데이터单元

MODBUS프로토콜定义了一个与基础통신层无关的简单프로토콜데이터单元(PDU)。简单프로토콜데이터单元的结构如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图10

PDU是一个与具体的传输네트워크无关的部分,包含기능 코드和데이터。对于特定总线或네트워크上的 MODBUS 프로토콜只是在PDU的基础上在적용데이터单元(ADU)上引入一些附加域。

데이터单元部分的개발是最基本的部分,主要是2各方面的类容:一是生成클라이언트(마스터)访问服务器(슬레이브)的命令部分;二是生成服务器(슬레이브)响应클라이언트(마스터)回复部分。

(2)、RTU的적용데이터单元

对于在串行链路上运行的Modbus프로토콜,其적용데이터单元(ADU)是在PDU的基础上,在前面加上주소域,后面加上데이터체크섬。형식如下图所示:

基于mnModbus协议栈的Modbus开发教程(完整版)插图11

주소域就是所访问슬레이브的주소,为一个8位无符号数,取值0-255,但0和255有固定含义不能使用。CRC 검증采用的是CRC16체크섬 방식。

(3)、TCP的적용데이터单元

在以太网链路上运行的Modbus프로토콜,其적용데이터单元(ADU)是在PDU的基础上추가上MBAP헤더形成的,具体형식如下图:

基于mnModbus协议栈的Modbus开发教程(完整版)插图12

对于MBAP 헤더,包括下列域:

域길이설명客户机服务器
事务元标识符2 个바이트MODBUS 请求/响应事务处理的识别码客户机启动服务器从수신的请求中重新복사
프로토콜 식별자2 个바이트0=MODBUS 프로토콜客户机启动服务器从수신的请求中重新복사
길이2 个바이트以下바이트的数量客户机启动(请求)服务器(响应)启动
유닛 식별자1 个바이트串行链路或其它总线上연결的远程슬레이브的识别码客户机启动服务器从수신的请求中重新복사

从上表中可知헤더为 7 个바이트长:

事务处理标识符:用于事务处理配对。在响应中,MODBUS 服务器복사请求的事务处理标识符。
프로토콜 식별자:用于系统内的多路复用。通过值 0 识别 MODBUS 프로토콜。

길이:길이域是下一个域的바이트数,包括유닛 식별자和데이터域。

유닛 식별자:为了系统内路由,使用这个域。专门用于通过以太网TCP-IP 네트워크和MODBUS串行链路之间的게이트웨이对MODBUS或MODBUS+串行链路슬레이브的통신。说的简单点就是串行链路中的주소域。MODBUS客户机在请求中设置这个域,在响应中服务器必须利用相同的值返回这个域。

2、데이터帧的具体组成分析

从以上对简单프로토콜基本데이터元、RTU적용데이터单元和TCP적용데이터单元프레임형식的分析,我们发现对于基本데이터单元部分已一致的,所以我们可以考虑来分层封装프로토콜操作部分:

基于mnModbus协议栈的Modbus开发教程(完整版)插图13

最开始实现Modbus基本데이터单元,这是데이터公用部分与具体的적용无关,只需要封装一次,对于这部分的개발只需要按照Modbus的표준프로토콜来개발就好,本次我们计划实现的功能有8个:

기능 코드名称实现설명
0x01读코일是对可读写型的状态量进行읽기
0x02读이산 입력是对只读型的状态量进行읽기
0x03读유지 레지스터是对可读写型的레지스터量进行읽기
0x04读입력 레지스터是对只读型的레지스터量进行읽기
0x05写单个코일是对单个的读写型的状态量进行쓰기
0x06写单个레지스터是对单个的读写型的레지스터量进行쓰기
0x0F写多个코일是对多个的读写型的状态量进行쓰기
0x10写多个레지스터是对多个的读写型的레지스터量进行쓰기

这8个也是Modbus프로토콜所定义的最主要的功能,现在对这几种기능 코드的프레임형식설명如下:

(1)读코일0x01

读코일就是都一种可以写的开关量,因为Modbus프로토콜起源于PLC적용,而코일是对PLC的DO출력的称呼,一般适用于마스터对슬레이브下达操作命令。读这种具有读写功能的状态量的데이터형식如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图14

其下发的命令형식为:域名+기능 코드+시작주소+数量。

(2)读이산 입력0x02

读状态입력是읽기一种只读开关量信号,对应于PLC中的数字입력量。읽기这种只读型开关입력量的형식如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图15

其下发的命令형식为:域名+기능 코드+시작주소+数量。

(3)读유지 레지스터0x03

유지 레지스터就是指可以读写的16位데이터,通过单个或多个유지 레지스터可以用来表示各种데이터,如8位整数、16为整数、32位整数、64位整数以及单双精度浮点数等。유지 레지스터 읽기 (FC03)的프레임형식如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图16

其下发的命令형식为:域名+기능 코드+시작주소+数量。

(4)读입력 레지스터0x04

입력 레지스터是一种只读形式的16位데이터。通过单个或多个입력 레지스터可以表示8位整数、16为整数、32位整数、64位整数以及单双精度浮点数等。입력 레지스터 읽기 (FC04)的프레임형식如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图17

其下发的命令형식为:域名+기능 코드+시작주소+数量。

(5)写单个코일0x05

写单个코일量就是对单个的可读写的开关量进行操作,但是其并非是直接写“0”或者“1”,而是在需要写“1”时보내기0xFF00;而在需要写“0”时보내기0x0000,其具体的프레임형식如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图18

其下发的命令형식为:域名+기능 코드+출력주소+출력值。命令的具体콘텐츠与读操作有区别但,형식却是完全一样,在编程时实际读和写可以封装在一起。

(6)写单个레지스터0x06

写单个레지스터就是对单个的유지 레지스터进行操作,데이터的형식依然是一样的,实际적용中只适用于对16位整型데이터的操作,对于浮点数等则不可以。

基于mnModbus协议栈的Modbus开发教程(完整版)插图19

其下发的命令형식为:域名+기능 코드+출력주소+출력值。命令的具体콘텐츠与读操作有区别但,형식却是完全一样,在编程时实际读和写可以封装在一起。

(7)写多个코일0x0F

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

基于mnModbus协议栈的Modbus开发教程(完整版)插图20

其下发的命令형식为:域名+기능 코드+시작주소+출력数量+바이트数+출력值。命令프레임与前面的几种读写操作有较大的区别,必须要单独处理。

(8)写多个레지스터0x10

写多个레지스터的就是对多个可读写레지스터同时进行操作,데이터프레임的형식与写多个코일是一致的。

基于mnModbus协议栈的Modbus开发教程(完整版)插图21

其下发的命令형식为:域名+기능 코드+시작주소+출력数量+바이트数+출력值。

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中常用的功能吗的支持。其次在분석完成后,我们要实现对应各种기능 코드的操作。具体架构如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图22

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中功能吗的操作。具体软件访问结构如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图23

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种기능 코드。其次我们根据분석的结果进行对应的操作,具体的软件访问结构如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图24

从上图中不难发现实际是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中功能吗的操作。具体软件访问结构如下:

基于mnModbus协议栈的Modbus开发教程(完整版)插图25

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位2진수码시퀀스,以一定的规则产生一个체크섬用的监督码(既CRC码)r位,并附在信息后边,构成一个新的2진수码시퀀스数共(k+r)位,最后보내기出去。在수신端,则根据信息码和CRC码之间所遵循的规则进行检验,以确定传送中是否出错。

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

基于mnModbus协议栈的Modbus开发教程(完整版)插图26

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

2、算法分析

      CRC 검증码的编码方法是用待보내기的2진수데이터t(x)除以生成多项式g(x),将最后的余数作为CRC 검증码。其实现步骤如下:

      设待보내기的데이터块是m位的2진수多项式t(x),生成多项式为r阶的g(x)。在데이터块的末尾추가r个0,데이터块的길이추가到m+r位,对应的2진수多项式为 。用生成多项式g(x)去除 ,求得余数为阶数为r-1的2진수多项式y(x)。此2진수多项式y(x)就是t(x)经过生成多项式g(x)编码的CRC 검증码。用 以模2的방식减去y(x),得到2진수多项式 。 就是包含了CRC 검증码的待보내기문자串。

      从CRC的编码规则可以看出,CRC编码实际上是将代보내기的m位2진수多项式t(x)转换成了可以被g(x)除尽的m+r位2진수多项式,所以解码时可以用수신到的데이터去除g(x),如果余数位零,则表示传输过程没有错误;如果余数不为零,则在传输过程中肯定存在错误。许多CRC的硬件解码电路就是按这种방식进行检错的。同时可以看作是由t(x)和CRC 검증码的组合,所以解码时将수신到的2진수데이터去掉尾部的r位데이터,得到的就是原始데이터。

      实际上,真正的CRC 계산通常与上面설명的还有些不同。这是因为这种最基本的CRC除法存在一个很明显的缺陷,就是데이터流的开头추가一些0并不影响最后체크섬的结果。为了弥补这一缺陷所以引入了两个概念:一个是“余数初始值”,另一个是“结果异或值”。所谓 “余数初始值”就是在계산CRC值前,为存储변수所赋的初值。对应的“结果异或值”就是在계산完成后,将변수值与这个值作最后的异或运算而得到체크섬结果。

名称체크섬位宽生成多项式除数(多项式)余数初始值结果异或值
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步:将데이터的第一个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个函数根据적용的具体需求来实现데이터对象的操作,实现几个如何实现根据各自的적용需求和데이터结构来确定。

当然,并非必须使用中断수신信息,也可以使用查询等방식,但是使用中断是一个比较好的方法,因为마스터命令的보내기一般并无规律,中断방식既可保证信息及时响应,也无须耗费太多的슬레이브자료,而有更多자료去处理其他的任务。

실제 프로젝트에 이 자료를 활용하시겠습니까?

도구 센터에서 프레임 분석, CRC 검증, 장비 디버깅을 진행하거나 요청을 제출하여 선정 및 연동 제안을 받아보세요.

엔지니어 회원

이 글을 실행 가능한 디버깅 자료로 활용하세요

가입하면 고급 프레임 분석, 자료 패키지 다운로드, 코드 예제, 엔지니어링 사례, 우선 기술 지원을 이용할 수 있어 실제 프로젝트 납품에 적합합니다.

고급 도구 무제한 이용
자료 패키지와 코드 패키지
완전한 엔지니어링 사례 라이브러리
우선 기술 지원 채널

답글 작성

이메일 주소는 공개되지 않습니다. 必填项已用 * 标注