1、更新设计
关于原来的协议栈在Modbus ASCII主站应用时所存在的局限性与Modbus RTU也是一样的,所以我们不分析它的不足,只讨论更新设计。我们将主站及其所访问的从站定义为通用的对象,而当我们在具体应用中使用时,再将其特例化为特定的主站和从站对象。

首先我们来考虑主站,原则上我们规划的每一个主站对象对应我们设备上的一个端口,这里所说端口就是指串口。那么在同一端口下,也就是在一个特定主站下,我们可以定义多个地址不同的从站。而在不同的端口上可以具有地址相同的从站。如下图所示:

从上图中我们可以发现,我们的目的就是让协议栈支持,多主站和多从站,并且在不同主站下,从站的地址重复不受影响。从上图看视乎一个主站对象可以同时管理254个从站对象,事实上还要受到带载能力的影响。

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

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

事实上,我们不可能会用到256个标志位,因为Modbus ASCII本身就是为简单应用而设定的。我们使用256个标志位,主要是考虑到站地址的取值范围,方便软件操作而定的。还有每个从站的写参数请求标志,我们将其存储于各个从站对象,因为不同的从站可能有很大区别,存储于各个从站更加灵活方便。

2、编码实现
我们已经设计了我们的更新,接下来我们就根据这一设计来实现它。我们主要从以下几个方面来操作:第一,实现主站对象类型和从站对象类型;第二,主站对象的实例化及从站对象的实例化;第三,读从站的主站操作过程;第四,写从站的主站操作过程。接下来我们将一一描述之。

2.1、定义对象类型
与在Modbus RTU一样,在Modbus ASCII协议栈的封装中,我们也需要定义主站对象和从站对象,自然也免不了要定义这两种类型。

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

/* 定义本地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;

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

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

/* 定义被访问ASCII从站对象类型 */

typedef struct AccessedASCIISlaveType{

uint8_t stationAddress;       //站地址

uint8_t cmdOrder;             //当前命令在命令列表中的位置

uint16_t commandNumber;       //命令列表中命令的总数

uint8_t (*pReadCommand)[17];   //读命令列表

uint8_t *pLastCommand;        //上一次发送的命令

uint32_t flagPresetCoil;      //预置线圈控制标志位

uint32_t flagPresetReg;       //预置寄存器控制标志位

}ASCIIAccessedSlaveType;

关于从站对象有三个字段需要说明一下。首先我们来看一看“读命令列表(uint8_t (*pReadCommand)[17])”字段,与Modbus RTU不同,它是17个字节,这是由Modbus ASCII消息格式决定的。如下:

还有就是flagPresetCoil和flagPresetReg字段。这两个字段用来表示对线圈和保持寄存器的写请求。

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

ASCIILocalMasterType hgraMaster;

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

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

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

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

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

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

/*解析收到的服务器相应信息*/

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

{

int i=0;

int j=0;

uint8_t *cmd=NULL;

/*判断是否为Modbus ASCII消息*/

if (0x3A != recievedMessage[0])

{

return ;

}

/*判断消息是否接收完整*/

if ((rxLength < 17) || (recievedMessage[rxLength - 2] != 0x0D) || (recievedMessage[rxLength - 1] != 0x0A))

{

return ;

}

uint16_t length = rxLength - 3;

uint8_t hexMessage[256];

if (!CovertAsciiMessageToHex(recievedMessage + 1, hexMessage, length))

{

return ;

}

/*校验接收到的数据是否正确*/

if (!CheckASCIIMessageIntegrity(hexMessage, length/2))

{

return ;

}

/*判断功能码是否有误*/

FunctionCode fuctionCode = (FunctionCode)hexMessage[1];

if (CheckFunctionCode(fuctionCode) != MB_OK)

{

return;

}

if ((command == NULL)||(!CheckMessageAgreeWithCommand(recievedMessage, command)))

{

while(i<master->slaveNumber)

{

if(master->pSlave[i].stationAddress==hexMessage[0])

{

break;

}

i++;

}

if(i>=master->slaveNumber)

{

return;

}

if((master->pSlave[i].pLastCommand==NULL)||(!CheckMessageAgreeWithCommand(recievedMessage,master->pSlave[i].pLastCommand)))

{

j=FindAsciiCommandForRecievedMessage(recievedMessage,master->pSlave[i].pReadCommand,master->pSlave[i].commandNumber);

if(j<0)

{

return;

}

cmd=master->pSlave[i].pReadCommand[j];

}

else

{

cmd=master->pSlave[i].pLastCommand;

}

}

else

{

cmd=command;

}

uint8_t hexCommand[256];

CovertAsciiMessageToHex(cmd + 1, hexCommand, 14);

uint16_t startAddress = (uint16_t)hexCommand[2];

startAddress = (startAddress << 8) + (uint16_t)hexCommand[3];

uint16_t quantity = (uint16_t)hexCommand[4];

quantity = (quantity << 8) + (uint16_t)hexCommand[5];

if ((fuctionCode >= ReadCoilStatus) && (fuctionCode <= ReadInputRegister))

{

HandleAsciiSlaveRespond[fuctionCode - 1](master,hexMessage,startAddress,quantity);

}

}

