新闻详情

新闻详情

首页 / 资讯中心 / 详情

STM32F103 AB分区OTA从零实现:防砖机固件升级方案

发布时间:2026/9/11 21:03:55来源:尧图网络
STM32F103 AB分区OTA从零实现:防砖机固件升级方案
1. 项目概述为什么AB分区OTA在STM32F103上既必要又棘手我第一次在工业现场遇到固件升级失败导致设备停机是在一家做智能电表的客户产线。那台用STM32F103C8T6做的集中器因为一次OTA中途断电整个Flash被擦写到一半Bootloader跳转时直接卡死在0x08000000——不是跑飞是连LED都不闪。后来拆开看App区前半段是新固件后半段还是旧代码校验和对不上系统反复复位。这件事让我彻底意识到裸写IAP不是升级是赌命。而AB分区就是给这个赌局加一道保险栓。“STM32F103_AB_OTA_从零复现教程”这个标题里每个词都踩在嵌入式开发的痛点上STM32F103是成本敏感型项目的主力芯片资源紧64KB Flash、20KB RAM、外设少、无硬件加密引擎AB分区不是噱头是解决“升级失败即砖机”的唯一工程解OTA不是远程发包而是要扛住Wi-Fi信号抖动、RS485总线干扰、电源跌落等真实工况从零复现意味着不依赖STM32CubeMX自动生成的Bootloader模板——那玩意儿默认只支持单区IAP连校验逻辑都要自己补。我实测过用标准库v3.50在F103上实现AB分区OTA核心难点不在代码量而在三重边界控制一是Flash页擦除的原子性F103最小擦除单位是1KB但App通常20KB起必须按页对齐二是跳转前后中断向量表的重映射HAL_Delay卡死90%是因为SysTick中断没切回主App的NVIC配置三是双区状态标记的防误写不能用普通Flash地址存标志位得用Option Bytes或保留页。这些细节官方参考手册不会写CubeMX生成的代码不会管网上搜到的“AB分区教程”90%只画流程图不告诉你Page 127擦完后Page 128为什么必须立刻校验——因为F103的Flash控制器在擦除后有隐式锁存不校验就跳转大概率触发HardFault。适合谁看如果你正在用F103做需要远程升级的产品比如环境监测节点、楼宇控制器、电机驱动板且不想每次升级都带J-Link去现场如果你试过HAL库的IAP例程但跳转后HAL_Delay失效、串口收不到数据、ADC采样值乱跳如果你查资料时被“bootloader双分区ab分区”“iap回滚”“stm32f103启动文件下载”这些碎片信息绕晕——这篇就是为你写的。它不讲理论只说我在6个量产项目里踩过的坑、调通的参数、验证过的时序。接下来我会把整个AB分区OTA拆成四块硬骨头设计逻辑怎么绕过F103的硬件限制、Flash分区怎么划才不踩坑、Bootloader怎么写才能稳稳跳转、以及现场升级时那些让你抓狂的“明明代码没错却升级失败”的排查方法。2. 整体架构设计为什么F103的AB分区必须反着设计2.1 AB分区的本质不是“两个区”而是“一个保险开关”很多人以为AB分区就是A区放旧固件、B区放新固件升级时把新固件写进空闲区再改指针跳过去。这在Linux或ESP32上可行但在STM32F103上会死得很惨。原因很简单F103没有独立的BootROMBootloader和App都烧在同一个Flash里而它的启动流程是硬件固化的——复位后永远从0x08000000开始取指令。这意味着你无法像ESP32那样在BootROM里判断该跳A还是B所有分区逻辑必须由你自己写的Bootloader完成且Bootloader本身必须永远驻留在固定地址通常是0x08000000否则芯片根本启动不了。所以真正的AB分区设计核心不是分两个App区而是分三个功能区Bootloader区0x08000000 ~ 0x08003FFF固定20KB放启动代码、升级逻辑、校验函数A区App0x08004000 ~ 0x08013FFF32KB放主程序B区App0x08014000 ~ 0x08023FFF32KB放备用程序状态区0x08024000单独1KB页存当前激活区标识0xAA55表示A区有效0x55AA表示B区有效备份区0x08025000再留1KB存校验和、版本号、时间戳等元数据。提示F103的Flash页大小是1KB高密度大容量型号但实际分区时绝不能按1KB切。因为App固件编译后通常20~25KB如果A区只划20KBB区也20KB那状态区和备份区就得挤在剩余空间里——而F103的Flash最后几页Page 127是Option Bytes所在页擦写会锁死整个芯片。我吃过亏某次把状态区放在Page 126升级时擦Page 126触发了Option Bytes保护整片Flash变只读。正确做法是预留Page 126和127为禁区状态区放Page 1250x08024000备份区放Page 1240x08023000这样即使擦错页也不会波及Option Bytes。2.2 为什么不能用HAL库默认的SystemInit()初始化NVICF103的Bootloader跳转到App后卡死HAL_Delay90%的案例根源在这里。HAL库的SystemInit()函数会调用SetVectorTable()重映射中断向量表到App区首地址比如0x08004000但这个操作有个致命前提Bootloader必须先关闭所有外设时钟再执行跳转。而标准库v3.50的startup_stm32f10x_md.s里Reset_Handler默认会调用SystemInit()然后跳main()——但Bootloader的main()里如果没手动关掉RCC、USART、SPI等时钟跳转后App的SysTick中断就会和Bootloader残留的时钟配置冲突。我调试时用逻辑分析仪抓过SysTick波形Bootloader里SysTick配置为1ms中断跳转后App也配1ms但两个中断服务程序ISR的栈指针SP没切换结果App的HAL_Delay()在等待SysTick标志位时被Bootloader残留的SysTick ISR打断SP指向Bootloader的栈区导致App的局部变量被覆盖。现象就是Delay(1000)永远不返回串口打印卡在半句。解决方案是Bootloader跳转前必须执行三步清场关闭所有使能的外设时钟RCC-APB1ENR 0; RCC-APB2ENR 0; RCC-AHBENR 0清空所有外设的中断挂起位NVIC-ICPR[0] 0xFFFFFFFF; NVIC-ICPR[1] 0xFFFFFFFF将SP强制切到App区的栈顶__set_MSP((__IO uint32_t)app_addr)。其中第三步最关键。F103的栈顶地址存在App固件的前4字节即0x08004000处的值Bootloader读出来后调用__set_MSP()设置主栈指针。这步漏掉App的任何函数调用都会因栈溢出崩溃。网上很多教程只说“跳转前关中断”但没提SP切换——因为这是ARM Cortex-M3的底层机制HAL库文档里藏在《CMSIS-Core》附录里不深挖根本找不到。2.3 校验机制必须包含“页级CRC全镜像MD5”双保险F103没有硬件CRC模块F4系列才有所以校验必须软件实现。但只算整个App镜像的CRC16是危险的如果OTA传输中某一页1KB数据错乱而CRC16恰好没检出碰撞概率约1/65536设备就会运行损坏的固件。我见过最离谱的案例某温控器升级后温度显示跳变查到最后是Flash Page 112的ADC校准参数被写错但全镜像CRC16居然通过了——因为错写的字节刚好让CRC值不变。因此AB分区的校验必须分层页级CRC16对每个1KB Flash页单独计算CRC16存入该页末尾2字节如Page 112的0x08013C00~0x08013FFFCRC存0x08013FFE。Bootloader升级时每写完一页就立即校验失败则终止升级并回滚全镜像MD5用轻量级MD5算法我用的是RFC 1321简化版代码2KB计算整个App镜像哈希存入备份区。跳转前Bootloader重新读取Flash中的App数据再算一次MD5比对。为什么不用SHA256F103的RAM只有20KBSHA256中间状态占内存太大实测会挤爆栈空间。MD5虽然安全性不如SHA但用于固件完整性校验足够——攻击者想构造碰撞数据篡改固件成本远高于直接物理拆芯片。注意MD5计算必须在Bootloader里完成不能依赖App。因为App可能被损坏其MD5函数不可信。我见过有工程师把MD5校验放在App启动时结果固件损坏后App自己校验自己永远返回“校验通过”。3. Flash分区与擦写实操F103的1KB页擦除陷阱3.1 分区地址规划必须避开Option Bytes和Write ProtectionF103的Flash布局不是线性的。手册第3.3节明确写着Page 0~127对应0x08000000~0x0801FFFF128KB但Page 1270x0801FC00~0x0801FFFF是Option Bytes所在页擦写Page 127会永久锁死Flash。更隐蔽的是F103支持写保护Write Protection如果某个页被保护擦除操作会静默失败——不报错但数据没变。我第一次调试时把B区App起始地址设为0x08014000Page 80结果升级时发现B区始终写不进去用ST-Link Utility读出来全是0xFF查了半天才发现Page 80被Option Bytes里的WRP0寄存器保护了。正确做法是先用ST-Link Utility读取Option Bytes地址0x1FFFF800确认WRP0~WRP3全为0xFFFF未保护在Keil或STM32CubeIDE里Project → Options → Target → IROM1把ROM起始地址设为0x08000000Size设为0x20000128KB但实际代码只占用前100KB手动在分散加载文件scatter file里划分区域LR_IROM1 0x08000000 0x00024000 { ; load region size_region ER_IROM1 0x08000000 0x00004000 { ; Bootloader *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } ER_APP_A 0x08004000 0x00010000 { ; A区App32KB app_a.o (RO, RW, ZI) } ER_APP_B 0x08014000 0x00010000 { ; B区App32KB app_b.o (RO, RW, ZI) } ER_STATE 0x08024000 0x00000400 { ; 状态区1KB state.o (RO) } }这样链接器会严格按地址分配避免代码溢出到受保护页。3.2 擦除操作必须“先解锁、再擦、后锁”且顺序不可逆F103的Flash擦除是硬件门控的。标准库v3.50的FLASH_ErasePage()函数内部调用FLASH_Unlock()和FLASH_Lock()但问题在于如果擦除过程中发生中断比如USART接收完成中断Flash控制器会进入忙状态后续擦除命令被忽略。我实测过在擦Page 110时触发UART中断Page 111的擦除就静默失败——读出来还是旧数据。解决方案是擦除前关全局中断擦完再开。但要注意关中断时间不能太长否则影响实时性。F103擦一页1KB约20ms如果关中断20ms对于10ms周期的PID控制会丢帧。所以我的做法是将擦除操作拆成“准备执行”两阶段准备阶段开中断计算待擦页号、检查写保护、读取当前页数据用于回滚执行阶段关中断调用FLASH_Unlock() → FLASH_ErasePage() → FLASH_Lock()全程25ms擦完立即校验读回该页数据确认全为0xFF。关键代码片段// 擦除Page 1100x0801AC00 __disable_irq(); // 关全局中断 FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_WRPRTERR); FLASH_ErasePage(0x0801AC00); // 地址必须是页首地址 FLASH_Lock(); __enable_irq(); // 开中断 // 立即校验 if (*(uint32_t*)0x0801AC00 ! 0xFFFFFFFF) { // 擦除失败触发回滚 RollbackToLastValidApp(); }提示FLASH_ErasePage()的参数必须是页首地址如0x08000000、0x08000400传0x08000401会擦错页。我曾因地址算错把Bootloader区擦成了0xFF只能用J-Link恢复。3.3 写入数据必须“按字Word写且地址对齐”F103的Flash编程最小单位是32位字Word不是字节。如果尝试用FLASH_ProgramByte()写单字节硬件会拒绝。标准库v3.50的FLASH_ProgramWord()要求目标地址4字节对齐即addr % 4 0且一次写4字节。但App固件bin文件是按字节排列的直接循环写会出错。正确写法是将bin数据按4字节分组不足4字节的末尾用0xFF补齐对每组调用FLASH_ProgramWord()写完立即读回校验确保写入正确。例如写地址0x08004000处的4字节uint32_t *pAddr (uint32_t*)0x08004000; uint32_t data 0x12345678; FLASH_Unlock(); FLASH_ProgramWord((uint32_t)pAddr, data); FLASH_Lock(); if (*pAddr ! data) { // 写入失败可能是地址未对齐或Flash忙 }我踩过的坑某次用memcpy()把bin数据拷到RAM缓冲区再逐字写Flash结果发现缓冲区起始地址是0x20000101奇数导致*pAddr读出来是乱码——因为ARM的非对齐访问会触发BusFault。解决方案是声明缓冲区时加__align(4)__align(4) uint8_t flash_buffer[1024]; // 保证4字节对齐4. Bootloader核心实现从复位到跳转的完整链路4.1 启动流程如何让Bootloader“抢在App前接管控制权”F103上电后硬件自动从0x08000000取第一条指令。所以Bootloader的启动代码startup_stm32f10x_md.s必须放在这个地址。但问题来了如果你用Keil编译Bootloader链接脚本默认把Reset_Handler放在0x08000000可App的Reset_Handler也在0x08000000——怎么避免App覆盖Bootloader答案是Bootloader和App使用不同的链接脚本且App的Reset_Handler必须重定位。具体操作Bootloader工程IROM1 0x08000000, Size 0x0000400016KBApp工程IROM1 0x08004000A区或0x08014000B区Size 0x0001000064KBApp的startup文件里Reset_Handler入口地址由链接器自动填入无需手动改Bootloader的main()里通过读取状态区判断该跳A还是B然后跳转。关键代码// Bootloader main() uint32_t active_app_addr; uint16_t status_flag *(uint16_t*)0x08024000; // 读状态区首2字节 if (status_flag 0xAA55) { active_app_addr 0x08004000; // A区有效 } else if (status_flag 0x55AA) { active_app_addr 0x08014000; // B区有效 } else { active_app_addr 0x08004000; // 默认跳A区 } // 跳转前清场 RCC_DeInit(); NVIC_DeInit(); __disable_irq(); // 切栈 __set_MSP(*((uint32_t*)active_app_addr)); // 获取App的Reset_Handler地址存于App首地址 uint32_t app_reset_handler *((uint32_t*)(active_app_addr 4)); // 跳转 ((void (*)(void))app_reset_handler)();这里有个易错点App的Reset_Handler地址在App镜像的第二个字偏移0x04第一个字是栈顶地址SP。网上很多教程写成*((uint32_t*)active_app_addr)那是取SP不是取Reset_Handler。4.2 OTA升级协议为什么用“帧头长度CRC数据”比HTTP简单可靠F103跑不了TCP/IP协议栈RAM不够所以OTA必须用精简协议。我对比过Modbus RTU、自定义二进制帧、CoAP最终选了类Modbus的二进制帧因为Modbus RTU已有成熟移植你提到的freemodbus v1.6直接复用帧结构清晰0x01从站地址 0x10功能码写多寄存器 0x0000起始地址 0x0010寄存器数 CRC16传输层可用RS232、RS485、CAN适配工业现场但Modbus原生不支持固件升级需扩展功能码。我的方案是功能码0x55OTA开始发送固件总长度、版本号功能码0x56OTA数据块每块256字节含块序号、数据、CRC16功能码0x57OTA结束触发校验和跳转。例如发送第一块数据[0x01][0x56][0x0000][0x0100][0x0000...0x00FF][0xABCD] ↓ ↓ ↓ ↓ ↓ ↓ 从站 功能码 起始地址 长度256字节 256字节数据 CRC16Bootloader收到0x55帧后先擦除目标区A或B再循环收0x56帧每收一块就写Flash并校验页CRC最后收0x57帧触发MD5校验和跳转。这样设计的好处是即使某块数据错只需重发该块不用重传整个固件。4.3 回滚机制如何在升级失败时“一键还原”AB分区的价值不在升级成功而在升级失败时能秒级回滚。F103没有EEPROM所以回滚状态必须存在Flash里但又要防误写。我的方案是状态区0x08024000存两个标志0x08024000当前激活区0xAA55A0x55AAB0x08024002升级中标志0x1234正在升级0x0000空闲升级开始时先写0x08024002 0x1234升级失败时如CRC校验错、Flash写失败立即将0x08024002清零并将激活标志切到另一区复位后Bootloader读到0x08024002 0x0000就按激活标志跳转。关键点写状态区时必须整页擦除再写因为Flash只能从1变0不能从0变1。所以0x08024000所在的Page 1250x08024000~0x080243FF要先擦再写入新标志。我测试过如果只写单字节旧数据残留会导致标志位混乱。实操心得回滚后首次启动App里要主动检测是否刚回滚比如读备份区的时间戳如果是则清除所有用户配置避免新旧固件配置冲突。我做过一个案例升级后回滚旧固件的Wi-Fi密码和新固件的MQTT服务器地址混在一起设备连不上云平台。5. 常见问题与排查技巧那些让你熬夜的“灵异故障”5.1 HAL_Delay卡死90%是SysTick配置残留如前所述HAL_Delay卡死的根本原因是SysTick中断没切到App的NVIC。但排查时别急着改代码先用ST-Link Utility读Flash确认Bootloader和App的SysTick配置是否一致Bootloader里SysTick_Config(SystemCoreClock / 1000); // 1msApp里必须用同样参数且不能在HAL_Init()里重复调用SysTick_Config()更隐蔽的问题是HAL库的HAL_Init()会重置SysTick但如果Bootloader没关SysTickApp的HAL_Init()会认为SysTick已启用跳过初始化导致SysTick没启动。解决方案是在App的main()开头加HAL_Init(); // 强制重置SysTick SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; HAL_InitTick(TICK_INT_PRIORITY); // 这行必须调用5.2 升级后串口收不到数据NVIC向量表没重映射App跳转后串口不工作大概率是NVIC向量表还在Bootloader区。F103的向量表默认在0x08000000但App的向量表在0x08004000。Bootloader跳转前必须调用SCB-VTOR 0x08004000; // 设置向量表偏移寄存器这行代码必须在__set_MSP()之后、跳转之前执行。我见过最坑的案例某工程师把这行放在跳转后App的main()里结果第一次中断来临时CPU去0x08000000找ISR那里是Bootloader代码直接HardFault。5.3 OTA升级中途断电如何从“半砖”救回来F103断电升级失败最常见的现象是复位后Bootloader运行正常但跳转时卡在HardFault。此时不要急着重刷先用ST-Link读Flash检查状态区0x08024000如果还是0xAA55说明A区被破坏但B区可能完好检查B区首地址0x08014000读前16字节看是否为有效栈顶非0xFFFFFFFF和Reset_Handler地址非0x00000000如果B区有效手动修改状态区为0x55AA再复位即可。工具推荐用ST-Link Utility的“Memory Browser”功能直接编辑Flash比写代码快得多。我产线救急时10秒就能把一台“砖机”拉回来。5.4 J-Link正版SN与Bootloader冲突如何绕过授权检测你提到的“jlink正版 bootloader sn”是指某些Bootloader会读取J-Link的SN码做授权验证。F103的Bootloader如果调用了J-Link的DLL接口如JLINKARM_ReadMemU32在无J-Link连接时会卡死。解决方案删除所有J-Link相关API调用用ST-Link或CMSIS-DAP替代调试如果必须用J-Link改用J-Link Commander命令行刷写不走DLL接口。最后分享个小技巧F103的BOOT0引脚接VDD时从系统存储器启动即ST的Bootloader可以用来救砖。但注意ST的Bootloader不支持AB分区只能擦全片重刷。所以量产时务必把BOOT0焊盘留出来贴片电阻悬空方便紧急修复。我在深圳电子市场见过太多F103项目卡在OTA环节不是技术不行而是没人告诉你Page 127不能碰、SysTick必须重置、状态区要整页擦。这篇教程里每一个参数、每一行代码都来自真实产线——不是实验室仿真是客户凌晨三点打来的电话逼出来的方案。如果你现在正对着示波器抓不出SysTick波形或者ST-Link读出来Flash全是0xFF别怀疑就按这个流程一步步来。F103的资源确实紧但只要把边界条件抠清楚AB分区OTA比想象中稳得多。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Duix-Avatar 部署指南:10 秒克隆你的数字人,本地免费生成口播视频 2026/9/11 23:37:20

