0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

使用协议栈实现Modbus ASCII从站应用

CHANBAEK 来源:木南创智 作者:尹家军 2022-12-13 17:12 次阅读

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

1 、何为ASCII从站

我们知道Modbus协议是一个主从协议,所以就存在主站和从站之分。所谓ASCII从站,简单来说就是被动响应主站请求的站点,所以我们可以说ASCII从站就是响应通讯的一方。

对于ASCII从站来说,它会生成数据,但他不会主动向外发送数据,只有当收到主站的数据请求后,从站才会根据主站的请求发送数据。这一过程如下图所示:

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

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

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

2 、如何实现ASCII从站

我们已经了解的从站总是响应主站的数据请求来实现数据的传送。下面我们来看看使用协议栈如何实现一个从站。

我们知道从站是数据的生产者,对于Modbus协议来说有四类数据:线圈、状态、输入寄存器和保持寄存器。所以在从站中我们要为这四种数据定义相应的地址,以便主站能够对应的访问。所以设计一个从站我们先来设计它的数据地址,在我们的例子中我们规定如下:

我们规定了每类数据类型的数量为8,对于从站来说除了生成这些数据外,还需要根据主站的数据请求来返回相应的数据响应。在我们的协议栈中实现了0x01、0x02、0x03、0x04、0x05、0x06、0x0F以及0x10等功能码。也就是说主站对象会生成面向这些功能码的从站数据请求。从站收到请求后,解析请求并根据请求生成响应的数据响应。可以表示为下图所示:

从上图我们明白协议栈中已经实现了对收到的主站数据请求进行解析以及根据解析生成对应的响应的函数。我们使用协议栈时,主要需要做两个方面的事情:解析数据请求和生成数据响应。

在协议栈中定义了一个解析函数,该函数将收到的数据请求消息解析,并根据解析的结果生成返回的数据响应。该函数的原型如下:

uint16_t ParsingAsciiMasterAccessCommand(uint8_t *receivedMessage,uint8_t *respondBytes, uint16_t rxLength, uint8_t StationAddress)

这个函数有四个参数:uint8_t receivedMessage是收到的数据请求消息; uint8_trespondBytes是返回的数据响应消息,也是函数需要生成的;uint16_t rxLength是接收到的数据请求消息的长度;uint8_t StationAddress本站的地址。而函数的返回值则是生成的数据响应详细的长度。

在解析的过程中,该函数判断消息的完整性,并根据不同的功能码调用不同的回调函数来实现,包括设置本地数据和获取本地数据的相关回调函数,在后续将讨论它们的实现。

3 ASCII****从站编码

我们已经描述了使用协议栈实现Modbus ASCII从站的方法和流程,接下来我们就来利用协议栈具体实现一个Modbus ASCII从站的实例。

我们调用解析函数对接收到的数据请求进行解析,具体调用方式如下所示:

respondLength=ParsingAsciiMasterAccessCommand(asciiSlaveRxBuffer,respondBytes, asciiSlaveRxLength,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_tquantity,uint16_t *registerValue)
{
  //如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}
 
/*获取想要读取的输入寄存器的值*/
__weak void GetInputRegister(uint16_t startAddress,uint16_tquantity,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_tregisterValue)
{
  //如果需要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_tquantity,uint16_t *registerValue)
{
  //如果需要Modbus TCP Server/RTU Slave应用中实现具体内容
}

我们需要做的工作就是根据我们具体实例中4类数据量的地址分配来实现这8个回调函数。当然,如果从站没有某一类数据量操作,回调函数则不需要编写。在我们的实例中我们将这几个函数实现如下:

/*获取想要读取的Coil量的值*/
void GetCoilStatus(uint16_t startAddress,uint16_tquantity,bool *statusList)
{
  uint16_tstart;
  uint16_tcount;
  /*先判断地址是否处于合法范围*/
 start=(startAddress>CoilStartAddress)?((startAddress<=CoilEndAddress)?startAddress:CoilEndAddress):CoilStartAddress;
 count=((start+quantity-1)<=CoilEndAddress)?quantity:(CoilEndAddress-start);
 
  for(inti=0;i/*获取想要读取的保持寄存器的值*/
void GetHoldingRegister(uint16_tstartAddress,uint16_t quantity,uint16_t *registerValue)
{
  uint16_tstart;
  uint16_tcount;
  /*先判断地址是否处于合法范围*/
 start=(startAddress>HoldingResterStartAddress)?((startAddress<=HoldingResterEndAddress)?startAddress:HoldingResterEndAddress):HoldingResterStartAddress;
 count=((start+quantity-1)<=HoldingResterEndAddress)?quantity:(HoldingResterEndAddress-start);
 
  for(inti=0;i/*设置单个线圈的值*/
void SetSingleCoil(uint16_t coilAddress,boolcoilValue)
{
  /*先判断地址是否处于合法范围*/
 if((4<=coilAddress)&&(coilAddress<=CoilEndAddress))
  {
   dPara.coil[coilAddress]=coilValue;
  }
 
 PresetSlaveCoilControll(coilAddress,coilAddress);
}
/*设置多个线圈的值*/
void SetMultipleCoil(uint16_tstartAddress,uint16_t quantity,bool *statusValue)
{
  uint16_tendAddress=startAddress+quantity-1;
 if((4<=startAddress)&&(startAddress<=CoilEndAddress)&&(4<=endAddress)&&(endAddress<=CoilEndAddress))
  {
    for(inti=0;iPresetSlaveCoilControll(startAddress,endAddress);
}
 
/*设置单个寄存器的值*/
void SetSingleRegister(uint16_tregisterAddress,uint16_t registerValue)
{
  boolnoError=(bool)(((41<=registerAddress)&&(registerAddress<=42))
                     ||((44<=registerAddress)&&(registerAddress<=45))
                     ||((50<=registerAddress)&&(registerAddress<=51))
                     ||((54<=registerAddress)&&(registerAddress<=55))
                     ||((58<=registerAddress)&&(registerAddress<=59)));
 if(noError)
  {
   aPara.holdingRegister[registerAddress]=registerValue;
  }
 
 WriteSlaveRegisterControll(registerAddress,registerAddress);
}
 
/*设置多个寄存器的值*/
void SetMultipleRegister(uint16_tstartAddress,uint16_t quantity,uint16_t *registerValue)
{
  uint16_tendAddress=startAddress+quantity-1;
 
  boolnoError=(bool)(((8<=startAddress)&&(startAddress<=15)&&(8<=endAddress)&&(endAddress<=15))
                     ||((41<=startAddress)&&(startAddress<=42)&&(41<=endAddress)&&(endAddress<=42))
                     ||((44<=startAddress)&&(startAddress<=47)&&(44<=endAddress)&&(endAddress<=47))
                     ||((50<=startAddress)&&(startAddress<=51)&&(50<=endAddress)&&(endAddress<=51))
                     ||((54<=startAddress)&&(startAddress<=55)&&(54<=endAddress)&&(endAddress<=55))
                     ||((58<=startAddress)&&(startAddress<=59)&&(58<=endAddress)&&(endAddress<=59))
                     ||((62<=startAddress)&&(startAddress<=67)&&(62<=endAddress)&&(endAddress<=67))
                     ||((72<=startAddress)&&(startAddress<=77)&&(72<=endAddress)&&(endAddress<=77))
                     ||((82<=startAddress)&&(startAddress<=87)&&(82<=endAddress)&&(endAddress<=87))
                     ||((92<=startAddress)&&(startAddress<=97)&&(92<=endAddress)&&(endAddress<=97))
                     ||((100<=startAddress)&&(startAddress<=115)&&(100<=endAddress)&&(endAddress<=115)));
 if(noError)
  {
    for(inti=0;iWriteSlaveRegisterControll(startAddress,endAddress);
}

到这里对从站的开发实际已经完成。对于这些回调函数并不是全部需要编写,而是要根据我们自己定义的从站各类参数的地址分配来实现。

4 ASCII****从站小结

我们使用协议栈实现了一个简单的Modbus ASCII从站应用。我们可以使用Modscan、Modbus poll以及各类串口通讯工具对其进行测试。在使用Modbus poll时将其数据格式设为ASCII即可。测试结果如下:

与Modbus RTU从站类似,Modbus ASCII从站的实现也较为简单,因为在同一台设备上只需实现一个从站,哪怕是通过不同的端口来访问。这一点与主站是不一样的,原因是从站的数据是自己产生,而且只需被动响应主站请求,而且理论上同一条总线只会有一个主站。

接下来我们来总结一下使用协议栈实现RTU从站的工作流程,或者说实现的步骤。首先从站要解析从主站送来的数据请求。在协议栈中已经封装了数据请求的解析函数、所以我们实现从站时首先就是调用这一函数来解析接收到的数据请求消息。

然后将解析函数返回的数据响应消息发送到主站就可以了。也就是说使用协议栈,只需要调用一下这个函数从站功能就实现了。这是因为这个函数实现了整个从站的响应过程,大致分三个步骤:第一步,解析收到的主站数据请求消息;第二步,根据解析的结果预置数据或者获取数据,预置和获取数据由8个回调函数实现;第三步,生成从站数据响应消息。说到这里我们已经清楚,RTU从站必须实现这些回调函数,其它工作则全由协议栈完成。

协议栈下载 https://github.com/foxclever/Modbus

示例下载: https://download.csdn.net/download/foxclever/12882021

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • MODBUS
    +关注

    关注

    27

    文章

    1440

    浏览量

    75693
  • ASCII
    +关注

    关注

    4

    文章

    169

    浏览量

    34531
  • 协议栈
    +关注

    关注

    2

    文章

    129

    浏览量

    33461
收藏 人收藏

    评论

    相关推荐

    Modbus ASCII的设计与实现

    前面我们已经分析了Modbus RTU的更新设计和具体实现(如果不清楚可查看前一篇文章)。其实Modbus ASCIIModbus RTU
    的头像 发表于 12-13 15:30 1000次阅读
    <b class='flag-5'>Modbus</b> <b class='flag-5'>ASCII</b>的设计与<b class='flag-5'>实现</b>

    Modbus协议转换芯片

    OD2122接口芯片OD2122是一款Modbus协议转换芯片,支持基于RS485、RS232C总线的Modbus(RTU)
    发表于 12-26 09:43

    linux平台实现modbus主机协议的动态库libMbpoll

    libMbopll动态库是面向linux平台设计的modbus主机协议,可以运行在x86平台以及各种嵌入式linux平台;协议提供了简单
    发表于 05-28 14:23

    linux平台实现modbus主机协议的动态库libMbpoll

    libMbopll动态库是面向linux平台设计的modbus主机协议,可以运行在x86平台以及各种嵌入式linux平台;协议提供了简单
    发表于 05-28 14:55

    Modbus库开发笔记之十一:关于Modbus协议开发的说明

    Modbus协议在串行链路上的实现指南》GB/T 19582.3-2008 《基于Modbus协议的工业自动化网络规范 第1部分:Modbus协议在TCP/IP上的
    发表于 08-27 20:32

    Modbus-RTU协议包括哪些呢

    Modbus-RTUModbus 一个工业上常用的通讯协议、一种通讯约定。Modbus协议包括RTU、ASCII、TCP。其中MODBUS-
    发表于 02-16 07:43

    Modbus on AT32 MCU

    Modbus机节点的协议。该协议使用ANSI C编写,并且支持多个变量。本应用指南将介绍如
    发表于 07-26 20:22

    基于RT-Thread实现的Agile Modbus协议

    1、Agile Modbus介绍  Agile Modbus 即:轻量型 modbus 协议,满足用户任何场景下的使用需求。  examp
    发表于 10-08 15:04

    基于Modbus RTU协议实现的1主多自组网无线通信形式

      本方案是基于Modbus RTU协议实现的1主多自组网无线通信形式,主为S7-1200 PLC,DTD433F作为
    发表于 03-10 14:54

    Modbus通讯协议的几种实现方式

    版权要求,不用支付额外费用、硬件要求简单容易部署、使用广泛便于系统集成。Modbus采用半双工的通讯方式,由1个子和多个组成,允许多个设备连接在同一个网络上进行通讯。   2.
    发表于 05-05 16:19

    Modbus协议的理解

    和使用的消息结构,而不管它们是经过何种网络进行通信的。标准的Modicon控制器使用RS232C实现串行的ModbusModbusASCII和RTU
    发表于 05-05 16:47

    Modbus转Profinet网关连接EthernetIP协议的PLC和Modbus协议的仪表

    本案例是客户现场有多个气体探测仪,但是无法直接接入罗克韦尔系统中,因为气体探测仪是标准的ModbusRTU协议,需要配置的数据要通过Modbus转EtherNET/IP网关来实现和PLC
    发表于 12-07 14:27

    PCS7下ASCII模式Modbus Master的实现

    PCS7下ASCII模式Modbus Master的实现说明。
    发表于 04-23 09:38 8次下载

    使用协议实现Modbus ASCII主站应用

    自从开源了我们自己开发的Modbus协议栈之后,有很多朋友建议我针对性的做几个示例。所以我们就基于平时我们的应用整理了几个简单但可以说明基本的应用方法的示例,这一篇中我们来使用协议实现Mod
    的头像 发表于 12-13 17:09 662次阅读
    使用<b class='flag-5'>协议</b>栈<b class='flag-5'>实现</b><b class='flag-5'>Modbus</b> <b class='flag-5'>ASCII</b>主站应用

    MODBUS ASCII传输模式介绍

    前面2期我们学习了,第一篇MODBUS协议基本介绍,第二篇MODBUS RTU传输模式介绍,今天我们来研究第三篇MODBUS ASCII传输模式介绍。
    的头像 发表于 07-19 15:58 2195次阅读
    <b class='flag-5'>MODBUS</b> <b class='flag-5'>ASCII</b>传输模式介绍