STM32F103代码远程升级(三)基于Ymodem协议串口升级程序的实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gin_love/article/details/82020348

在实现了简单的串口更新代码之后,便开始考虑到了传输的数据的完整性、正确性和安全性,因此想到了在数据传输中添加通信协议,最常用的通信传输协议有:XModem、YModem、ZModem等,此次选用的协议是YModem协议。当然我们也可以自定义协议,只是自定义协议也需要我们自定义发送端。


一、YModem协议简介

YModem协议是XModem的改进协议,它最常用于调制解调器之间的文件传输的协议,具有快速,稳定传输的优点。它的传输速度比XModem快,这是由于它可以一次传输1024字节的信息块,同时它还支持传输多个文件,也就是常说的批文件传输。
YModem分成YModem-1K与YModem-g。
YModem-1K用1024字节信息块传输取代标准的128字节传输,数据使用CRC校验,保证数据传输的正确性。它每传输一个信息块数据时,就会等待接收端回应ACK信号,接收到回应后,才会继续传输下一个信息块,保证数据已经全部接收。
YModem-g传输形式与YModem-1K差不多,只是它去掉了数据的CRC校验码,同时在发送完一个数据块信息后,它不会等待接收端的ACK信号,而直接传输下一个数据块。正是它没有涉及错误校验与等待响应,才使得它的传输速度比YModem-1K来得快。
此次我用的就是YModem-1K传输。
http://ziye334.blog.163.com/blog/static/224306191201481010478799/ 此篇文章对YModem协议介绍地很详细。


二、YModem的数据格式

1、起始帧的数据格式

YModem的起始帧用于传输文件名与文件的大小,注意该数据包号为0,帧长=3字节的数据首部+128字节数据+2个字节CRC16校验码 = 133字节。数据结构为:

SOH 00 FF filename[ ] filezise[ ] NUL[ ] CRCH CRCL

2、数据帧的数据格式

YModem的数据帧从第二包数据开始,注意该数据包号为1。帧长 = 3字节的数据首部+1024字节数据+2字节的CRC16校验码 = 1029字节。数据结构为:

STX [num] [~num] data[ ] 1A …1A CRCH CRCL

其中的第二个字节为传输的数据包包号,第三个字节为数据包号取反组成。若文件数据的最后一包数据在128~1024之间,则数据部分剩余空间全部用0x1A填充。
注意,存在一种特殊情况:如果文件的大小小于或等于128字节或者文件数据最后剩余的数据小于128字节,则YModem会选择使用SOH数据帧,即用128字节来传输数据,如果数据不满128字节,剩余的数据用0x1A填充。这时数据帧的结构就变成了:
文件大小小于128字节:

SOH 01 FE data[ ] 1A …1A CRCH CRCL

文件最后剩余数据小于128字节:

SOH [num] [~~num] data[ ] 1A…1A CRCH CRCL

3、结束帧的数据格式

当传输结束时,YModem还会再传一包结束数据,只是数据内容为空。帧长=3字节首部+128字节的数据+2字节CRC16校验码 = 133字节,其数据帧结构为:

SOH 00 FF NUL[128] CRCH CRCL

4、文件传输过程

此处借用上面提到博客的图。以示说明。
YModem文件传输过程
特别注意的是,在文件传输结束时发送端发送了结束标识EOT之后待收到接收端的回复后,还会再发送一包空数据包以表示传输真正结束。

三、基于Ymodem协议串口升级程序的实现过程

1、串口工具的使用

此次我使用的串口工具为 ttermpro.exe ,该串口工具支持Kermit、Xmodem、Ymodem、ZModem等通信协议,其中XModem和YModem协议采用的是CRC16_XModem校验法则。
运行该程序后先选择串口,如图:
选择串口
然后再配置通信波特率,一般默认为9600,我们可以根据自己需要修改成相应波特率,如图:
修改波特率
紧接着就可以选择相应的通信协议进行文件传输了,如图:
选择通信协议

2、具体代码的实现

此次我不再使用上篇文章中的代码,而是从官网上下的stm32f4_iap_using_usart官方F4xx的例程,刚好该例程中也是使用了YModem通信协议。
我将该例程移植到我的stm32F103的工程下,并且把外部按键触发升级程序修改为了软件触发。具体实现是:使用stm32中的备份寄存器作为标识位,当该位被修改,则重启程序进入Bootloader升级程序,在Bootloader程序中也根据备份寄存器中的值进行相应的升级操作。
而要使用备份寄存器,首先得使能该寄存器,查看数据手册后得到如下操作:

/**************************************************************************************************
** 函数名称 : BKP_Configuration
** 功能描述 : 使能BKP寄存器
** 入口参数 : 无
** 出口参数 : 无
** 返 回 值 : 无
** 其他说明 : 无
***************************************************************************************************/  
void BKP_Configuration(void)
{
    /* 使能PWR和BKP时钟 */
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR|RCC_APB1Periph_BKP,ENABLE);
    /* 使能对后备寄存器的访问 */ 
      PWR->CR|=1<<8;//置PWR_CR寄存器的第八位DBP为1-->使能对后备寄存器和RTC的访问
}

在理解了YModem的数据结构和传输过程之后,再看例程中对YModem接收的处理就比较清晰了.

/**************************************************************************************************
** 函数名称 : Receive_Packet
** 功能描述 : 从发送方基于串口查询方式接收一个数据包
** 入口参数 : <data>[in] 接收到的数据缓存区
              <length>[in] 接收的数据长度
              <timeout>[in] 最大等待接收时间
** 出口参数 : length
              0   序号和补码校验不成功
              -1  发送方中止传输
              >0  正常的数据包长度
** 返 回 值 : 0 正常返回
              -1 超时或者包错误
              1  用户中止传输
** 作 者 :
** 日 期 :
** 其他说明 : 无
***************************************************************************************************/  
 int32_t Receive_Packet (uint8_t *data, int32_t *length, uint32_t timeout)
{
  uint16_t i, packet_size;
  uint8_t c =0;
  *length = 0;
  //printf("time_out = %x\r\n",timeout);
  if (Receive_Byte(&c, timeout) != 0)
  {     
    return -1; //超时返回-1
  }

  switch (c) //c表示接收到的数据的第一个字节
  {
    case SOH: //数据包开始
      packet_size = PACKET_SIZE;
      break;
    case STX://正文开始
      packet_size = PACKET_1K_SIZE;
      break;
    case EOT: //数据包结束
      return -2;
    case CA:  //发送方中止传输
      if ((Receive_Byte(&c, timeout) == 0) && (c == CA))
      { 
        *length = -1; 
        return 0;
      }
      else
      { 
        return -1; //中止传输返回-1
      }
    case ABORT1://A
    case ABORT2://a
//  case CAN:
            //用户中止传输
      return 1;
    default:
      return -1;
  }

  *data = c;
  for (i = 1; i < (packet_size + PACKET_OVERHEAD); i ++)
  {
    if (Receive_Byte(data + i, timeout) != 0) //获取剩下的数据(以字节为单位)
    { 
      return -1;//接收数据超时
    }
  }
  uint8_t temp1 =0;
  uint8_t temp2 =0;
  temp1 = data[PACKET_SEQNO_INDEX];
  temp2 = data[PACKET_SEQNO_COMP_INDEX];
  if (temp1 != ((temp2 ^ 0xff) & 0xff))//校验序号和补码
  {     
    printf("temp1 != ((temp2 ^ 0xff) & 0xff)  \r\n");
    return -1;
  }
    //序号和补码校验不成功则 length = 0;
  *length = packet_size; //获取数据包长度

  return 0;
}
/**************************************************************************************************
** 函数名称 : Receive
** 功能描述 : 基于Ymodem协议获取文件
** 入口参数 : <buf>[in] 文件首地址
** 出口参数 : 无
** 返 回 值 : 文件大小
** 作 者 :
** 日 期 :
** 其他说明 : 无
***************************************************************************************************/  
int32_t Receive (uint8_t *buf)
{
  uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD], file_size[FILE_SIZE_LENGTH], *file_ptr, *buf_ptr;
  int32_t i, j, packet_length, session_done, file_done, packets_received, errors, session_begin, size = 0;

  FlashDestination = ApplicationAddress; //初始化闪存目标地址
  memset(packet_data,0,PACKET_1K_SIZE + PACKET_OVERHEAD); //初始化为0

  for (session_done = 0, errors = 0, session_begin = 0; ;)
  {
    for (packets_received = 0, file_done = 0, buf_ptr = buf; ;)
    {
      switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT))//0,-1,1
      {
//---------case 0  正常返回
        case 0: 
                case -2:  //收到结束标志,读取结束包 SOH 00 FF 00…00[128个00] CRCH CRCL
          errors = 0;
          switch (packet_length)
          {
            /* 由发送方终止传输*/ 
            case - 1:
              Send_Byte(ACK);   
              return 0;
            /*  数据包中序号和补码不匹配,终止数据发送*/ 
            case 0:            
              Send_Byte(ACK);
              file_done = 1;
              break;
            /* length>0 正常的数据包*/
            default:
              if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff))//检查数据包中的序号和接收到的数据包序号是否一致
              {
                Send_Byte(NAK);//发送应答NAK,接收失败要求重发
              }
              else
              {
                if (packets_received == 0)//第一包,包含文件名,文件大小
                {

                  if (packet_data[PACKET_HEADER] != 0)//去除3个字节的首部,读取128B的数据包
                  {
                                        //取出文件名--32B用于存储
                    for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH);)
                    {
                      file_name[i++] = *file_ptr++;
                    }
                    file_name[i++] = '\0';

                                        //取出文件大小--2B用于存储
                    for (i = 0, file_ptr ++; (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH);)
                    {
                      file_size[i++] = *file_ptr++;
                    }
                    file_size[i++] = '\0';

                    Str2Int(file_size, &size); //字符转整型                   

                    //文件大小是否超出flash存储大小
                    if (size > (FLASH_SIZE - 1))
                    {

                      Send_Byte(CA); //中止通信
                      Send_Byte(CA);
                      return -1;
                    }
                    //擦除用户应用程序将被加载的所需的页面
                    //定义需要被擦除的页面号
                    NbrOfPage = FLASH_PagesMask(size);

                    // 擦除指定的flash页面
                    for (EraseCounter = 0; (EraseCounter < NbrOfPage) && (FLASHStatus == FLASH_COMPLETE); EraseCounter++)
                    {
                      FLASHStatus = FLASH_ErasePage(FlashDestination + (PageSize * EraseCounter));
                    }
                                        //如果文件大小小于20K,一个页面大小2K
                                        if(NbrOfPage < 10) 
                                        {
                                            delay2_ms(100);
                                        }
                    Send_Byte(ACK); //发送应答ACK
                    Send_Byte(CRC16); //发送“C”,等待接收下一包数据包
                  }
                                    //文件名首字节为空
                  else
                  {
                    Send_Byte(ACK);
                    file_done = 1;  //文件传输中止
                    session_done = 1; //传输中止
                    break;
                  }
                }// if (packets_received == 0)

                //packets_received > 0,1024B数据包开始传输
                else
                {

                                    //取出数据
                                    if(check(1,&packet_data[PACKET_DATA_INDEX],1024))//增加CRC校验验证
                                    {
                                            RamSource = (uint32_t)&packet_data[PACKET_DATA_INDEX];
                                            for (j = 0;(j < packet_length) && (FlashDestination <  ApplicationAddress + size);j += 4)
                                            {
//                                              BKP->DR6 = 2;//表示正在以Ymodem协议向Flash中写程序
                                                /*将收到的数据写到flash中*/
                                                FLASH_ProgramWord(FlashDestination, *(uint32_t*)RamSource); 

                                                if (*(uint32_t*)FlashDestination != *(uint32_t*)RamSource)//写入数据是否一致
                                                {
                                                    /* 中止通信 */
                                                    Send_Byte(CA);
                                                    Send_Byte(CA);
                                                    return -2;
                                                }
                                                FlashDestination += 4;//目标地址后移
                                                RamSource += 4;
                                            }//for
                                            Send_Byte(ACK);
                                    }
                                    else
                                    {   //CRC验证不通过
                                          Send_Byte(NAK);
                                    }
                }//else
                packets_received ++; //接收到的数据包加1
                session_begin = 1;
              }//else--序号一致
          }//switch   packet_length
          memset(packet_data,0,PACKET_1K_SIZE + PACKET_OVERHEAD);//数据包清0
          break;
