一、引言

        在嵌入式开发领域,程序的现场升级功能是非常实用的,它允许用户在产品已经部署到现场后,仍能方便地对程序进行更新和维护。使用STM32实现U盘升级程序(IAP)功能,可以极大地提高产品的可维护性和用户体验。本文将详细介绍如何实现基于STM32的U盘IAP功能,主要分为Bootloader层和App层的开发。

-------------------------------------------------------------结尾附上源码---------------------------------------------------------------

二、软硬件准备

  1. 硬件:正点原子STM32F407ZGT6最小系统板
  2. 软件:CUBEMx版本:MX.6.12.0
  3. 调试工具:sscom串口调试助手

三、CUBEmx配置

         本文暂不详细介绍CUBEmx的配置步骤,因为网上资源丰富,大家可以参考上一篇(读写U盘)配置教程,里面有提到大佬的配置链接:

单片机读取U盘 FATFS文件系统 USB MSC STM32f105 GD32f305 读取U盘 exFAT FAT32_gd32f105usb例程-CSDN博客https://blog.csdn.net/2202_75941163/article/details/145942897

注:读写U盘 与 U盘IAP升级所需的HAL库基本配置是一样的

四、分区管理

        为了实现IAP功能,需要对STM32的Flash存储器进行分区管理。一般将Flash存储器分为两个主要区域:Bootloader区和App区。Bootloader区用于存放Bootloader程序,而App区用于存放用户应用程序。

#define BOOTLOADER_START_ADDR    0x08000000    // Bootloader起始地址

#define BOOTLOADER_SIZE            0x00010000    // Bootloader大小(64KB)

#define FLASH_USER_START_ADDR      0x08010000   // App起始地址

#define FLASH_USER_END_ADDR       (0x08010000 + APP_Size)  // APP结束地址

#define filename "APP.bin"                     // APP_Size为U盘bin文件大小

五、Bootloader层开发

5.1 U盘接口实现

        为了实现U盘升级功能,需要在Bootloader中实现USB设备接口。STM32提供了丰富的USB外设功能,通过FatFS文件管理系统与USB Host功能,可以实现识别U盘升级Bin文件。在代码中,通过调用f_mount函数挂载U盘,f_open函数打开U盘中的固件文件,然后使用f_read函数将固件数据读取到RAM缓冲区中。

5.2 升级流程控制

在Bootloader中,升级流程控制的实现基于对U盘检测和用户操作的响应。具体流程如下:

  1. U盘检测与初始化:系统上电后,Bootloader首先检测U盘是否插入。这是通过USB主机功能实现的,一旦检测到U盘,便初始化FatFS文件系统,为后续的文件操作做准备。    
  2. 固件文件读取:在成功挂载U盘后,Bootloader尝试打开并读取固件文件。文件读取操作通过FatFS的f_read函数完成,将固件数据从U盘读取到内部RAM缓冲区中。

  1. 固件数据校验:读取固件数据后,需要对数据进行校验,确保数据的完整性和正确性。这一步骤对于防止因数据损坏导致的升级失败至关重要。
  2. Flash擦除与写入:如果固件数据校验通过,Bootloader将执行Flash擦除操作,为新的固件写入腾出空间。擦除操作针对应用程序区域的Flash扇区进行。擦除完成后,将RAM缓冲区中的固件数据写入Flash。
  3. 跳转到应用程序:在固件成功写入Flash后,Bootloader设置好应用程序的堆栈指针和程序计数器,然后跳转到应用程序的入口点,开始运行新的应用程序。
  4. 错误处理与重试:如果在升级过程中任何一步出现错误,例如U盘读取失败、数据校验错误或Flash写入失败,Bootloader将留在当前模式下,等待用户重新发起升级操作或进行故障排除。

5.3 bootloader.c

#include "bootloader.h"
#include "main.h"

extern ApplicationTypeDef Appli_state;
FRESULT res;
static FLASH_EraseInitTypeDef EraseInitStruct;

uint8_t RAM_Buffer[RAM_BUFFER_SIZE];                         // 用于暂存从U盘读取的固件数据
uint32_t APP_Size;                                           // 从U盘读取的固件大小
uint32_t FirstSector = 0;
uint32_t NbOfSectors = 0;
uint32_t SectorError = 0;
uint32_t Address = 0;

