Modbus协议栈应用实例之五:Modbus ASCII主站应用

自从开源了我们自己开发的Modbus协议栈之后,有很多朋友建议我针对性的做几个示例。所以我们就基于平时我们的应用整理了几个简单但可以说明基本的应用方法的示例,这一篇中我们来使用协议栈实现Modbus ASCII主站应用。

1、何为ASCII主站

我们知道Modbus协议是一个主从协议,所以就存在主站和从站之分。所谓主站,简单来说就是能够主动发起通讯的站点,所以我们可以说主站就是发起通讯的一方。

对于ASCII主站来说,它的数据需要从从站获取,所以主站要通过通讯的方式与从站实现数据交流。在Modbus ASCII协议中从站不会主动向外发送数据,所以只有在ASCII主站发送数据请求,从站才会向其返回请求的数据。这一过程如下图所示:

从上图我们不难看出,首先主站要主动发起数据请求,这也是它为什么被称之为主站的缘由。它首先告诉从站我需要哪些数据。然后从站按照主站的请求返回数据。主站得到响应后解析数据,这样就完成了主从站之间的一次数据通讯。所以主站就需要主动发起每一次数据通讯的对象。

虽然Modbus ASCII与Modbus RTU都是基于串行链路来实现的,但在数据传输的报文格式上存在较大区别。相比于Modbus RTU,Modbus ASCII采用ASCII码的形式来发送报文,并且有确定的起始字符和结束字符。具体结构如下:

在ASCII模式下,每个8位的字节被拆分成两个ASCII字符进行发送。对于数据部分,根据具体发送的数据量来确定长度。校验方式则采用的是LRC校验方式。LRC校验较为简单,把每一个需要传输的数据字节迭加后取反加1即可。

2、如何实现ASCII主站

我们已经简单的说明了什么是ASCII的主站,那么如何实现这一主站呢?其实在协议栈中,我们已经实现了主站的数据请求命令的合成以及响应数据的解析,所以我们使用协议栈来实现ASCII主站时,我们需要做的就是控制何时将协议栈合成的主站请求命令发出以及如何解析数据响应进而得到想要的数据的过程。

在我们的协议栈中实现了0x01、0x02、0x03、0x04、0x05、0x06、0x0F以及0x10等功能码。也就是说主站对象可以生成面向这些功能码的从站数据请求。也可以解析面向这些功能码的从站数据响应。可以表示为下图所示:

从上图我们很清楚,协议栈已经实现了面向这些功能码的数据请求命令的生成以及数据响应消息的解析。我们使用协议栈时需要做的就是要告诉协议栈我要生成哪些数据请求命令以及如何解析数据响应消息。

2.1、怎么生成数据请求

对于数据请求,我们不一定需要面向全部功能码的请求,我们只需要根据我们的需求合成我们想要的请求。

在协议栈中,针对数据请求的生成我们定义了一个从站访问命令生成函数。该函数的原型如下:

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

该函数有3个参数,其中ObjAccessInfo objInfo为对象访问信息;void *dataList为数据列表指针,该参数主要用于写从站功能的命令生成;uint8_t *commandBytes为返回的从站访问命令。

ObjAccessInfo是一个结构体,向函数传递我们想要生成的从站访问命令的相关信息,包括站地址,功能码,起始地址和数量。该结构体的定义如下:

/*定义用于传递要访问从站(服务器)的信息*/
typedef struct{uint8_t unitID;FunctionCode functionCode;uint16_t startingAddress;uint16_t quantity;
}ObjAccessInfo;

2.2、怎么解析数据响应

对于数据响应,我们同样不需要考虑全部的操作码,我们一般需要考虑读请求的响应,因为他们的数据需要解析。而对于写请求返回数响应只是告诉主站成功或者不成功,即使不成功只需要在写一次就可以了,不存在数据更新的问题。

在协议栈中,我们实现了主站解析从站数据响应的解析函数。使用这一函数我们只需要将收到的数据响应报文传递给解析函数就可以完成解析。该函数的原型定义如下:

void ParsingAsciiSlaveRespondMessage(AsciiLocalMasterType *master,uint8_t *recievedMessage, uint8_t *command,uint16_t rxLength)

这个函数有4个参数,其中RTULocalMasterType *master为主站对象;uint8_t *recievedMessage为接收到的响应消息;uint8_t *command为发送的命令序列。uint16_t rxLength是接受到的数据响应消息的长度。将这几个参数传递给解析函数就可实现数据响应的解析。

