正点原子STM32F407 U盘升级程序(IAP)OTA Bootloader APP USB升级+FATFS+USB Host+FreeRTOS 附上源码
STM32 U盘升级程序(IAP)是一种实用的嵌入式开发技术,允许用户通过U盘对设备进行程序升级,提高产品的可维护性和用户体验。本文详细介绍基于STM32的U盘IAP功能实现,涵盖Bootloader和App层开发。通过合理分区管理Flash存储器,确保数据传输的完整性和正确性,实现稳定可靠的升级过程。该功能适用于需要现场升级的嵌入式产品,具有较高的实用价值。
一、引言
在嵌入式开发领域,程序的现场升级功能是非常实用的,它允许用户在产品已经部署到现场后,仍能方便地对程序进行更新和维护。使用STM32实现U盘升级程序(IAP)功能,可以极大地提高产品的可维护性和用户体验。本文将详细介绍如何实现基于STM32的U盘IAP功能,主要分为Bootloader层和App层的开发。
-------------------------------------------------------------结尾附上源码---------------------------------------------------------------
二、软硬件准备
- 硬件:正点原子STM32F407ZGT6最小系统板
- 软件:CUBEMx版本:MX.6.12.0
- 调试工具:sscom串口调试助手
三、CUBEmx配置
本文暂不详细介绍CUBEmx的配置步骤,因为网上资源丰富,大家可以参考上一篇(读写U盘)配置教程,里面有提到大佬的配置链接:
注:读写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盘检测和用户操作的响应。具体流程如下:
- U盘检测与初始化:系统上电后,Bootloader首先检测U盘是否插入。这是通过USB主机功能实现的,一旦检测到U盘,便初始化FatFS文件系统,为后续的文件操作做准备。
- 固件文件读取:在成功挂载U盘后,Bootloader尝试打开并读取固件文件。文件读取操作通过FatFS的f_read函数完成,将固件数据从U盘读取到内部RAM缓冲区中。
- 固件数据校验:读取固件数据后,需要对数据进行校验,确保数据的完整性和正确性。这一步骤对于防止因数据损坏导致的升级失败至关重要。
- Flash擦除与写入:如果固件数据校验通过,Bootloader将执行Flash擦除操作,为新的固件写入腾出空间。擦除操作针对应用程序区域的Flash扇区进行。擦除完成后,将RAM缓冲区中的固件数据写入Flash。
- 跳转到应用程序:在固件成功写入Flash后,Bootloader设置好应用程序的堆栈指针和程序计数器,然后跳转到应用程序的入口点,开始运行新的应用程序。
- 错误处理与重试:如果在升级过程中任何一步出现错误,例如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功能时,需要注意以下几点:
- 数据校验:在数据传输过程中,要进行严格的数据校验,确保数据的完整性和正确性。
- 分区大小:合理设置Bootloader区和App区的大小,确保App区有足够的空间存放用户程序。
- 兼容性:确保Bootloader和App之间的接口兼容,避免因接口不匹配导致的问题。
- 稳定性:在升级过程中,要确保系统的稳定性,避免因意外断电等因素导致升级失败。
八、实验过程
从APP中复制 .Bin文件到U盘中

修改刚刚复制到U盘中的Bin文件名为APP.bin
然后将U盘插入USB OTG口,重新上电或复位,即可实现U盘IAP升级,实验现象如下所示

九、总结
通过以上步骤,可以实现基于STM32的U盘IAP功能。该功能允许用户通过U盘方便地对设备进行程序升级,极大地提高了产品的可维护性和用户体验。在实际开发中,可以根据具体需求对上述方案进行优化和扩展,以满足不同的应用场景。
十、源码分享
作者为即将毕业的大学生,为提升技术水平,最近开始编写技术分享文章。如有问题或建议,欢迎在评论区交流,我会及时回复。感谢您的阅读!
转载声明:如需转载,请标注原作者及出处。
更多推荐



所有评论(0)