volatile uint32_t data32 = 0 ;
volatile uint32_t MemoryProgramStatus = 0 ;
uint8_t errorcode;
uint32_t *p;

uint8_t SystemUpdateFlag = 0, state = 0;                     // 状态标志变量
uint16_t t = 0;

typedef  void (*pFunction)(void);
pFunction Jump_To_Application;

uint32_t JumpAddress;


uint32_t FLASH_Erase_Write(void)
{
  uint32_t i = 0;
  HAL_FLASH_Unlock();                                                // 解锁Flash
  FirstSector = GetSector(FLASH_USER_START_ADDR);                    // 获取应用程序起始地址所在的扇区编号
  NbOfSectors = GetSector(FLASH_USER_END_ADDR) - FirstSector + 1;    // 计算需要擦除的扇区数量
  printf("擦除的扇区数量为%d",NbOfSectors);
    
  // 配置 Flash 擦除结构体
  EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS;              // 设置擦除类型为扇区擦除
  EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3;             // 设置电压范围
  EraseInitStruct.Sector = FirstSector;                             // 设置起始扇区
  EraseInitStruct.NbSectors = NbOfSectors;                          // 设置扇区数量
    
  // 执行Flash擦除
  if(HAL_FLASHEx_Erase(&EraseInitStruct, &SectorError) != HAL_OK) 
  {
    errorcode = HAL_FLASH_GetError();  // 获取错误码
    printf("errorcode %d", errorcode);
    Error_Handler();
  }
  
  // 禁用和清除Flash缓存
  __HAL_FLASH_DATA_CACHE_DISABLE();
  __HAL_FLASH_INSTRUCTION_CACHE_DISABLE();

  __HAL_FLASH_DATA_CACHE_RESET();
  __HAL_FLASH_INSTRUCTION_CACHE_RESET();

  __HAL_FLASH_INSTRUCTION_CACHE_ENABLE();
  __HAL_FLASH_DATA_CACHE_ENABLE();
    
    
  Address = FLASH_USER_START_ADDR;              // 设置Flash写入起始地址
  
  // 遍历 RAM_Buffer,将数据写入 Flash
  printf("正在写入数据 请稍后... ...\r\n");
  while (Address < FLASH_USER_END_ADDR)
  {
    p = (uint32_t *)&RAM_Buffer[i];             // 获取要写入的数据
    if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address, *p) == HAL_OK)           // 将数据写入 Flash
    {
      Address = Address + 4;   // 地址递增 4 字节(32位)
      i = i + 4;
    }
    else
    {
      printf("Address-error\r\n");
      Error_Handler();
    }
  }
  printf("数据写入完毕\r\n");
  HAL_FLASH_Lock();                                 // 锁定Flash

  // 验证Flash写入是否成功
  Address = FLASH_USER_START_ADDR;                  // 重置地址指针
  MemoryProgramStatus = 0x0;                        //初始化验证状态变量

  // 遍历 Flash 写入范围,验证数据
  while (Address < FLASH_USER_END_ADDR)
  {
    data32 = *(__IO uint32_t*)Address;      // 读取Flash中的数据

    if (data32 != *(uint32_t*)RAM_Buffer)   // 比较Flash数据和原始数据
    {
      MemoryProgramStatus++;
    }
    Address = Address + 4;
  }
    return HAL_OK;
}

/**********************************************************************
**** 函数名: GetSector()
**** 功  能: 获取Flash的扇区
**** 参  数: Address   Flash的地址
**** 返回值: 扇区编号
**** 时  间: 2025年3月10日
**** 设  计: 
**** 备  注: STM32F407的Flash大小(1M)    1个扇区16KB(0x4000字节)   一共11个扇区
**********************************************************************/

