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


在实现了简单的串口更新代码之后,便开始考虑到了传输的数据的完整性、正确性和安全性,因此想到了在数据传输中添加通信协议,最常用的通信传输协议有: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传输。
YModem协议简介
此篇文章对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、Bootloader中不要让程序卡死或者进入某个死循环,应在适当的地方进行软件复位;
3、在Keil环境下涉及内存拷贝时,尽量不用memcpy()
4、注意数组长度越界或者溢出错误;
5、注意YModem协议第一包数据的包号为00。


下载链接

因为大家都留言寻求 ttermpro.exe 的软件包,所以我特意更新了博客,将下载链接放在这里,需要的小伙伴自取哟
百度网盘链接
提取码:jysq


参考链接

YModem协议理解
YModem协议简介
Ymodem协议详解

  • 42
    点赞
  • 335
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论
以下是STM32F103的Ymodem升级程序示例代码: ``` #include "stm32f10x.h" #include "stdio.h" #define FLASH_APP1_ADDR 0x08010000 // APP1地址 #define FLASH_APP2_ADDR 0x08020000 // APP2地址 #define FLASH_APP_SIZE 0x00010000 // 每个APP的大小为64KB #define FLASH_PAGE_SIZE 0x400 // Flash每页大小为1KB #define YMODEM_SOH 0x01 #define YMODEM_STX 0x02 #define YMODEM_EOT 0x04 #define YMODEM_ACK 0x06 #define YMODEM_NAK 0x15 #define YMODEM_CAN 0x18 #define YMODEM_C 0x43 #define YMODEM_TIMEOUT 1000 // 超时时间 #define YMODEM_MAX_SIZE 64 // Ymodem每个数据包大小为128字节,实际使用时使用64字节即可 static uint8_t ymodem_get_char(uint8_t *data); static uint8_t ymodem_wait_ack(void); static uint8_t ymodem_calc_checksum(uint8_t *data, uint32_t size); static uint8_t ymodem_write_packet(uint8_t *data, uint32_t size); static void ymodem_write_flash(uint8_t *data, uint32_t size); static uint8_t ymodem_file_receive(uint32_t addr); int main(void) { if (ymodem_file_receive(FLASH_APP1_ADDR) == YMODEM_ACK) { // APP1接收成功,重启执行APP1 NVIC_SystemReset(); } else if (ymodem_file_receive(FLASH_APP2_ADDR) == YMODEM_ACK) { // APP2接收成功,重启执行APP2 NVIC_SystemReset(); } while (1); } static uint8_t ymodem_get_char(uint8_t *data) { uint32_t timer = YMODEM_TIMEOUT; while (timer--) { if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET) { *data = USART_ReceiveData(USART1); return YMODEM_ACK; } } return YMODEM_NAK; } static uint8_t ymodem_wait_ack(void) { uint8_t ret = 0; uint8_t data = YMODEM_NAK; uint32_t timer = YMODEM_TIMEOUT; while (timer--) { ret = ymodem_get_char(&data); if (ret == YMODEM_ACK) { return YMODEM_ACK; } } return YMODEM_NAK; } static uint8_t ymodem_calc_checksum(uint8_t *data, uint32_t size) { uint8_t checksum = 0; uint32_t i; for (i = 0; i < size; i++) { checksum += data[i]; } return checksum; } static uint8_t ymodem_write_packet(uint8_t *data, uint32_t size) { uint8_t ret = 0; uint8_t packet[YMODEM_MAX_SIZE + 6]; // 数据包大小为64字节,加上头部3字节和尾部3字节 uint32_t i; packet[0] = size == 128 ? YMODEM_SOH : YMODEM_STX; // 根据数据包大小确定头类型 packet[1] = 0xFF - size; // 数据包大小的补码 packet[2] = size == 128 ? 0x00 : 0x01; // 序号,128字节为0,64字节为1 for (i = 0; i < size; i++) { packet[i + 3] = data[i]; } for (i = size + 3; i < YMODEM_MAX_SIZE + 3; i++) { packet[i] = 0x00; // 填充0 } uint8_t checksum = ymodem_calc_checksum(&packet[3], size); packet[YMODEM_MAX_SIZE + 3] = checksum; packet[YMODEM_MAX_SIZE + 4] = 0x00; // 结束标志 packet[YMODEM_MAX_SIZE + 5] = 0x00; // 结束标志 for (i = 0; i < 3; i++) { ret = USART_SendData(USART1, packet[i]); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); } for (i = 3; i < YMODEM_MAX_SIZE + 6; i++) { ret = USART_SendData(USART1, packet[i]); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); } return ymodem_wait_ack(); } static void ymodem_write_flash(uint8_t *data, uint32_t size) { uint32_t i; uint32_t flash_addr = FLASH_APP1_ADDR; // 默认写入APP1 uint8_t flash_erase = 0; // 是否需要擦除 // 判断写入目标地址 if (*((uint32_t *)data) == 0x08020000) { flash_addr = FLASH_APP2_ADDR; } // 判断是否需要擦除 if (flash_addr == FLASH_APP1_ADDR) { if (*((uint32_t *)FLASH_APP1_ADDR) != 0xFFFFFFFF) { flash_erase = 1; } } else { if (*((uint32_t *)FLASH_APP2_ADDR) != 0xFFFFFFFF) { flash_erase = 1; } } // 擦除Flash if (flash_erase) { FLASH_Unlock(); FLASH_ErasePage(flash_addr); FLASH_Lock(); } // 写入数据到Flash FLASH_Unlock(); for (i = 0; i < size; i += 4) { FLASH_ProgramWord(flash_addr + i, *((uint32_t *)(data + i))); } FLASH_Lock(); } static uint8_t ymodem_file_receive(uint32_t addr) { uint8_t ret = 0; uint8_t data[YMODEM_MAX_SIZE]; uint32_t size = 0; uint32_t i; // 发送开始传输命令 uint8_t cmd = YMODEM_C; for (i = 0; i < 3; i++) { ret = USART_SendData(USART1, cmd); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); } while (1) { // 等待数据包 ret = ymodem_wait_ack(); if (ret != YMODEM_SOH && ret != YMODEM_STX) { return YMODEM_NAK; } // 接收数据包 for (i = 0; i < YMODEM_MAX_SIZE; i++) { ret = ymodem_get_char(&data[i]); if (ret != YMODEM_ACK) { return YMODEM_NAK; } } // 校验数据包 if (data[YMODEM_MAX_SIZE - 2] != ymodem_calc_checksum(data, YMODEM_MAX_SIZE - 2)) { return YMODEM_NAK; } // 处理数据包 if (data[0] == YMODEM_SOH) { size = 128; } else { size = 64; } // 数据包序号为0时表示结束 if (data[1] == 0x00) { // 发送确认结束命令 for (i = 0; i < 3; i++) { ret = USART_SendData(USART1, YMODEM_ACK); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); } break; } // 写入Flash ymodem_write_flash(&data[3], size); // 发送确认命令 for (i = 0; i < 3; i++) { ret = USART_SendData(USART1, YMODEM_ACK); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); } } return YMODEM_ACK; } ``` 该示例代码实现了一个Ymodem协议的文件接收程序,可以将接收到的数据包写入Flash中。在主函数中,先尝试接收APP1,如果接收成功则重启执行APP1;如果接收失败则尝试接收APP2,如果接收成功则重启执行APP2。如果两个APP都接收失败,则程序将被卡在while(1)循环中,等待重新启动。 在使用该示例代码时,需要在USART1的RX和TX引脚上接上一个串口转USB模块,并将该模块连接到PC上。然后,在PC上使用Ymodem协议的传输工具发送.bin文件即可。 需要注意的是,该示例代码只是一个简单的示例,实际使用时还需要根据具体的应用场景进行修改和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

微芯供氧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值