AsciiLocalMasterType 是一个结构体,用以生命一个主站对象,这个对象就是我们要实现各种操作的主站,这一结构体的定义如下:

/* 定义本地ASCII主站对象类型 */
typedef struct LocalASCIIMasterType{uint32_t flagWriteSlave[8];   //写一个站控制标志位,最多256个站,与站地址对应。uint16_t slaveNumber;         //从站列表中从站的数量uint16_t readOrder;           //当前从站在从站列表中的位置AsciiAccessedSlaveType *pSlave;         //从站列表UpdateCoilStatusType pUpdateCoilStatus;       //更新线圈量函数UpdateInputStatusType pUpdateInputStatus;     //更新输入状态量函数UpdateHoldingRegisterType pUpdateHoldingRegister;     //更新保持寄存器量函数UpdateInputResgisterType pUpdateInputResgister;       //更新输入寄存器量函数
}AsciiLocalMasterType;

3ASCII主站编码

我们已经设计了一个简单的ASCII主站示例,接下来我们就来编码实现并验证这一示例。

3.1、定义ASCII主站对象

首先我们要声明一个主站对象,这是我们操作的基础。在接下来的各种操作中我们都是基于这一对象来实现的。具体操作如下:

AsciiLocalMasterType asciiMaster;

定义了这个主站对象后,我们还需要对这一对象进行初始化。协议栈同样提供了一个主站对象的初始化函数。函数的原型定义如下:

/*初始化ASCII主站对象*/
void InitializeASCIIMasterObject(AsciiLocalMasterType *master,uint16_t slaveNumber,AsciiAccessedSlaveType *pSlave,UpdateCoilStatusType pUpdateCoilStatus,UpdateInputStatusType pUpdateInputStatus,UpdateHoldingRegisterType pUpdateHoldingRegister,UpdateInputResgisterType pUpdateInputResgister)

该函数的参数除了主站对象外,还有从站的数量即从站对象列表,还有四个数据更新函数指针。这几个函数指针将应用于数据响应的解析过程中,具体在后面描述。使用这一初始化函数实现对主站对象的初始化,使其能够实现各项操作,具体如下:

/*初始化RTU主站对象*/

  InitializeASCIIMasterObject(&asciiMaster,2,asciiSlave,NULL,NULL,NULL,NULL);

这里我们将几个数据处理函数指针变量传入NULL,表示初始化为默认的操作函数,当然我们也可以编写这些函数,在后续的数据解析时将会详细说明。

3.2、生成主站数据请求

在前面,我们已经描述了数据请求命令的生成函数,该函数有一个ObjAccessInfo参数,这个参数用于传递需要生成命令的信息。这是一个结构体,我们需要定义一个对象变量。

ObjAccessInfo asciiInfo;

然后使用这个对象来实现数据请求的生成。具体操作如下所示:

/* 生成1号从站访问命令 */asciiInfo.unitID=asciiSlave[0].stationAddress;asciiInfo.functionCode=ReadCoilStatus;asciiInfo.startingAddress=0x0000;asciiInfo.quantity=8;CreateAccessAsciiSlaveCommand(asciiInfo,NULL,aSlave1ReadCommand[0]);

生成的数据请求什么时候发送给完全由主进程来实现已经与协议栈没有关系了。

3.3、解析从站数据响应

收到数据响应后我们需要对其进行解析。前面我们已经介绍了解析从站数据响应的函数。具体的调用形式如下:

ParsingAsciiSlaveRespondMessage(&asciiMaster,asciiRxBuffer,NULL,asciiRxLength);

我们对asciiMaster主站对象收到的从站响应asciiRxBuffer进行解析。最后传入的NULL表示我们不指定主站发送的数据请求,而是让主站从请求列表中去自己查找。

当然我们需要实现数据更新处理回调函数。这几个函数是在对象初始化的时候以函数指针的形式传递的。原型如下:

/*更新读回来的线圈状态*/
__weak void UpdateCoilStatus(uint8_t salveAddress,uint16_t startAddress,uint16_t quantity,bool *stateValue)
{//在客户端(主站)应用中实现
}/*更新读回来的输入状态值*/
__weak void UpdateInputStatus(uint8_t salveAddress,uint16_t startAddress,uint16_t quantity,bool *stateValue)
{//在客户端(主站)应用中实现
}/*更新读回来的保持寄存器*/
__weak void UpdateHoldingRegister(uint8_t salveAddress,uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
{//在客户端(主站)应用中实现
}/*更新读回来的输入寄存器*/
__weak void UpdateInputResgister(uint8_t salveAddress,uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
{//在客户端(主站)应用中实现
}

我们可根据需要重定义这些函数,当然我们没有响应的数据可以不必实现,如我们没有使用输入寄存器,那么更新输入寄存器的回调函数则可以不用重定义。如下在我们的例子中重定义为:

/*更新读回来的保持寄存器*/void UpdateHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
{uint16_t startRegister=HoldingResterEndAddress+1;switch(salveAddress){case BPQStationAddress:       //更新读取的变频器参数{startRegister=36;break;}case PUMPStationAddress:      //更新蠕动泵{startRegister=HoldingResterEndAddress+1;break;}case JIG1StationAddress:      //更新摆臂小电机{startRegister=48;break;}case JIG2StationAddress:      //更新摆臂小电机{startRegister=52;break;}case JIG3StationAddress:      //更新摆臂小电机{startRegister=56;break;}case HLPStationAddress:       //更新红外温度{aPara.phyPara.hlpObjectTemperature=registerValue[0]/100.0;startRegister=HoldingResterEndAddress+1;break;}case ROL1StationAddress:      //更新摆臂控制{startRegister=quantity<3?60:62;break;}case ROL2StationAddress:      //更新摆臂控制{startRegister=quantity<3?70:72;break;}case ROL3StationAddress:      //更新摆臂控制{startRegister=quantity<3?80:82;break;}case DRUMStationAddress:      //更新滚筒电机{startRegister=quantity<3?90:92;break;}default:                      //故障态{startRegister=HoldingResterEndAddress+1;break;}}if(startRegister<=HoldingResterEndAddress){for(int i=0;i<quantity;i++){aPara.holdingRegister[startRegister+i]=registerValue[i];}}
}/*更新读回来的输入寄存器*/
void UpdateInputResgister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
{uint16_t startRegister=HoldingResterEndAddress+1;switch(salveAddress){case BPQStationAddress:       //更新读取的变频器参数{startRegister=HoldingResterEndAddress+1;break;}case PUMPStationAddress:      //更新蠕动泵{//aPara.phyPara.pumpRotateSpeed=registerValue[1]; //第一版背板startRegister=HoldingResterEndAddress+1;break;}case JIG1StationAddress:      //更新摆臂小电机{startRegister=HoldingResterEndAddress+1;break;}case JIG2StationAddress:      //更新摆臂小电机{startRegister=HoldingResterEndAddress+1;break;}case JIG3StationAddress:      //更新摆臂小电机{startRegister=HoldingResterEndAddress+1;break;}case ROL1StationAddress:      //更新摆臂控制{startRegister=HoldingResterEndAddress+1;break;}case ROL2StationAddress:      //更新摆臂控制{startRegister=HoldingResterEndAddress+1;break;}case ROL3StationAddress:      //更新摆臂控制{startRegister=HoldingResterEndAddress+1;break;}case DRUMStationAddress:      //更新滚筒电机{startRegister=HoldingResterEndAddress+1;break;}default:                      //故障态{startRegister=HoldingResterEndAddress+1;break;}}if(startRegister<=HoldingResterEndAddress){for(int i=0;i<quantity;i++){aPara.holdingRegister[startRegister+i]=registerValue[i];}}
}

4ASCII主站小结

我们实现了这个ASCII主站实例,我们可以使用如Modsim这样的软件在PC上模拟Modbus ASCII从站来测试这个主站应用。如果自己编写报文也可使用如串口助手之类的软件测试。这里我们使用Modsim模拟从站,以AccessPort监视其收发状态,测试结果如下图:

在使用协议栈实现ASCII主站时需要注意,协议栈支持在同一设备上以不同的通讯端口实现不同的主站应用,而且每一台主站都支持多个从站。具体实现只需要根据协议栈定义就可以了。

我们来总结一下使用协议栈实现主站应用的步骤,以方便大家使用协议栈实现Modbus ASCII主站应用。

第一步,使用主站对象类型声明一个主站对象。然后对这个主站对象进行初始化。初始化主站对象时。需要指定从站数量,从站列表以及更新数据的回调函数指针。

第二步,生成访问从站的数据请求列表。这个数据请求列表是按每一台从站来划分的,将列表的指针存在对应的从站对象中。然后在需要的时候发送相应的数据请求。

第三步,解析接收的从站数据响应。协议栈已经定义好了解析函数,只需传入消息就可自动解析。但是更新数据的回调函数必须根据具体的变量来编写。可以每台主站独立编写也可使用默认的函数。不过建议每台主站独立编写,这样比较清晰。

源码下载:https://download.csdn.net/download/foxclever/12882021

欢迎关注:

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/499349.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Modbus协议栈应用实例之六:Modbus ASCII从站应用

自从开源了我们自己开发的Modbus协议栈之后&#xff0c;有很多朋友建议我针对性的做几个示例。所以我们就基于平时我们的应用整理了几个简单但可以说明基本的应用方法的示例&#xff0c;这一篇中我们来使用协议栈实现Modbus ASCII从站应用。 1、何为ASCII从站 我们知道Modbus…

外设驱动库开发笔记15:DHT11温湿度传感器驱动

与DS18B20一样DHT11也是采用单总线&#xff0c;但所不同的是DHT11可同时实现温度和湿度的检测。在我们的产品中经常使用它来检测环境的温湿度信息。这一篇我们将设计并封装DHT11的驱动程序&#xff0c;以方便重复使用。 1、功能概述 DHT11数字温湿度传感器是一款含有已校准数…

外设驱动库开发笔记16:MS5536C压力变送器驱动

压力检测也是经常会遇到的需求&#xff0c;比如环境压力或者低压气体等都会用到压力检测。这类检测压力都比较低&#xff0c;一般不会超过大气压&#xff0c;有时甚至是负压。这一篇我们要讨论的MS5536C就属于这类器件。接下来我们将设计并实现MS5536C的驱动。 1、功能概述 M…

外设驱动库开发笔记17:MS5803压力变送器驱动

很多时候我们需要检测被控对象的绝对压力&#xff0c;而且在我们的多款产品中也有这样的需求。当然检测绝对压力的传感器有很多&#xff0c;我们经常使用MS5803来实现压力检测。本篇中我们将设计并实现MS5803系列压力传感器的驱动。 1、功能概述 MS5803系列产品包含压阻传感器…

通讯接口应用笔记1:RS485通讯上下拉电阻的选择

RS485是一种常见的通讯接口方式&#xff0c;在我们的实际产品中也是多次使用。但我们平常并不会去过多考虑某一实现的细节问题&#xff0c;不过最近我们遇到了一个因如上下拉电阻的选择问题而造成的通讯故障&#xff0c;所以在这一片中我们来讨论一下RS485总线上下拉电阻的选择…

外设驱动库开发笔记18:MS5837压力变送器驱动

绝对压力的检测是常见的需求。在我们的系统中也常常会遇到。而MS5837压力传感器也是我们进场会采用的方案。在这篇里我们将讨论并实现MS5837压力传感器的驱动。 1、功能概述 MS5837压力传感器是一种可用于电路板上&#xff0c;适用于检测10-1200mbar压力范围的传感器&#xf…

外设驱动库开发笔记19:BMP280压力温度传感器驱动

压力和温度监测在嵌入式系统开发中是非常常见的需求&#xff0c;特别是对环境大气压力和温度的检测需求就更常见了。我们一般都会选择一些封装较小操作比较方便的压力传感器。BMP280就是满足这一要求的器件。在这一篇中我们将设计并实现BMP280的驱动。 1、功能概述 BMP280是一…

一个简单的空气质量数据监测站项目

大气质量数据监测站用于测试空气质量监测及数据采集&#xff0c;实现野外或者室内空气质量的检测。并通过网络将数据上传到OneNet​显示。​ 1、项目概述 本项目是一个定制项目&#xff0c;要求采集大气的压力、温度、湿度、PM25、位置等数据并上传到指定的后台服务器。但有时…

一个基于STM32实现的多组分气体分析仪项目

本篇将简要的总结一下一个基于STM32F412ZG实现的多组分气体分析仪的项目。简要描述该项目的软硬件设计及其验证。 一、项目概述 多组分气体分析仪是我公司近期研发的三个主要产品之一。采用模块化设计&#xff0c;可增减配置&#xff0c;可分析混合气体中的氧气、氢气、甲烷、…

外设驱动库开发笔记20:BME280压力湿度温度传感器驱动

嵌入式产品开发中&#xff0c;我们常常会有检测环境温度、压力、湿度的需求。如果有一个集成有这3个传感器的元件&#xff0c;无疑将是很方便的。博世的BME280就能实现这一要求。在这一篇中我们将讨论BME280的驱动设计与实现。 1、功能概述 BME280是一款专为移动应用而开发的…

外设驱动库开发笔记21:BME680环境传感器驱动

环境传感器是一类我们很常用的传感器。它可以方便我们获取压力、温度、湿度以及空气质量等数据。在这一篇中&#xff0c;我们将分析BME680环境传感器的功能&#xff0c;并设计和实现BME680环境传感器的驱动。 1、功能概述 BME680是一款专为移动应用和可穿戴设备开发的集成环境…

外设驱动库开发笔记22:ADXL345三轴数字加速度计驱动

移动设备的广泛应用增加对移动过程中各种参数的检测需求。ADXL345三轴数字加速度计可以用来检测加速度、进而测量倾斜角度等。在这一篇中&#xff0c;我们将讨论ADXL345三轴数字加速度计驱动程序的设计与实现。 1、功能概述 ADXL345是一款小而薄的超低功耗3轴加速度计&#x…

外设驱动库开发笔记23:AT24Cxx外部存储器驱动

在我们的应用开发过程中&#xff0c;经常会使用到外部的EEPROM外部存储器来保存一些参数和配置数据等。而比较常用的就是AT24Cxx系列产品&#xff0c;这一节我们来开发用于操作AT24Cxx系列产品的驱动。 1、功能概述 AT24Cxx系列EEPROM包括从1Kbit到2Mbit的各种容量。AT24Cxx系…

外设驱动库开发笔记24:FM24xxx系列FRAM存储器驱动

虽然说使用EEPROM保存参数很有效&#xff0c;但操作及使用次数均有一下限制。当我们的一些参数需要不定时修改或存储时&#xff0c;使用FRAM就更为方便一点。这一节我们就来设计并实现FM24xxx系列FRAM的驱动。 1、功能概述 我们首先说一下铁电随机存取存储器&#xff0c;F-RA…

外设驱动库开发笔记25:FM25xxx FRAM存储器驱动

在我们的项目中&#xff0c;时常会有参数或数据需要保存。铁电存储器的优良性能和操作方便常常被我们选用。FM25xxx FRAM存储器就是我们经常使用到的一系列铁电存储器&#xff0c;这一篇我们将讨论FM25xxx FRAM存储器的驱动设计、实现及使用。 1、功能概述 FM25xxx FRAM存储器…

步进电机驱动技术1:基于TMC2660的步进电机驱动

步进电机的应用非常广泛&#xff0c;在各种设备中经常会遇到&#xff0c;而步进电机的驱动则是使用步进电机必不可少的部分&#xff0c;可以有多种方式来实现步进电机的驱动&#xff0c;在这里我们来考虑一下基于TMC2660驱动芯片的步进电机驱动。 1、功能概述 TMC2660是德国T…

外设驱动库开发笔记26:nRF24L01无线通讯驱动

现在无线在我们的生活中无处不在。而我们开发的物联网产品也大量使用无线通讯。在这一篇文章中&#xff0c;我们将讨论nRF24L01无线通讯模块驱动程序的开发与实现。 1、功能概述 nRF24L01是一款工作在2.4~2.5GHz世界通用ISM 频段的单片无线收发器芯片无线收发器包括&#xff…

外设驱动库开发笔记27:ESP8266无线通讯驱动

我们的物联网产品所使用的平台都支持无线通讯&#xff0c;而且无线通讯本身更的成本较低&#xff0c;受到大家的欢迎。在本篇文章中&#xff0c;我们将详细讨论并实现ESP8266无线通讯模块的驱动。 1、功能概述 ESP8266是由乐鑫公司出品的一款物联网芯片&#xff0c;因为价格较…

外设驱动库开发笔记28:W5500以太网控制器

以太网通讯是一种被广泛使用的数据通讯方式。在嵌入式应用中也经常使用&#xff0c;但协议栈的实现并不是一件容易的事。不过有些以太网控制器就带有协议栈&#xff0c;如W5500。在本篇中我们将讨论如何设计并实现W5500以太网控制器的驱动。 1、功能概述 W5500是WIZnet开发的…

外设驱动库开发笔记29:DS17887实时时钟驱动

一些时候&#xff0c;在我们的嵌入式产品中需要记录时间&#xff0c;所以我们就需要获取时钟&#xff0c;当然实现的方式多种多样&#xff0c;有的MCU本身就有这一功能&#xff0c;不过精度较低。当我们的应用要求较高时就需要使用专门的实时时钟芯片&#xff0c;如DS17887。在…