static uint32_t GetSector(uint32_t Address)
{
  uint32_t sector = 0;              // 初始化扇区编号为0
  
   // 判断地址所在的扇区
  if((Address < ADDR_FLASH_SECTOR_1) && (Address >= ADDR_FLASH_SECTOR_0))
  {
    sector = FLASH_SECTOR_0;  
  }
  else if((Address < ADDR_FLASH_SECTOR_2) && (Address >= ADDR_FLASH_SECTOR_1))
  {
    sector = FLASH_SECTOR_1;  
  }
  else if((Address < ADDR_FLASH_SECTOR_3) && (Address >= ADDR_FLASH_SECTOR_2))
  {
    sector = FLASH_SECTOR_2;  
  }
  else if((Address < ADDR_FLASH_SECTOR_4) && (Address >= ADDR_FLASH_SECTOR_3))
  {
    sector = FLASH_SECTOR_3;  
  }
  else if((Address < ADDR_FLASH_SECTOR_5) && (Address >= ADDR_FLASH_SECTOR_4))
  {
    sector = FLASH_SECTOR_4;  
  }
  else if((Address < ADDR_FLASH_SECTOR_6) && (Address >= ADDR_FLASH_SECTOR_5))
  {
    sector = FLASH_SECTOR_5;  
  }
  else if((Address < ADDR_FLASH_SECTOR_7) && (Address >= ADDR_FLASH_SECTOR_6))
  {
    sector = FLASH_SECTOR_6;  
  }
  else if((Address < ADDR_FLASH_SECTOR_8) && (Address >= ADDR_FLASH_SECTOR_7))
  {
    sector = FLASH_SECTOR_7;  
  }
  else if((Address < ADDR_FLASH_SECTOR_9) && (Address >= ADDR_FLASH_SECTOR_8))
  {
    sector = FLASH_SECTOR_8;  
  }
  else if((Address < ADDR_FLASH_SECTOR_10) && (Address >= ADDR_FLASH_SECTOR_9))
  {
    sector = FLASH_SECTOR_9;  
  }
  else if((Address < ADDR_FLASH_SECTOR_11) && (Address >= ADDR_FLASH_SECTOR_10))
  {
    sector = FLASH_SECTOR_10;  
  }
  else /* (Address < FLASH_END_ADDR) && (Address >= ADDR_FLASH_SECTOR_11) */
  {
    sector = FLASH_SECTOR_11;
  }

  return sector;      // 返回扇区编号
}

/**********************************************************************
**** 函数名: jumpToApp()
**** 功  能: 跳转到appa运行程序
**** 参  数:
**** 返回值: 无
**** 时  间: 2025年3月11日
**** 设  计: 
**** 备  注: 
**********************************************************************/
void jumpToApp()
{ 
    // 检查应用程序的栈顶地址是否有效
    // 应用程序的栈顶地址存储在FLASH_USER_START_ADDR处
    // 有效栈顶地址的高16位必须是0x2000(即位于SRAM区域)
    if (((*(__IO uint32_t*)FLASH_USER_START_ADDR) & 0x2FFE0000 ) == 0x20000000)
    {
//    printf("ADDR == 0x20000000\r\n");
      printf("跳转到应用程序\r\n");
      JumpAddress = *(__IO uint32_t*) (FLASH_USER_START_ADDR + 4);    // 应用程序的入口地址存储在FLASH_USER_START_ADDR + 4处
      Jump_To_Application = (pFunction) JumpAddress;                  // 将入口地址转换为函数指针
        
      // 设置栈指针(MSP)为应用程序的栈顶地址 
      __set_MSP(*(__IO uint32_t*) FLASH_USER_START_ADDR);             //应用程序的栈顶地址存储在FLASH_USER_START_ADDR处
        
      __HAL_UART_DISABLE(&huart1);                                    // 禁用串口
      __HAL_RCC_USB_OTG_FS_CLK_DISABLE();                             // 禁用USB时钟
      __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TC);                   // 清除串口标志
      __HAL_UART_DISABLE_IT(&huart1, UART_IT_RXNE);                   // 禁用串口中断
      Jump_To_Application();                                          // 跳转到应用程序
    }
    printf("ADDR != 0x20000000\r\n");     //  栈顶地址无效
    printf("跳转到应用程序失败!\r\n");
}

