尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F407基于HAL库的U盘IAP升级方案实战

STM32F407基于HAL库的U盘IAP升级方案实战 简介面向嵌入式开发者的STM32F407引导加载程序在线升级方案基于硬件抽象层库实现通过USB接口读取U盘中的应用程序固件并进行更新。方案包含引导加载程序与用户程序两个完整工程覆盖U盘文件系统识别、升级文件检测、指定存储区擦写、串口状态打印及指示灯闪烁指示等关键环节适合需要为基于该芯片的产品增加USB升级能力的开发者参考或直接移植。资源包共六百八十个文件其中头文件二百五十个、C语言源文件二百二十个并包含工程配置文件、烧录脚本及编译生成的固件等压缩包大小约二十七兆字节目录结构清晰便于按模块对照学习。已有二百七十六人学习使用尤其适用于正在调试在线升级流程或希望理解USB主机与文件系统配合的嵌入式工程师。整套方案不仅给出可运行的工程代码还完整展示了从U盘设备枚举、文件系统挂载到固件读取与存储区写入的整个实现思路有助于快速定位在线升级中的常见问题缩短产品固件维护升级的开发周期。 做嵌入式开发的朋友应该都有过这种经历产品已经交付或者贴片生产了结果发现固件有Bug需要更新偏偏产品外壳封死了调试口也引不出来只能拆机用烧录器重新擦写。这时候如果当初给MCU留了一个IAP升级通道就完全不用这么被动。最近我在一个基于STM32F407的工控项目里就把BootLoader的IAP升级方案完整落地了。这个项目外部存储用的是U盘所以我直接采用了USB接口 Host模式读U盘的方式来做升级整个方案基于HAL库开发。和传统的串口Ymodem、CAN升级、网络升级相比U盘升级的体验非常接近消费电子把升级文件.bin拷进U盘插上开发板上电自动识别并完成升级不需要上位机不需要串口线现场操作人员零学习成本。写这篇文章之前我搜索整理了一些相关的热搜词和网络热词发现大家对“STM32F407探索者开发板V2和V3怎么判断”“HAL库驱动DHT11”“STM32F103C8T6的HAL库BootLoader”这类问题关注度都很高说明现在用HAL库做BootLoader已经是很普遍的需求了。不过网上的资料大多是串口IAP真正把HAL库、USB Host、FatFs、Flash擦写、APP跳转这一整套链路完整串起来讲透的文章并不多。这篇文章我就把自己踩过的坑和最终跑通的方案完完整整写出来希望能帮大家少走弯路。1. 为什么选U盘作为升级载体先想清楚IAP的通信通道很多人在做IAP方案选型的时候第一个念头就是“用串口嘛简单”。确实串口IAP是最经典的做法HAL库下用Ymodem协议配合超级终端或者自定义上位机几百行代码就能跑通。但它有几个天生的痛点一是速度慢115200波特率下传一个100KB的固件要将近一分钟二是需要一条串口线现场调试人员未必有那个环境三是没有文件系统参与数据校验和断点续传都要自己在协议层实现。U盘方案最核心的优势是“人机交互简单”。把固件拷贝到U盘里插上去上电就自动完成升级。这一整套交互对最终用户来说完全无感不需要安装任何驱动也不需要理解任何协议。特别是对于设备已经部署到现场的情况一个普通操作工也能轻松完成升级动作。从技术链路来看U盘方案在STM32F407上其实是由几个模块共同协作完成的USB Host驱动F4系列内部有完整的USB OTG FS/HS外设HAL库提供HAL_HCD_*这一组Host模式API用来枚举U盘、管理端点通信。Mass Storage ClassMSCU盘是标准的大容量存储设备HAL库通过USBH_MSC_*来处理它与SCSI命令集的交互。FatFs文件系统U盘上的数据是以FAT16/FAT32/exFAT文件系统组织的我们需要把FatFs挂载到MSC设备之上才能用f_open、f_read这样的接口去读取固件文件。内部Flash擦写读到的bin文件数据要写入到APP分区的Flash地址这需要操作STM32F407的Flash控制器执行扇区擦除和编程操作。跳转逻辑BootLoader验证完固件后需要通过修改向量表、重设栈指针、跳转到APP入口这几个关键动作把CPU控制权交给用户程序。在USB Host模式下STM32F407需要外接一个USB Host接口的座子通常是Type-A母座U盘直接插在这个座子上。这里有个硬件的坑要提醒大家不是所有的USB座子都支持Host模式必须使用带ID线检测的USB OTG座或者直接把ID引脚拉低同时VBUS要能够对外输出5V供电。我在第一版设计里用了普通的USB转串口小板上的那种Type-A母座做测试结果发现VBUS是由外部供电还是由MCU板子供电在枚举的时候表现差异很大。建议使用F407 discovery板或者探索者板那种标准的USB OTG接口外围电路齐全调试起来省心。为什么选择F407而不是F103这里顺便说一句。F407的主频是168MHz内置192KB SRAM和1MB FlashUSB OTG FS/HS都支持。做U盘IAP的话Flash容量大、RAM充足跑FatFs和USB Host协议栈不会有内存压力。F103虽然在很多老项目里还是主力但它的USB只有Device模式没有Host模式要做U盘升级就得外挂CH376之类的芯片复杂度反而上去了。所以如果你是新项目选型直接上F407是更合理的。2. 内存布局与跳转核心BootLoader最关键的两个数字IAP的本质是“一个程序去启动另一个程序”。整个系统Flash需要分成两个区域这个划分是从链接脚本层面就要确定的。我项目的F407芯片Flash是1MB实际布局如下区域起始地址大小存放内容BootLoader0x0800000032KBBootLoader程序 升级逻辑APP0x08008000960KB用户应用程序标志位区域0x08007C001KB升级标志、固件版本等BootLoader放在0x08000000这是STM32的默认启动地址上电后CPU从这一地址取向量表。APP从0x08008000开始也就是偏移了32KB。这个偏移值不是随便定的它必须是你BootLoader实际占用Flash大小的整数倍并且要匹配Flash扇区的大小因为STM32F407的Flash擦除最小单位是扇区sector不同位置扇区大小不同。F407的Flash扇区布局是固定的Sector 016KB地址0x08000000 ~ 0x08003FFFSector 116KB地址0x08004000 ~ 0x08007FFFSector 216KB地址0x08008000 ~ 0x0800BFFFSector 316KB地址0x0800C000 ~ 0x0800FFFFSector 464KB地址0x08010000 ~ 0x0801FFFFSector 5~11128KB每个依次向上扩展我BootLoader编译出来约20KB左右如果放在Sector 016KB放不下需要Sector 0 Sector 1共32KB。那么APP就必须从Sector 20x08008000开始正好是32KB偏移处。这个位置选得非常整齐。如果你只是把BootLoader放在Sector 0而APP偏移设置为0x08004000那么BootLoader一旦超过16KB就会踩进APP区域后果非常严重。所以编译完BootLoader之后第一时间看生成的.map文件确认实际占用空间再回头定APP偏移。别偷懒跳过这一步。APP工程这边需要配置两个关键地方KeilMDK-ARM中的IROM1起始地址和大小默认是整个Flash的0x08000000和0x100000需要改成0x08008000和0xF8000960KB。中断向量表重映射在APP的main函数最早位置调用SCB-VTOR APP_FLASH_ADDR;告诉CPU中断向量表已经从0x08000000换到了APP地址处。不做这一步APP里的任何一个中断定时器、串口、USB等触发后CPU都会去原来的向量表取中断入口取到的全是BootLoader的中断处理函数程序必死无疑。这两件事是IAP方案里“常识级别”的操作但也是新手最容易忽略的地方。我见过不少人在跳转成功后main函数起不来或者起来了但一进中断就HardFault十有八九就是向量表没有重映射。跳转的代码逻辑其实非常简洁就是一个函数指针的调用typedef void (*pFunction)(void); void JumpToApp(uint32_t app_addr) { uint32_t app_stack_addr *(volatile uint32_t *)app_addr; pFunction app_entry (pFunction)*(volatile uint32_t *)(app_addr 4); if ((app_stack_addr 0x2FFE0000) 0x20000000) { __disable_irq(); HAL_UART_DeInit(huart1); // 如有需要反初始化外设 SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; /* 设置主栈指针 */ __set_MSP(app_stack_addr); /* 跳转到APP的Reset_Handler */ app_entry(); } else { /* 栈顶地址非法说明APP区没有有效固件 */ } }app_stack_addr是APP程序起始4字节处的值那是APP的初始栈指针__initial_sp。判断它的高16位是否为0x2000是为了确认这个地址落在SRAM范围内防止从没有烧写过固件的Flash空白区取值后跳进一个非法地址。我实际做的时候还加了HAL_RCC_DeInit()把时钟配置恢复默认以及把用到的外设都DeInit一遍不然跳转之后外设中断挂起状态可能残留。跳转之后APP的Reset_Handler会重新配置时钟、重设向量表整个系统相当于“冷启动”了一遍但此时它运行在APP的Flash地址上。这是非常干净的状态。3. USB Host FatFs 的组合怎么把U盘里的文件变成能读的数据流U盘升级方案的技术难点并不在“跳转”那只是几十行代码的事。真正的核心工作量在USB Host枚举U盘 FatFs挂载文件系统 读取bin文件这一条链路上。这也是整个系统里出错率最高的部分。3.1 CubeMX里怎么配置USB OTG FS为Host模式我使用的是STM32CubeMX自动生成初始化代码再在生成框架的基础上做业务逻辑。在CubeMX里需要配置选择USB_OTG_FSMode选择Host_Only。在Middleware里勾选USB_HOSTClass选择Mass Storage Host Class。HAL库会自动包含MSC相关的驱动。再勾选FATFS配置连接为USB。这里要注意FatFs在CubeMX里支持多种底层接口我们这里选USB它会自动生成disk_ioctl、disk_read、disk_write、disk_status这几个底层函数的空壳真正实现要跟USBH的MSC回调对接。外设时钟部分要确保USB的48MHz时钟来源正确。F407的USB OTG FS要求48MHz的时钟输入CubeMX里配置RCC时会自动算出不需要手动干预。但是如果你用了HS高速模式还需要外接ULPI PHY芯片一般的U盘场景用FS全速12Mbps就够了速度虽然不算快但对几十KB到几百KB的固件升级来说已经完全够用。3.2 设备枚举和挂载两个易错点USB Host枚举U盘的过程对用户来说是透明的插入U盘后MSC的驱动会自动完成设备描述符读取、配置、Set Address、SCSI INQUIRY、READ CAPACITY等一系列操作然后通过回调通知上层“设备就绪”。这里有一个很常见的坑U盘的枚举和FATFS的挂载不是同步完成的。你在main里初始化完USBH之后不能直接调用f_mount因为此时U盘可能还没有完成枚举。你必须等待USBH_MSC_*状态变成APPLICATION_READY或者注册回调函数在设备就绪后再挂载文件系统。我用的HAL库标准流程是在USBH_MSC_ApplicationCallback里处理状态变化。这个回调是事件驱动的设备的插入、移除、枚举成功、枚举失败都会触发。在枚举成功的回调里执行f_mount、在移除的回调里执行f_unmount逻辑上就顺了。需要说明的是这个“基于回调函数”的写法不是唯一的方案在很多量产项目里也有直接在while(1)里轮询USBH_MSC_GetState直到状态就绪再挂载的做法。两种方式都能运行稳定关键看你的整体架构更偏向事件驱动还是轮询驱动。我的经验是回调方式代码结构更清晰尤其是需要支持U盘热插拔的场景轮询方式要小心状态机的处理顺序容易在拔插过程中漏掉某个边界事件。另外USB Host模式下U盘的热插拔本身就有一定的不确定性。USB协议允许设备在枚举过程中随时断连所以代码里对枚举失败、超时、设备移除都要有保护逻辑不能让系统卡死在等待U盘响应的循环里。F407作为Host对U盘供电能力有限如果U盘功耗较大比如一些老款机械移动硬盘或者劣质U盘会出现枚举成功但读取不稳定、中途掉线的问题。最好选择USB 2.0标准且功耗低的正规U盘测试容量上FAT32格式的16GB及以下U盘兼容性最佳。64GB以上的U盘大概率是exFAT格式FatFs是支持exFAT的但需要开启宏定义_FS_EXFATCubeMX默认生成的代码里需要手动确认这个宏是否打开。实际项目里我建议统一要求使用FAT32格式的U盘省得在文件系统兼容性上反复折腾。3.3 FatFs读取bin文件注意f_read的缓冲长度文件挂载成功之后读取bin文件就简单了FIL file; UINT bytes_read; FRESULT res; res f_open(file, 0:/firmware.bin, FA_READ); if (res FR_OK) { do { res f_read(file, buffer, sizeof(buffer), bytes_read); if (bytes_read 0) { ProgramFlash(app_write_addr, buffer, bytes_read); app_write_addr bytes_read; } } while (bytes_read 0 res FR_OK); f_close(file); }这里有个性能问题需要特别注意。f_read一次读多大、用什么缓冲区直接影响Flash擦写策略。F407的Flash编程支持16bit、32bit、64bitHAL库底层是按双字64bit一次写入的设计。缓冲区建议用__attribute__((aligned(4)))对齐到4字节并且大小设为Flash最小擦除单元扇区的整数倍或者能被编程单元整除的值。我用的缓冲区是4KB因为F407的Flash编程是每次写8个字节双字4KB正好是它的整数倍不会出现最后几个字节不足一个双字导致写失败的问题。读文件的过程中还要对读取的每一帧数据做合法性判断。bin文件是纯二进制数据首位不重要但整个文件长度必须与预期APP大小匹配。一个稳妥的做法是在bin文件的末尾追加一个自定义的尾部结构里面写入固件版本号、文件长度、CRC32校验值。BootLoader读完整个文件后先校验CRC通过后再写入Flash。也可以在读到文件末尾时单独读取尾部的固定偏移字节来校验长度。这个做法完全取决于你如何生产bin文件——keil里build完是纯bin需要写一个小脚本在build后自动追加头部或尾部信息。4. Flash擦写策略与升级标志从“读文件”到“烧进Flash”之间那点事Flash的擦写逻辑是整个BootLoader里最需要谨慎处理的部分。F407的Flash特点是可写但必须先擦除擦除的最小单位是扇区且擦除后数据全为0xFF。所以你的写入流程一定要设计成“扇区对齐”的形式否则会出现一个扇区里既有新数据又有旧数据残留的混乱情况。4.1 扇区擦除与写入的代码骨架HAL库操作F407片内Flash的API很简单就三个函数HAL_FLASH_Unlock()解锁Flash控制器默认上电后是锁定状态。HAL_FLASHEx_Erase()执行扇区擦除。HAL_FLASH_Program()执行编程支持FLASH_TYPEPROGRAM_BYTE、HALFWORD、WORD、DOUBLEWORD。HAL_FLASH_Lock()操作完成后重新加锁。我的擦写流程是这样的void ProgramFlash(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t sector_to_erase; // 判断当前地址属于哪个扇区执行扇区擦除 for (int i 0; i sizeof(flash_sector_table)/sizeof(flash_sector_table[0]); i) { if (addr flash_sector_table[i].addr addr flash_sector_table[i].addr flash_sector_table[i].size) { sector_to_erase flash_sector_table[i].sector_num; break; } } // 先把整个扇区擦除干净 FLASH_EraseInitTypeDef erase_init {0}; erase_init.TypeErase FLASH_TYPEERASE_SECTORS; erase_init.Sector sector_to_erase; erase_init.NbSectors 1; erase_init.VoltageRange FLASH_VOLTAGE_RANGE_3; uint32_t page_error 0; HAL_FLASHEx_Erase(erase_init, page_error); // 按双字64bit写入 for (uint32_t i 0; i len; i 8) { uint64_t data64 0; memcpy(data64, data[i], 8); HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr i, data64); } }这里我把扇区表做成了查找表通过地址反向查扇区号。在升级超大固件或者文件长度跨越多个扇区时还需要逐扇区循环擦除不能一次性把APP区域所有扇区都擦掉——因为如果中途断电整个APP就全毁了没有任何挽救余地。稳妥的升级流程是“边擦边写每擦一个扇区马上写满该扇区”这样中途断电时最多只是损坏当前扇区如果配合双备份A/B分区甚至可以做到升级失败自动回滚。4.2 升级标志位如何判断“上电后要不要进入升级模式”标题里“插入U盘上电识别升级文件”这个交互看起来只是一个动作其实背后涉及一套升级触发策略的判断逻辑。BootLoader上电后需要决定两件事是直接跳转APP运行正常固件还是留在BootLoader进入升级流程。我采用的策略是双条件混合无条件升级如果检测到U盘插入且存在指定文件名的bin文件则直接执行升级。条件升级如果系统中有一个标志位写在备份寄存器或者Flash特定区域被APP设置则说明APP请求了“重新升级”BootLoader会优先进入升级模式。在实际嵌入式项目里升级标志位的存储位置通常是RTC备份寄存器BKP Register或者Flash末尾的一个专有区域。RTC备份寄存器在系统复位后不会丢失同时不需要擦写Flash容易操作。但如果使用掉电不保存的备份寄存器需要确保RTC电源域一直有电。我的项目里用了一片SRAM——F407支持在备份SRAM中保留数据也可以用HAL_PWR_EnableBkUpAccess()操作。如果你的项目没有使用外部电池更建议直接在Flash末尾做一个专门的“升级状态区”每次写入时会擦写一个扇区虽然慢但只有升级时才会操作一次完全可接受。这个“插入U盘上电识别升级文件”的判断实际逻辑是这样的uint8_t CheckUpgradeRequest(void) { // 检查升级标志 if (ReadUpgradeFlag() UPGRADE_REQUESTED) return 1; // 检查U盘是否存在升级文件 if (USBH_MSC_GetState(hUsbHostFS) APPLICATION_READY) { FIL file; if (f_open(file, UPGRADE_FILE_PATH, FA_READ) FR_OK) { f_close(file); return 1; } } return 0; }需要注意一点USB Host的初始化需要时间。F407的USBH库在main函数里调用MX_USB_HOST_Init()后U盘枚举是异步进行的从插入U盘到MSC设备就绪可能需要几百毫秒甚至更久。如果你的BootLoader在main里马上就去查询USB状态大概率查不到设备。我在系统里加了一个超时机制在跳转APP之前等待2秒如果2秒内U盘就绪且发现了升级文件就进入升级流程否则认为没有升级需求直接跳转APP。这个设计在用户体验上表现为——插着U盘上电如果U盘里有升级文件系统会暂停在BootLoader界面执行升级如果U盘里没有升级文件几秒后就直接进入正常程序。这个等待时间是个可以调节的权衡等太短可能U盘来不及枚举等太长会让正常开机变慢。2秒是我实测下来比较均衡的值。如果产品对开机速度敏感可以改成“同时检测按键U盘”的组合只有按键按下且U盘有文件才升级否则直接进APP。4.3 APP端如何触发“进入BootLoader升级”APP端的升级请求通常是用户通过某个指令串口命令、按键组合、蓝牙、网络等触发。APP只需要把升级标志写好然后软复位void EnterBootloader(void) { WriteUpgradeFlag(UPGRADE_REQUESTED); // 置位升级标志 HAL_NVIC_SystemReset(); // 软复位 }BootLoader上电后读取到这个标志就会执行升级流程。升级完成后清除标志在升级成功跳转APP前把标志改写为正常启动模式。之所以要这一套标志位机制是因为BootLoader无法判断“用户的真实意图”它只能根据这些状态来决定行为。直接跳转APP是默认动作只有标志置位或者检测到升级文件时才执行升级这个设计要保证在异常断电等情况下也能稳定判断。5. 从CubeMX到KeilBootLoader和APP两个工程的搭建细节5.1 BootLoader工程配置BootLoader工程用CubeMX生成关键配置项如下Flash地址保持默认0x08000000大小按需改成32KB或更大。使能USB_OTG_FS Host模式使能MSC Class。使能FATFS底层连接到USB。串口打印调试信息非必需但我强烈建议保留一个调试串口兼容Ymodem串口IAP的日志输出会大大缩短调试验证时间。打开SysTick中断FatFs和USBHost内部某些超时机制依赖它。我在调试时曾经把SysTick关了结果U盘枚举总是超时排查了很久才发现是Tick源丢了这个细节写出来提醒一下。BootLoader的main函数流程大致是int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USB_HOST_Init(); // 等待USB枚举和FatFs挂载带超时 for (uint32_t i 0; i 2000; i) { MX_USB_HOST_Process(); if (USBH_MSC_GetState(hUsbHostFS) APPLICATION_READY) { f_mount(SDFatFS, , 0); break; } HAL_Delay(1); } if (CheckUpgradeRequest()) { if (DoUpgrade() UPGRADE_OK) { ClearUpgradeFlag(); JumpToApp(APP_FLASH_ADDR); } else { // 升级失败处理失败逻辑比如继续等待或者直接跳转旧APP } } else { JumpToApp(APP_FLASH_ADDR); } }注意MX_USB_HOST_Process()这个函数它是USB Host状态机的主循环函数必须在while循环里持续被调用否则USB协议栈无法推进状态。这个函数在整个系统里是必须周期性执行的你在等待U盘插入、等待枚举、升级过程中的每个循环里都要调用它不然U盘会一直处于半死不活的状态。这一点新手经常会漏写代码的时候需要特别留意。5.2 APP工程配置APP工程同样用CubeMX生成但有两个关键改动链接脚本IROM1在MDK的Options for Target里把IROM1的Start改为0x08008000Size改为0xF8000。向量表偏移在main函数最开始进入所有外设初始化之前加一行SCB-VTOR 0x08008000;。如果你用的是GCC/arm-none-eabi工具链则需要修改链接脚本.ld中的FLASH (rx) : ORIGIN 0x08008000, LENGTH 960K。本质上和Keil的IROM1是同一个意思。另外一个很容易踩的坑是APP工程编译生成的hex文件不能直接用于IAP必须转换成bin文件因为BootLoader是按纯二进制数据烧录的。Keil里在User页签的After Build栏添加一句fromelf.exe --bin -o ./Objects/firmware.bin ./Objects/firmware.axfGCC下用objcopy -O binary firmware.elf firmware.bin即可。生成的bin文件拷到U盘根目录文件名要和BootLoader里f_open的路径完全一致大小写、后缀我用的名字是firmware.bin路径写0:/firmware.bin。注意FatFs的路径分隔符是/不是\盘符号0表示f_mount挂载的卷号。5.3 为什么默认第一个扇区放BootLoader而不是APP这里可能会有人问为什么不把APP放在0x08000000BootLoader放在后面原因在于STM32的启动机制固定从0x08000000开始取向量表。如果把BootLoader放在后面的地址CPU上电后无法启动它——除非你用的是带额外BOOT引脚的配置从SRAM或System Memory启动那都是特殊场景。常规量产方案一定是BootLoader在前APP在后这个顺序不要颠倒。如果你想玩A/B双分区就把APP区域再切成两个分区——A分区负责运行B分区负责接收新固件升级时从B分区的全量固件或A与B的差异做启动切换。F407的1MB Flash在这种架构下可以实现“升级失败自动回滚”代价是APP可用空间减半是否值得需要根据产品需求评估。6. 调试中踩过的坑和最终的验证结果这个项目从开始写代码到最终稳定跑通我前后花了将近两天时间中间踩了几个很典型的坑。挑几个印象最深的分享一下。6.1 坑一U盘能枚举但f_mount一直返回FR_NOT_READY这个坑占了整个调试时间的三分之一。现象是USBH里MSC已经是APPLICATION_READY状态了但f_mount返回FR_NOT_READY。查了半天发现是CubeMX生成的FatFs底层disk_status函数返回错误——它内部有一个标志判断U盘是否插入但这个标志的更新是在USBH_MSC_ApplicationCallback里做的。我在回调里挂载FatFs但挂载的盘符路径写错了。FatFs挂载时第二个参数传的是根目录但这只是卷标真正决定底层对应哪个物理驱动的是disk_initialize函数的实现。CubeMX生成代码里USB对应的disk是USBH_MSC_Status这本来没错但我在f_mount之前没有显式调用disk_initialize而FatFs在挂载时会检查设备状态如果底层状态是STA_NOINIT就会返回FR_NOT_READY。最终我在回调里先调用了f_mount的初始化逻辑让FatFs在这个路径上完成底层初始化问题才解决。这个问题的本质是FatFs对底层驱动的初始化时机要求在“设备物理就绪之后”而USB Host的状态切换是异步事件两者之间需要有一个明确的先后关系。建议在APPLICATION_READY回调里先加一个延时比如100ms在设备真正稳定后再f_mount。我在代码里加了50ms延时后这个问题就不再出现了。6.2 坑二跳转APP后进HardFault排查半天发现是中断残留跳转失败的表现是APP的main函数其实执行到了串口打印了日志但只要一使能中断就死机。最终定位到问题是BootLoader里打开过USB中断和SysTick跳转前没有关掉它们的中断使能。虽然我在跳转代码里调了__disable_irq()但CubeMX生成的USB中断处理函数里有pending标志残留跳转后CPU进入APP使能中断的瞬间就跑进了USB中断服务函数而这个函数在APP程序中已经没有正确映射了。解决方案是跳转前把用过的外设都DeInit干净尤其是USB Host、UART、DMA这些会产生中断的外设。一个比较保险的做法是HAL_UART_DeInit(huart1); HAL_USB_DeInit(hUsbDeviceHS); // 其实Host模式下没有这个接口需要调HAL_HCD_DeInit HAL_RCC_DeInit();然后再清空SysTick和PendSV标志。HAL_RCC_DeInit()会把时钟树恢复成复位默认状态APP的SystemClock_Config会重新配置这一步对保证系统干净非常重要。很多人只做了__set_MSP就跳转那样遗留问题一堆尤其是用过RTOS的项目跳转前如果不把所有硬件资源完全释放APP起来后大概率会碰到各种诡异的问题。6.3 坑三U盘升级中途拔掉U盘Flash里写了一堆残留数据升级过程中如果用户突然拔掉U盘BootLoader的f_read会返回错误。如果我们的代码处理不当直接退出了升级流程但此时Flash里有一部分扇区已经被擦除并写入了新数据另一部分还是老固件这个状态下的APP是没法跑的。更糟的是升级标志位没有清除下次上电又进BootLoader又发现U盘不在了直接死循环。我最终的方案是增加一个简单的“升级状态机”把升级过程拆成几个阶段UPGRADE_IDLE空闲状态UPGRADE_READING正在读取文件并写入FlashUPGRADE_DONE完成校验UPGRADE_FAILED失败任何一个阶段检测到异常文件读取失败、Flash写入失败、U盘移除都进入UPGRADE_FAILED然后清除升级标志并跳转旧APP。因为旧APP所在区域只有到了要写它的那个扇区才会被擦除如果升级过程没有跨到该扇区旧APP大概率还是完好的。如果已经擦了一半那就只能再插上U盘重试了。所以这里也呼应前面说的“边擦边写”策略——在代码设计层面尽量避免一次性擦除整个APP区域能显著降低这种拔插场景下的风险。这套方案在最终样机上实测的表现是准备一个16GB FAT32格式U盘放入firmware.bin约120KB插入开发板Host口上电后BootLoader等待约2秒识别文件然后开始擦写大概3~5秒完成升级自动重启进入APP。反复测试了20多轮包括使用不同品牌U盘、不同大小固件、升级途中断电等情况整体稳定性达到了量产交付标准。如果要给一个结论HAL库 USB Host FatFs做U盘IAP这条路是可行的而且对现场维护人员非常友好。它的开发门槛主要体现在链路较长——USB协议栈、文件系统、Flash驱动、跳转逻辑每一个环节都要求基本功扎实。但只要理解了我上面讲的这几个关键点尤其是内存布局、扇区擦除策略和跳转时的环境清理这个方案基本就能一次跑通。我个人的体会是IAP BootLoader看起来是个小功能实际上对系统级理解的要求很高你要同时懂MCU的存储架构、外设驱动、文件系统、中断机制还得考虑异常场景断电、拔插、非法固件下系统不会变砖。这也是为什么它值得被认真对待——毕竟一旦部署到现场你不可能再到每一个设备旁边去接烧录器。先把这些细节想清楚后面的量产和维护就会省心很多。本文还有配套的精品资源点击获取
返回列表