解析函数的主要部分是在检查接收到的消息是否是合法的Modbus ASCII消息。检查没问题则调用协议站解析。而最后调用的数据处理函数则是我们需要在具体应用中编写。在前面主站初始化时,回调函数我们初始化为NULL,实际在协议占中有弱化的函数定义,需要针对具体的寄存器和变量地址实现操作。特别要说明的是,解析Modbus ASCII消息时,在去除开始字符和结束字符后,需要将ASCII码转化为二进制数才能完成解析。

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

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

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

3、回归验证
考虑到Modbus ASCII和Modbus RTU的相似性,我们设计同样的的网络结构。但考虑到Modbus ASCII一般用于小数据量通讯,所以我们设计相对简单的从站。所以我们设计的网络为:协议栈建立2个主机,每个主机管理2个从站,每个从站有8个线圈及2个保持寄存器。具体结构如图:

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

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

/* 修改从站线圈量使能控制 */

static void PresetSlaveCoilControll(uint16_t startAddress,uint16_t endAddress)

{

if((8<=startAddress)&&(startAddress<=15)&&(8<=endAddress)&&(endAddress<=15))

{

ModifyWriteRTUSlaveEnableFlag(&hgraMaster,hgraMaster.pSlave[0].stationAddress,true);

if((startAddress<=8)&&(8<=endAddress))

{

hgraMaster.pSlave[0].flagPresetCoil|=0x01;

}

if((startAddress<=9)&&(9<=endAddress))

{

hgraMaster.pSlave[0].flagPresetCoil|=0x02;

}

if((startAddress<=10)&&(10<=endAddress))

{

hgraMaster.pSlave[0].flagPresetCoil|=0x04;

}

if((startAddress<=11)&&(11<=endAddress))

{

hgraMaster.pSlave[0].flagPresetCoil|=0x08;

}

if((startAddress<=12)&&(12<=endAddress))

{

hgraMaster.pSlave[0].flagPresetCoil|=0x10;

}

if((startAddress<=13)&&(13<=endAddress))

{

hgraMaster.pSlave[0].flagPresetCoil|=0x20;

}

if((startAddress<=14)&&(14<=endAddress))

{

hgraMaster.pSlave[0].flagPresetCoil|=0x40;

}

if((startAddress<=15)&&(15<=endAddress))

{

hgraMaster.pSlave[0].flagPresetCoil|=0x80;

}

}

if((16<=startAddress)&&(startAddress<=23)&&(16<=endAddress)&&(endAddress<=23))

{

ModifyWriteRTUSlaveEnableFlag(&hgraMaster,hgraMaster.pSlave[1].stationAddress,true);

if((startAddress<=16)&&(16<=endAddress))

{

hgraMaster.pSlave[1].flagPresetCoil|=0x01;

}

if((startAddress<=17)&&(17<=endAddress))

{

hgraMaster.pSlave[1].flagPresetCoil|=0x02;

}

if((startAddress<=18)&&(18<=endAddress))

{

hgraMaster.pSlave[1].flagPresetCoil|=0x04;

}

if((startAddress<=19)&&(19<=endAddress))

{

hgraMaster.pSlave[1].flagPresetCoil|=0x08;

}

if((startAddress<=20)&&(20<=endAddress))

{

hgraMaster.pSlave[1].flagPresetCoil|=0x10;

}

if((startAddress<=21)&&(21<=endAddress))

{

hgraMaster.pSlave[1].flagPresetCoil|=0x20;

}

if((startAddress<=22)&&(22<=endAddress))

{

hgraMaster.pSlave[1].flagPresetCoil|=0x40;

}

if((startAddress<=23)&&(23<=endAddress))

{

hgraMaster.pSlave[1].flagPresetCoil|=0x80;

}

}

if((24<=startAddress)&&(startAddress<=31)&&(24<=endAddress)&&(endAddress<=31))

{

ModifyWriteRTUSlaveEnableFlag(&hgpjMaster,hgpjMaster.pSlave[0].stationAddress,true);

if((startAddress<=24)&&(24<=endAddress))

{

hgpjMaster.pSlave[0].flagPresetCoil|=0x01;

}

if((startAddress<=25)&&(25<=endAddress))

{

hgpjMaster.pSlave[0].flagPresetCoil|=0x02;

}

if((startAddress<=26)&&(26<=endAddress))

{

hgpjMaster.pSlave[0].flagPresetCoil|=0x04;

}

if((startAddress<=27)&&(27<=endAddress))

{

hgpjMaster.pSlave[0].flagPresetCoil|=0x08;

}

if((startAddress<=28)&&(28<=endAddress))

{

hgpjMaster.pSlave[0].flagPresetCoil|=0x10;

}

if((startAddress<=29)&&(29<=endAddress))

{

hgpjMaster.pSlave[0].flagPresetCoil|=0x20;

}

if((startAddress<=30)&&(30<=endAddress))

{

hgpjMaster.pSlave[0].flagPresetCoil|=0x40;

}

if((startAddress<=31)&&(31<=endAddress))

{

hgpjMaster.pSlave[0].flagPresetCoil|=0x80;

}

}

if((32<=startAddress)&&(startAddress<=39)&&(32<=endAddress)&&(endAddress<=39))

{

ModifyWriteRTUSlaveEnableFlag(&hgpjMaster,hgpjMaster.pSlave[1].stationAddress,true);

if((startAddress<=32)&&(32<=endAddress))

{

hgpjMaster.pSlave[1].flagPresetCoil|=0x01;

}

if((startAddress<=33)&&(33<=endAddress))

{

hgpjMaster.pSlave[1].flagPresetCoil|=0x02;

}

if((startAddress<=34)&&(34<=endAddress))

{

hgpjMaster.pSlave[1].flagPresetCoil|=0x04;

}

if((startAddress<=35)&&(35<=endAddress))

{

hgpjMaster.pSlave[1].flagPresetCoil|=0x08;

}

if((startAddress<=36)&&(36<=endAddress))

{

hgpjMaster.pSlave[1].flagPresetCoil|=0x10;

}

if((startAddress<=37)&&(37<=endAddress))

{

hgpjMaster.pSlave[1].flagPresetCoil|=0x20;

}

if((startAddress<=38)&&(38<=endAddress))

{

hgpjMaster.pSlave[1].flagPresetCoil|=0x40;

}

if((startAddress<=39)&&(39<=endAddress))

{

hgpjMaster.pSlave[1].flagPresetCoil|=0x80;

}

}

}

