Modbus协议栈实现Modbus RTU多主站支持

时间:2023-12-30 21:10:20

  前面我们已经详细讲解过Modbus协议栈的开发过程,并且利用协议栈封装了Modbus RTU主站和从站,Modbus TCP服务器与客户端,Modbus ASCII主站与从站应用。但在使用过程中,我们发现一些使用不便和受限的地方,所以我们就想要更新一下协议栈,主要是应用站的封装。

1、存在的局限性

  在原有的协议栈中,我们所封装的Modbus RTU主站是一个特定的主站,即它只是一个主站。在通常的应用中不会有什么问题,但在有些应用场合就会显现出它的局限性。

  首先,作为一个特定的主站,带多个从站时,写从站的处理变的非常复杂,需要分辨不同的从站,不同的变量。当有多个端口时,还需要分辨不同的端口。

  其次,作为一个特定的主站,带多个从站时,读从站的处理,即使是不同的端口也不能分辨相同站地址的从站。因为同一主站的解析函数是同一个,所以即使在不同端口也很难分辨,除非在解析前传递端口信息,这其实是将多余的信息传递到协议栈。这样做不但程序不够明晰也缺乏一般性。

  最后,将所有的Modbus从站通讯都作为唯一的一个特定从站来处理,使得各部分混杂在一起,程序结构很不清晰,对象也不明确。

2、更新设计

  考虑到前述的局限性,我们将主站及其带访问的从站定义为通用的对象,而当我们在具体应用中使用时,再将其特例化为特定的主站和从站对象。

  首先我们来考虑主站,原则上我们规划的每一个主站对象对应我们设备上的一个端口,那么在同一端口下,也就是在一个特定主站下,我们可以定义多个地址不同的从站,但不同端口下不受影响。如下图所示:

Modbus协议栈实现Modbus RTU多主站支持

  从上图中我们可以发现,我们的目的就是让协议栈支持,多主站和多从站,并且在不同主站下,从站的地址重复不受影响。接下来我们还需要考虑从站对象。主站对从站的操作无非两类:读从站信息和写从站信息。

  对于读从站信息来说,主站需要发送请求命令,等待从站返回响应信息,然后主站解析收到的信息并更新对应的参数值。有两点需要我们考虑,第一返回的响应消息是没有对应的寄存器地址的,所以要想在解析的时候定位寄存器就必须知道发送的命令,为了便于分辨我们将命令存放在从站对象中。第二在解析响应时,如果两条命令的响应类似是没法分辨的,所以我们还需要记住上一条命令是什么。也存储于从站对象中。

  而对于写从站操作,无论写的要求来自于哪里,对于协议栈来说肯定是其它的数据处理进程发过来的,所接到要求后我们需要记录是哪一个主站管理的哪一个从站的哪些参数。对于主站我们不需要分辨,因为每个主站都是独立的处理进程,但是对于从站和参数我们就需要分辨。每一个主站可以带的站地址为0到255,但0和255已有定义,所以实际是1到254个。所以我们使用一个256位的变量,每位对应站号来标志其是否有需要写的请求。记录于主站,具体如下:

Modbus协议栈实现Modbus RTU多主站支持

  而每个从站的写参数请求标志则存储于各个从站对象,因为不同的从站可能有很大区别,存储于各个从站更加灵活。

3、如何实现

  我们已经设计了我们的更新,但具体如何实现它呢?我们主要从以下几个方面来实现它。第一,实现主站对象类型和从站对象类型。第二,主站对象的实例化及从站对象的实例化。第三,读从站的主站操作过程。第四,写从站的主站操作过程。接下来我们将一一描述之。

3.1、定义对象类型

  在Modbus RTU协议栈的封装中,我们需要定义主站对象和从站对象,自然也需要定义这两种类型。至于其功能前述已经描述过。

  首先我们来定义本地主站的类型,其成员包括:一个uint32_t的写从站标志数组;从站数量字段;从站顺序字段;本主站所管理的从站列表;4个数据更新函数指针。具体定义如下:

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

  关于主站对象类型,在前面的更新设计中已经讲的很清楚了,只有两个需要说明一下。第一,从站列表是用来记录本主站所管理的从站对象。第二,readOrder字段表示为当前访问从站在列表中的位置,而slaveNumber是从站对象的数量,即列表的长度。具体如下图所示:

Modbus协议栈实现Modbus RTU多主站支持

  还需要定义从站对象,此从站对象只是便于主站而用于表示真实的从站。主站的从站列表中就是此对象。具体结构如下:

 /* 定义被访问RTU从站对象类型 */
typedef struct AccessedRTUSlaveType{
uint8_t stationAddress; //站地址
uint8_t cmdOrder; //当前命令在命令列表中的位置
uint16_t commandNumber; //命令列表中命令的总数
uint8_t (*pReadCommand)[]; //读命令列表
uint8_t *pLastCommand; //上一次发送的命令
uint32_t flagPresetCoil; //预置线圈控制标志位
uint32_t flagPresetReg; //预置寄存器控制标志位
}RTUAccessedSlaveType;

  关于从站对象有两个字段需要说一下,就是flagPresetCoil和flagPresetReg字段。这两个字段用来表示对线圈和保持寄存器的写请求。

3.2、实例化对象

  我们定义了主站即从站对象类型,我们在使用时就需要实例化这些对象。一般来说一个硬件端口我们将其实例化为一个主站对象。

  RTULocalMasterType hgraMaster;

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

  InitializeRTUMasterObject(&hgraMaster,2,hgraSlave,NULL,NULL,NULL,NULL);

  而一个主站对象会管理1到254个从站对象,所以我们可以将多个从站对象实例组成数组,并将其赋予主站管理。

  RTUAccessedSlaveType hgraSlave[]={{1,0,2,slave1ReadCommand,NULL,0x00,0x00},{2,0,2,slave2ReadCommand,NULL,0x00,0x00}};

  所以,根据主站和从站实例化的条件,我们需要先实例化从站对象才能完整实例化主站对象。在主站的初始化中,我们这里将4的数据处理函数指针初始化为NULL,有一个默认的处理函数会复制给它,该函数是上一版本的延续,在简单应用时简化操作。从站的上一个发送的命令指针也被赋值为NULL,因为初始时还没有命令发送。

3.3、读从站操作

  读从站操作原理上与以前的版本是一样的。按照一定的顺序给从站发送命令再对收到的消息进行解析。我们对主站及其所管理的从站进行了定义,将发送命令保存于从站对象,将从站列表保存于主站对象,所以我们需要对解析函数进行修改。

 /*解析收到的服务器相应信息*/
/*uint8_t *recievedMessage,接收到的消息列表*/
/*uint8_t *command,发送的读操作命令,若为NULL则在命令列表中查找*/
void ParsingSlaveRespondMessage(RTULocalMasterType *master,uint8_t *recievedMessage,uint8_t *command)
{
int i=;
int j=;
uint16_t startAddress;
uint16_t quantity;
uint8_t *cmd=NULL; /*如果不是读操作的反回信息不需要处理*/
if(recievedMessage[]>0x04)
{
return;
} /*判断功能码是否有误*/
FunctionCode fuctionCode=(FunctionCode)recievedMessage[];
if (CheckFunctionCode(fuctionCode) != MB_OK)
{
return;
} /*校验接收到的信息是否有错*/
uint16_t byteCount=recievedMessage[];
bool chechMessageNoError=CheckRTUMessageIntegrity(recievedMessage,byteCount+);
if(!chechMessageNoError)
{
return;
} if((command==NULL)||(!CheckMessageAgreeWithCommand(recievedMessage,command)))
{
while(i<master->slaveNumber)
{
if(master->pSlave[i].stationAddress==recievedMessage[])
{
break;
}
i++;
} if(i>=master->slaveNumber)
{
return;
} if((master->pSlave[i].pLastCommand==NULL)||(!CheckMessageAgreeWithCommand(recievedMessage,master->pSlave[i].pLastCommand)))
{
j=FindCommandForRecievedMessage(recievedMessage,master->pSlave[i].pReadCommand,master->pSlave[i].commandNumber); if(j<)
{
return;
} cmd=master->pSlave[i].pReadCommand[j];
}
else
{
cmd=master->pSlave[i].pLastCommand;
}
}
else
{
cmd=command;
} startAddress=(uint16_t)cmd[];
startAddress=(startAddress<<)+(uint16_t)cmd[];
quantity=(uint16_t)cmd[];
quantity=(quantity<<)+(uint16_t)cmd[]; if((fuctionCode>=ReadCoilStatus)&&(fuctionCode<=ReadInputRegister))
{
HandleSlaveRespond[fuctionCode-](master,recievedMessage,startAddress,quantity);
}
}

  解析函数的主要部分是在检查接收到的消息是否是合法的Modbus RTU消息。检查没问题则调用协议站解析。而最后调用的数据处理函数则是我们需要在具体应用中编写。在前面主站初始化时,回调函数我们初始化为NULL,实际在协议栈中有弱化的函数定义,需要针对具体的寄存器和变量地址实现操作。

