自从开源了我们自己开发的Modbus协议栈之后,有很多朋友建议我针对性的做几个示例。所以我们就基于平时我们的应用整理了几个简单但可以说明基本的应用方法的示例,这一篇中我们将使用协议栈实现一个Modbus RTU从站应用。
Modbus协议是一个主从协议,那肯定就有主站和从站之分。所谓从站就是被动动响应通讯的对象,所以从站总是响应通讯的一方。
对于RTU从站来说,它是数据的数据的生产者,从站通过响应主站数据请求的方式将数据发送给主站。这一过程如下图所示:
从上图我们不难看出,首先主站要主动发起数据请求,这也是它为什么被称之为主站的缘由。它首先告诉从站我需要哪些数据。然后从站按照主站的请求返回数据。主站得到响应后解析数据,这样就完成了主从站之间的一次数据通讯。所以主站就需要主动发起每一次数据通讯的对象。
我们已经了解的从站总是响应主站的数据请求来实现数据的传送。下面我们来看看使用协议栈如何实现一个从站。
我们知道从站是数据的生产者,对于Modbus协议来说有四类数据:线圈、状态、输入寄存器和保持寄存器。所以在从站中我们要为这四种数据定义相应的地址,以便主站能够对应的访问。所以设计一个从站我们先来设计它的数据地址,在我们的例子中我们规定如下:
我们规定了每类数据类型的数量为8,对于从站来说除了生成这些数据外,还需要根据主站的数据请求来返回相应的数据响应。在我们的协议栈中实现了0x01、0x02、0x03、0x04、0x05、0x06、0x0F以及0x10等功能码。也就是说主站对象会生成面向这些功能码的从站数据请求。从站收到请求后,解析请求并根据请求生成响应的数据响应。可以表示为下图所示:
从上图我们明白协议栈中已经实现了对收到的主站数据请求进行解析以及根据解析生成对应的响应的函数。我们使用协议栈时,主要需要做两个方面的事情:解析数据请求和生成数据响应。
在协议栈中定义了一个解析函数,该函数将收到的数据请求消息解析,并根据解析的结果生成返回的数据响应。该函数的原型如下:
uint16_t ParsingMasterAccessCommand(uint8_t *receivedMessage, uint8_t *respondBytes, uint16_t rxLength, uint8_t StationAddress)
这个函数有四个参数:uint8_t *receivedMessage是收到的数据请求消息; uint8_t *respondBytes是返回的数据响应消息,也是函数需要生成的;uint16_t rxLength是接收到的数据请求消息的长度;uint8_t StationAddress本站的地址。而函数的返回值则是生成的数据响应详细的长度。
在解析的过程中,该函数判断消息的完整性,并根据不同的功能码调用不同的回调函数来实现,包括设置本地数据和获取本地数据的相关回调函数,在后续将讨论它们的实现。
我们已经详述了使用协议栈实现RTU从站的方法,接下来我们就来利用协议栈具体开发一个RTU从站的实例。
我们调用解析函数对接收到的数据请求进行解析,具体调用方式如下所示:
respondLength=ParsingMasterAccessCommand(hgudRxBuffer,respondBytes,hgudRxLength,StationAddress);
返回值会有3种情况,返回值为0则表示接收到的数据请求消息是错误的。返回值为65535则表示返回的消息尚未接收完整。返回的是一个合适的数值则表示解析成功,返回了数据响应的长度。
当然我们需要实现8个回调函数,分别是获取线圈量、获取状态量、获取输入寄存器和获取保持寄存器,以及预置单个线圈量、预置多个线圈量、预置单个保持寄存器和预置多个保持寄存器。函数原型定义如下:
/*获取想要读取的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应用中实现具体内容
}
/*获取想要读取的保持寄存器的值*/
__weak void GetHoldingRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
{
//如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}
/*获取想要读取的输入寄存器的值*/
__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应用中实现具体内容
}
/*设置单个寄存器的值*/
__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应用中实现具体内容
}
/*设置多个寄存器的值*/
__weak void SetMultipleRegister(uint16_t startAddress,uint16_t quantity,uint16_t *registerValue)
{
//如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}
我们需要做的工作就是根据我们具体实例中4类数据量的地址分配来实现这8个回调函数。当然,如果从站没有某一类数据量操作,回调函数则不需要编写。在我们的实例中我们将这几个函数实现如下:
/*获取想要读取的Coil量的值*/
void GetCoilStatus(uint16_t startAddress,uint16_t quantity,bool *statusList)
{
uint16_t start;
uint16_t count;
/*先判断地址是否处于合法范围*/
start=(startAddress>CoilStartAddress)?((startAddress<=CoilEndAddress)?startAddress:CoilEndAddress):CoilStartAddress;
count=((start+quantity-1)<=CoilEndAddress)?quantity:(CoilEndAddress-start);
for(int i=0;iHoldingResterStartAddress)?((startAddress<=HoldingResterEndAddress)?startAddress:HoldingResterEndAddress):HoldingResterStartAddress;
count=((start+quantity-1)<=HoldingResterEndAddress)?quantity:(HoldingResterEndAddress-start);
for(int i=0;i
到这里对从站的开发实际已经完成。对于这些回调函数并不是全部需要编写,而是要根据我们自己定义的从站各类参数的地址分配来实现。
我们实现了一个简单的RTU从站实例,我们可以通过一些RTU主站软件来测试它。这样的软件有很多,常见的如Modscan、Modbus Poll等。这里我们使用Modbus Poll来测试一下,如下图所示:
RTU从站的实现相对较简单,因为在同一台设备上只需实现一个从站,哪怕是通过不同的端口来访问。这一点与主站是不一样的,原因是从站的数据是自己产生,而且只需被动响应主站请求,而且理论上同一条总线只会有一个主站。
接下来我们来总结一下使用协议栈实现RTU从站的工作流程,或者说实现的步骤。首先从站要解析从主站送来的数据请求。在协议栈中已经封装了数据请求的解析函数、所以我们实现从站时首先就是调用这一函数来解析接收到的数据请求消息。
然后将解析函数返回的数据响应消息发送到主站就可以了。也就是说使用协议栈,只需要调用一下这个函数从站功能就实现了。这是因为这个函数实现了整个从站的响应过程,大致分三个步骤:第一步,解析收到的主站数据请求消息;第二步,根据解析的结果预置数据或者获取数据,预置和获取数据由8个回调函数实现;第三步,生成从站数据响应消息。说到这里我们已经清楚,RTU从站必须实现这些回调函数,其它工作则全由协议栈完成。