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

资讯详情

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

STM32 SD卡IAP固件升级:Bootloader设计与避坑指南

STM32 SD卡IAP固件升级:Bootloader设计与避坑指南 简介嵌入式设备维护中通过SD卡升级STM32固件是常见的远程更新方式。该场景下的开发资料包面向需要实现应用内编程IAP在线升级的嵌入式工程师覆盖引导加载程序、文件系统与固件写入等关键环节适合正在设计引导程序或有单片机开发基础的人群参考。资源共五百三十二个文件压缩后约十三兆字节其中一百三十八个C源码与一百二十个H头文件构成完整代码工程配合工程配置文件可直接打开编译三个bin与三个hex固件文件分别对应引导程序和应用程序其他编译中间文件、图片及说明文档可辅助分析工程结构与操作配置。已有二百七十人学习下载。通过这份资料可以掌握引导程序在上电后如何识别SD卡、读取并校验固件以及将数据写入内部存储器的完整流程同时可参考设计中关于外设兼容、分块处理和异常回退等策略便于在自身项目中快速实现稳定可靠的单片机固件升级功能。 干嵌入式这行难免会遇到一个场景板子已经焊好、外壳已经扣上甚至设备已经发到客户现场突然发现固件有bug要更新或者想增加个功能。这时候你总不能跟客户说“把板子拆开我用J-Link重新烧一下”。在产线上也一样几十上百块板子挨个接调试器刷程序效率低不说还容易出错。我自己在多个项目里用SD卡给STM32做固件升级IAP算是一条非常实用的路。用法很简单把新固件拷进一张SD卡插到设备上上电或者按个按键芯片自己就能完成擦除、写入、跳转。这篇就把这套方案从原理到代码、再到避坑完整记录下来。这套方案特别适合三类人刚接触IAP的嵌入式开发新手正在做产品量产方案的老手以及需要做现场远程升级支持的项目维护者。SD卡的介入让整个升级流程完全不依赖调试器也不需要联网一张读卡器加一张TF卡就能完成成本和门槛都低。1. 方案整体思路与架构拆解1.1 为什么选SD卡做IAP搞定产线和售后的升级痛点IAP的全称是In-Application Programming本质就是“程序里自带升级功能”。STM32内部Flash可以一边跑程序一边擦写只要把接收固件、写入Flash的代码提前固化在芯片里后续就能在运行状态下更新自己。常见的升级介质有好几种串口、CAN、以太网、USB、SD卡我都试过。串口和CAN需要上位机配合适合开发阶段不适合终端用户操作客户不会用串口助手。以太网和USB DFU升级体验不错但对没有网络模块、没有USB接口的低成本板子根本不适用。SD卡方案胜在三点一是存储量大一个固件几百KB连大文件都能放二是操作门槛低把文件拷进去插上就行终端用户也能教三是产线很方便做一张母卡就能批量复制比一个个接调试器快得多。所以如果板子上本来就有SD卡槽或者设备本身就需要存储日志/数据那用SD卡升级几乎是零额外成本。就算板子上没有卡槽加一个TF卡座也就是几毛钱的事比加网络模块或者USB座划算得多。1.2 Bootloader APP 的双区架构SD卡升级的核心架构是芯片内部Flash要分成两个区Bootloader区和APP区。Bootloader区固化在Flash起始地址上电最先运行主要负责初始化、检测升级标志、读SD卡、擦写Flash、跳转到APP。APP区真正的业务程序用户功能都在这里。APP需要预留一个固定偏移量编译的时候指定起始地址。上电后的流程是这样的Bootloader检查有没有升级需求有就执行升级升级完跳进APP没有升级需求就直接跳进APP。这样哪怕APP写坏了、中途断电了最多就是回到Bootloader状态设备不会完全变砖重新插卡再来一次就行。这种双区结构还有个隐性好处Bootloader本身非常精简只在必要时刷新一次一旦跑起来基本就不动了。稳定压倒一切越简单的代码越不容易出错。1.3 Flash空间分配以STM32F103RCT6为例不同的芯片Flash大小不一样分区逻辑是通用的就是“Bootloader够用就行APP越大越好”。拿我常用的STM32F103RCT6举例Flash一共256KB起始地址0x08000000我这样分区域起始地址大小作用Bootloader0x0800000032KB启动代码、升级逻辑APP0x08008000224KB业务固件参数区Flash末尾2KB2KB升级标志、状态记录Bootloader给32KB用STM32标准库或者HAL库写点读卡、擦写、跳转的逻辑编译出来大约15KB空间非常宽裕。APP起始地址必须与Flash的擦除扇区对齐STM32F103的Flash扇区是1KB一页所以APP放在0x0800800032KB对齐处完全没问题。你在Bootloader里跳转到APP时地址也就是这个0x08008000。提示分区的时候别把Bootloader给得太抠。虽然实际代码可能只有12KB但后面你要加加密、加校验、加日志空间一下就紧张了。宁可多留4KB不要后面改分区改到崩溃。2. 核心细节解析SD卡驱动、文件系统与固件包设计2.1 SD卡工作模式怎么选SPI模式还是SDIO模式STM32读SD卡有两种硬件接口SPI模式和SDIO模式。这个选择直接影响接线和工程复杂度。SDIO模式速度快可到几十MB/s四线并行传输但引脚是固定的比如STM32F1的SDIO只能挂在PC8~PC12上板子布线自由度小。而且SDIO驱动代码复杂度高处理状态机、超时、CRC错误新手摸半天不一定跑顺。SPI模式速度慢一些实际测试读一个256KB的固件SPI跑18MHz时大概一两秒但我个人强烈推荐SPI模式。理由是接线太自由了SCK、MISO、MOSI、CS四根线可以映射到任意SPI引脚板子布局非常灵活尤其适合F103这种不支持SDIO的性价比芯片。代码也简单一个SPI驱动加一个卡初始化函数就能跑。还有一点SPI模式的兼容性在低速下更好很多老卡、杂牌卡SDIO下不稳定SPI模式反而能读。有个容易踩坑的细节标准SD卡SPI模式需要四根线加电源地全程用3.3V电平。有些人看到芯片GPIO是5V容忍就以为能直连5V逻辑千万别SD卡不支持5V一接就烧。MISO这条线上最好加上拉电阻10k左右否则长走线时容易读到乱码。网上有人用“三线SPI”去接SD卡就是省了CS线我试过遇到卡内部分区或者状态机切换时非常容易出错。CS必须单独用GPIO控制该拉低拉低该拉高拉高别省。2.2 文件系统引入FATFS把文件操作变成“复制粘贴”如果直接把裸扇区数据写在SD卡上升级时会很痛苦。万一你升级脚本里算错一个扇区偏移写进去的固件就是坏的。所以我项目的标准做法是引入FATFSFatFs文件系统库把SD卡格式化成FAT32固件就是个普通文件Bootloader打开“update.bin”然后读内容、写Flash。FATFS是一个著名的开源文件系统代码量不大在嵌入式领域几乎人手一份。接入它不需要自己处理FAT表、目录项等底层逻辑只需要对接几个底层函数disk_initialize初始化SD卡、disk_read读扇区、disk_write写扇区、disk_ioctl获取卡容量等。我用的比较多的是FATFS R0.14以上版本接口更清晰对长文件名支持也更好。写磁盘操作时要注意FATFS写操作是按扇区写的你最好在写之前先确认目标扇区已被擦除。但这不是大问题因为最终我们还是把固件文件读到内存缓冲区再直接写STM32内部FlashSD卡本身只做读源。有一个容易被忽略的点SD卡的逻辑扇区大小是512字节而STM32内部Flash的编程最小单位是“字”32位4字节擦除最小单位是“页”F103是1KB/页。两者不对齐没关系只要你读文件时按块读比如每次读512字节缓冲再按4字节为一行写入Flash就没问题。但擦除必须按页对齐你要擦除整个APP区起始地址若是0x08008000则Flash擦除页地址必须从0x08008000开始这是页首地址满足要求。2.3 固件包格式不是简单放个bin就行最开始我直接把编译出来的bin文件拷进SD卡Bootloader找到就写结果有两次陷入坑一次是烧了旧版本的固件程序跑起来行为怪怪的排查半天才知道SD卡里那个bin是几周前的还有一次是SD卡上有其他文件Bootloader误抓了一个不完整的副本写进去直接启动失败。后来我养成了一个习惯固件文件头部加一个自定义报头打包成自己的格式比如.fw文件结构下面这样魔数固定值0xA5A5A5A5用来标识这是合法升级文件固件版本例如0x0102表示v1.2固件长度后面紧跟着的bin数据长度固件CRC32对整个bin数据算一次CRC32用于完整性校验目标芯片ID例如0x0410防止把别的板子的固件刷进来Bootloader读到这个文件头后先校验魔数和芯片ID再按长度读取整个bin内容并一边算CRC跟文件头里存的CRC比对全部通过才开始擦写。这样能保证写进去的固件数据是完整的、版本是正确的。同时我引入了升级标志位机制在Flash参数区存一个4字节值比如0xCAFEBABE表示“需要升级”。用户把SD卡插好程序检测到卡里存在合法的升级文件就置上标志位然后软复位重启。重启后Bootloader看到标志位执行升级流程完成后清除标志位再跳转APP。这种“先置标志再复位”的设计其实是借鉴了OTA领域的标准做法——升级过程必须是可断点恢复的。3. 实操过程从Bootloader到APP一步步跑通3.1 初始化工程用STM32CubeMX搭底子用STM32CubeMX生成工程先把用到的外设配置好。需要勾选SPI用于SD卡通信、一个GPIO输出控制SD卡CS片选、一个GPIO输入可选读卡检测引脚、UART调试日志输出有需要可以再开一个按键GPIO输入用来手动触发升级。SPI初期配成低速模式因为SD卡刚上电时支持的最高SPI时钟只有400kHz。初始化完成后可以切换到高速模式比如分频到18MHz整个读取速度会快很多。CubeMX生成的SPI初始化代码已经配置好了要改的只是速率分频系数。我习惯的做法是卡初始化阶段用一个慢速SPI句柄等CMD0、ACMD41、CMD58这些初始化命令全部返回成功后再改SPI分频系数切高速但同一个SPI总线上代码实现要清晰。3.2 Bootloader核心代码读卡、校验、擦写、跳转Bootloader的逻辑可以用下面的伪代码概括我在实际项目中就是按这个骨架填的int main(void) { /* 初始化时钟、串口、SPI、GPIO */ init_all(); /* 检查参数区的升级标志位 */ if (check_update_flag() NEED_UPDATE) { if (sd_card_init() ! OK) { log(SD card init failed); clear_update_flag(); jump_to_app(); } f_mount(sdfs, , 1); if (f_open(fp, 0:/update.fw, FA_READ) FR_OK) { /* 1. 读取并解析文件头校验魔数、芯片ID、CRC */ read_header(header); if (verify_file(header) ! OK) { f_close(fp); clear_update_flag(); jump_to_app(); } /* 2. 擦除APP区 */ flash_erase_pages(APP_ADDR, app_pages); /* 3. 按扇区读取固件数据写入内部Flash */ uint8_t buffer[512]; while (f_read(fp, buffer, sizeof(buffer), br) FR_OK br 0) { flash_write_words(write_addr, (uint32_t *)buffer, br / 4); write_addr br; } if (write_addr header.firmware_len APP_ADDR) log(update success); f_close(fp); } /* 升级完成或异常都清除标志位 */ clear_update_flag(); } jmp_to_app(); }写Flash时要特别注意F103的Flash必须先擦后写而且擦除是按页1KB进行的写是按字4字节进行的。如果你要写的数据不是4字节对齐的数据长度就要做缓冲区补全处理最后不足4字节的部分补齐0xFF再写入。我为了省事编译固件时让链接器对齐到4字节这样处理起来简单很多。跳转函数是整个流程的关键写不好直接进HardFault。标准写法如下static void jump_to_app(void) { uint32_t app_entry APP_ADDR; uint32_t app_sp *(volatile uint32_t *)app_entry; /* 检查栈顶是否在RAM范围内防止跳到一个随机地址 */ if ((app_sp 0xFFF00000) ! 0x20000000) { log(invalid app stack addr, stay in bootloader); while (1); } void (*app_reset)(void) (void (*)(void))(*(volatile uint32_t *)(app_entry 4)); /* 关闭全局中断避免中断在跳转过程中响应 */ __disable_irq(); __set_MSP(app_sp); app_reset(); while (1); }这个校验逻辑我后来一直保留着它就像一个保险丝Booterloader只认“栈顶地址在RAM范围”这个合法的APP入口特征不满足就不跳转待在Bootloader里等重新升级防止程序飞去外太空。3.3 APP侧的三处改动缺一不可APP端工程不能直接用默认配置需要改三处第一在main函数最前面重新定位中断向量表。ARM Cortex-M3上电后默认从0x08000000读取向量表而你的APP在0x08008000如果你不做任何处理中断一来就会跳到Bootloader的向量表APP里配置的中断处理函数根本不会执行。解决方式是在main最开头执行一行代码SCB-VTOR 0x08008000;HAL库的SystemInit函数在启动文件里已经被调用了有时重新定位需要在SystemInit里做但更稳妥的是在main函数第一条语句执行。如果芯片支持也可以用HAL库的HAL_SYSTICK_Config和设置VTOR的封装函数。第二修改链接地址。MDK里选择Options for Target在Target页把IROM1的起始地址从0x08000000改成0x08008000size改成0x00038000224KB这样编译出来的程序才能跑到APP区地址。第三生成bin文件。MDK编译默认生成的是hex文件hex自带地址信息但Bootloader里解析hex比较麻烦所以我统一用bin格式。在MDK的User页里加After Build命令fromelf --bin -o ./app.bin ./build/app.axf编译完之后你会得到一个裸的bin文件大小为实际代码大小通常就几十KB到一百多KB。把这个bin文件用一个小工具加上文件头生成update.fw拷进SD卡升级就指日可待了。4. 踩坑记录与排查技巧4.1 跳转APP后死机或HardFault先查三件事这是SD卡升级里最常见的坑我在项目里踩了绝对不止一次。跳转完黑屏、串口没输出、板子重启循环通常原因就三个。第一个原因是中断向量表没有偏移。APP的main里没有执行SCB-VTORAPP_ADDR或者执行时机太晚在系统初始化之后才设导致SysTick异常、串口中断一进来就乱了。第二个原因是跳转前没有关全局中断。Bootloader如果在跳转瞬间还有定时器中断在响应跳到APP后中断状态混乱很容易卡死。所以在跳转前务必__disable_irq()跳转函数最后再恢复但不用自己恢复因为APP启动代码会重新初始化中断。第三个原因是栈顶指针非法。跳转前检查一下*(volatile uint32_t *)APP_ADDR是否为RAM地址范围不是就说明APP区根本没有有效程序不要盲目跳。如果你的BOOT和APP都用了RTOS跳转前还要额外注意关掉RTOS的调度器和相关外设时钟这个更麻烦建议第一版先裸机跑通再说。4.2 SD卡初始化失败串口打印卡在CMD0/ACMD41初始化SD卡时最常见的现象就是卡返回错误日志停留在“send CMD0”然后超时。逐个排查这几个地方SPI速率。卡刚上电的时SPI时钟必须≤400kHz。如果你用一个1.8MHz甚至更快的速率去初始化很多卡直接不上电应答。片选信号。CS必须在发送命令前拉低命令发送后拉高。顺序错了卡不理会你。MISO的上拉。有的板子没接上拉MISO在空闲时电平不定读回的数据全是乱码。在MISO上挂一个10k电阻到3.3V能解决大部分杂牌卡兼容问题。电源稳定。SD卡在初始化时会拉一下电流特别是TF卡通过转接板插的时候如果供电回路设计不太好电压会被拉低到3.0V以下导致卡复位。在卡电源引脚附近加一个10uF~100uF的电容效果非常明显。卡的类型。市面上有的卡对ACMD41的响应很慢有的卡需要发CMD1MMC模式才能初始化成功。我写代码时做了一个循环先发ACMD41如果超时再试CMD1两种模式都支持兼容性明显提升。4.3 升级中途断电或者写坏怎么保证不死砖设备在升级过程中突然断电是最丑但也最真实的场景谁也拦不住。Bootloader的设计必须保证哪怕写了一半断电下次上电还能重新升级。具体做法是在Flash参数区记录升级状态比如0xA5A50001表示“准备升级”0xA5A50002表示“升级中”0xA5A50003表示“升级完成”。Bootloader上电后看到状态是“升级中”就知道上一次升级没有完成不启动APP继续等待重新升级。这样就算写到一半断电最多是APP区数据不完整但Bootloader完好还能重新升级。另外写Flash之前先擦除参数区里“升级完成”的标记代表旧固件已经不再可靠。升级成功后再写“升级完成”再跳转。逻辑顺序搞反了会造成一种可怕的场景APP写得一塌糊涂标志却显示“升级完成”Bootloader直接跳进去然后卡死。如果你希望升级更保险可以做双APP备份就是ABC三区结构这适合产品已经量产、误升级会造成严重损失的场景。不过那样代码量会翻倍第一个版本还是把基本流程跑通为主。4.4 文件系统挂载失败、读文件报错挂载FATFS时如果返回FR_NO_FILESYSTEM通常不是因为代码问题而是SD卡没格式化或者格式不对。SD卡尽量用FAT32格式FAT16也行不要用exFAT很多FATFS版本默认没开启exFAT支持。格式化时不要选快速格式化格式化彻底一点文件系统表干净读文件时错误少。如果卡里有多个分区或者有隐藏分区FATFS默认只挂载第一个分区你固件放第二个分区读了半天都是空的。最简单的做法是一张卡只分一个区。有读者问过我是否要开FATFS的长文件名支持我的建议是升级文件最好用短文件名比如UPDATE.FW一来避免长文件名编码和Unicode相关的坑二来访问更快。如果一定要支持中文文件名需要在配置里开启LFN并处理编码转换纯属给自己找事。5. 进阶建议固件校验与防篡改最后聊聊固件安全。SD卡升级有个天然劣势——SD卡是物理可访问的别人把卡拿走就能拿到你的固件文件甚至改一改再放回去烧出恶意版本。如果你的产品有固件保护需求不要只依赖CRC校验CRC只是完整性校验不是安全校验。我现在的方案是把固件包的校验升级成SHA256 签名认证。对bin内容算SHA256然后用私钥对这个哈希做签名把签名放到文件头里。Bootloader里固化了公钥升级时先用公钥验签名签名通过才允许写入。这样哪怕别人改了固件内容没有私钥也伪造不了合法签名。代价是Bootloader体积会增加一些启动时间稍长但对于有抄板、篡改风险的产品这笔投资很值。另外还可以做加密存储把SD卡里的固件文件用AES加密Bootloader解密后再写入Flash这样即使卡里的文件被拷出去别人也得不到明文固件。密钥可以存在芯片内部Flash的选项字节区域或者其他受保护区域。这套方案我落地过逻辑不复杂Bootloader里多一个解密步骤但能有效防止固件被直接读取。我个人实际操作中体会最深的一点是SD卡升级不是难在哪个单一环节而是整个链条上的细节太多任何一环出问题都会导向“升级失败”这个糟糕的结果。建议你拿到文章后先把最简单的流程跑通不加密、不校验版本就先跳转成功一次再逐步加安全校验这样遇到问题也能准确定位。做完了你会发现这套升级方案以后在项目里可以一直复用性价比极高。本文还有配套的精品资源点击获取
返回列表