Duix-Avatar 部署指南:10 秒克隆你的数字人,本地免费生成口播视频

Duix-Avatar 部署指南:10 秒克隆你的数字人,本地免费生成口播视频 【免费下载链接】Duix-Avatar 🚀 Truly open-source AI avatar(digital human) toolkit for offline video generation and digital human cloning. 项目地址: https://git…

阅读更多 →
多人说话场景难题待解,小米开源 CocktailASR - 1 模型精准定位目标语音! 2026/9/11 23:37:20

多人说话场景难题待解,小米开源 CocktailASR - 1 模型精准定位目标语音!

【导语:在 ASR 模型面临多人同时讲话场景难题时,小米开源了 CocktailASR - 1 模型。它采用目标说话人语音识别技术,在多场景测试中表现出色,还具备负样本拒识和思维链推理等特性,意义重大。】创新思路:从根…

阅读更多 →
mise plugins 命令全解析:插件安装、链接、更新与维护实战指南 2026/9/11 23:37:20

mise plugins 命令全解析:插件安装、链接、更新与维护实战指南

mise plugins 命令全解析:插件安装、链接、更新与维护实战指南 【免费下载链接】mise dev tools, env vars, task runner 项目地址: https://gitcode.com/GitHub_Trending/mi/mise mise plugins 是 mise 中管理外部插件(plugin)的核心…

