ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32F407 U盘IAP固件升级方案:BootLoader与Flash擦写详解

2026/9/1 5:28:05 拓冰建站 浏览量
STM32F407 U盘IAP固件升级方案:BootLoader与Flash擦写详解 简介一套面向STM32F407嵌入式开发者的BootLoader IAP升级工程基于HAL库实现通过USB接口读取U盘内的“APP.bin”文件上电后可自动识别并完成固件升级。压缩包采用ZIP格式共680个文件包含250个.h头文件与220个.c源文件并提供Keil工程配置、编译生成的HEX/BIN固件、链接脚本及调试辅助文件整体约27MB便于直接查看和烧录验证。已有276人学习该资源。工程分为BootLoader与BootLoader_APP两大模块BootLoader部分实现FAT32文件系统识别、USB枚举、固件查找、指定Flash地址安全擦写等关键流程并处理了U盘兼容性与升级文件校验APP部分提供串口打印和1秒周期指示灯闪烁示例串口可输出运行状态信息帮助开发者实时掌握升级进度。资源目录结构完整可直接在STM32F407开发板上运行也可迁移至实际产品中是学习IAP机制和HAL库编程的实用参考。 做量产设备的朋友应该都有这种体会产品都已经发出去了突然发现固件里有Bug或者客户要加个小功能这时候没法拆机也不能把用户设备都寄回来就只能想固件升级的办法。我最初用的是串口IAP现场还得带电脑、装驱动、开上位机操作门槛还是高。后来把方案换成了“U盘升级”——把编译好的bin文件拷贝进U盘插到设备上上电自动识别并完成升级。这套方案做下来现场几乎零门槛客户只需要插U盘、上电、等指示灯闪烁完就搞定了。这篇文章就完整记录这套基于STM32F407 HAL库的BootLoader IAP实现方案重点是USB Host接口识别U盘、读取升级文件、擦写内置Flash并跳转到App。整个方案从分区规划、CubeMX配置、关键代码实现到踩坑排错都会说到。如果你正要做一个免拆机、免电脑的固件升级方案这篇很适合照着做。1. 整体方案设计与思路拆解1.1 升级方式对比为什么最终选择U盘做IAP升级常见的方式有这么几种串口Ymodem、以太网TFTP、CAN总线、无线BLE/WiFi以及USB U盘。首版方案我用的串口方式现场问题非常多笔记本没有RS232串口需要转接、驱动不兼容、上位机版本不匹配、波特率设置错误客户操作一次能卡住好几次。后来对比了几种方案最终锁定了U盘升级理由很简单不需要电脑和线缆客户只要插入U盘、上电即可几乎不需要培训。一个U盘可以批量升级多台设备适合产线场景。不需要额外的无线模组和网关成本近乎为零。从USB Mass Storage设备读取升级文件掉电损坏概率比串口传输更低。这套方案也适合室内外的固定设备比如工控屏、采集终端、小型网关等。如果设备是移动场合比如无人机、手持终端那还是绕开U盘形式用无线空升级更合适。方案选型没有绝对的好坏关键是找到符合自己产品使用场景的那一个。1.2 IAP的基本工作流程IAPIn Application Programming的本质是“在应用内编程”。芯片上电后先进入BootLoader程序由BootLoader判断是否需要升级。如果需要升级则从外部介质读取新固件并写入内部Flash如果不需要直接跳转到App应用程序运行。这套方案的完整升级流程是这样的设备上电BootLoader开始执行初始化时钟、调试串口、USB Host与GPIO。BootLoader检测升级触发条件本方案使用跳线帽/按键控制。条件成立时USB Host枚举U盘FatFS挂载文件系统查找根目录下的update.bin。找到升级文件后读取文件内容校验CRC擦除App区域的Flash扇区然后逐块写入。写入完成后复位或直接跳转到App地址运行。App启动后设置中断向量表偏移SCB-VTOR恢复正常功能。如果U盘中不存在升级文件或者文件校验失败BootLoader直接跳转App运行保证设备不因为异常升级文件变成“砖头”。这个安全兜底逻辑非常关键后面会细说。1.3 技术栈选型HAL库、USB Host与FatFS的组合整套方案的技术栈为STM32F407 STM32CubeMX HAL库 USB Host MSC类 FatFS文件系统。选择HAL库而不是标准外设库原因很简单USB Host协议栈和FatFS这两个模块标准库底下基本没有现成的、维护良好的移植方案而HAL库在CubeMX里可以直接生成USB Host和FatFS中间件能省掉很多底层移植工作把精力集中在业务逻辑上。USB部分使用STM32F407的OTG FS接口运行在Host模式通过Mass Storage ClassMSC访问U盘。文件系统层挂载FatFS应用层只需要调用f_open、f_read等标准文件接口不需要直接面对USB协议和U盘扇区细节。2. 核心细节解析与实操要点2.1 Flash分区规划BootLoader该占多大空间STM32F407内部Flash是1MB以F407VGT6/ZGT6为例从0x08000000开始扇区大小不是均匀的这一点规划地址时要特别小心。前4个扇区Sector0~3每个只有16KBSector4是64KBSector5~11每个是128KB。我这个项目的分区方式如下区域地址范围大小用途BootLoader0x08000000 - 0x0800FFFF64KB升级引导程序占Sector0~3App0x08010000 - 0x0809FFFF576KB应用程序从Sector4开始App备份/参数区0x080A0000 - 0x080FFFFF384KB预留可做固件备份或参数存储这里BootLoader分配64KB而不是16KB是我第一版踩坑后改的。原因是USB Host协议栈、FatFS、以及Flash驱动加在一起16KB根本无法容纳。如果压缩代码把BootLoader压缩到16KB之后想加功能就非常被动。从Sector4开始放App还有一个好处如果App确实需要把整个高地址区都当作自己的代码段也不会误擦BootLoader两者扇区完全隔离互不干扰。提示STM32F4系列擦除Flash以扇区为单位最小擦除单位是16KB。规划分区时一定要对齐扇区边界否则擦除App区域时会把相邻区域的代码一起擦掉。2.2 中断向量表重映射与跳转函数BootLoader跳转App是整套系统最核心、也最容易出问题的地方。App程序编译链接时向量表默认在0x08000000但App实际运行在0x08010000所以App必须把向量表偏移量改过来。STM32F4上通过设置SCB-VTOR寄存器实现SCB-VTOR 0x08010000;这个设置要求在App启动的最早期完成一般在SystemInit函数末尾或者main函数第一句执行。如果向量表没有偏移过来一旦发生中断CPU会从0x08000000处取中断向量——也就是从BootLoader的向量表里找中断服务程序结果就是各种诡异死机。BootLoader跳转前还要做几件清理工作关闭所有已开启的外设中断防止跳转后残留中断触发。调用HAL_DeInit()恢复外设的默认状态。关闭SysTick定时器因为App会重新配置SysTick。设置主栈指针MSP为App向量表中的初始栈顶值。跳转函数的核心代码如下#define APP_START_ADDR 0x08010000 typedef void (*pFunction)(void); void JumpToApp(uint32_t appAddr) { pFunction jumpToApp; uint32_t appStack; // 1. 关闭全局中断 __disable_irq(); // 2. 反初始化BootLoader用到的外设恢复默认状态 HAL_UART_DeInit(huart6); HAL_DeInit(); // 3. 关闭SysTick SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; // 4. 从App向量表读出初始栈顶地址和复位处理函数地址 appStack *(volatile uint32_t *)appAddr; jumpToApp (pFunction)(*(volatile uint32_t *)(appAddr 4)); // 5. 重映射中断向量表到App区域 SCB-VTOR appAddr; // 6. 设置MSP并跳转 __set_MSP(appStack); jumpToApp(); }注意最后一步__set_MSP必须在跳转之前执行而不是在复位函数里执行。跳转后再设置栈指针前面已经压栈的地址就乱了程序必定跑飞。2.3 USB Host枚举与U盘识别不是插上就能用STM32F407的USB OTG FS接口做Host硬件上需要外接5V电源给U盘供电USB D/D-信号线直接连接芯片的PA11、PA12。这里有个容易忽视的点OTG FS的5V电源检测是通过PA9OTG_FS_VBUS实现的VBUS引脚需要连接到USB座的VBUS检测脚否则系统判断不到U盘插入。CubeMX中配置USB_OTG_FS为Host模式并开启Middleware里的USB_HOSTClass选择Mass Storage Class。HAL库的USB Host协议栈会自动完成枚举、地址分配、配置描述符解析等流程最终通过USBH_MSC_Read和USBH_MSC_Write接口读写U盘扇区。我在应用中检查U盘是否就绪用官方提供的回调函数或轮询状态都可以// 获取MSC类状态 USBH_StatusTypeDef mscStatus USBH_MSC_GetLUNInfo(hUsbHostFS, 0);U盘驱动这一层我踩过两个大坑第一U盘供电不足。有些U盘工作电流较大或者USB座子电源走线太细导致枚举过程中反复复位。解决方法是VBUS引脚使用粗走线并在USB座子旁边并联22uF以上电容。第二USB 3.0的大容量U盘64GB/128GB在F407上不能用这种U盘需要更大的启动电流枚举耗时太长并且F407的Host栈版本兼容性不好。实测下来找一个小容量USB 2.0 U盘4GB或者8GB格式成FAT32反而非常稳定。2.4 FatFS文件系统挂载与固件读取USB Host枚举成功只是完成了“底层设备识别”要看到U盘里的文件还需要文件系统层配合。CubeMX会自动生成FatFS中间件底层接口需要把disk_read和disk_write对接给USBH_MSC_Read和USBH_MSC_Write。这个对接是在ffconf.h和usb_disk.c里完成的CubeMX生成的工程中已经有模板一般只需要确认宏配置正确。FatFS的磁盘号、扇区大小等参数在ffconf.h里配置。比较关键的是_USE_MKFS和_USE_EXPAND这些宏如果只是读取升级文件保持默认即可。挂载和读取文件的代码长这样FATFS fs; FIL file; FRESULT res; // 挂载U盘 res f_mount(fs, 0:, 1); if (res ! FR_OK) { // 挂载失败可以跳转App或反复重试 return; } // 打开升级文件 res f_open(file, 0:update.bin, FA_READ); if (res ! FR_OK) { f_mount(NULL, 0:, 0); return; } // 读取文件头判断魔数、版本、文件长度等 // 然后循环读取数据写入Flash另一个细节大U盘打开文件前最好先调用f_stat检查文件是否存在再调用f_open避免直接打开失败时调试日志不够清晰。文件名称尽量使用全大写、短文件名比如UPDATE.BIN不要使用中文或超长文件名避免长文件名LFN支持不完全导致读取失败。3. 实操过程与核心环节实现3.1 CubeMX工程搭建我用的开发环境是STM32CubeMX 6.6 Keil MDK 5.36芯片选STM32F407ZGT6。CubeMX里的关键配置项如下RCCHSE外部晶振使用PLL倍频到168MHz主频。时钟树APB2总线84MHzAPB1总线42MHzUSB时钟48MHz。F407的USB模块必须工作在本机48MHz时钟下这一点经常有人配置错误。SYSDebug选Serial Wire用于调试Timebase Source改为TIM6避免占用SysTick。USART6用作调试日志输出波特率115200。USB_OTG_FS模式选Host_Only激活VBUS Sense。MiddlewareUSB_HOST开启Class选择Mass Storage ClassFATFS开启连接USB Disk。GPIOPA0作为升级触发引脚内部上拉外部接跳线帽到地触发升级模式。生成代码后在main.c中按需调整优先级和初始化顺序。USB Host初始化建议放在外设初始化之后、主循环之前。这里尤其注意板载USB_OTG_FS的PA9VBUS检测必须接对否则Host永远检测不到U盘插入。我用正点原子探索者的时候默认接法就是PA9是VBUS检测但有时需要外部短接不同开发板差异不小需要实测。3.2 BootLoader主流程上电检测与升级判断主流程的逻辑非常直接上电后初始化检查升级触发引脚如果触发则进入U盘升级流程否则立即跳转App。这样可以保证正常启动时不做任何USB枚举和FatFS挂载启动耗时几乎为0。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USB_DEVICE_Init(); MX_FATFS_Init(); MX_USART6_UART_Init(); // 调试打印 printf(bootloader start\r\n); // 检查升级触发条件PA0被外部跳线拉低则进入升级模式 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { printf(upgrade mode enter\r\n); if (USBH_MSC_UpgradeTask() 0) { printf(upgrade success\r\n); } else { printf(upgrade failed, jump to app\r\n); } } // 跳转App JumpToApp(APP_START_ADDR); while(1); }USBH_MSC_UpgradeTask函数的执行流程为调用HAL_Delay等待U盘稳定等待USBH_IsDeviceConnected返回真等待MSC枚举完成f_mount挂载f_open打开UPDATE.BIN读取并校验然后擦写Flash。这里的每个等待都必须加超时比如枚举超时10秒、打开文件超时3秒超时后直接返回失败并跳转App避免设备卡死在等待状态。3.3 Flash擦写函数注意扇区对齐与写入粒度STM32F4内部Flash写入时需要保证目标地址已经擦除且地址对齐到16位推荐32位。Flash擦除以扇区为单位写前要计算出目标地址属于哪个扇区。我封装了一个擦除App区域并写入文件数据的函数#define APP_FLASH_START 0x08010000 #define APP_FLASH_END 0x0809FFFF #define FLASH_SECTOR_SIZE (64 * 1024) void Flash_EraseAppArea(void) { uint32_t addr APP_FLASH_START; uint32_t sector; HAL_FLASH_Unlock(); // 依次擦除App区域覆盖到的扇区 // F407从0x08010000开始是Sector4大小为64KB之后Sector5~11为128KB while (addr APP_FLASH_END) { FLASH_EraseInitTypeDef erase {0}; uint32_t pageError 0; erase.TypeErase FLASH_TYPEERASE_SECTORS; erase.Sector GetFlashSector(addr); erase.NbSectors 1; erase.VoltageRange FLASH_VOLTAGE_RANGE_3; if (HAL_FLASHEx_Erase(erase, pageError) ! HAL_OK) { printf(erase failed at 0x%08x\r\n, addr); break; } // 计算下一个扇区地址 if (addr 0x08010000 64*1024) addr 0x08010000 64*1024; // 第一个扇区64KB else addr 128 * 1024; // 后续扇区128KB } HAL_FLASH_Lock(); }写入时按4字节对齐使用FLASH_TYPEPROGRAM_WORD类型调用HAL_FLASH_Program。从文件读取buffer时每次读出的字节数必须是4的倍数否则最后一个不足4字节的尾巴要单独处理。实际写入代码中我会用uint32_t指针操作缓冲区保证编程地址和长度都对齐。uint32_t Flash_WriteData(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t i; uint32_t *p32 (uint32_t *)data; HAL_FLASH_Unlock(); for (i 0; i len / 4; i) { if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr i * 4, p32[i]) ! HAL_OK) { HAL_FLASH_Lock(); return 1; } } HAL_FLASH_Lock(); return 0; }注意擦除Flash期间不能有中断访问Flash区域也不建议使用HAL_Delay这类依赖SysTick的延时。如果你在擦写Flash过程中开了定时器中断并且中断里有访问Flash的代码程序会卡死在Flash硬件层。生产代码里擦写期间我会关闭全局中断擦写完成后再打开。3.4 App端工程配置改VTOR、改IROM1App端要做两个核心修改缺一不可。第一个是工程链接地址在Keil的Options for Target - Target - IROM1中修改起始地址为0x08010000Size根据实际App大小填写比如0x90000。ROM起始地址不改编译出来的代码会直接链接到0x08000000跳转过去必死。第二个是中断向量表偏移。在App工程的main函数最开头或者SystemInit里加一句SCB-VTOR 0x08010000;如果你使用的是HAL库有些版本自动生成的代码会带有_ARM_LIB_STACK宏或者把VTOR设置放在system_stm32f4xx.c中。最稳的做法是直接在main开头的SystemClock_Config之前强制设置一遍这样可以屏蔽掉不同启动文件版本带来的差异。App编译时建议开启CRC校验或者在App中实现一个软复位功能等App运行后主动跳到BootLoader完成后续升级。我同事做的方案里App收到上位机的升级指令后在内存某个固定地址写入一个特殊标记然后软复位进入BootLoader。BootLoader上电后检查这个标记如果存在就直接进入升级模式这样就不需要外接跳线帽了。这个扩展思路启动时会稍微复杂一点但更适合远程/远程控制升级的场景。4. 常见问题与排查技巧实录4.1 U盘枚举失败、反复复位现象是上电后U盘指示灯亮了一下又灭USBH_IsDeviceConnected一直在0/1之间跳变调试串口没有任何打印。排查方向VBUS供电是否稳定。实测U盘工作电流波动会导致OTG_FS的过流检测反复触发检查USB座子附近的过流保护电路。PA9VBUS Sense引脚是否连接到USB座子的VBUS检测。很多开发板需要外部接跳线没接的话Host侧检测不到5V枚举直接失败。U盘和USB线质量。换了USB 2.0小容量U盘、短线问题立刻消失。长线在F407这种内置PHY上容易信号劣化建议总长不超过20cm。4.2 FatFS挂载失败、打开UPDATE.BIN失败现象是U盘枚举成功USBH_IsDeviceConnected为真但f_mount或f_open返回FR_NOT_READY或FR_NO_FILESYSTEM。排查方向检查disk_ioctl的实现GET_SECTOR_SIZE、GET_BLOCK_SIZE这些必须正确返回否则FatFS会误判U盘参数。CubeMX生成的usb_disk.c里需要根据MSC实际返回的扇区大小填充不要写死512。U盘格式必须是FAT32或FAT16。exFAT格式的U盘F407上的FatFS不支持。把U盘重新格式化为FAT32簇大小选默认即可。确认打开的是根目录文件。如果U盘里有多个分区FatFS只识别第一个可用分区。4.3 跳转App以后程序跑飞、死机现象是BootLoader打印“jump to app”后App没有任何反应调试器也连不上或者程序在HardFault_Handler里循环。这类问题八成是向量表偏移或中断清理问题。排查方向确认App工程IROM1起始地址和BootLoader里的APP_START_ADDR完全一致。确认App工程确实把SCB-VTOR设置成了0x08010000并且是在任何外设初始化之前设置。跳转前是否关闭了BootLoader用过的外设中断。如果BootLoader使用了串口中断跳转后串口中存在未处理事件App初始化时会触发意外中断。跳转前统一DeInit比较省事。检查App链接出来的bin文件头部是否正确。bin文件前4字节是初始栈指针紧接着4字节是复位向量。可以用十六进制编辑器打开bin文件检查这两个值是否合理。4.4 Flash擦写失败、擦完没数据现象是升级过程中提示擦除失败或者擦写完成后跳转App运行还是旧版本代码。排查方向Flash上电后默认是锁定的写前必须HAL_FLASH_Unlock写完后HAL_FLASH_Lock。漏掉UnlockHAL_FLASH_Program会返回HAL_ERROR。擦除回调FLASH_EraseInitTypeDef中的VoltageRange在电压不足时也会失败。F407正常工作电压3.3V时用FLASH_VOLTAGE_RANGE_3即可。写数据时长度不对齐最后一个字丢失。确保文件大小按4字节补齐读到不足4字节时剩余字节用0xFF填充再写入。4.5 常见问题速查表现象原因解决办法U盘枚举反复复位VBUS供电不足检查过流保护电路加大电容换短线USBH_IsDeviceConnected一直为0PA9 VBUS_SENSE未连接确认硬件接线或引脚配置f_mount返回FR_NO_FILESYSTEMU盘是exFAT格式或存在多分区格式化为FAT32仅使用第一个分区f_open返回FR_NOT_READYdisk_ioctl实现不完整补齐GET_SECTOR_SIZE等ioctl命令跳转App死机向量表偏移未设置在App早期设置SCB-VTOR APP地址跳转App死机App链接地址不对修改Keil IROM1起始地址为0x08010000Flash_Program返回错误Flash未解锁写前调用HAL_FLASH_Unlock()Flash_Program返回错误地址未擦除或未对齐先擦扇区地址4字节对齐升级后App还是旧版本bin文件校验有误检查文件偏移和CRC校验逻辑结尾这套基于STM32F407 HAL库的U盘IAP方案整体难度并不高核心就三块BootLoader跳转、USB Host FatFS驱动U盘、内部Flash擦写。真正花时间的是细节调试——U盘枚举兼容性、Flash擦写错误处理、跳转时中断清理每一个坑都能让程序“闷声死掉”。我做这套方案踩过最多的坑就是跳转App后死机最后发现是App工程里忘了改ROM地址。还有一个印象深刻的问题是U盘枚举失败排查了很久才发现是开发板上PA9的VBUS_SENSE没有接好。如果你也打算做这个方案建议先把BootLoader和App的分区地址印在便利贴上贴到工作台上写代码的时候反复对照比什么都管用。这个方案后续还可以扩展比如在U盘中放置包含版本号的配置文件BootLoader比对版本号决定是否升级或者在App中实现网络下载固件到U盘再由BootLoader应用组合成双通道升级。我目前正在往这个方向继续做等验证完毕再写一篇落地文章分享出来。本文还有配套的精品资源点击获取