资讯详情

资讯详情

STM32F103 AB分区OTA实战:设计原理、代码实现与踩坑记录

STM32F103跑AB分区OTA说实话第一次听到这个组合我还愣了一下。F103是Cortex-M3的老古董了Flash只有几十到两百多KB一般大家做OTA都是单分区下载完直接覆盖用AB分区的确实少。但仔细一想这个需求在工业设备、物联网节点、甚至一些量产消费产品里非常真实——升级到一半掉电单分区方案直接变砖返修成本高到想哭而AB分区是升级失败还能回滚用户体验和售后压力完全是两回事。这篇文章我就把我从零复现STM32F103 AB OTA的整套思路、代码、踩坑记录全部摊开讲适合已经有F103开发基础、想给自己的产品加可靠升级机制的朋友也适合刚接触OTA、想搞明白AB分区到底是怎么一回事的入门者。1. AB OTA的整体设计思路1.1 为什么F103这种小资源芯片也要做AB分区很多人的第一反应是F103这么点Flash分两个区放固件不是浪费吗没错如果只做一个简单的远程升级单分区确实省事APP接收固件写入另一个临时区域校验完擦掉自己、把新固件搬过来再复位。但这里有个致命问题——如果擦掉自己之后、新固件还没写完突然掉电或者新固件本身有Bug启动不了设备就永远停在半砖状态只能开盖用仿真器救。AB分区的本质是永远有一份已知能跑的固件兜底。系统里有A、B两个完全独立的APP区Bootloader选择其中一个启动。升级时往不用的那个分区写写完校验通过、标记一下再重启切换过去。万一新的起不来Bootloader发现启动失败自动回滚到旧的。整个过程可用的那个分区始终没有被破坏这就是AB方案最核心的价值。F103做这件事完全可行关键在于Flash容量。我用的STM32F103RCT6256KB Flash、48KB RAM算是F103里比较舒服的配置。分完两个96KB的APP区还剩48KB给Bootloader和参数区够用。如果只有64KB的C8T6两个APP区各分24KBBootloader 8KB勉强也能做只是功能受限后面会详细讲。1.2 升级流程里的三个角色整个AB OTA系统至少包含三部分Bootloader引导程序、APP业务固件、升级服务器/上位机负责发固件。Bootloader上电最先运行检查启动标志、校验APP有效性、决定跳A还是跳B同时也是唯一的Flash搬移执行者。APP业务逻辑本身具备从服务器下载固件的能力网口、Wi-Fi模块、4G模块都行但APP不擦自己它只把新固件写入另一个APP分区然后设置标志、复位。升级服务器提供固件文件核心是一个增量/全量包 版本信息。全量包最简单我用的就是全量包。这里面有一条很容易踩的规则APP不碰自己的分区也无法直接跳转到另一个APPBootloader没被绕过。所有切换动作只能通过写标志 复位由Bootloader来完成。这样保证哪怕APP把自己跑崩了复位后Bootloader依然能做出正确选择。1.3 分区规划与状态标志F103的Flash是0x08000000开始按扇区组织。注意F103的扇区不是均匀的前4个扇区每个16KB后面才是64KB一个。我按这个特性做了如下规划区域地址范围大小用途Bootloader0x08000000 - 0x08003FFF16KB引导程序上电入口APP_A0x08004000 - 0x0801BFFF96KB运行区AAPP_B0x0801C000 - 0x08033FFF96KB运行区B参数区0x08034000 - 0x0803FFFF48KB启动标志、确认标志、升级计数参数区我单独划出来是因为F103擦Flash必须按扇区来如果标志和APP挤在同一个扇区擦写APP时会连标志一起擦掉逻辑就乱了。参数区分两个8KB区域交替写避免频繁擦写把Flash寿命耗尽——F103的Flash擦写寿命标称1万次虽然实际远超但设计上要养成好习惯。Bootloader里维护一个4字节的启动控制结构体存放在参数区固定地址上#define PARAM_BASE 0x08034000 typedef struct { uint32_t magic; // 0x5A5AA5A5 表示结构有效 uint32_t boot_target; // 下一次启动的目标分区0A1B uint32_t boot_tries; // 当前分区连续启动尝试次数 uint32_t confirmed; // 当前分区是否已被APP确认可用 } boot_param_t;每次Bootloader启动时读这个结构体根据里面的boot_tries和confirmed决定跳哪个区。逻辑上要解决的事情是升级后第一次启动是试验启动APP跑起来后主动确认自己没问题才把confirmed置1如果一直没确认、复位了好几次Bootloader就不能再往这个分区跳了要回滚到另一个。2. 硬件与环境准备2.1 最小系统清单虽然ST官方有完整的开发板但真正做产品、自己画板的人用最小系统就够。我复现时用的是自己焊的板子核心就这几样STM32F103RCT6主控LQFP64封装8MHz无源晶振 两个20pF负载电容复位电路10k电阻上拉 0.1uF电容到地3.3V电源AMS1117-3.3或者MP2315都行注意F103的VDDA引脚一定单独滤波BOOT0/BOOT1跳线帽调试下载用一个按键接PA0做APP里的触发升级按钮方便演示串口1PA9/PA10接USB转串口用于日志输出千万别省掉串口调试口。做OTA没有串口日志出了问题就像蒙眼开车光靠LED指示灯猜效率低到怀疑人生。2.2 开发环境与固件库F103的开发环境我推荐再老练一点的说法不要一上来就用CubeMX HAL库做Bootloader。不是说HAL不好而是Bootloader代码量小、逻辑死板用标准库或寄存器更直观也更容易控制Flash操作的时序。等APP层再用HAL/CubeMX加速开发两边各取所长。我用的组合是Keil MDK 5.37AC5AC6也试过F103上兼容性不如AC5稳定STM32F10x标准外设库V3.5ST官方最后的经典版本网上随处可下ST-Link V2下载器便宜好用串口助手 Xshell看日志标准库V3.5对F103来说堪称老而弥坚该有的外设驱动全都有Flash编程用FLASH_ProgramWord、FLASH_ErasePage逻辑非常直白。不过要注意标准库默认的FLASH_ProgramWord是每次写一个字在Bootloader里用没问题如果要追求速度可以改成整字写入但Bootloader不追求速度稳定第一。2.3 Keil工程的三个关键点Bootloader工程和普通APP工程有几个不同的地方第一IROM1起始地址和大小必须设置成Bootloader自己的区域。Keil里Options for Target - Target - IROM1Start0x8000000Size0x4000。如果不改链接器会把代码链接到0x08000000之后的连续地址超出Bootloader区域运行时就会和APP冲突。第二使用微库Target一页勾选Use MicroLIB。Bootloader里我用了printf打印日志MicroLIB的printf体积小不占太多Flash。第三启动文件不能丢。F103的startup_stm32f10x_hd.s放在工程里它负责初始化堆栈、调用SystemInit和main。有些精简Bootloader会自己写启动代码但F103这种成熟芯片完全没必要直接用ST官方的启动文件最省心。我这里补一句Bootloader编译时不要用--split_section之类的优化裁剪选项因为Bootloader需要精确控制代码位置链接脚本越简单越不容易出诡异问题。默认工程配置就够了。3. Bootloader的具体实现3.1 上电主流程三态判断Bootloader的核心就是一个有限状态机。我把它简化成三个分支参数区解析失败或magic不对 - 进入出厂模式尝试校验AA有效跳AA无效跳BB也无效就停在Bootloader等待串口下载。正常启动模式confirmed 1- 直接跳当前boot_target指定的分区。试验启动模式confirmed 0且boot_tries大于0- 说明上次升级完还没被确认减少boot_tries如果减到0则回滚到另一个分区否则跳转。main函数的主流程大概长这样int main(void) { // 初始化时钟、串口、Flash接口 SystemInit(); UART1_Init(115200); boot_param_t param; read_boot_param(param); if (param.magic ! BOOT_PARAM_MAGIC) { // 出厂状态默认尝试A回退B param.boot_target 0; param.boot_tries 0; param.confirmed 1; save_boot_param(param); } if (param.confirmed 0) { // 试验启动次数减一 if (param.boot_tries 0) { param.boot_tries--; save_boot_param(param); } if (param.boot_tries 0) { // 试验启动耗尽回滚到另一个分区 param.boot_target 1 - param.boot_target; param.confirmed 1; param.boot_tries MAX_BOOT_TRIES; save_boot_param(param); } } // 根据 boot_target 校验并跳转 uint32_t app_addr (param.boot_target 0) ? APP_A_ADDR : APP_B_ADDR; if (check_app_valid(app_addr)) { printf(boot to %s\r\n, param.boot_target 0 ? A : B); jump_to_app(app_addr); } else { // 当前分区无效尝试另一个 uint32_t backup_addr (param.boot_target 0) ? APP_B_ADDR : APP_A_ADDR; printf(primary invalid, try backup\r\n); if (check_app_valid(backup_addr)) { param.boot_target 1 - param.boot_target; save_boot_param(param); jump_to_app(backup_addr); } } // 两个分区都无效停留在Bootloader开串口等待恢复 printf(no valid app, stay in bootloader\r\n); while(1); }这里有个细节要注意MAX_BOOT_TRIES我设的是3。也就是升级后APP最多有3次冷启动机会去自我确认超过3次还没确认Bootloader就认为这个固件起不来强制回滚。3次听起来少但实际非常合理——正常固件启动后不到1秒就能跑完初始化、确认标志如果连3次都撑不过去那确实是有严重问题没必要继续尝试。3.2 固件包头与校验Bootloader判断一个分区里的固件是否有效靠的是固件包头和CRC32。在讲我之前先强调固件校验一定要在跳转前做而且要做得足够可靠。很多人图省事只校验一个magic结果Flash里其实是一段残缺数据跳转进去就hardfault这类事故我见过太多。我在每个APP分区的起始地址放了48字节的包头结构如下#define HEADER_SIZE 48 #define FW_MAGIC 0x46415731 // FAW1 typedef struct { uint32_t magic; // FW_MAGIC uint32_t version; // 固件版本号比如 0x00010002 表示 1.0.2 uint32_t length; // 固件有效代码长度不含包头 uint32_t crc32; // 固件数据的CRC32 uint32_t chip_id; // 目标芯片ID防止跨型号烧录 uint32_t reserved[5]; } fw_header_t;Bootloader的check_app_valid函数就是读取这个包头检查magic、chip_id然后对从app_addr HEADER_SIZE开始、长度为length的数据做CRC32计算与包头里的crc32比对。全部通过才允许跳转。CRC32算法我用的是标准ZIP格式查表法速度在F103上完全没问题。96KB的数据用查表法算一遍大约几十毫秒Bootloader启动多这几十毫秒完全可接受。这里贴出Bootloader里的CRC32轮子代码static uint32_t crc32_table[256]; void crc32_init(void) { for (uint32_t i 0; i 256; i) { uint32_t c i; for (int k 0; k 8; k) { c (c 1) ? (0xEDB88320 ^ (c 1)) : (c 1); } crc32_table[i] c; } } uint32_t crc32_bytes(const uint8_t *buf, uint32_t len) { uint32_t crc 0xFFFFFFFF; for (uint32_t i 0; i len; i) { crc (crc 8) ^ crc32_table[(crc ^ buf[i]) 0xFF]; } return crc ^ 0xFFFFFFFF; }生成包头、写包头这个动作放在打包固件时由上位机完成Bootloader只负责校验。顺便提一句很多产品会省略chip_id但Debug时一旦拿错MCU的固件刷进去跳转后外设初始化全乱排查起来特别痛苦。留着这个字段Bootloader里几行代码就能挡住绝大多数误刷。3.3 Flash擦写函数与跳转Bootloader里最关键的两个操作一个是读一个是跳转。读是校验用的跳转是执行用的。跳转函数算是AB OTA的灵魂所在网上各种跳转跑飞的帖子一大半都是因为这个函数写错了。正确的做法是typedef void (*p_function)(void); void jump_to_app(uint32_t app_addr) { uint32_t sp_value *(volatile uint32_t *)app_addr; p_function app_entry (p_function)(*(volatile uint32_t *)(app_addr 4)); // 检查栈顶地址是否在RAM范围内 if ((sp_value 0xFFF00000) ! 0x20000000) { printf(invalid SP 0x%08X\r\n, sp_value); return; } __disable_irq(); // 把外设恢复到复位状态 RCC_DeInit(); SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; // 关掉所有中断 for (int i 0; i 8; i) { NVIC-ICER[i] 0xFFFFFFFF; NVIC-ICPR[i] 0xFFFFFFFF; } __enable_irq(); // 把MSP指向APP的栈顶 __set_MSP(sp_value); // 跳转 app_entry(); }这里有几个非常关键的点第一跳转前要做RCC_DeInit()和中断清理。因为Bootloader初始化过串口、可能开了定时器如果不清理APP启动时会发现一串来路不明的中断挂在那一旦触发就进HardFault。RCC_DeInit()把所有时钟复位到默认状态是最省事的清场方式。第二检查栈顶地址是否在RAM范围。这个检查能拦住一批低级错误比如Flash里根本没有有效数据读出来的栈顶是0xFFFFFFFF或者别的乱值硬跳过去必死。我见过好多案例就是跳转前没做这个检查结果死在跳转函数里还查不出来的。第三跳转后APP要自己重新设置向量表。Bootloader跳转前不会也不能替APP设置VTOR因为设置成哪个地址是APP自己的事。这就是接下来APP端要做的工作。Flash擦写方面Bootloader不直接参与OTA下载流程时其实用不到擦写功能擦写发生在APP侧但Bootloader作为兜底恢复工具最好也保留一个通过串口接收固件并写入另一个分区的功能。我用标准库的Flash接口写了一段注意F103写Flash之前必须先擦扇区而且擦除期间CPU会暂停执行Flash上的代码——这会导致擦除时中断全部挂起所以擦除时一定不能有中断依赖逻辑在跑。void flash_write_buffer(uint32_t addr, uint8_t *buf, uint32_t len) { // 擦除目标扇区F103前4个扇区16KB后面每个64KB FLASH_Unlock(); for (uint32_t i 0; i len; i 4) { if (FLASH_ProgramWord(addr i, *(uint32_t *)(buf i)) ! FLASH_COMPLETE) { printf(flash write failed at 0x%08X\r\n, addr i); break; } } FLASH_Lock(); }扇区地址的计算要留意F103的扇区起始地址不是均匀分布的前4个扇区是16KB间隔到第4个扇区之后变成64KB间隔。我写了一个小的扇区映射函数避免擦错地方uint32_t flash_get_sector_addr(uint32_t addr) { // 0x08000000 前4个扇区16KB 后面64KB扇区 if (addr 0x08010000) { return addr ~0x3FFFu; // 16KB对齐 } return addr ~0xFFFFu; // 64KB对齐 }注意这个只适用于中等容量F10364KB Flash是类似划分和高密度256KB的前半段如果你的芯片是F103C8T6这种64KB的只有前4个16KB扇区逻辑简单很多。3.4 参数区的写入与磨损均衡参数区如果设计得太简单比如每次启动都往同一个地址写几天就写穿了。我做了一个最简单的双备份Ping-Pong机制参数区起始地址0x08034000分成两块0x08034000和0x08038000注意0x08034000之后第三个扇区刚好是64KB边界但参数区整体在最后48KB跨了两个64KB扇区所以我把参数区设计成两个独立64KB扇区只使用每个扇区的前256字节。写入时先写块A写完把magic写好下次再写就写块B交替使用。读的时候先看A的magic对不对不对就看B两个都不对就返回空。这样写寿命翻倍虽然算不上真正意义的磨损均衡但配合只在启动时写一次、APP确认时写一次的频率足够了。这个设计很简单但确实是我在项目里踩过Flash写穿坑之后学乖的。4. APP端适配4.1 APP编译链接与起始地址APP要把自己链接到0x08004000或0x0801C000这一步做错Bootloader跳过去必死。Keil里设置APP工程Options for Target - Target - IROM1Start0x8004000Size0x18000对应96KB的APP_A如果做APP_B的工程就设置Start0x801C000同时把IRAM1保持默认0x20000000大小按芯片实际RAM设RCT6是48KB。要强调的一点是A和B两个APP在编译时其实是一样的除非你要在两个分区里区分版本号。也就是说你可以用同一个工程编译出两份分别刷到A和B只是烧录地址不同。但我们做AB OTA时老固件在A新固件下载到B所以新固件的编译地址要匹配Bootloader选中的那个空闲分区。我这边的做法是APP代码里通过宏APP_SLOT_A和APP_SLOT_B控制链接地址编译时传不同的宏生成两个固件。4.2 中断向量表重定位这是新手最容易翻车的地方。F103的默认向量表在0x08000000也就是Bootloader的位置。APP跳转过来后如果不在启动代码里把向量表搬到自己的地址任何中断一来CPU去0x08000000查向量表拿到的就是Bootloader的中断处理函数地址后果不堪设想通常是进HardFault或者直接卡死。对F103来说有两种做法一种是在SystemInit()后面、进main前调用SCB-VTOR APP_A_ADDR;把向量表指向APP所在地址。用标准库时可以在main()第一行就干这事int main(void) { SCB-VTOR APP_ADDR; // 关键把这行放在外设初始化之前 SystemInit(); // ... 其余初始化 }另一种是修改启动文件startup_stm32f10x_hd.s里的复位向量处理在__main之前设置VTOR。我推荐在main里做简单直白逻辑外露。注意VTOR是在Cortex-M3上才有的寄存器F103支持得很好。如果你用的芯片是更老的M0比如F0系列它没有VTOR就得用复制向量表到RAM改起始地址的骚操作那是另一个话题。4.3 APP里的OTA升级逻辑APP这边主动发起升级通常长这样通过HTTP从服务器下载新固件到接收缓冲区用SD卡或者外部Flash做中转更好F103 RAM不够直接接收大固件。把下载来的固件带包头写入另一个APP分区。怎么知道哪个是另一个要么在编译时写死要么从Bootloader的参数区读。我推荐从参数区读这样固件可以同时支持A、B两个位置。写入完成后对数据重新算一遍CRC32与包头里的CRC32比对。不一致说明传输或写入有问题直接放弃绝不能置标志位。校验通过后写Bootloader参数boot_target指向新分区confirmed清零表示待确认boot_tries 3。执行NVIC_SystemReset()复位。APP侧的Flash写入代码和Bootloader的类似但APP是在运行状态下写Flash写的是另一个分区不会影响自己当前运行的代码。不过要注意如果APP自己也在Flash里执行擦除Flash期间CPU取指会暂停可能导致看门狗超时。所以擦写另一个分区时要么关了看门狗要么把擦写函数放到RAM里执行。把函数放到RAM执行的写法不复杂Keil里用__ramfunc修饰函数即可__ramfunc void flash_write_slot(uint32_t dst, uint8_t *src, uint32_t len) { // 在RAM里执行擦写代码 }这个坑我是真实踩过的第一次做AB OTA时APP一边跑一边擦除B分区擦除的那一瞬间IWDG触发复位导致升级永远失败查了半天才发现是Flash擦除期间代码取指暂停太久的锅。APP在下载固件时还有一个规划问题整包96KB固件不可能一次性塞进F103的48KB RAM。我的做法是分包接收每包1KB收到一包就写一包Flash。Flash写入的最小单位是16位半字1KB对齐完全没问题。传输协议用HTTP分块或者自定义分帧都行只要保证数据完整性和顺序即可。4.4 升级确认机制新固件启动后如果不主动确认Bootloader会把这次启动当作试验启动最多3次机会。所以APP必须在初始化完成后、确认自己一切正常时调用一个上报/确认函数把参数区的confirmed置1。确认的时机很重要不是main函数一进来就确认而要等关键外设都初始化完、传感器自检通过、业务逻辑能正常跑起来之后再确认。否则假设你因为某个外设初始化失败导致系统跑飞复位后Bootloader还傻傻地认为新固件已经确认成功就不会回滚了。我一般把确认动作放在已经成功进入主循环、并且完成第一轮业务协程调度之后void app_init_done(void) { boot_param_t param; read_boot_param(param); if (param.confirmed 0) { param.confirmed 1; param.boot_tries 0; save_boot_param(param); } }这个函数在初始化流程尾部调用。这样即使新固件跑起来后没过几秒才崩也已经算确认完成Bootloader不会再回滚。是否要更严格比如运行5分钟无异常才确认取决于你的产品对稳定性的要求。工业场景建议加一个看门狗 延迟确认比如运行120秒无重启再确认。5. 上位机打包与服务器部署5.1 固件打包工具Bootloader要求APP分区起始地址放一个48字节的包头这个包头不可能靠编译器自动生成需要一个小的打包脚本在编译完成后处理*.bin文件。我用Python写了一个pack_fw.py流程如下读入app.bin计算长度、CRC32。生成48字节包头填入magic、version、length、crc32、chip_id等。将包头和固件数据拼接输出app_v1.0.2.bin这个文件就是最终要烧录到另一个分区的固件包。import struct import zlib def pack_fw(src_bin, out_bin, version, chip_id): with open(src_bin, rb) as f: data f.read() length len(data) crc32 zlib.crc32(data) 0xFFFFFFFF magic 0x46415731 # FW_MAGIC header struct.pack( 5I4I, magic, version, length, crc32, chip_id, 0, 0, 0, 0 # reserved[0..3] ) # 补足48字节 header header.ljust(48, b\x00) with open(out_bin, wb) as f: f.write(header) f.write(data) print(fpack ok: {out_bin}, len{length}, crc{crc32:08X})version我建议用(major 16) | (minor 8) | patch的整型编码简单且便于比较。比如0x00010002表示1.0.2。这样Bootloader里如果要判断新固件版本必须高于当前版本直接比较整型就行。注意Python的zlib.crc32算出来的是标准CRC32和我在Bootloader里用查表法实现的CRC32ZIP多项式完全一致。这里有一个常见的坑CRC实现有几十种变体参数稍微不一样初始值、反射、异或输出结果就完全不同打包脚本和Bootloader必须用同一套参数。我在Bootloader里专门写了单元测试用一组已知字节和标准CRC结果比对确保两边一致。5.2 用nginx做简易升级文件服务器APP的固件下载功能最简单的方式就是HTTP GET拉取一个文件。调试阶段完全不用写复杂的服务器代码用nginx做一个静态文件服务器就够了。nginx配置里加一个location把固件目录暴露出来server { listen 8080; server_name _; root /var/www/ota; autoindex on; location /fw/ { alias /var/www/ota/fw/; default_type application/octet-stream; } }然后在/var/www/ota/fw/目录下放打包好的固件文件命名尽量带版本号比如app_v1.0.2.bin。APP请求的URL就是http://服务器IP:8080/fw/app_v1.0.2.bin。这里有个生产环境要注意的点不能拿文件大小当版本判断依据。一定要在请求URL或响应里带版本信息。最简单的做法是服务器上放一个latest.json{ version: 262146, url: /fw/app_v1.0.2.bin, size: 98288, crc32: DEADBEEF }APP先请求latest.json解析出最新版本号和自己当前版本比较需要升级再去拉url指定的文件。这样版本的维护方统一在服务器APP只需要一个解析JSON的小库F103 RAM小建议手动撸一个极简JSON解析器只提取自己关心的几个字段。nginx做静态服务器的好处是零业务代码、性能稳定、支持断点续传HTTP Range和ETag缓存这些都免费获得。如果不想搭服务器局域网调试阶段也可以用Python的http.server临时顶一下但正式产品里还是nginx更靠谱。5.3 一次完整的AB升级实验讲完各部分实现我把整个升级链路串一遍方便你照着做实验先用烧录器把Bootloader烧到0x08000000把旧固件包带包头烧到APP_A。上电Bootloader校验A区有效跳转到AAPP正常运行打印slot A version 1.0.1。我改了代码里一个功能把version改成1.0.2编译、用打包工具生成新固件包放到nginx目录。APP侧按下PA0按键触发升级流程请求latest.json发现版本有新于是下载新固件包按每包1KB写入APP_B。全部写完APP计算CRC32比对通过写Bootloader参数boot_target1confirmed0boot_tries3然后复位。Bootloader重启读到boot_target1、confirmed0进入试验启动逻辑boot_tries-12跳转到B区。APP_B运行打印slot B version 1.0.2初始化完调用app_init_done()把confirmed置1。升级完成。如果第7步故意不确认比如我在B区代码里加了死循环那么复位3次之后Bootloader会把目标切回A实现回滚。整个流程跑通AB OTA的核心机制就完全掌握了。一套下来大约半天时间建议你也亲手走一遍。6. 常见问题与排查思路我把复现过程中最容易遇到的问题整理成一张速查表基本覆盖99%的翻车场景。问题现象根因解决方案跳转后HardFault复位后LED不闪、串口无日志跳转前没清RCC/中断或SP检查没做跳转函数里加RCC_DeInit、NVIC清理检查SP地址中断不生效APP能跑主循环但按键中断没反应VTOR没设置或设置地址错误main最前面加SCB-VTOR APP_ADDR确认宏地址正确CRC校验失败升级时提示校验失败、不置标志打包脚本和Bootloader的CRC32参数不一致统一使用标准ZIP CRC32用已知数据测试两端算法Flash擦除导致看门狗复位擦写另一个分区时系统重启擦除期间CPU暂停取指IWDG超时把擦写函数放RAM执行或临时喂狗升级后反复重启计数3次后回滚到旧版本APP没调用确认函数或确认时机太晚初始化完成且自检通过后尽早确认必要时延迟确认两个分区都无效Bootloader卡在串口等待烧录时没带包头或烧录地址错用打包工具生成带包头固件烧录地址核对分区表串口打印乱码Bootloader日志正常APP日志乱码两边波特率或时钟配置不一致检查系统时钟配置确认SYSCLK72MHz波特率一致6.1 跳转后跑飞的正确排查顺序很多新手遇到跳转后跑飞就开始怀疑Bootloader代码其实90%是APP侧的问题。我总结一个排查顺序第一步确认APP能单独运行。把APP烧到自己的分区地址然后用调试器直接从0x08004000运行看能不能跑起来。如果单独运行就死说明APP工程本身有问题别怪Bootloader。第二步确认Bootloader校验通过、确实执行了跳转。在跳转前打印一条日志比如jump to 0x08004000看串口有没有这条。没有就是校验没过有但卡死问题多半在清外设/向量表。第三步确认APP的VTOR设置。单独运行APP时F103默认向量表在0x08000000因为调试器可能加载的是整个芯片的烧录算法掩盖了问题。但在真实跳转场景没有VTOR设置就是死路一条。所以单独运行能起不代表跳转后能起别被这个假象迷惑。第四步用调试器配合仿真看现场。在跳转函数里设断点查看sp_value和app_entry的值是否正确。app_entry一般是某个地址反汇编检查是不是Reset_Handler的入口。6.2 容量规划和扩展思路如果你用的是C8T664KB Flash分区就得精打细算区域地址范围大小说明Bootloader0x08000000 - 0x08001FFF8KB尽量精简去掉不必要的库函数APP_A0x08002000 - 0x08007FFF24KB编译选项开-Os压缩代码APP_B0x08008000 - 0x0800DFFF24KB同上参数区0x0800E000 - 0x0800FFFF8KB双备份标志C8T6这样的容量APP功能得控制得非常克制否则24KB根本不够塞。实际产品里如果Flash吃紧建议上外部SPI Flash存固件包或者改用压缩固件比如用LZMA压缩Bootloader解压但这些都是进阶话题F103本身算力一般压缩解压要考虑时间成本。后续如果要扩展可以考虑这几个方向在Bootloader里集成串口/YModem升级通道作为设备变砖后的兜底恢复手段。加入固件签名验证比如HMAC-SHA256防止恶意固件被刷入对在公网上的IoT设备这步不能省。把OTA协议从HTTP升级到HTTPS/MQTT或者加服务器端设备认证保证升级链路安全。引入增量升级只传输差异部分节省流量和Flash写入时间。我自己做完这套AB OTA后最大的体会是分区规划决定架构质感参数区位置、Bootloader大小、APP地址、确认机制这些在最开始花一小时想清楚后面能少熬三个通宵。做OTA千万不要等产品快量产了才想起来加那时候Flash布局早就定死了硬塞进去只会处处掣肘。趁项目早期把AB机制铺好后面每次迭代升级都有退路心里踏实得多。最后分享一个小习惯每次做OTA实验前先把当前能跑的双分区固件完整备份到一个known_good文件夹。升级测试翻车时烧回去只要10秒能省下大量救砖的时间。这个习惯我用了好几年从来没因为OTA实验把开发板彻底搞废过。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →