单片机与plc吧 关注:10贴子:51
  • 6回复贴,共1

基于51单片机上实现modbus协议

取消只看楼主收藏回复


modbus,是一种通讯协议,单片机需要和modbus通讯协议的设备通讯时就得遵循此协议。
协议发送给询问方。 Modbus协议包括ASCIIRTUTCP等,并没有规定物理层。此协议定义了控制器能够认识和使用的消息结构,而不管它们是经过何种网络进行通信的。标准的Modicon控制器使用RS232C实现串行的Modbus
ModbusASCIIRTU协议规定了消息、数据的结构、命令和就答的方式,数据通讯采用Maser/Slave方式,Master端发出数据请求消息,Slave端接收到正确消息后就可以发送数据到Master端以响应请求;Master端也可以直接发消息修改Slave端的数据,实现双向读写。 Modbus协议需要对数据进行校验,串行协议中除有奇偶校验外,ASCII模式采用LRC校验,RTU模式采用16CRC校验,但TCP模式没有额外规定校验,因为TCP协议是一个面向连接的可靠协议。另外,Modbus采用主从方式定时收发数据,在实际使用中如果某Slave站点断开后(如故障或关机),Master端可以诊断出来,而当故障修复后,网络又可自动接通。因此,Modbus协议的可靠性较好。 下面我来简单的给大家介绍一下,对于ModbusASCIIRTUTCP协议来说,其中TCPRTU协议非常类似,我们只要把RTU协议的两个字节的校验码去掉,然后在RTU协议的开始加上50和一个6并通过TCP/IP网络协议发送出去即可。