//------case 1 由用户输入A(a)中止传输
        case 1: 
          Send_Byte(CA);//发送字节CA
          Send_Byte(CA);
          return -3;
//------返回-1    超时或者包错误 
//              case -2:
//                  Send_Byte(ACK);
//                break;
        default:
          if (session_begin > 0)
          {
            errors ++;
          }
          if (errors > MAX_ERRORS)
          {
            Send_Byte(NAK);
            Send_Byte(CA);
            Send_Byte(CA);
            return 0;
          }
          Send_Byte(CRC16);//发送“C”
          break;
      }//switch   Receive_Packet

      if (file_done != 0) //文件传输中止
      {
        break;
      }
    }//for 2 内循环
    if (session_done != 0) //传输中止
    {
      break;
    }
  }//for 1 外循环
  return (int32_t)size;
}

以上两段代码为YModem协议接收数据及处理数据的过程,在此之前我们需先初始化系统以及结构,同时需要注意以下几点:
1、Bootloader中尽可能不使用中断,因此此处串口接收数据采用查询接收方式;
2、Bootloaderz中不要让程序卡死或者进入某个死循环,应在适当的地方进行软件复位;
3、在Keil环境下涉及内存拷贝时,尽量不用memcpy()
4、注意数组长度越界或者溢出错误;
5、注意YModem协议第一包数据的包号为00。


参考链接

https://blog.csdn.net/IDOshi201109/article/details/50901615
http://ziye334.blog.163.com/blog/static/224306191201481010478799/
https://blog.csdn.net/lcmsir/article/details/80550821

猜你喜欢

转载自blog.csdn.net/gin_love/article/details/82020348