STM32:串口Bootloader+Ymodern

一、前言

  1. 今天分享一下串口bootloader,利用串口bootloader程序可以更新芯片的APP程序,对于一些不便于拆卸的产品,通过对外引出的通信端口来升级程序的维护方式非常好用。
  2. 串口bootloader网上可以找到很多,例如RT-Thread的通用bootloader,还可以通过http来更新APP程序,不过暂时只支持F1和F4系列的,或者我们可以自己写一个简单的串口bootloader,当然这也需要编写配套的上位机软件才能进行烧写,这样就有点麻烦,不过好在STM32为我们提供了串口bootloader的例程,使用起来相当方便,只需要使用支持Ymodern协议的PC软件,就可以对STM32进行固件更新,而且Ymodern协议是分包写入flash中,对于更新较大的APP的程序,使用小量的RAM空间就能完成,那么本次我们就来移植一下,工程以STM32F303RCT6为例。
  3. 移植需要芯片的一个UART,以及连接PC串口端的转换工具,在此之前,必须调通UART能和PC串口端通信,例如写一个简单的验证程序,PC端发送任意数,芯片UART就将接收到的任意数回应给PC,因为本人在移植的时候,就因为串口端的转换工具异常(TTL转RS232),浪费了不少时间,甚至一度怀疑官方IAP程序有问题,最后还是用示波器发现转换工具在PC发送时,无端回应一些错误数据给PC,在此提醒大家做好移植前的硬件准备,工欲善其事必先利其器!(本文使用的模板工程已上传至:https://gitee.com/INT_TANG/some-project/tree/master/STM32/Bootloader

二、移植准备

1. 如果我们安装了STM32CubeMX,并生成过对应芯片的工程,那么可以在其安装目录下,找到对应芯片型号的Cube包,也可以从官网上下载Cube包,串口bootloader例程一般位于文件夹Cube包下的…\Projects\STM32XXX_EVAL\Applications\IAP中。

STM32:串口Bootloader+Ymodern

1.不一定每个文件夹都有IAP例程,STM32提供多种开发板,我们选择相似的芯片型号里面去查找即可。

2. 位于IAP\IAP_Main\Src,存放的就是串口bootloader的实现。

STM32:串口Bootloader+Ymodern

1.跟串口相关,主要是格式转换,我们也可以自己实现printf函数达到格式输出要求。
2.跟芯片的Flash相关,包含Flash的擦除,写入,读取等相关。
3.实现串口bootloader的菜单项,通过PC终端操作,选择写入/读取固件,跳转APP程序。
4.Ymodern协议的主要实现。

3. Ymodern协议

关于Ymodern协议解析可以参考链接:https://blog.csdn.net/INT_TANG/article/details/117334848
或者可以阅读官方的Ymodern,文件已上传到我的仓库:https://gitee.com/INT_TANG/doc/tree/master/XYmodern

三、代码分析

1. 查看例程main.c文件

STM32:串口Bootloader+Ymodern

  • 70行通过外部按钮触发是否进入bootloader模式。
  • 73行将flash初始化。
  • 75行是串口的初始化。
    STM32:串口Bootloader+Ymodern
  • 77行是bootloader的实现。
2. Main_Menu ()函数

STM32:串口Bootloader+Ymodern
STM32:串口Bootloader+Ymodern

  • 162行清除串口的收发器。
  • 165行接收一个命令字节。
  • 171行串口下载更新flash。
  • 180行获取APP固件的复位函数地址。
  • 184行设置堆栈指针,在向量表分配中,第一个分配的是堆栈指针,第二个是复位函数入口地址,这与cortex-M内核的架构有关。
  • 185行跳转到APP固件执行。
    可以看出该函数实现一个菜单项,输入不同的命令执行相应操作,本次我们重点分析SerialDownoad()函数。
3. SerialDownoad()函数

STM32:串口Bootloader+Ymodern
可以看出,其主要调用的函数是Ymodem_Receive()

4. Ymodem_Receive()函数
COM_StatusTypeDef Ymodem_Receive ( uint32_t *p_size )
{
  uint32_t i, packet_length, session_done = 0, file_done, errors = 0, session_begin = 0;
  uint32_t flashdestination, ramsource, filesize;
  uint8_t *file_ptr;
  uint8_t file_size[FILE_SIZE_LENGTH], tmp, packets_received;
  COM_StatusTypeDef result = COM_OK;

  /* APP的起始地址,即从该地址开始写入APP固件,APPLICATION_ADDRESS在flash_if.h文件中定义 */
  flashdestination = APPLICATION_ADDRESS;

/* 等待文件完成或串口的状态 */
  while ((session_done == 0) && (result == COM_OK))
  {
    packets_received = 0;
    file_done = 0;
    /* 等待文件完成或串口的状态 */
    while ((file_done == 0) && (result == COM_OK))
    {
    	/* ReceivePacket主要接收串口的数据,并验证是否符合Ymodern协议的数据包 */
      switch (ReceivePacket(aPacketData, &packet_length, DOWNLOAD_TIMEOUT))
      {
        case HAL_OK:
          errors = 0;
          switch (packet_length)
          {
            case 2:
              /* PC传输中断 */
              Serial_PutByte(ACK);
              result = COM_ABORT;
              break;
            case 0:
              /* 传输结束 */
              Serial_PutByte(ACK);
              file_done = 1;
              break;
            default:
              /* 判断是否接收正确的数据包数,不成功则回应NAK,此时PC会重发数据包 */
              if (aPacketData[PACKET_NUMBER_INDEX] != packets_received)
              {
                Serial_PutByte(NAK);
              }
              else
              {
              	/* 第一包数据包和传输完成后传送的结束包,都是以0数据包传送 */
                if (packets_received == 0)
                {
                  /* 第一包数据传输的是文件名和文件大小 */
                  if (aPacketData[PACKET_DATA_INDEX] != 0)
                  {
                    /* File name extraction */
                    i = 0;
                    file_ptr = aPacketData + PACKET_DATA_INDEX;
                    while ( (*file_ptr != 0) && (i < FILE_NAME_LENGTH))
                    {
                      aFileName[i++] = *file_ptr++;
                    }

                    /* File size extraction */
                    aFileName[i++] = '\0';
                    i = 0;
                    file_ptr ++;
                    while ( (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH))
                    {
                      file_size[i++] = *file_ptr++;
                    }
                    file_size[i++] = '\0';
                    Str2Int(file_size, &filesize);

                    /* Test the size of the image to be sent */
                    /* Image size is greater than Flash size */
                    /* 验证文件大小是否超过允许的APP固件大小,USER_FLASH_SIZE 在flash_if.h文件中定义 */
                    if (*p_size > (USER_FLASH_SIZE + 1))
                    {
                      /* End session */
                      tmp = CA;
                      HAL_UART_Transmit(&UartHandle, &tmp, 1, NAK_TIMEOUT);
                      HAL_UART_Transmit(&UartHandle, &tmp, 1, NAK_TIMEOUT);
                      result = COM_LIMIT;
                    }
                    /* 写入flash前需擦除该区域 */
                    FLASH_If_Erase(APPLICATION_ADDRESS);
                    *p_size = filesize;

                    Serial_PutByte(ACK);
                    Serial_PutByte(CRC16);
                  }
                  /* 文件名为空,说明是结束包 */
                  else
                  {
                    Serial_PutByte(ACK);
                    file_done = 1;
                    session_done = 1;
                    break;
                  }
                }
                else /* Data packet */
                {
                  ramsource = (uint32_t) & aPacketData[PACKET_DATA_INDEX];

                  /* 写入flash */
                  if (FLASH_If_Write(flashdestination, (uint32_t*) ramsource, packet_length/4) == FLASHIF_OK)                   
                  {
                    flashdestination += packet_length;
                    Serial_PutByte(ACK);
                  }
                  else /* An error occurred while writing to Flash memory */
                  {
                    /* End session */
                    Serial_PutByte(CA);
                    Serial_PutByte(CA);
                    result = COM_DATA;
                  }
                }
                packets_received ++;
                session_begin = 1;
              }
              break;
          }
          break;
        case HAL_BUSY: /* Abort actually */
          Serial_PutByte(CA);
          Serial_PutByte(CA);
          result = COM_ABORT;
          break;
        default:
          if (session_begin > 0)
          {
            errors ++;
          }
          if (errors > MAX_ERRORS)
          {
            /* Abort communication */
            Serial_PutByte(CA);
            Serial_PutByte(CA);
          }
          else
          {
          	/* 若函数ReceivePacket超时没有收到数据,则发送‘C’应答,通知PC启动传输 */
            Serial_PutByte(CRC16); /* Ask for a packet */
          }
          break;
      }
    }
  }
  return result;
}
5. ReceivePacket()函数
static HAL_StatusTypeDef ReceivePacket(uint8_t *p_data, uint32_t *p_length, uint32_t timeout)
{
  uint32_t crc;
  uint32_t packet_size = 0;
  HAL_StatusTypeDef status;
  uint8_t char1;

  *p_length = 0;
  status = HAL_UART_Receive(&UartHandle, &char1, 1, timeout);
	
  /* 判断接收的数据头是哪一种包类型 */
  if (status == HAL_OK)
  {
    switch (char1)
    {
      case SOH:
        packet_size = PACKET_SIZE;
        break;
      case STX:
        packet_size = PACKET_1K_SIZE;
        break;
      case EOT:
        break;
      case CA:
        if ((HAL_UART_Receive(&UartHandle, &char1, 1, timeout) == HAL_OK) && (char1 == CA))
        {
        /* PC主动停止传输 */
          packet_size = 2;
        }
        else
        {
          status = HAL_ERROR;
        }
        break;
      case ABORT1:
      case ABORT2:
        status = HAL_BUSY;
        break;
      default:
        status = HAL_ERROR;
        break;
    }
    *p_data = char1;

	/* 如果是正常的数据包 */
    if (packet_size >= PACKET_SIZE )
    {
      status = HAL_UART_Receive(&UartHandle, &p_data[PACKET_NUMBER_INDEX], packet_size + PACKET_OVERHEAD_SIZE, timeout);

      /* Simple packet sanity check */
      if (status == HAL_OK )
      {
        if (p_data[PACKET_NUMBER_INDEX] != ((p_data[PACKET_CNUMBER_INDEX]) ^ NEGATIVE_BYTE))
        {
          packet_size = 0;
          status = HAL_ERROR;
        }
        else
        {
          /* Check packet CRC */
          crc = p_data[ packet_size + PACKET_DATA_INDEX ] << 8;
          crc += p_data[ packet_size + PACKET_DATA_INDEX + 1 ];
          if (Cal_CRC16(&p_data[PACKET_DATA_INDEX], packet_size) != crc )
          {
            packet_size = 0;
            status = HAL_ERROR;
          }
        }
      }
      else
      {
        packet_size = 0;
      }
    }
  }
  *p_length = packet_size;
  return status;
}

四、移植

1. 准备一个可用的串口工程,创建BOOT文件,并从官方提供的IAP例程中复制出以下文件。

STM32:串口Bootloader+Ymodern

其中的SerilaCOM.c和SerilaCOM.h是自行创建的,主要是串口的初始化,STM32例程将串口初始化放到了main文件中,不便于移植到其他工程中,我们将其独立出来。

STM32:串口Bootloader+Ymodern

2. flash_if.h文件修改

STM32:串口Bootloader+Ymodern

在上图中发现,官方例程只检查了8-39区是否存在写保护,如果我们的APP程序很大,超过这些区域,则可能因为被写保护而无法写入,而在这个检查写保护没检查出来是不行的,我们根据使用的型号容量进行修改。

STM32:串口Bootloader+Ymodern

以上设置是有必要的,这划分了flash的boot区和APP分区,分区信息如下:

STM32:串口Bootloader+Ymodern

3. 添加一些延时

STM32:串口Bootloader+Ymodern

某些PC终端软件,在特殊情况下,例如中断传输,传输完成等,会有一定的反应时间,如果不添加延时,则可能看不到这些信息的有效输出。

4. 修改Ymodem_Receive()函数

STM32:串口Bootloader+Ymodern

上图中,根据Ymodern协议,第一包先发送文件名,文件大小,但本人发现在此之前,*p_size是没有被任何数赋值到,认为此处代码有错,应该是将上一行代码中的filesize比较,得出文件大小是否超出flash大小,从而是否执行更新,应修改为 if (filesize > (USER_FLASH_SIZE + 1))

5. bootloader工程设置

STM32:串口Bootloader+Ymodern

此处设置的是链接地址,boot是首先运行的程序,所以需要链接到0x8000000,大小需要和设置的APP地址对应,同样,RAM我们也划分开来,分配专有的RAM区,从而使得boot区和APP区使用的资源隔离开。

6. APP程序的工程设置

STM32:串口Bootloader+Ymodern

设置APP工程的链接地址为0x8004000,这个地址不是固定的,是由bootloader工程里面的APPLICATION_ADDRESS决定的,这是bootloader跳转到的地址。同样,RAM分区我们也需要隔离开,同bootloader区分开。

STM32:串口Bootloader+Ymodern

设置向量表的偏移,这里已经告诉需要设置满足条件偏移量0x200(不同芯片偏移可能不一样),也就是APP的起始链接地址需要满足的条件,同时也是bootloader工程里面的APPLICATION_ADDRESS需要满足的条件。为了不改动官方的源码,我们选择在外部设置。

STM32:串口Bootloader+Ymodern

偏移到APP程序的链接地址。如果使用RTOS,需要在之前设置,比如RTThread:

STM32:串口Bootloader+Ymodern

至此,就移植完成bootloader,可以看出步骤非常简单。

五、下载

使用SecureCRT软件。
STM32:串口Bootloader+Ymodern
STM32:串口Bootloader+Ymodern
STM32:串口Bootloader+Ymodern
STM32:串口Bootloader+Ymodern
STM32:串口Bootloader+Ymodern

注意,如果你的APP固件大于30K以上,需要设置如下:

STM32:串口Bootloader+Ymodern

原因在于Ymodern使用一个字节作为数据包计数,当使用128数据包模式时(128*255/1024约等于31K),该字节会溢出并重新计数,但STM32的源码存在一定漏洞。

STM32:串口Bootloader+Ymodern

对于RAM容量紧张的芯片,可能没有分配到那么大的串口接收缓冲区,那么使用128数据包模式就很有必要了,这样串口接收缓冲区就只需要少量RAM,继续修改一下Ymodem_Receive函数。

COM_StatusTypeDef Ymodem_Receive ( uint32_t *p_size )
{
  /* 省略部分代码 */
  uint32_t flashdestination, ramsource, filesize=0;//将记录文件大小的变量filesize清零
  uint32_t alreadyfilesize=0;	//新建变量alreadyfilesize用于记录已接收到的数据
  
 /* 省略部分代码 */
 				/* 判断文件是否已经接收完成 */
                if ((packets_received == 0)&&(alreadyfilesize >= filesize))
                {
                  /* File name packet */
                  if (aPacketData[PACKET_DATA_INDEX] != 0)
  				  {
  				  	/* 省略部分代码 */
                  }
                  /* File header packet is empty, end session */
                  else
                  {
                    /* 省略部分代码 */
                  }
                }
                else /* Data packet */
                {
                  ramsource = (uint32_t) & aPacketData[PACKET_DATA_INDEX];

                  /* Write received data in Flash */
                  if (FLASH_If_Write(flashdestination, (uint32_t*) ramsource, packet_length/4) == FLASHIF_OK)                   
                  {
                    flashdestination += packet_length;
                    /* 记录已接收数据大小 */
					alreadyfilesize += packet_length;
                    Serial_PutByte(ACK);
                  }
                  else /* An error occurred while writing to Flash memory */
                  {
                    /* End session */
                    Serial_PutByte(CA);
                    Serial_PutByte(CA);
                    result = COM_DATA;
                  }
                }
           /* 省略部分代码 */
}

六、通用性

对比了H7系列的IAP例程,它们的基本架构相同,且提供的函数接口相同,也就是说,当移植好了上面的工程模板(F303),如果迁移到H7/F1/F4等等系列,只需复制对应型号的flash_if.c和flash_if.h文件(主要是flash的容量和分配区域不同),同时修改flash_if.h支持的APP固件大小,起始扇区,保护读取扇区,以及修改SerilaCOM.c即可以做到通用,其他文件不需要更改,非常方便应用到不同系列的bootloader。

上一篇:音视频解码流程


下一篇:doris查询时连接报错