与Modbus RTU一样也是在请求修改进程中置位索要写的从站的写请求标志位和对应参数的写请求标志位。然后在主站对象的进程中检测标志位,根据标志位的状态来实现操作。
---------------------

实现Modbus ASCII多主站应用的更多相关文章

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

    前面我们已经详细讲解过Modbus协议栈的开发过程,并且利用协议栈封装了Modbus RTU主站和从站,Modbus TCP服务器与客户端,Modbus ASCII主站与从站应用.但在使用过程中,我们 ...

  2. Modbus库开发笔记:Modbus ASCII Master开发

    这一节我们来封装Modbus ASCII Master应用,Modbus ASCII主站的开发与RTU主站的开发是一致的.同样的我们也不是做具体的应用,而是实现ASCII主站的基本功能.我们将ASCI ...

  3. Modbus库开发笔记:Modbus ASCII Slave开发

    与Modbus RTU在串行链路上分为Slave和Master一样,Modbus ASCII也分为Slave和Master,这一节我们就来开发Slave.对于Modbus ASCII从站来说,需要实现 ...

  4. modbus ASCII和MODBUS RTU区别

    下表是MODBUS ASCII协议和RTU协议的比较: 协议 开始标记 结束标记 校验 传输效率 程序处理 ASCII :(冒号) CR,LF LRC 低 直观,简单,易调试 RTU 无 无 CRC ...

  5. MODBUS ASCII和RTU两种模式的区别、优缺点

    转自:http://blog.sina.com.cn/s/blog_89f286ad0102uzju.html 下表是MODBUS ASCII协议和RTU协议的比较: 协议 开始标记 结束标记 校验 ...

  6. STM32F407的Modbus做为主站与从站通讯

    在调试STM32F407的串口Modbus通讯之前,也使用过Modbus通讯,只不过都是在PLC或则昆仑通态的触摸屏上使用直接调用现成的库里面的模块,驱动就可以,相对于STM32来,使用PLC库里面的 ...

  7. Modbus 协议-Ascii,RTU

    Modbus 协议之 Ascii 下载地址:http://download.csdn.net/detail/woxpp/5043249 1.提供Modbus Ascii 相关发送与接收代码 2.提供M ...

  8. Modbus教程| Modbus协议,ASCII和RTU帧,Modbus工作

    转载自:https://www.rfwireless-world.com/Tutorials/Modbus-Protocol-tutorial.html 这个Modbus教程涵盖了modbus协议基础 ...

  9. 如何快速掌握plc或工控机与其他设备的modbus通讯协议?包括格式与实际过程 RT,本人从事工控行业多年,对于PLC与触摸屏也算比较熟悉,唯独对这个通讯协议比较难理解,请教高人指导,从什么地方开始下手,或者是说如何正确理解报文格式或正确写入

    Modbus协议是OSI模型的第七层的应用层通讯协议,定义了不同类型设备间交换信息方式,以及信息的格式. Modbus的工作方式是请求/应答,每次通讯都是主站先发送指令,可以是广播,或是向特定从站的单 ...

