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

资讯详情

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

STM32F103 Bootloader工程详解:从Keil配置到Flash分区与固件升级

STM32F103 Bootloader工程详解:从Keil配置到Flash分区与固件升级 简介本资源是面向嵌入式开发工程师与STM32进阶学习者的Keil uVision5工程级BOOTLOADER完整实现专为STM32F103ZET6芯片设计解决固件在线升级、多应用切换及安全启动等典型工业场景需求。压缩包共390个文件涵盖69个.o目标文件、69个.crf编译中间文件、53个.h头文件、48个.c源码、114个.d依赖文件及8个.s汇编启动文件辅以.axf、.bin、.hex、.map等关键输出文件完整呈现从启动配置、外设初始化RCC/TIM/FLASH/I2C/ADC、存储介质访问到跳转执行的全流程代码结构包体大小为12.4MB。已有783人下载学习可直接导入Keil uVision5环境编译调试无需额外配置即可复现BOOTLOADER硬件初始化、固件校验加载与向量表重定向等核心功能特别适合理解STM32启动机制、定制安全启动流程或开展OTA升级开发实践。 从网上下载了一个名为“BOOTLOADRT_keil_keiluvision5_stm32f103zet6_BOOTLOADER程序_zip”的压缩包解压后是一个基于Keil uVision5的STM32F103ZET6 Bootloader工程。我猜不少人拿到这类东西后第一反应是打开uvprojx直接编译然后把hex烧进去结果发现自己的板子毫无反应或者升级功能永远跑不通。这篇文章就围绕这个典型的嵌入式Bootloader工程把启动流程、Flash分区、Keil工程配置、关键代码、实测踩坑和进阶设计一次讲透。不管你手上正好是这个包还是在做其他F103系列芯片的固件升级都能从这里找到可以直接落地的思路。1. 这个zip包里装的到底是什么从工程名反推Bootloader的完整需求1.1 一张Filelist看懂Bootloader工程结构压缩包的名字挺长“BOOTLOADRT_keil_keiluvision5_stm32f103zet6_BOOTLOADER程序_zip”拆开看就是四个信息Bootloader程序、Keil uVision5工程、目标芯片STM32F103ZET6、打包格式zip。很多初学者拿到手后容易被一堆文件吓到其实一个标准的STM32 Bootloader工程文件类别就那么几种启动文件startup_stm32f10x_hd.sF103ZET6属于高密度器件必须用hd版本。系统初始化system_stm32f10x.c里面包含SystemInit和时钟配置。外设驱动GPIO、USART、Flash、CRC等标准外设库或HAL库文件。核心业务bootloader.c/main.c负责接收升级数据、擦写Flash、跳转App。协议文件ymodem.c / custom_protocol.c不同工程差异很大。分散加载文件stm32f10x_flash.sct由Keil根据Target配置自动生成也可能手动改过。输出文件Bootloader.hex、Bootloader.map、Bootloader.axf等编译产物。拿到工程后我建议不要急着编译先看三样东西readme或说明文档里有没有Flash分区表Keil的Options for Target中IROM1的起始地址和大小App工程的起始地址是否和Bootloader预留的地址一致。后面两个不对功能必然跑不起来。1.2 “BootloadRT”里的RT是什么我见过的几种叫法工程名里的“RT”没有统一标准。有人说是RealTime强调Bootloader要支持超时重传和实时反馈有人说是RT-Thread的缩写表示Bootloader里面跑了RTOS线程还有人只是随手写的“Runtime”或者“RemoteTerminal”。就F103ZET6这颗Cortex-M3内核芯片来说Bootloader这种场景我强烈不建议上RTOS。128KB的RAM不小但也谈不上宽裕Bootloader只需要在启动阶段和升级阶段工作一个裸机的状态机足够解决所有问题。引入RTOS之后中断优先级、临界区保护、调度器切换这些问题反而会引入新的不确定性调试难度成倍增加。所以看到“RT”理解成“这是一个带实时反馈/超时机制的Bootloader”就够用了。2. 上电到App运行的链路向量表、Flash分区和Keil地址配置2.1 STM32F103上电后第一条指令到底在哪里很多人以为Bootloader就是“先说你好再跳过去”但实际上Cortex-M3的启动过程有严格顺序。STM32F103上电后内核从地址0x08000000读初始栈顶指针MSP从0x08000004读复位向量然后跳转到SystemInit和我们自己写的main函数。如果没有Bootloader0x08000000就是App的起始地址。一旦加了BootloaderApp就不能再占0x08000000否则上电后跑的是AppBootloader根本没有执行机会。正确的做法是让Bootloader占据Flash低地址区App后移。例如把Bootloader放在0x08000000长度64KBApp放在0x08010000长度448KB。这是F103ZET6512KB Flash非常常见的一种划分。App的IROM1配置成0x08010000size 0x70000这样编译器生成的App代码、中断向量表都从0x08010000开始。2.2 中断向量表偏移Bootloader跳转App的命门如果只改IROM1地址而不处理中断向量表App即使能跳到main一旦产生中断串口、定时器、按键程序就会跑回0x08000000附近去取中断处理函数大概率取到的是Bootloader的异常向量结果就是HardFault或者直接跑飞。解决方法是把Cortex-M3的向量表基地址寄存器VTOR0xE000ED08指向App的起始地址。在STM32F10x标准库中SystemInit函数默认把所有中断向量表放在0x08000000。所以App工程里必须重新设置VTOR。通常在system_stm32f10x.c里加上宏定义VECT_TAB_OFFSET或者在main函数最前面自行设置。我习惯在SystemInit末尾做#if defined(VECT_TAB_OFFSET) SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET; #else SCB-VTOR FLASH_BASE; #endif其中FLASH_BASE就是0x08000000。App工程在Keil里定义宏VECT_TAB_OFFSET0x10000就能把向量表定位到0x08010000。Bootloader不需要设置这个宏因为它本身从0x08000000启动。2.3 F103ZET6的Flash分区与MDK配置F103ZET6的Flash不是均匀分扇区的。它的Flash组织是前4个扇区各16KB然后是1个64KB扇区再往后是7个128KB扇区。如果Bootloader要从0x08000000分配64KB正好覆盖前4个16KB扇区而0x08010000起步就是那个64KB扇区。这个地址选得非常巧妙App从0x08010000开始不需要跨扇区擦除。Keil里Bootloader工程的配置在Options for Target - Target页面参数BootloaderAppIROM1 Start0x080000000x08010000IROM1 Size0x100000x70000IRAM1 Start0x200000000x20000000IRAM1 Size0x200000x20000Define宏无VECT_TAB_OFFSET0x10000如果你在Bootloader里也想使用部分RAM存放升级包那可以考虑在IRAM1上拆一块例如给Bootloader分配RAM区域为0x20000000大小0x18000余下0x20018000起作为接收缓冲区。不过F103ZET6有64KB RAMBootloader自己用不了多少最简单还是全部留给运行环境。还有一个容易被忽略的点烧录时Bootloader的编程算法必须覆盖0x08000000到0x08010000App的编程算法要能覆盖0x08010000到0x0807FFFF。Keil里Flash Download栏目默认使用STM32F10x High-density Flash算法它支持512KB范围通常不用改。但如果你把App起始地址改成0x08020000同时烧录文件选错了有可能会出现烧录一半擦除整个芯片的情况。3. 在Keil uVision5里把Bootloader跑起来的完整过程3.1 工程结构为什么我建议Bootloader和App分两个工程有人喜欢把Bootloader和App放在同一个Keil工程里通过宏切换。这样做省了工程管理但坏处非常明显当Bootloader和App使用了不同的优化等级、不同的库版本时一次编译可能把两边都改了升级风险更大。我做过很多个量产Bootloader最终都回归到两个独立工程Bootloader工程只维护升级逻辑稳定后几乎不动App工程专注业务功能。这样版本管理清晰编译产物隔离也不会出现“改了一行App代码Bootloader却变了”的诡异问题。如果你下载的zip包里只有一个Bootloader工程我建议你同样新建一个App工程按照上一节的地址配置才能完整测试跳转和升级链路。3.2 升级协议选择自定义帧格式还是Ymodem串口升级协议有很多种最常见的两个方案是Ymodem协议和自定义帧协议。Ymodem是Xmodem的扩展支持文件名和文件大小信息校验用CRC16很多串口工具SecureCRT、Xshell直接支持发送文件省去写上位机的麻烦。如果Bootloader打算用“超级终端”手动升级Ymodem是首选。但如果你要做量产设备、上位机是自研的或者需要通过蓝牙/4G模块空中升级自定义帧协议会更灵活。一个最小可用的自定义协议至少要有帧头、包序号、数据长度、数据、CRC32、帧尾。比如字段长度说明帧头2字节0xAA 0x55命令1字节0x01握手0x02数据0x03结束包序号2字节从0开始累加数据长度2字节不超过1024数据N字节固件内容或命令参数CRC324字节对数据段做CRC32帧尾2字节0x0D 0x0A无论用哪种协议Bootloader都必须实现超时重传和“错误包序号不写入Flash”的机制。在线升级不是把串口收到的每一个字节都直接按顺序写Flash就能完事的网络抖动、线材干扰、静电都可能导致数据错位。3.3 核心代码跳转、Flash擦写、CRC校验跳转函数是整个Bootloader的核心。一个健壮的跳转函数不仅要设置向量表还要在跳转前关闭外设中断、恢复默认时钟防止App启动时遇到被Bootloader改过的外设状态。#define APP_ADDR 0x08010000 typedef void (*pFunction)(void); void JumpToApp(void) { uint32_t sp *(volatile uint32_t *)APP_ADDR; uint32_t pc *(volatile uint32_t *)(APP_ADDR 4); pFunction app_entry; // 检查栈顶指针是否落在SRAM范围内防止跳到垃圾数据 if ((sp 0xFFF00000) ! 0x20000000) { return; } // 关闭全局中断 __disable_irq(); // 关闭可能开启的外设和复位时钟到默认状态 RCC_DeInit(); // 重定向中断向量表到App起始地址 SCB-VTOR APP_ADDR; // 设置主栈指针取出App复位向量并跳转 __set_MSP(sp); app_entry (pFunction)pc; app_entry(); }Flash操作部分用标准外设库实现比较简单。操作前要解锁Flash擦除指定扇区然后逐字编程。以写入1024字节为例#define FLASH_BASE_ADDR 0x08010000 #define PAGE_SIZE 1024 FLASH_Unlock(); FLASH_ErasePage(FLASH_BASE_ADDR); // 注意只擦除App首扇区 for (uint32_t i 0; i PAGE_SIZE; i 2) { FLASH_ProgramHalfWord(FLASH_BASE_ADDR i, buffer[i] | (buffer[i 1] 8)); } FLASH_Lock();这里有个很容易犯错的地方F103系列Flash编程宽度是16位也就是半字。别直接用FLASH_ProgramWord它其实在底层也是分两次操作但写两个地址对于需要严格控制的场景容易踩坑。更安全的是按半字循环写入。擦除时也要看清扇区边界0x08010000起始的扇区是64KB扇区一擦就是整个64KB如果你的App固件只有几KB只擦首扇区就够了但如果App超过64KB则要依次擦除后续128KB扇区。CRC校验可以用STM32F103内置的CRC外设也可以自己软件算CRC32。内置CRC外设简单但它的CRC32算法是固定的和PC端常见的zlib CRC32不一定一致需要统一。我建议升级文件在PC端生成一个CRC32随固件一起发送Bootloader收完App后先算本地CRC再和接收到的CRC比不一致就拒绝跳转保留旧固件运行。3.4 用ST-Link实测整个升级链路把Bootloader烧进芯片后第一次测试不要直接升级。先在Bootloader里加一个超时逻辑上电后等待3秒如果串口没收到握手命令就自动跳转到App。没有这个逻辑的话Bootloader会一直阻塞在串口接收旧App永远跑不起来。实测步骤大概是烧录Bootloader到0x08000000。烧录App到0x08010000。复位板卡确认Bootloader在等待握手3秒后跳转到AppApp的LED或者串口日志正常。进入升级模式上位机发送握手再发送App bin和CRCBootloader收到后擦除、写入、校验。校验成功后复位看是否自动运行新App。尝试在App运行状态下再次触发升级确认Bootloader和App之间的“运行标志”切换正常。这里最常用的切换机制是在RAM里放一个标志位或者用一个无效的备份寄存器值。比如Bootloader先查询某个备份寄存器是否为0xA5如果是则进入升级模式否则直接跳App。App收到升级命令后先把备份寄存器设为0xA5然后复位。4. 实测中躲不过去的坑向量表、扇区擦除、断电与zip解压4.1 跳转后HardFault的排查链路我最常遇到的现象是Bootloader能打印“Jump to App”然后就卡死了进调试器一看停在HardFault_Handler。这种问题大多数人第一反应是“跳转地址不对”。但地址不对通常连跳都不会跳。实际上先检查这个顺序先用J-Link/ST-Link读0x08010000处的数据确认App是否真的烧进去了。很多“跳转失败”是因为App根本没烧录到0x08010000而是用了默认的0x08000000。确认App工程里VECT_TAB_OFFSET宏是否生效。如果没有生效擦掉App中断向量表下面几个函数试试如果是向量表错误往往一发生中断就死。确认跳转前没有开FreeRTOS/ucOS等需要维持时钟节拍的东西。跳转前调用RCC_DeInit()和__disable_irq()可以解决大部分中断冲突。在跳转函数里临时读一下App的第一个半字如果全0xFF说明那个地址是空的。排查这类问题除了看调试器寄存器还要善用Keil的Memory窗口。把地址跳到0x08010000前8字节是栈指针初始值和复位向量正常情况下后者指向0x0801xxxx且不等于0xFFFFFFFF。4.2 Flash扇区擦除边界搞错App区被抹了一半F103ZET6的Flash擦除是按扇区整块擦除不能按字节擦。有人以为从0x08010000开始擦1KB结果调用FLASH_ErasePage(0x08010000)时没意识到它会擦除整个64KB扇区导致升级包只有8KB却把0x08010000到0x0801FFFF全部擦成了0xFF。如果这期间掉电App区就是半残状态。解决办法有几种在Bootloader里不做“整包写入前擦全Flash”的操作而是边收边擦。每收到一个扇区大小的数据只擦当前扇区写完后继续。这样即使写入中途失败最多损失一个扇区不会把整个App区抹掉。更保守的做法是擦写前先在内存中缓存一整个扇区的数据擦后立即写回保证掉电时旧固件还能恢复在无备份区的情况下这是下策。最稳的方案还是做一个备份区或A/B分区在升级过程中旧固件始终完整保留。4.3 栈顶指针不加校验升级一个坏固件直接失控Bootloader跳转前最常见的偷懒写法是__set_MSP(*(uint32_t *)APP_ADDR); jump (void (*)(void))(*(uint32_t *)(APP_ADDR 4)); jump();如果App地址根本没烧录内容或者只有半截固件读到的栈顶指针可能是0xFFFFFFFF跳转后内核立刻异常。更危险的是如果栈顶指针落在合法SRAM范围内但对应内容并非有效栈程序不会马上死而是跑着跑着到处乱跳比直接HardFault更难查。所以跳转前必须做合法性校验至少检查两点栈顶指针是否落在SRAM地址范围内((sp 0xFFF00000) 0x20000000)。复位向量是否落在App地址范围内((pc 0xFFF80000) 0x08000000)更严格一点可以要求pc APP_ADDR。我见过有人在量产线上因为固件烧录异常导致整批设备“升完级变砖”最后定位到就是跳转前没做地址合法性判断把一个0xFF当成了有效跳转入口。4.4 升级掉电的补救临时存储区与回滚状态机掉电是升级场景里绕不开的问题。哪怕是插座供电的设备也可能因为用户在升级过程中拔电而遇到“Bootloader还活着但App已经被擦了一半”的尴尬局面。如果你不想做A/B分区最简单的补救方案是在Flash里另外开辟一个临时存储区先把整个固件包收完、校验通过再擦写App区。F103ZET6本身有512KB FlashBootloader占64KBApp区占448KB如果要再腾一个备份区就捉襟见肘。但可以在App区内部做临时缓冲只用于保存“升级过程中被覆盖”的旧扇区数据。实现思路是把App区视为A区和B区交替或者直接把App分成两段。升级时先写B区全部校验成功后更新引导记录再改为从B启动。如果没有A/B分区那么至少要保证“先收完整包再擦写”的流程。也就是说接收数据和Flash写入分开接收缓冲区可以很大也可以使用外部串行Flash/SD卡。很多路由器、安卓设备都是先把固件写入缓存分区再触发升级就是这个道理。4.5 关于zip压缩包本身的坑解压报错和工程路径最后补一个题外话但很多人确实会卡在这里。标题里带了“zip”字样说明工程是以压缩包形式发布的。从这类压缩包解压出来的Keil工程最常见的问题有三个解压后直接打开uvprojx提示找不到芯片或找不到PACK。这种情况通常是你的Keil MDK没有安装对应Device Family Pack。F103需要安装Keil.STM32F1xx_DFP。工程路径或文件名包含中文、特殊字符导致编译过程中编译器无法访问中间文件。Keil在这个问题上特别矫情我一般把所有工程都放在纯英文路径下比如D:\work\stm32_boot\。用某些在线解压工具或压缩软件解压时丢失了文件夹结构。Bootloader工程里有些相对路径依赖特定的文件夹层级建议用较新版本的解压软件且不要用“预览”功能老老实实解压到本地。另外zip文件本身也可能损坏。如果解压提示“file is not a zip file”或“Could not find EOCD”先确认文件是否下载完整再用命令行工具重新解压。很多网盘下载的zip容易断点不用急着怀疑工程有问题。5. 有没有更稳的玩法A/B双分区、回滚和UDS Bootloader5.1 A/B分区比“改改地址”复杂在哪里网上很多关于A/B分区的介绍一听就是“把Flash分成两个区一个跑一个备”。但真正实现时要处理的细节远不止“两份代码”存储健康标记。每次App启动后要有一个“运行确认”机制比如App在启动10秒后把某个标志置为成功否则Bootloader认为该分区不可用自动回滚到另一个分区。升级过程可以交叉也就是你可以在A区正常运行时升级B区升级完成后切换启动分区。这样的话升级期间即使断电也不影响当前正在跑的系统。切换启动分区的动作必须原子化。不能出现“标记已经写着从B启动但B还没写完”的中间态。F103ZET6的512KB Flash做A/B分区确实有点紧。如果每个App分区只有224KB很多大的App放不下。但如果是移植到F407或者更高容量的芯片这套思路就很值了。你在F103上把Bootloader和A/B状态管理写明白换芯片时只要把Flash驱动改掉业务逻辑完全复用。5.2 状态机驱动的回滚设计回滚功能不复杂但很讲究逻辑顺序。我习惯用状态机来表示Bootloader的升级生命周期状态说明跳转条件IDLE上电正常等待启动App或升级请求收到升级命令进入RECEIVERECEIVE正在接收固件包收包完整且CRC通过进入VERIFYVERIFY校验固件合法性校验通过进入WRITEWRITE擦写目标分区写入完成进入UPDATE_METAUPDATE_META更新启动标记写标记完成进入REBOOTREBOOT软复位复位后从新分区启动ROLLBACK启动失败或新分区不可用从保留分区重新启动在APP主动触发升级时Bootloader不要一上来就擦除App区。先保存一个“当前分区可用”的标记然后在另一个分区写入新固件。如果写入失败Bootloader在下次启动时检测到当前分区不可用就自动从保留分区启动同时向上位机报告错误码。5.3 从自定义协议到UDS Bootloader的迁移成本如果你的产品最终要过车规或者和诊断工具对接那迟早会接触UDS Bootloader。UDSUnified Diagnostic Services本身是应用层协议它定义了0x10、0x27、0x34、0x36、0x37等诊断服务通过CAN或DoIP承载。迁移到UDS Bootloader不只是把帧协议换了需要增加会话管理和安全访问机制比如0x27服务里的seed-key验证。需要按NRCNegative Response Code处理各种错误不再是简单回一个ACK。对应的上位机要从自研小工具换成CANalyzer、PCAN等诊断工具。我的建议是如果只是做个人项目、毕业设计或小批量产品自定义协议完全够用先把Bootloader的核心哲学“中断、地址、校验、回滚”搞清楚。如果真的要上UDS最好先完整读完UDS规范文档再基于成熟的分层架构去设计否则几千行代码写完协议栈可能还是有兼容性问题。F103ZET6的Bootloader本身没有太多秘密难的是把整个升级链路想清楚Flash怎么分、向量表怎么偏、怎么校验、怎么回滚。我在实际做量产设备时Bootloader代码往往只有几百行但它的每一个if、每一个宏定义背后几乎都是从变砖经验里抠出来的。最后再提一个小习惯不管工程量多小都要保留Bootloader的hex文件、App的bin文件以及对应的版本号记录。否则过了几个月你根本不知道哪一版固件才是能烧的“黄金版本”。本文还有配套的精品资源点击获取
返回列表