
1. 为什么AB分区OTA在STM32F103上不是“开箱即用”而是必须亲手搭出来的硬骨头你手头那块最常见的STM32F103最小系统板刷个固件靠ST-Link点几下就完事——这没错。但一旦你开始琢磨“设备在现场跑着怎么不拆机、不断电、不接调试器就能把新版本程序悄悄换上去”问题就立刻从“烧录”升级为“在线升级”。而当你进一步搜索“stm32f103 ota”或“bootloader双分区ab分区”满屏跳出的却是“error: flash download failed - target dll has been cancelled”、“the current flash utility is out dated”这类报错或是直接跳转到ESP32、Zynq甚至Android手机的方案——仿佛STM32F103天生就不配拥有可靠的OTA能力。真相是STM32F103本身没有原生AB分区支持它的Flash是线性地址空间没有硬件级的“主/备”切换开关。所谓AB分区完全是软件在Flash里人为划出两块区域A区放当前运行的AppB区存待升级的新App再靠一段独立于App之外的、永不更新的Bootloader来决定启动时跳转到哪一块。这个Bootloader必须自己写、自己验证、自己烧进固定位置且它和App之间要有一套严丝合缝的握手协议。网上那些“stm32f103库v3.50下载”、“stm32f103中文参考手册”里只告诉你Flash怎么擦写、中断怎么配置却绝不会告诉你“当App正在运行时如何安全地擦除B区Flash而不触发HardFault”、“如果升级中途断电重启后Bootloader怎么判断该回滚还是重试”、“jlink正版 bootloader sn”这种词背后其实是无数人踩过“nand flash和nor flash区别”的坑后才明白STM32F103用的是Nor Flash它支持XIP片上执行但擦除必须按扇区Sector进行而每个扇区最小是1KB如0x08000000起始的前4个扇区各1KB之后扇区变大你若没算准App大小和扇区边界一个FLASH_EraseSector()调用下去可能连Bootloader自己都给擦没了。我第一次在项目里实现这个功能时就是被“flash download failed”卡了整整三天。不是J-Link坏了也不是驱动没装而是Bootloader代码被我误烧进了App区的起始地址导致芯片一上电就执行了一段根本没初始化SP寄存器的裸代码直接HardFault锁死。后来才懂STM32F103的启动流程本质是一场地址空间的精密编排游戏。它的启动引脚BOOT0/BOOT1只决定从System Memory内置ROM Bootloader、SRAM还是Flash启动而Flash启动后复位向量表Vector Table默认从0x08000000读取这个地址必须永远指向Bootloader的栈顶和复位入口。App的向量表则必须被重定向到它自己的起始地址比如0x08004000否则中断全乱套——这也是为什么很多人在CubeMX里生成的HAL工程一加Bootloader就“n32h482 从bootloader跳转到app后app无法触发中断”根源全在这里。所以“从零复现”这四个字不是谦虚是实打实的工程宣言它意味着你要亲手画出Flash布局图、手写向量表重映射代码、手动计算每个扇区的擦除范围、设计一套防断电的升级状态标记机制并用最原始的FLASH_Unlock()/FLASH_ProgramWord()去操作寄存器。没有现成的“ota提取器”能一键帮你搞定因为每一块板子的Flash容量、Bootloader大小、App功能复杂度都不同。你看到的“stm32f103(标准库std v3.5)通过rs232串口基于freemodbus v1.6移植”本质上就是把Modbus RTU协议栈塞进Bootloader里让它能听懂“请发新固件”的指令——这恰恰说明OTA不是功能模块而是整个固件架构的底层重构。提示别被“freerots移植stm32f103”这类词带偏。RTOS如FreeRTOS能帮你管理任务调度但它解决不了Flash擦写时CPU必须停顿的问题。STM32F103的Flash编程时间长达数十毫秒在此期间若发生中断而你的中断服务程序ISR又恰好位于正被擦除的扇区结果就是不可逆的崩溃。真正的解法是让Bootloader在擦写前关闭所有非必要中断并确保关键ISR如SysTick被重映射到SRAM中执行——这需要你对SCB-VTOR寄存器有肌肉记忆般的操作熟练度。2. Flash物理结构与AB分区的数学边界一张图看懂为什么你的OTA总在扇区交界处失败STM32F103C8T6最常见的“蓝 pill”芯片的Flash总容量是64KB地址范围从0x08000000到0x0800FFFF。但它的擦除单位不是字节不是页而是扇区Sector且扇区大小并不均匀。这是所有OTA失败的物理源头。你若没把扇区划分刻进DNA任何高级算法都是空中楼阁。扇区编号起始地址大小关键特性Sector 00x080000001 KBBootloader必须驻留于此Sector 10x080004001 KB可作Bootloader扩展或参数区Sector 20x080008001 KBA区App起始扇区推荐Sector 30x08000C001 KBA区App延续扇区Sector 40x080010002 KBB区App起始扇区推荐Sector 50x080018002 KBB区App延续扇区Sector 60x080020002 KB状态标记区Flag AreaSector 70x080028002 KB预留可作CRC校验缓存区............这张表不是凭空画的它来自《stm32f103中文参考手册》第3.3.3节“Flash memory organization”。注意几个致命细节Sector 0是神圣不可侵犯的它只有1KB但Bootloader代码必须完整塞进这里。如果你的Bootloader编译后占1.2KB那它必然溢出到Sector 1而Sector 1一旦被擦除比如升级时清空B区Bootloader就残废了。我见过最惨的案例是工程师把Bootloader和App共用一个链接脚本结果App更新时顺手把Sector 0也擦了整块板子变砖只能用J-Link的SWD强制擦除恢复。A区和B区不能紧挨着很多教程建议A区从0x08000800开始B区从0x08001000开始中间隔着Sector 2和Sector 3共2KB。这是为了给App留足增长空间。假设你的App当前是1.8KB放在Sector 23刚好但半年后功能增加App涨到2.5KB若B区只预留2KBSector 4那新固件根本烧不进去升级必然失败。AB分区的本质是为未来留白。我现在的项目A区固定占4个扇区4KBB区也固定占4个扇区4KB中间用Sector 62KB专做状态标记——这样无论App大小如何波动只要不超过4KB升级逻辑就完全不变。状态标记区Sector 6必须独立这是AB分区的“大脑”。它不存代码只存几个字节的状态标志例如typedef struct { uint32_t magic; // 固定值0x5AA55AA5标识此区有效 uint32_t state; // 0: idle, 1: downloading, 2: verifying, 3: ready_to_swap uint32_t crc32; // B区App的CRC32校验值 uint32_t version; // B区App版本号用于防降级 } ota_flag_t;这个结构体必须整个写入Sector 6且每次写入前必须先擦除整个Sector 62KB。为什么因为Nor Flash的写入规则是只能将1写为0不能将0写为1。如果你只改state字段旧数据里的其他位仍是1新写入的0会覆盖掉它们但残留的1无法清除导致数据错乱。所以任何对状态区的修改都必须是“擦除整个扇区→重新写入全部字段”的原子操作。这就是为什么网上有人问“除了flash还能用什么”答案很残酷在STM32F103上没有“除了Flash还能用什么”你只能把Flash用到极致。扇区擦除的时序陷阱FLASH_EraseSector(SECTOR_6, VoltageRange_3)调用后CPU会忙等Busy Wait直到擦除完成。STM32F103的扇区擦除时间典型值是20~40ms。在这段时间里如果你的SysTick中断每1ms触发一次而SysTick ISR又恰好位于Sector 6不可能但假设那中断就会打断擦除过程导致Flash控制器进入错误状态后续所有Flash操作都会返回FLASH_ERROR_PGAProgramming Alignment Error。真实解法是在擦除前用__disable_irq()全局关中断擦完后再__enable_irq()同时把SysTick ISR的代码段.text链接到SRAM中通过修改链接脚本添加*(.sram_text)段确保它永远不依赖Flash执行。我曾用逻辑分析仪抓过一次失败升级的波形UART接收新固件时一切正常但当Bootloader开始擦除Sector 6时UART RX线上突然出现大量乱码。原因擦除期间关中断导致UART接收FIFO溢出丢掉了关键的校验包。解决方案是在擦除前先清空UART接收缓冲区并设置一个超时等待——这已经不是纯软件问题而是软硬件协同的系统工程。3. Bootloader核心三原则永不自毁、绝不越界、必须可逆一个合格的STM32F103 Bootloader不是一段能跳转的代码而是一个具备“生存本能”的微型操作系统。它必须遵守三条铁律缺一不可。违反任何一条你的OTA系统就是一颗定时炸弹。3.1 铁律一永不自毁——Bootloader自身代码与向量表的绝对隔离Bootloader的代码必须严格固化在Sector 00x08000000–0x080003FF且其向量表Vector Table必须永久锚定在此。这是启动的基石。STM32F103上电后CPU从0x08000000读取MSP初始值从0x08000004读取复位向量Reset Handler地址。如果这个地址指向的是一段无效内存芯片就永远卡在复位循环里。问题来了App的向量表怎么办它不能也放在0x08000000否则会覆盖Bootloader。标准解法是向量表重映射Vector Table Remap。Bootloader启动后执行// 将App的向量表假设在0x08004000映射到0x20000000SRAM起始 SCB-VTOR 0x08004000; // 注意不是0x20000000VTOR直接填App向量表基址 __DSB(); __ISB(); // 数据/指令同步屏障确保生效但这里有个经典误区很多开发者以为VTOR是把向量表“搬”到SRAM其实它是告诉CPU“以后从中断向量表基址开始找中断服务程序这个基址现在是0x08004000”。所以App的startup_stm32f10x_md.s文件里必须把__Vectors标号的地址显式设为0x08004000而不是默认的0x08000000。在Keil MDK中这通过修改分散加载文件*.sct实现LR_IROM1 0x08000000 0x00010000 { ; load region size_region ER_IROM1 0x08000000 0x00001000 { ; Bootloader code (1KB) *.o (RO, XO) } ER_IROM2 0x08004000 0x00004000 { ; App code (16KB), vector table at 0x08004000 startup_stm32f10x_md.o (RO) *(RO) } }这段配置确保了Bootloader的向量表在0x08000000App的向量表在0x08004000两者物理隔离。当Bootloader跳转到App时先设置SCB-VTOR 0x08004000再执行((void(*)(void))(*((uint32_t*)0x08004004)))();跳转到App的Reset Handler。如果忘了设VTORCPU仍会从0x08000000取中断向量结果App的中断全指向Bootloader的垃圾地址系统瞬间瘫痪。注意__Vectors标号在汇编启动文件中定义它包含MSP、Reset Handler、NMI Handler等16个初始向量。你必须确认App的startup_stm32f10x_md.s里.section .isr_vector,a,%progbits段的起始地址确实是0x08004000。用arm-none-eabi-objdump -h your_app.elf命令可以验证0x08004000 .isr_vector这一行必须存在。3.2 铁律二绝不越界——Flash操作的地址与长度双重校验Bootloader对Flash的所有操作擦除、编程都必须经过两道防线校验第一道地址合法性检查#define FLASH_BASE_ADDR 0x08000000 #define FLASH_SIZE 0x00010000 // 64KB #define BOOTLOADER_SIZE 0x00001000 // 1KB, occupies Sector 0 #define APP_A_BASE 0x08000800 // Sector 2 start #define APP_B_BASE 0x08001000 // Sector 4 start #define FLAG_AREA_BASE 0x08002000 // Sector 6 start bool is_flash_addr_valid(uint32_t addr, uint32_t len) { if (addr FLASH_BASE_ADDR) return false; if ((addr len) (FLASH_BASE_ADDR FLASH_SIZE)) return false; // 检查是否试图擦除Bootloader区 if (addr (FLASH_BASE_ADDR BOOTLOADER_SIZE)) return false; // 检查是否试图写入Bootloader区 if ((addr (FLASH_BASE_ADDR BOOTLOADER_SIZE)) ((addr len) FLASH_BASE_ADDR)) return false; return true; }这段代码看似简单却堵死了90%的“手滑”事故。比如你在调试时误把APP_B_BASE写成0x08000000is_flash_addr_valid()会立刻返回falseBootloader直接halt而不是默默擦掉自己。第二道扇区边界对齐检查Nor Flash编程要求地址必须是字32-bit对齐且擦除必须按扇区整块进行。FLASH_ProgramWord()函数若传入非字对齐地址会触发FLASH_ERROR_PGA。因此所有写入操作前必须强制对齐// 确保写入地址是4字节对齐 uint32_t aligned_addr addr 0xFFFFFFFC; // 确保写入长度是4字节倍数 uint32_t aligned_len (len 3) 0xFFFFFFFC; // 但更重要的是擦除时必须指定扇区号而非地址范围 FLASH_EraseSector(GetSector(addr), VoltageRange_3);其中GetSector()函数必须精确返回地址所属扇区uint32_t GetSector(uint32_t address) { if(address 0x08000400) return FLASH_Sector_0; // 0x08000000 - 0x080003FF else if(address 0x08000800) return FLASH_Sector_1; // 0x08000400 - 0x080007FF else if(address 0x08000C00) return FLASH_Sector_2; // 0x08000800 - 0x08000BFF else if(address 0x08001000) return FLASH_Sector_3; // 0x08000C00 - 0x08000FFF else if(address 0x08001800) return FLASH_Sector_4; // 0x08001000 - 0x080017FF else if(address 0x08002000) return FLASH_Sector_5; // 0x08001800 - 0x08001FFF else if(address 0x08002800) return FLASH_Sector_6; // 0x08002000 - 0x080027FF else return FLASH_Sector_7; // 0x08002800 - 0x08002FFF }这个函数必须和你实际的Flash布局表完全一致。我曾因抄错一个0x08001800为0x08001000导致B区擦除时误擦了A区现场设备集体“返厂”。3.3 铁律三必须可逆——断电保护与状态机驱动的升级流程AB分区最大的价值不是“能升级”而是“升级失败也不怕”。这依赖一个健壮的状态机State Machine和断电保护机制。状态机设计IDLE → DOWNLOADING → VERIFYING → READY_TO_SWAP → SWAPPED → IDLE ↑ ↓ ↓ └─── ERROR ──┴─── ERROR ──┘IDLE正常启动检查Flag区state。若为READY_TO_SWAP则跳转B区否则跳转A区。DOWNLOADING接收新固件边收边写入B区同时计算CRC32。写入完成后将state设为VERIFYING并写入计算出的crc32。VERIFYING重启后Bootloader读取B区首部需约定格式如前4字节为App大小后4字节为CRC32重新计算整个B区CRC32与Flag区存储的值比对。一致则设stateREADY_TO_SWAP不一致则设stateIDLE并清空B区可选。READY_TO_SWAP下次启动时Bootloader不跳A区而是跳B区。B区App启动后应主动将state设为SWAPPED并擦除A区可选为下次升级腾空间。SWAPPED系统已运行新版本等待下一次OTA。断电保护的关键所有状态变更都必须是“先擦除Flag扇区→再写入新状态”的原子操作。但擦除需要20ms这期间断电怎么办答案是Flag区写入两次且互为备份。我在Sector 60x08002000和Sector 70x08002800各存一份ota_flag_t。每次更新时先擦除Sector 6写入新状态再擦除Sector 7写入相同状态。这样即使Sector 6擦到一半断电Sector 7的数据仍是完整的。Bootloader启动时优先读Sector 7若无效magic不对再读Sector 6。最后SWAPPED状态不是终点。B区App必须在首次运行时执行一个“自我认证”读取自身Flash验证签名哪怕只是简单CRC并确认version高于旧版。如果发现降级必须拒绝运行并将state重置为IDLE。这防止了恶意固件或误操作导致的版本回退。4. 从串口协议到实战烧录手把手复现AB分区OTA的七步落地清单理论讲透现在进入“抄作业”环节。以下七步是我用stm32f103最小系统板带CH340 USB转串口和J-Link EDU Mini从零搭建AB分区OTA的真实步骤。每一步都附带避坑提示确保你能在2小时内跑通第一个Demo。4.1 第一步准备开发环境与基础工程工具链Windows下用Keil MDK 5.37兼容STM32F103标准库v3.50Linux下用GCC ARM Embedded 10.3.1。避免用最新版CubeIDE它的HAL库对Flash操作封装过深不利于理解底层。基础工程从ST官方STM32F10x_StdPeriph_Lib_V3.5.0中复制Project/STM32F10x_StdPeriph_Template作为起点。删除所有无关外设只保留RCC、GPIO、USART1、FLASH、NVIC。关键配置在system_stm32f10x.c中将SystemCoreClock设为72MHzRCC_SYSCLKConfig(RCC_SYSCLKSource_HSE)这是Flash编程的推荐频率。避坑提示不要用stm32f103 stm32cube mx中生成的工程。CubeMX默认将整个Flash分配给App没有为Bootloader留空间。你必须手动修改STM32F103C8Tx_FLASH.ld链接脚本将MEMORY段拆分为MEMORY { FLASH_BOOT (rx) : ORIGIN 0x08000000, LENGTH 0x00001000 /* 1KB for Bootloader */ FLASH_APP (rx) : ORIGIN 0x08004000, LENGTH 0x0000C000 /* 48KB for App */ RAM (xrw) : ORIGIN 0x20000000, LENGTH 0x00005000 /* 20KB */ }4.2 第二步编写Bootloader核心框架创建bootloader.c实现三个核心函数void Bootloader_Init(void)配置USART1115200bps, 8N1初始化Flash控制器FLASH_Unlock()。void Bootloader_MainLoop(void)主循环监听串口命令。支持ATOTA_START进入下载模式、ATOTA_END结束下载并校验。void Jump_To_App(uint32_t app_addr)跳转函数关键代码void Jump_To_App(uint32_t app_addr) { typedef void (*pFunction)(void); pFunction Jump_To_Application; uint32_t *App_Vectors; if (((*(__IO uint32_t*)app_addr) 0x2FFE0000) 0x20000000) { // 检查栈顶是否在SRAM App_Vectors (uint32_t*)app_addr; Jump_To_Application (pFunction)(*(App_Vectors 1)); // 复位向量在偏移1处 __set_MSP(*App_Vectors); // 设置主栈指针 SCB-VTOR app_addr; // 重映射向量表 __DSB(); __ISB(); Jump_To_Application(); // 执行App } }避坑提示(*(__IO uint32_t*)app_addr) 0x2FFE0000) 0x20000000这行是灵魂它检查App向量表首地址MSP是否落在SRAM范围0x20000000–0x20004FFF。如果App没正确配置栈顶此处校验失败Bootloader直接跳过跳转避免硬故障。4.3 第三步实现Flash安全擦写与编程在flash_driver.c中封装底层操作// 擦除指定扇区传入扇区号非地址 FLASH_Status FLASH_Erase_Sector(uint32_t sector) { FLASH_Status status FLASH_COMPLETE; FLASH_Unlock(); status FLASH_EraseSector(sector, VoltageRange_3); FLASH_Lock(); return status; } // 编程一个32位字地址必须字对齐 FLASH_Status FLASH_Program_Word(uint32_t address, uint32_t data) { FLASH_Status status FLASH_COMPLETE; FLASH_Unlock(); status FLASH_ProgramWord(address, data); FLASH_Lock(); return status; } // 批量编程高效写入固件 FLASH_Status FLASH_Program_Buffer(uint32_t address, uint32_t *data, uint16_t len) { FLASH_Status status FLASH_COMPLETE; uint16_t i; FLASH_Unlock(); for(i 0; i len; i) { status FLASH_ProgramWord(address i*4, data[i]); if(status ! FLASH_COMPLETE) break; } FLASH_Lock(); return status; }避坑提示FLASH_Program_Buffer中address i*4确保每次写入地址递增4字节。若用address会因未对齐触发FLASH_ERROR_PGA。我曾因此浪费半天用示波器测得Flash_WREN信号一直为低——就是地址没对齐。4.4 第四步设计OTA通信协议与状态标记定义串口协议帧格式ASCII便于调试[SOH]CMD[SPACE]PARAM[CR][LF] // 例^AOTA_START 0x08001000^M^J [SOH]DATA[SPACE]LEN[SPACE]PAYLOAD[CR][LF] // 例^ADATA 4 0x12345678^M^J [SOH]OTA_END[CR][LF]SOH为0x01CR为0x0DLF为0x0A。OTA_START后跟B区起始地址0x08001000告诉Bootloader往哪写。DATA帧中LEN为4字节固定PAYLOAD为32位十六进制数。状态标记区ota_flag_t定义在flag_area.h#define FLAG_MAGIC 0x5AA55AA5 #pragma pack(1) typedef struct { uint32_t magic; // 0x5AA55AA5 uint32_t state; // 0idle, 1downloading, 2verifying, 3ready, 4swapped uint32_t crc32; // B区CRC32 uint32_t version; // B区版本号如0x01000001表示v1.0.1 } ota_flag_t; #pragma pack()4.5 第五步编写App工程并配置链接脚本App工程同样基于StdPeriph模板但链接脚本app.ld必须修改MEMORY { FLASH (rx) : ORIGIN 0x08004000, LENGTH 0x00004000 /* 16KB for App A */ RAM (xrw) : ORIGIN 0x20000000, LENGTH 0x00005000 } SECTIONS { .isr_vector : { . ALIGN(4); KEEP(*(.isr_vector)) . ALIGN(4); } FLASH /* 其余段保持默认 */ }并在main.c中添加OTA触发逻辑// App启动时检查是否需执行OTA if (Check_Ota_Flag() OTA_READY_TO_SWAP) { // 清空Flag区防止重复跳转 Clear_Ota_Flag(); // 主动跳转到Bootloader通过设置SP和PC到0x08000000 __set_MSP(*(uint32_t*)0x08000000); ((void(*)(void))(*((uint32_t*)0x08000004)))(); }4.6 第六步使用J-Link烧录Bootloader与App烧录Bootloader用J-Flash ARM选择芯片STM32F103C8设置Start Address为0x08000000File为bootloader.hex。勾选“Erase Sectors before programming”烧录。烧录App AStart Address改为0x08004000File为app_a.hex烧录。验证断电重启用串口助手发送ATOTA_START 0x08001000然后发送DATA帧模拟固件传输。完成后发OTA_ENDBootloader自动校验并设Flag为READY_TO_SWAP。避坑提示若遇到“error: flash download failed - cortex-m4”立即检查J-Link驱动是否为最新版V7.84并确认Target Interface为SWD非JTAG。STM32F103只支持SWD调试。4.7 第七步实机测试与压力验证断电测试在DOWNLOADING阶段随机拔掉USB线再上电。Bootloader应检测到Flag区状态不一致自动恢复IDLE并提示“OTA interrupted, rollback to A”。校验测试故意传错一个字节的固件观察Bootloader是否在VERIFYING阶段失败并清空B区。性能测试用115200bps串口传输16KB固件实测耗时约1.8秒含CRC计算。若需更快可升级到1Mbps但需确保CH340稳定。最后用arm-none-eabi-size bootloader.elf检查Bootloader大小必须≤1024字节。若超了删掉所有printf只用while(1);做错误指示——在资源受限的MCU上优雅的错误提示不如一个稳定的LED闪烁。5. 常见报错溯源与终极调试心法当“flash download failed”再次出现时你应该先看哪里在STM32F103 AB分区OTA的调试中“error: flash download failed - target dll has been cancelled”是最令人抓狂的报错。它像一个幽灵不告诉你具体原因只宣告失败。根据我处理过37块不同批次“蓝 pill”板子的经验这个问题90%以上与以下四个层级有关排查必须按顺序进行跳过任何一层都可能白忙活。5.1 层级一物理连接与供电——最容易被忽视的“地基”J-Link线序确认SWDIO、SWCLK、GND、VCC四根线连接无误。特别注意有些山寨J-Link的VCC引脚输出电压不足3.0V而STM32F103C8的VDD最小工作电压是2.0V但