void UP_Data(void)
{
    while(t < 1010)
    {
        MX_USB_HOST_Process();
        HAL_Delay(1);
        if(SystemUpdateFlag == 0 && Appli_state == APPLICATION_READY)       // 检查是否准备好进行固件更新
        {
            printf("检测到升级程序(U盘已经插入)\r\n");
            SystemUpdateFlag = 1;
            t = 1000;
            state = 1;
            
            //挂载U盘
            res = f_mount(&USBHFatFS, (TCHAR const*)USBHPath, 0);
            if(res != FR_OK)
            {
                printf("U盘挂载失败  %d\r\n", res);
                Error_Handler();
            }
            else
            {
                printf("U盘挂载成功\r\n");
            }
            
            //打开U盘文件
            res = f_open(&USBHFile, filename, FA_READ);
            if(res != FR_OK)
            {
                printf("打开U盘文件失败  %d\r\n", res);
                Error_Handler();
            }
            else
            {
                printf("打开U盘文件成功\r\n");
            }
            
            //读取U盘文件           读取固件数据到RAM_Buffer
            res = f_read(&USBHFile, RAM_Buffer, sizeof(RAM_Buffer), (void *)&APP_Size);
            if(res != FR_OK)
            {
                printf("读取U盘文件失败  %d\r\n", res);
                Error_Handler();
            }
            else
            {
                printf("读取U盘文件成功\r\n");
            }   
            
             // 检查固件大小是否合法
            if((0<APP_Size) && (APP_Size<FLASH_USER_END_ADDR))     // 确保固件大小在合理范围内
            {
                printf("APP_Size大小为 : %d \r\n",APP_Size);
                printf("FLASH开始擦除\r\n");    //FLASH擦除
                FLASH_Erase_Write();          // 调用Flash擦除和写入函数
                jumpToApp();                  // 跳转到新应用程序            
            }
            else
            {
                printf("APP_Size_Erase\r\n");   //bin文件大小不符合
            }
            f_close(&USBHFile);
        }
        else
        {
            t++;
        }
        
        // 如果t超过1000且未进入更新流程,直接跳转到应用程序
        if(state == 0 && t > 1000)
        {
            state = 1;
            printf("\r\n未检测到升级程序\r\n");
            jumpToApp();
        }
    }
}

5.4 bootloader.h

#ifndef __BOOTLOADER_H
#define __BOOTLOADER_H

#include "stm32f4xx_hal.h"
#include "usb_host.h"
#include "fatfs.h"
#include "usart.h"

#define ADDR_FLASH_SECTOR_0     ((uint32_t)0x08000000) /* Base @ of Sector 0, 16 Kbytes */
#define ADDR_FLASH_SECTOR_1     ((uint32_t)0x08004000) /* Base @ of Sector 1, 16 Kbytes */
#define ADDR_FLASH_SECTOR_2     ((uint32_t)0x08008000) /* Base @ of Sector 2, 16 Kbytes */
#define ADDR_FLASH_SECTOR_3     ((uint32_t)0x0800C000) /* Base @ of Sector 3, 16 Kbytes */
#define ADDR_FLASH_SECTOR_4     ((uint32_t)0x08010000) /* Base @ of Sector 4, 64 Kbytes */
#define ADDR_FLASH_SECTOR_5     ((uint32_t)0x08020000) /* Base @ of Sector 5, 128 Kbytes */
#define ADDR_FLASH_SECTOR_6     ((uint32_t)0x08040000) /* Base @ of Sector 6, 128 Kbytes */
#define ADDR_FLASH_SECTOR_7     ((uint32_t)0x08060000) /* Base @ of Sector 7, 128 Kbytes */
#define ADDR_FLASH_SECTOR_8     ((uint32_t)0x08080000) /* Base @ of Sector 8, 128 Kbytes */
#define ADDR_FLASH_SECTOR_9     ((uint32_t)0x080A0000) /* Base @ of Sector 9, 128 Kbytes */
#define ADDR_FLASH_SECTOR_10    ((uint32_t)0x080C0000) /* Base @ of Sector 10, 128 Kbytes */
#define ADDR_FLASH_SECTOR_11    ((uint32_t)0x080E0000) /* Base @ of Sector 11, 128 Kbytes */

#define RAM_BUFFER_SIZE               ((uint32_t)30*1024)       /*KBytes*/

#define filename "APP.bin"             //识别U盘文件名称