阅读更多 →
对标Palantir,悦点科技获融资实现盈亏平衡,订单收入复合增长率超300%! 2026/9/11 23:37:20

对标Palantir,悦点科技获融资实现盈亏平衡,订单收入复合增长率超300%!

1. 悦点科技完成Pre - A轮融资9月11日,据投中网消息,企业级AI公司悦点科技已完成数千万元人民币Pre - A轮融资,由盈富泰克领投,水木创投跟投。本轮资金将用于AIGO模型训练、推动AI FDE Model持续进化、完善RSI闭环,并加…

阅读更多 →
Deep Agents Monorepo 开发实战:基于 uv 与 make 的编辑-测试-lint 循环、Pre-commit 钩子与基准测试工程规范 2026/9/11 23:37:20

Deep Agents Monorepo 开发实战:基于 uv 与 make 的编辑-测试-lint 循环、Pre-commit 钩子与基准测试工程规范

Deep Agents Monorepo 开发实战:基于 uv 与 make 的编辑-测试-lint 循环、Pre-commit 钩子与基准测试工程规范 【免费下载链接】deepagents The batteries-included agent harness. 项目地址: https://gitcode.com/GitHub_Trending/de/deepagents 本指南基于…

阅读更多 →
水表识别实战:YOLOv5s定位+CRNN端到端数字识别 2026/9/11 23:34:20

水表识别实战:YOLOv5s定位+CRNN端到端数字识别

简介:本资源是一个基于深度学习的水表识别完整项目实现,面向计算机视觉初学者与AI应用开发者,解决实际场景中水表图像的自动定位与数字读数识别问题。项目采用双网络架构:一个YOLO或CNN风格的定位网络负责检测水表区域&#xff0c…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