随机推荐

  1. 树屋阶梯(codevs 1741)

    题目描述 Description 暑假期间,小龙报名了一个模拟野外生存作战训练班来锻炼体魄,训练的第一个晚上,教官就给他们出了个难题.由于地上露营湿气重,必须选择在高处的树屋露营.小龙分配的树屋建立在 ...

  2. node.js 如何完美的从命令行接收参数所传递进来的值

    https://segmentfault.com/q/1010000000367285

  3. servlet和Spring的DispatcherServlet详解

    Servlet是什么 1. Servlet是服务器端运行的一个程序,是一个被编译好的Java类.它不是框架等. 2. Web容器的启动,需要依赖Servlet.当web服务器开始执行时,servlet ...

  4. PHP array_flip()

    定义和用法 array_flip() 函数返回一个反转后的数组,如果同一值出现了多次,则最后一个键名将作为它的值,所有其他的键名都将丢失. 如果原数组中的值的数据类型不是字符串或整数,函数将报错. 语 ...

  5. HDU--1054--Strategic Game【最小点覆盖】

    链接:pid=1054">http://acm.hdu.edu.cn/showproblem.php?pid=1054 题意:一个熊孩子玩策略游戏,他须要用最少的士兵守卫最多的道路.假 ...

  6. STM32的独立看门狗

    STM32 内 部自带了 2 个看门狗:独立看门狗(IWDG)和窗体看门狗(WWDG) STM32 的独立看门狗由内部专门的 40Khz 低速时钟驱动.即使主时钟发生问题.它也仍然 有效. 这里须要注 ...

  7. hdu5242 上海邀请赛 优先队列+贪心

    题意是给你一棵树    n个点 n-1条边   起点是1   每一个点都有权值 每次能从根节点走到叶子节点   经行k次游戏 每次都是从1開始    拿过的点的权值不能拿第二次   问最大权值和. 開 ...

  8. 多个结果显示成一个group_concat函数

    需求:获取班级.课程中文名.老师 扩展:一个班级一门课程,老师可能多个,想把多个教师显示成在一个结果里 解决方案:加个group by 参考资料:https://www.cnblogs.com/zhu ...

  9. 自己定义控件-仿iphone之ToggleButton&amp;VoiceSeekBar

    由于项目中须要使用开关切换button,和声音滑动控件,可是原生Android5.0版本号以下的控件实在是太挫了.尽管网上已经有非常多关于这两个控件的blog.可是我实在是找不到像iPhone这样简洁 ...

  10. Android实战简易教程-第四十一枪(显示倒计时的Button-适用于获取验证码)

    近期在做获取验证码的功能.考虑到优良的用户体验,决定制作一个拥有倒计时提示的Button按钮,在网上查了一些资料,非常是简单的就能实现.我写了一个小Demo,大家能够应用到自己的项目中. 一.代码 1 ...