#define FLASH_USER_START_ADDR   ADDR_FLASH_SECTOR_4   /* Start @ of user Flash area */
#define FLASH_USER_END_ADDR     ADDR_FLASH_SECTOR_4 + APP_Size 
uint32_t FLASH_Erase_Write(void);
static uint32_t GetSector(uint32_t Address);
void jumpToApp(void);
void UP_Data(void);

#endif

六、App层开发

6.1 App程序设计

       APP程序设计可以按照自己的需要进行书写(试验过程建议先进行 升级亮灯)

       本例程设计:使用FreeRTOS 随机写了几个外设任务,串口通信、LED亮灭、按键检测、DA信号DMA转换等任务供学习使用;

6.2 App魔术棒配置

IRAM1 (0x20000000 起始地址):

        这是片上SRAM,通常用于存储变量、数据结构以及堆栈等。STM32F407ZET6中,SRAM的大小是128KB(0x20000字节)。SRAM是通用的随机存取存储器,用于程序运行时的数据存储。

IRAM2 (0x10000000 起始地址):

        这是CCM RAM,它是一种紧耦合存储器,直接连接到CPU,具有更快的访问速度。在STM32F407ZGT6中,CCM RAM的大小是64KB(0x10000字节)。CCM RAM通常用于存储需要快速访问的数据,例如实时数据处理或缓存。

6.3底层代码修改

6.4 Bin文件生成

        使用fromelf.exe --bin -o "$L@L.bin" "#L"

        这条命令的含义是:在工程编译完成后,自动调用fromelf.exe工具,将生成的elf格式的可执行文件(通常是.axf文件)转换为bin格式的文件。其中,--bin参数指定输出为bin格式,-o参数指定输出文件的路径和名称,"#L"表示输入的elf文件路径和名称

注:直接在工程文件中搜索.Bin文件即可

七、注意事项

        在实现STM32 U盘IAP功能时,需要注意以下几点:

  1. 数据校验:在数据传输过程中,要进行严格的数据校验,确保数据的完整性和正确性。
  2. 分区大小:合理设置Bootloader区和App区的大小,确保App区有足够的空间存放用户程序。
  3. 兼容性:确保Bootloader和App之间的接口兼容,避免因接口不匹配导致的问题。
  4. 稳定性:在升级过程中,要确保系统的稳定性,避免因意外断电等因素导致升级失败。

八、实验过程

       从APP中复制 .Bin文件到U盘中

      修改刚刚复制到U盘中的Bin文件名为APP.bin

      然后将U盘插入USB OTG口,重新上电或复位,即可实现U盘IAP升级,实验现象如下所示

九、总结

       通过以上步骤,可以实现基于STM32的U盘IAP功能。该功能允许用户通过U盘方便地对设备进行程序升级,极大地提高了产品的可维护性和用户体验。在实际开发中,可以根据具体需求对上述方案进行优化和扩展,以满足不同的应用场景。

十、源码分享

U-disk_IAP: STM32 U盘升级程序(IAP)是一种实用的嵌入式开发技术,允许用户通过U盘对设备进行程序升级,提高产品的可维护性和用户体验。本文详细介绍基于STM32的U盘IAP功能实现,涵盖Bootloader和App层开发。通过合理分区管理Flash存储器,确保数据传输的完整性和正确性,实现稳定可靠的升级过程。该功能适用于需要现场升级的嵌入式产品,具有较高的实用价值。https://gitee.com/Lucky_17wow/U-disk_IAP

        作者为即将毕业的大学生,为提升技术水平,最近开始编写技术分享文章。如有问题或建议,欢迎在评论区交流,我会及时回复。感谢您的阅读!


转载声明:如需转载,请标注原作者及出处。


Logo

「智能机器人开发者大赛」官方平台,致力于为开发者和参赛选手提供赛事技术指导、行业标准解读及团队实战案例解析;聚焦智能机器人开发全栈技术闭环,助力开发者攻克技术瓶颈,促进软硬件集成、场景应用及商业化落地的深度研讨。 加入智能机器人开发者社区iRobot Developer,与全球极客并肩突破技术边界,定义机器人开发的未来范式!

更多推荐