3.4、写从站操作

  写从站操作则是在其它进程请求后,我们标识需要写的对象再统一处理。对具体哪个从站的写标识存于主站实例。而该从站的哪些变量需要写则记录在从站实例中。

  所以在进程检测到需要写一个从站时则置位对应的位,即改变flagWriteSlave中的对应位。而需要写该站的哪些变量则标记flagPresetCoil和flagPresetReg的对应位。修改这些标识都在其它请求更改的进程中实现,而具体的写操作则在本主站进程中,检测到标志位的变化统一执行。

  这部分不修改协议栈的代码,因为各站及各变量都至于具体对象相关联,所以在具体的应用中修改。

4、回归验证

  为了验证我们前面的更新设计是符合要求的,我们设计一个难度较高的实验系统。这一实验系统包括Modbus网关,上位Modbus主站以及下位的Modbus从站。我们所要实现的是Modbus网关部分,其具体结构图设计如下:

Modbus协议栈实现Modbus RTU多主站支持

  从上图我们知道,该Modbus网关需要实现一个Modbus从站用于和上位的通讯;需要实现两个Modbus主站用于和下位的通讯。

  在这个实验中,读操作没有什么需要说的,只需要发送命令,解析返回消息即可。所以我们重点描述一下写操作,为了方便操作,在需要写的连续段,我们只要找到第一个请求写的位置后,就将后续连续可写数据一次性写入。修改写标志位的代码如下:

 /* 写从站寄存器控制 */
static void WriteSlaveRegisterControll(uint16_t startAddress,uint16_t endAddress)
{
if((<=startAddress)&&(startAddress<=)&&(<=endAddress)&&(endAddress<=))
{
ModifyWriteRTUSlaveEnableFlag(&hgraMaster,hgraMaster.pSlave[].stationAddress,true); if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x01;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x02;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x04;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x08;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x10;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x20;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x40;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x80;
} if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x100;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x200;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x400;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x800;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x1000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x2000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x4000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x8000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x10000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x20000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x40000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x80000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x100000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x200000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x400000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x800000;
} if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x1000000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x2000000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x4000000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x8000000;
}
} if((<=startAddress)&&(startAddress<=)&&(<=endAddress)&&(endAddress<=))
{
ModifyWriteRTUSlaveEnableFlag(&hgraMaster,hgraMaster.pSlave[].stationAddress,true); if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x01;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x02;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x04;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x08;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x10;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x20;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x40;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x80;
} if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x100;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x200;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x400;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x800;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x1000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x2000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x4000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x8000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x10000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x20000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x40000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x80000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x100000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x200000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x400000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x800000;
} if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x1000000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x2000000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x4000000;
}
if((startAddress<=)&&(<=endAddress))
{
hgraMaster.pSlave[].flagPresetReg|=0x8000000;
}
} if((<=startAddress)&&(startAddress<=)&&(<=endAddress)&&(endAddress<=))
{
ModifyWriteRTUSlaveEnableFlag(&hgpjMaster,hgpjMaster.pSlave[].stationAddress,true);
} if((<=startAddress)&&(startAddress<=)&&(<=endAddress)&&(endAddress<=))
{
ModifyWriteRTUSlaveEnableFlag(&hgpjMaster,hgpjMaster.pSlave[].stationAddress,true);
}
}

  然后在主站对象的进程中检测标志位,根据标志位的状态来实现操作,具体的操作代码很简单,且不具普遍性,在此不贴出。

5、几点注意

  虽然我们对主站对象和从站对象进行了封装,但我们在使用时人需要注意一些问题。

  (1)、4个回调函数的定义,这4个回调函数用于处理从粘返回的信息,对应Modbus定义的四种数据,需要根据主站对象管理的从站情况来实现。

  (2)、对于写操作标识符,一般都是在请求进程置位,在主站对象所在的进程检测并操作,然后复位。

告之:源代码可上Github下载:https://github.com/foxclever/Modbus

欢迎关注:

Modbus协议栈实现Modbus RTU多主站支持