IP属地:广东1楼2012-12-17 09:56回复
    所以在这里我仅介绍一下ModbusASCIIRTU协议。 下表是ASCII协议和RTU协议进行的比较: 协议 开始标记 结束标记 校验 传输效率 程序处理
    ASCII :(冒号) CR,LF LRC 低 直观,简单,易调试
    RTU 无 无 CRC 高 不直观,稍复杂


    通过比较可以看到,ASCII协议和RTU协议相比拥有开始和结束标记,因此在进行程序处理时能更加方便,而且由于传输的都是可见的ASCII字符,所以进行调试时就更加的直观,另外它的LRC校验也比较容易。但是因为它传输的都是可见的ASCII字符,RTU传输的数据每一个字节ASCII都要用两个字节来传输,比如RTU传输一个十六进制数0xF9,ASCII就需要传输’F’’9’ASCII0x390x46两个字节,这样它的传输的效率就比较低。所以一般来说,如果所需要传输的数据量较小可以考虑使用ASCII协议,如果所需传输的数据量比较大,最好能使用RTU协议。

    下面对两种协议的校验进行一下介绍。

    1LRC校验


    IP属地:广东2楼2012-12-17 09:56
    回复
      2025-12-22 01:14:34
      广告
      不感兴趣
      开通SVIP免广告

      LRC域是一个包含一个8位二进制值的字节。LRC值由传输设备来计算并放到消息帧中,接收设备在接收消息的过程中计算LRC,并将它和接收到消息中LRC域中的值比较,如果两值不等,说明有错误。

      LRC校验比较简单,它在ASCII协议中使用,检测了消息域中除开始的冒号及结束的回车换行号外的内容。它仅仅是把每一个需要传输的数据按字节叠加后取反加1即可。下面是它的VC代码:

      BYTE GetCheckCode(const char * pSendBuf, int nEnd)//获得校验码

      {

      BYTE byLrc = 0;

      char pBuf[4];

      int nData = 0;

      for(i=1; i<end; i+=2) //i初始为1,避开开始标记冒号

      {

      //每两个需要发送的ASCII码转化为一个十六进制数

      pBuf [0] = pSendBuf [i];

      pBuf [1] = pSendBuf [i+1];

      pBuf [2] = '\0';

      sscanf(pBuf,"%x",& nData);

      byLrc += nData;

      }

      byLrc = ~ byLrc;

      byLrc ++;

      return byLrc;

      }


      IP属地:广东3楼2012-12-17 09:56
      回复
        2.CRC校验

        CRC域是两个字节,包含一16位的二进制值。它由传输设备计算后加入到消息中。接收设备重新计算收到消息的CRC,并与接收到的CRC域中的值比较,如果两值不同,则有误。

        CRC是先调入一值是全“1”16位寄存器,然后调用一过程将消息中连续的8位字节各当前寄存器中的值进行处理。仅每个字符中的8Bit数据对CRC有效,起始位和停止位以及奇偶校验位均无效。

        CRC产生过程中,每个8位字符都单独和寄存器内容相或(OR),结果向最低有效位方向移动,最高有效位以0填充。LSB被提取出来检测,如果LSB1,寄存器单独和预置的值或一下,如果LSB0,则不进行。整个过程要重复8次。在最后一位(第8位)完成后,下一个8位字节又单独和寄存器的当前值相或。最终寄存器中的值,是消息中所有的字节都执行之后的CRC值。

        CRC添加到消息中时,低字节先加入,然后高字节。下面是它的VC代码:


        IP属地:广东4楼2012-12-17 09:59
        回复

          WORD GetCheckCode(const char * pSendBuf, int nEnd)//获得校验码

          {

          WORD wCrc = WORD(0xFFFF);

          for(int i=0; i<nEnd; i++)

          {

          wCrc ^= WORD(BYTE(pSendBuf[i]));

          for(int j=0; j<8; j++)

          {

          if(wCrc & 1)

          {

          wCrc >>= 1;

          wCrc ^= 0xA001;

          }

          else

          {

          wCrc >>= 1;

          }

          }

          }

          return wCrc;

          }

          对于一条RTU协议的命令可以简单的通过以下的步骤转化为ASCII协议的命令:


          IP属地:广东5楼2012-12-17 09:59
          回复
            1、 把命令的CRC校验去掉,并且计算出LRC校验取代。
            2、 把生成的命令串的每一个字节转化成对应的两个字节的ASCII码,比如0x03转化成0x30,0x330ASCII码和3ASCII码)。
            3、 在命令的开头加上起始标记“:”,它的ASCII码为0x3A
            4、 在命令的尾部加上结束标记CR,LF0xD,0xA),此处的CR,LF表示回车和换行的ASCII码。



            IP属地:广东6楼2012-12-17 09:59
            回复

              <2>返回的字节个数:表示数据的字节个数,也就是数据1,2...n中的n的值。
              <3>数据1...n:由于每一个数据是一个8位的数,所以每一个数据表示8个开关量的值,每一位为0表示对应的开关断开,为1表示闭合。比如例子中,表示20号(索引号为19)开关闭合,21号断开,22闭合,23闭合,24断开,25断开,26闭合,27闭合...如果询问的开关量不是8的整倍数,那么最后一个字节的高位部分无意义,置为0。
              <4>CRC校验同上。
              2、读只可读数字量寄存器(输入状态):
              和读取线圈状态类似,只是第二个字节的命令号不再是1而是2。
              3、写数字量(线圈状态):
              计算机发送命令:[设备地址] [命令号05] [需下置的寄存器地址高8位] [低8位] [下置的数据高8位] [低8位] [CRC校验的低8位] [CRC校验的高8位]
              例:[11][05][00][AC][FF][00][CRC低][CRC高]
              意义如下:
              <1>设备地址和上面的相同。
              <2>命令号:写数字量的命令号固定为05。
              <3>需下置的寄存器地址高8位,低8位:表明了需要下置的开关的地址。
              <4>下置的数据高8位,低8位:表明需要下置的开关量的状态。例子中为把该开关闭合。注意,此处只可以是[FF][00]表示闭合[00][00]表示断开,其他数值非法。
              <5>注意此命令一条只能下置一个开关量的状态。
              设备响应:如果成功把计算机发送的命令原样返回,否则不响应。
              4、读可读写模拟量寄存器(保持寄存器):
              计算机发送命令:[设备地址] [命令号03] [起始寄存器地址高8位] [低8位] [读取的寄存器数高8位] [低8位] [CRC校验的低8位] [CRC校验的高8位]
              例:[11][03][00][6B][00][03][CRC低][CRC高]
              意义如下:
              <1>设备地址和上面的相同。
              <2>命令号:读模拟量的命令号固定为03。
              <3>起始地址高8位、低8位:表示想读取的模拟量的起始地址(起始地址为0)。比如例子中的起始地址为107。
              <4>寄存器数高8位、低8位:表示从起始地址开始读多少个模拟量。例子中为3个模拟量。注意,在返回的信息中一个模拟量需要返回两个字节。
              设备响应:[设备地址] [命令号03] [返回的字节个数][数据1][数据2]...[数据n][CRC校验的低8位] [CRC校验的高8位]
              例:[11][03][06][02][2B][00][00][00][64][CRC低][CRC高]
              意义如下:
              <1>设备地址和命令号和上面的相同。
              <2>返回的字节个数:表示数据的字节个数,也就是数据1,2...n中的n的值。例子中返回了3个模拟量的数据,因为一个模拟量需要2个字节所以共6个字节。
              <3>数据1...n:其中[数据1][数据2]分别是第1个模拟量的高8位和低8位,[数据3][数据4]是第2个模拟量的高8位和低8位,以此类推。例子中返回的值分别是555,0,100。
              <4>CRC校验同上。
              5、读只可读模拟量寄存器(输入寄存器):
              和读取保存寄存器类似,只是第二个字节的命令号不再是2而是4。
              6、写单个模拟量寄存器(保持寄存器):
              计算机发送命令:[设备地址] [命令号06] [需下置的寄存器地址高8位] [低8位] [下置的数据高8位] [低8位] [CRC校验的低8位] [CRC校验的高8位]
              例:[11][06][00][01][00][03][CRC低][CRC高]
              意义如下:
              <1>设备地址和上面的相同。
              <2>命令号:写模拟量的命令号固定为06。
              <3>需下置的寄存器地址高8位,低8位:表明了需要下置的模拟量寄存器的地址。
              <4>下置的数据高8位,低8位:表明需要下置的模拟量数据。比如例子中就把1号寄存器的值设为3。
              <5>注意此命令一条只能下置一个模拟量的状态。
              设备响应:如果成功把计算机发送的命令原样返回,否则不响应。


              IP属地:广东8楼2012-12-17 10:04
              回复