资讯动态

STM32F103 AB分区OTA升级方案:从Bootloader到双分区回滚完整实战

发布时间:2026/9/8 23:54:34 来源:尧图企业网站定制
前几天整理抽屉翻出一块吃灰的STM32F103C8T6最小系统板突然想到去年做的一个很有意思的小项目在这块不到十块钱的板子上从零搭出一套完整的AB分区OTA升级方案。这个标题看起来很技术其实拆开就三件事Bootloader怎么写、App怎么改、上位机怎么传。我在踩了一堆坑之后把这个过程完整复现了一遍现在把整个心路历程和代码思路整理出来。如果你手里有F103最小系统板会标准库开发又想让设备具备升级失败能自动回滚的能力这篇文章应该能帮你省下不少试错时间。很多人可能用过ESP32的OTA那是方案商把一切都封装好了你只管调用。但STM32F103上做AB分区OTA所有逻辑都得自己动手写反而能让你把OTA的本质看得清清楚楚。整个方案不依赖操作系统不依赖额外芯片就用芯片自带的内部Flash和一组串口能从零开始把Bootloader、双分区切换、固件校验、断点续传这些概念逐个落地。下面我把整个复现过程、关键代码和踩过的坑一次性讲透。1. 项目核心拆解AB分区OTA到底解决什么问题1.1 为什么是STM32F103这个芯片做OTA的优势选STM32F103作为AB分区OTA的载体不是因为它的性能有多强而是因为它的资源结构对OTA学习来说刚刚好。C8T6是64KB内部Flash20KB SRAMFlash按页擦除中容量型号每页1KBRCT6则是256KB Flash每页2KB。这样的页结构非常直观擦写逻辑很容易理解不像某些芯片有复杂的扇区组合。另外一个关键点是功耗和成本。F103的价格低到可以随便造早期学习阶段我烧废过好几块板子每次十几块钱完全不心疼。它的片上Flash支持1万次擦写实际跑下来通常不止对于搞OTA实验和日常升级完全够用。更重要的是F103没有硬件加密、没有TrustZone这种情况下做AB分区所有安全机制都必须自己设计。这个裸奔的过程反而是最好的学习机会。等以后切换到带硬件加密的芯片做车规级OTA再往里面加签名验签也就顺理成章了。1.2 AB分区与传统方案的区别一张表看懂传统的Bootloader加App单分区方案Flash里只有一个App区Bootloader负责把新固件下载并写入这个区。这样做的问题是如果升级过程中途断电、通信干扰导致写入损坏或者新固件本身有严重Bug设备启动后立刻崩溃基本就变砖了必须拿ST-Link重新烧录。AB双分区方案则完全不同。Flash里同时存在两个应用分区分别叫AppA和AppB。Bootloader启动时只引导两个分区中当前标记为有效的那一个。升级时把新固件写入非当前运行的分区写入并校验通过后再切换启动目标。如果新固件启动后不正常Bootloader还能自动回退到另一个分区。维度传统单分区AB双分区Flash占用小需要预留两倍应用区升级失败风险高极易变砖低具备天然回滚能力掉电恢复需要重新烧录自动切换旧分区实现复杂度低中等远程升级安全性弱强这套思路其实在汽车ECU、智能家电、工业控制器里很常见Android系统很早就用A/B无缝升级。STM32的Flash资源相对紧张但只要合理规划双分区完全跑得起来。2. 硬件准备与Flash分区规划动手前先算清楚地址2.1 硬件清单与连接方式先把手头的硬件理一遍这套项目需要的设备非常普通STM32F103C8T6最小系统板最好带板载LED和USB转串口某宝十几块钱。一块ST-Link V2或者任何支持SWD下载的调试器烧Bootloader和首次烧App用。USB转TTL模块CP2102或CH340均可如果板子自带USB转TTL这根线可以省。杜邦线若干。连接方式很简单USB转TTL的TX接STM32的PA10USART1_RXRX接PA9USART1_TXGND共地。下载器只需要接SWDIO、SWCLK、GND、3V3四根线。我在整个调试过程中最常用的是板载USB转串口省了一堆飞线。唯一要注意的是有些板子的USB转串口和STM32的PA9/PA10之间有一个跳线帽如果发现串口收不到数据先检查这个跳线帽有没有插好。2.2 Flash分区表和地址计算动手写代码之前最重要的一件事就是把Flash分区表定下来。分区定不好后面所有地址都会乱套。STM32F103C8T6的Flash起始地址是0x08000000总容量64KB结束地址0x0800FFFF共64页每页1KB。我的分区表是这么安排的区域起始地址大小页范围Bootloader0x0800000016KB第0~15页AppA0x0800400022KB第16~37页AppB0x0800980022KB第38~59页标志区0x0800F0004KB第60~63页算一下0x4000 0x5800 0x5800 0x1000 0x10000 64KB正好把整个Flash占满没有浪费。Bootloader分配16KB看似奢侈但考虑到后面可能要加串口升级协议、CRC32校验、甚至日志功能16KB很充裕。App区给22KB意味着整个应用工程编译出来的bin文件不能超过22KB。如果你要跑较复杂的协议栈可以用RCT6或者ZET6加大Flash分区比例照抄即可。标志区单独占最后4KB。为什么单独留区因为Flash不能原位修改写标志前必须整页擦除。把标志结构体放在一个独立页里每次更新都先擦除这页再写入逻辑干净不会误伤App代码。实际使用中我只用到其中一页剩余几页留着以后做升级日志、版本记录等扩展功能。如果你用的是RCT6256KB Flash可以把分区调整成Bootloader 48KB、AppA 96KB、AppB 96KB、标志区 16KB同样可以做到完全覆盖。3. Bootloader实现跳转、擦写、状态机三大核心3.1 跳转函数Bootloader最核心的代码Bootloader的核心任务之一是能从复位后引导到AppA或AppB。这部分代码几乎每个做F103升级的人都会写但里面有非常多的细节坑。先看完整代码void jump_to_app(uint32_t app_addr) { typedef void (*pFunction)(void); pFunction jump_func; uint32_t app_stack *(volatile uint32_t *)app_addr; if ((app_stack 0xFFF00000) ! 0x20000000) { return; // 栈顶地址非法说明这个分区没有有效固件 } uint32_t app_reset *(volatile uint32_t *)(app_addr 4); __disable_irq(); SCB-VTOR app_addr; jump_func (pFunction)app_reset; __set_MSP(*(volatile uint32_t *)app_addr); jump_func(); }这里有几个理由需要讲明白。第一为什么要检查App起始地址的第一个32位数据因为Cortex-M3上电后MSP主栈指针就从Flash起始4字节加载而App的起始4字节正好是其栈顶指针。RAM地址范围在0x20000000到0x20004FFFC8T6是20KB RAM所以一个合法的栈顶地址通常都是0x2000xxxx。用0xFFF00000做掩码是一个工程上常见的宽松校验能过滤掉大量因Flash空白或写入错乱导致的非法地址。第二为什么要读App地址加4处的数据因为那是App的Reset_Handler入口地址本质上是一个函数指针。跳转就是把这个函数指针取出来调用。第三为什么跳转前要设置SCB-VTOR向量表偏移寄存器决定中断向量表的位置。App自己跑起来后如果发生任何中断CPU都要去向量表找对应的中断处理函数。不把向量表切到App区中断一出现就直接跑飞。第四为什么用__set_MSP因为我们要把主栈指针切到App的栈顶。如果先调用函数再改MSP当前函数栈就乱了。所以标准流程是先把reset地址取到局部变量再用内联汇编或CMSIS函数设置MSP最后直接调用函数指针这样不会再执行Bootloader后面任何代码。第五__disable_irq(); 这行很多人会漏掉。如果Bootloader里开了USART中断跳转到App之前没关掉在SCB-VTOR切换的瞬间任何一个挂起的中断都可能导致CPU进入异常。这个坑我实际调试时遇到过现象是App启动后随机死机排查了很久才发现是Bootloader残留中断导致。所以跳转前务必关中断然后在App的SystemInit后会重新开启自己需要的中断。3.2 Flash读写模块写给App数据的第一关OTA接收到的固件最终要写入内部Flash。F103的Flash操作有几个硬性规定写之前必须擦除整页擦除操作是按页进行的编程单位是半字16位也就是一次最少写2个字节。标准库3.5已经把这些封装好了但很多人第一次用还是会翻车。以C8T6为例页大小1KB一页地址范围是连续的1KB空间。Flash写入必须遵循擦除-编程-查验的流程。下面是我在项目中使用的擦除函数和写函数void flash_erase_partition(uint32_t start_addr, uint32_t size) { uint32_t page_count (size 1023) / 1024; uint32_t i; FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); for (i 0; i page_count; i) { FLASH_ErasePage(start_addr i * 1024); } FLASH_Lock(); } FLASH_Status flash_write_bytes(uint32_t addr, uint8_t *data, uint32_t len) { uint16_t *half_ptr (uint16_t *)data; uint32_t half_count (len 1) / 2; uint32_t i; FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); for (i 0; i half_count; i) { if (FLASH_ProgramHalfWord(addr i * 2, half_ptr[i]) ! FLASH_COMPLETE) { FLASH_Lock(); return FLASH_BUSY; } } FLASH_Lock(); return FLASH_COMPLETE; }注意几个容易出错的地方。第一FLASH_Unlock和FLASH_Lock必须配对而且每做一次擦除或编程操作Standard Peripherals Library要求先ClearFlag否则可能出现误报错误。第二FLASH_ProgramHalfWord的地址必须偶对齐如果传入的缓冲区地址不是2字节对齐的直接强转uint16_t指针可能会导致HardFault或者产生非对齐访问。第三数据长度如果是奇数最后一次写入会越界1个字节这就是我在写函数里用(len1)/2计算半字数并让调用方在缓冲区末尾补0xFF的原因。补0xFF不会污染数据因为Flash擦除后本来就是0xFF。有一个更隐蔽的问题是跨页写。如果你的写函数只负责在某个地址上写N个字节并不自动处理页边界那么在OTA固件传输时如果一帧数据跨越了两个页的边界你必须拆分写入。最简单的处理方案是设备端维护一个1KB的页缓存攒满1KB后一次性擦写一页这样就不会出现跨页问题。我实际使用的OTA接收逻辑就是按这个思路做的。3.3 AB启动状态机从复位到跳转的完整流程AB分区能不能正常工作关键看Bootloader的启动决策逻辑。参考Android的A/B机制我设计了一套精简状态机把整个流程固定为以下几个步骤Bootloader上电初始化时钟、串口、LED。读取标志区得到当前active_slot、两个分区的固件CRC和启动计数。进入一个短暂等待窗口默认500ms在这期间监听串口升级指令。如果收到升级指令进入固件接收模式解析协议帧写入非活动分区。如果没有升级指令则校验active_slot对应分区是否有效。校验通过跳转执行该分区。校验失败切换到另一个分区再次校验。两个分区都无效进入错误处理LED快闪报错不跳转。标志区结构体我定义成下面这样typedef struct { uint32_t magic; // 固定为0xA5A5A5A5 uint8_t active_slot; // 0表示AppA1表示AppB uint8_t update_flag; // 0空闲1表示已收到新固件等待重启切换 uint8_t boot_count; // 当前分区启动计数用于看门狗回滚 uint8_t reserved; uint32_t app_a_crc32; uint32_t app_b_crc32; uint32_t header_crc; // 整个结构体的CRC32 } boot_flags_t;每次需要修改标志时流程都是整页擦除-写入新的标志结构体-再读出比对。不能直接往已写入的Flash地址再写新值因为Flash只能把1写成0不能把0写成1。整个状态机跑起来非常稳定。另外我加了一个简单的boot_count机制Bootloader跳转App前先把boot_count加1并写回标志区App启动成功后会把boot_count清零。如果App本身崩溃导致系统不断复位boot_count会一直增加超过阈值比如3次后Bootloader判定该分区不可用自动切换分区并复位。这相当于给AB分区增加了一道软件看门狗防线对真实场景很有价值。4. App端改造向量表偏移与工程配置4.1 Keil工程配置让App的编译地址搬到0x08004000App整个工程的改动非常小但每一项都是致命的。我用的是Keil MDK加标准库3.5第一步先把工程的IROM1地址改掉。在Options for Target - Target界面里IROM1的Start填0x08004000Size填0x5800这样链接器就会把所有代码和只读数据放在0x08004000之后。同理AppB的IROM1 Start改成0x08009800Size也是0x5800。第二步也是新手最容易忽略的是修改向量表偏移。标准库3.5的system_stm32f10x.c里自带一段向量表设置逻辑它默认把VECT_TAB_OFFSET定义为0。你可以直接在工程C/C选项卡里的Define一栏加上VECT_TAB_OFFSET0x4000这样SystemInit执行时SCB-VTOR就会自动变成FLASH_BASE加0x4000也就是0x08004000。这个方法比我手动在main函数里写SCB-VTOR要干净得多也符合官方库的设计思路。第三步检查启动文件startup_stm32f10x_md.s确认中断向量表前面几个向量没有被人为修改过。正常情况完全不用动。用ST-Link把AppA的bin文件烧到0x08004000不是0x08000000上电后Bootloader会负责跳转。如果先烧Bootloader再烧App到指定地址整个过程是不冲突的。有一点别忘了Bootloader和App虽然是两个独立工程但都必须使用相同的时钟配置。F103默认用外部8MHz晶振经PLL倍频到72MHz如果App里故意把主频设成48MHzBootloader跳转过去也能跑只是串口波特率、定时器周期全部都会偏调试时容易一头雾水。4.2 App端标志上报与升级触发让Bootloader知道我活着AB分区能够自动回滚依赖的核心概念是当前分区是否启动成功。App启动成功后应当尽快通知Bootloader。这个通知动作在Android A/B机制里叫mark boot successful。在F103上我的实现方式很直接App进入main函数后先做基础初始化点亮LED跑2秒自检如果一切正常就调用一个接口把标志区里的boot_count清零。如果App在半路崩了boot_count不会被清零Bootloader看到计数超过阈值就会自动切到另一个分区。这套逻辑非常朴素但真实有效。另一个思路是用独立看门狗IWDG强制回退Bootloader跳转前启动IWDGApp必须在窗口期内喂狗否则系统复位Bootloader发现boot_count超限切换分区。这种方式即使App死循环卡死也能兜住比纯软件标志更可靠。App如何触发进入升级模式同样写一个标志。上位机通过串口给App发送固定指令例如0x55 0xAA 0x11App收到后把标志区里的update_flag设为1然后执行NVIC_SystemReset()软复位。复位后Bootloader读取update_flag发现为1就不跳转直接进入固件接收流程。这样用户不需要物理按键远程下发一条指令就能让设备重启进入升级状态完全符合真实OTA场景。5. 固件传输协议设计和上位机对话的规矩5.1 自定义协议帧格式与CRC校验STM32的Flash不能像文件系统那样随便改写接收固件时必须保证每一帧数据都正确、不乱序、不丢失。所以我在Bootloader和上位机之间定义了一个精简协议每一帧固定格式如下字段长度说明帧头2字节固定0xAA 0x55帧类型1字节0x01开始帧0x02数据帧0x03结束帧数据区N字节不同帧类型携带不同参数CRC162字节数据区的CRC16帧头和帧类型不参与开始帧的数据区包含目标分区号1字节、固件总长度4字节、固件整体CRC324字节。数据帧的数据区包含帧序号2字节和最多256字节的固件数据。结束帧的数据区为空但保留整体CRC字段用于确认。CRC16我选用了Modbus CRC16算法网上随手就能找到查表版速度很快。CRC32则用经典的IEEE 802.3多项式作用有两个一是上位机在发送前整包算一个CRC放在开始帧里二是Bootloader接收完所有数据后对写入的整个分区重新计算CRC和开始帧声明的CRC对比一致才把标志区的active_slot切过去。双保险设计避免每帧都对但整包错的诡异情况。帧序号的作用是让Bootloader发现丢帧后能精确请求重传。我的上位机逻辑是每发一帧等ACK收到NAK或者超时则重发当前帧。Bootloader侧维护一个期望序号只有序号匹配的帧才被写入缓存其他一律返回NAK。这比无脑收数据要稳得多。5.2 Python上位机脚本5分钟写出一个升级工具上位机我直接用Python加pyserial代码不长但已经拿到真实板子上验证过。核心逻辑就是上面协议的具体实现。下面是我项目中使用的核心发送逻辑import serial import struct import zlib import time def crc16(data): crc 0xFFFF for byte in data: crc ^ byte for _ in range(8): if crc 1: crc (crc 1) ^ 0xA001 else: crc 1 return crc def send_frame(ser, frame_type, payload): frame bytes([0xAA, 0x55, frame_type]) payload crc crc16(payload) frame struct.pack(H, crc) ser.write(frame) time.sleep(0.01) def ota_update(port, bin_path, target_slot, baud115200): with open(bin_path, rb) as f: firmware f.read() ser serial.Serial(port, baud, timeout1) header struct.pack(BI, target_slot, len(firmware)) header struct.pack(I, zlib.crc32(firmware) 0xFFFFFFFF) send_frame(ser, 0x01, header) ack ser.read(1) if ack ! b\x01: print(start transfer rejected) return False seq 0 for offset in range(0, len(firmware), 256): chunk firmware[offset:offset 256] if len(chunk) 256: chunk b\xFF * (256 - len(chunk)) payload struct.pack(H, seq) chunk for retry in range(3): send_frame(ser, 0x02, payload) resp ser.read(1) if resp b\x01: break else: print(frame retry exhausted) return False seq 1 send_frame(ser, 0x03, b) final ser.read(1) if final b\x01: print(OTA done) return True else: print(OTA failed) return False这段脚本有几个细节值得说明。第一不足256字节的最后一帧用0xFF补齐正好和Flash擦除后的默认值保持一致不会污染数据。第二所有帧都按串口阻塞方式发送实际跑115200波特率22KB固件大约2秒传完非常快。第三上位机脚本里可以再加进度条和错误统计但核心链路就上面几行。上位机脚本在开始帧里自动选择目标分区也非常实用。最稳妥的方式是让Bootloader在上电后返回当前active_slot脚本解析后自动选择另一个分区写入。这样即使Bootloader多次切换分区脚本始终不会写错地方。6. 从零复现全流程编译、烧录、升级、回滚验证6.1 逐步操作清单从空板到能OTA在正式复现时我建议严格按照下面的顺序走一遍这样可以最大程度减少烧完Bootloader但App地址不对之类的混乱情况。先用ST-Link把Bootloader工程烧进0x08000000。烧录时选择全片擦除还是只擦除Bootloader区域都可以我一般选Erase Sectors速度更快。编译AppA工程。在Keil里配置好IROM1和VECT_TAB_OFFSET之后用User页的After Build命令生成bin文件。标准写法是fromelf.exe --bin -o ./Objects/app.bin ./Objects/app.axf用ST-Link把AppA的bin文件下载到0x08004000。如果是用Keil直接下载注意不要选擦除整个芯片只擦除用到的扇区。编译AppB工程。地址改为0x08009800VECT_TAB_OFFSET改为0x9800生成app_b.bin。这次不要用ST-Link下载AppB而是用Python脚本通过串口OTA传一次验证整个链路通不通。脚本参数里target_slot写1因为当前active_slot是0。串口输出显示OTA done之后把板子断电重新上电观察Bootloader是否自动引导了AppB。再把AppA通过OTA传回去反复切换几次确认AB两个分区都在工作。整个流程跑下来最紧张的其实不是代码逻辑而是每次烧录完第一次上电的那一秒钟。如果LED正常闪烁心跳踏实一半如果黑屏接下来就是漫长的排查。用fromelf生成bin文件这一步很多新手会卡在命令找不到。注意Keil MDK的ARMCC工具链里自带fromelf路径类似于C:\Keil_v5\ARM\ARMCC\bin。如果你的工程用的ARMClang编译器fromelf同样存在只是路径变成C:\Keil_v5\ARM\ARMCLANG\bin。还有一种更省事的方式直接在Keil的Output页勾选Create HEX FileBootloader不认HEX就转个格式不过bin文件操作起来更简单我强烈建议直接生成bin。6.2 回滚场景验证故意整坏一个分区OTA方案有没有效果不能只看升级成功更关键的是看升级失败时能不能活下来。有一句话我特别认同AB分区的价值不体现在平时而体现在升级失败的那个瞬间。我做了两种故障模拟。第一种人为损坏AppB的分区头。用ST-Link的Memory窗口直接往0x08009800写入全0x00这样Bootloader读取AppB栈顶地址时会发现它不在RAM合法范围内判定分区无效自动回退到AppA。实际效果就是设备看起来什么都没发生继续跑着旧固件。这个动作模拟的是写入固件损坏的最坏情况。第二种让AppB启动后故意延时清零标志。我在AppB的main函数里加入一个3秒延时在延时结束前手动断电再上电。由于boot_count没有清零Bootloader下次启动时看到该分区的boot_count超过阈值自动回滚到AppA。这套流程模拟的是新固件能跑但跑一段时间崩溃的场景。第二种模拟做完我对AB分区机制才算真正有了信心。它不再是纸面上的概念而是真真切切能在断电瞬间保护设备的东西。做OTA项目最怕的就是升级失败后设备失联有了回滚哪怕新固件把自己搞崩了设备下一次上电依然能回到上一个稳定版本。7. 常见问题与排坑实录7.1 问题定位速查表我不是第一次做这类项目时就一次成功的中间遇到过几个典型的坑。下面把常见现象和排查方向整理成一张表供大家直接对照现象可能原因排查与解决方法烧录App后上电无反应App的IROM1地址或VECT_TAB_OFFSET没改对检查Target页面IROM1检查C/C里是否有VECT_TAB_OFFSET宏跳转后直接HardFault向量表偏移设置太晚或没有设置确认App的SystemInit执行时SCB-VTOR已经指向App区域串口收不到Bootloader的升级指令等待窗口太短或者板载USB转TTL跳线帽没接把等待窗口延长到1秒检查PA9/PA10与USB转TTL的连通性OTA传输一半卡死帧序号不连续Bootloader反复NAK检查上位机seq是否按uint16正确递增查CRC16计算是否一致升级成功后设备还是老版本目标分区被固定写死没有选择非活动分区升级前先读取active_slot动态决定写入AppA还是AppB第二次升级怎么都失败第一次升级后活动分区切到B第二次还在写B覆盖了正在运行的固件必须写非活动分区同上一行原因Flash擦写报PGERR或WRPRTERR地址越界、Flash被读保护、或未按半字对齐写检查分区地址范围确认没碰Bootloader区域写之前Unlock其中最隐蔽的是第二次升级失败这个坑。我第一次调试时第一次OTA非常顺利AppB跑起来了但第二次OTA直接死在半路。后来才发现我的Bootloader升级逻辑固定写AppB而当前运行的是AppB相当于边运行边擦写自己的代码区能成功才怪。正确的AB逻辑永远是新固件写对方的分区不是自己所在的分区。7.2 我踩过的几个坑一次性说给你听除了上面表格里的问题还有几个小经验值得单独讲。第一个坑是跳转前的串口中断残留。我在Bootloader里开了USART1接收中断用于检测升级指令第一次写完跳转函数后没有在跳转前关中断。结果App启动后偶尔会出现莫名其妙的串口数据错乱严重的时候直接死机。后来在jump_to_app函数里加了__disable_irq()问题彻底消失。如果你用的是HAL库还要注意把UART的DMA、空闲中断一并停掉。第二个坑是FLASH_ProgramHalfWord写入时缓冲区对齐。我的OTA接收缓冲区定义成了一个uint8_t数组初始地址是自然对齐的但某些编译器在结构体嵌套后可能让指针变得不对齐。一旦用uint16_t指针去访问非对齐的uint8_t缓冲区在Cortex-M3上不会像x86那样自动处理而是直接进HardFault。解决办法是在定义缓冲区时用__attribute__((aligned(4)))显式对齐或者用memcpy拷贝到对齐的临时变量再写入。第三个坑是外部晶振没起振导致Bootloader和App频率不一致。有一次我手头板子上的8MHz晶振虚焊Bootloader用内部HSI跑了跳转到App后App又尝试用外部HSE结果整个系统变慢且串口乱码。最后发现是晶振问题换了一块板子后一切正常。遇到串口波特率不对的情况先查晶振和PLL配置不要急着改上位机波特率。第四个坑和Flash页大小有关。不同容量的F103页大小不一样C8T6是1KB一页RCT6是2KB一页。我早期在一份代码里写死了1KB擦除后来换到RCT6上跑擦除函数明显错乱因为FLASH_ErasePage每次实际擦除的是2KB。如果你的Bootloader要兼容多种容量芯片最好在启动时根据芯片型号动态计算页大小或者统一用2KB对齐的分区方案。最后再分享一个小技巧。我在Bootloader的接收逻辑里加了一个串口空闲超时判定当连续50ms没有收到新数据时认为当前传输结束。这个技巧在处理最后一帧不足256字节时特别有用不需要额外的结束标记直接在超时后对缓存数据进行最后一次Flash写入即可。实际调试节奏快了很多也少掉了很多协议状态机的边界条件。按这套流程完整复现一遍你会对STM32的内部Flash、Bootloader跳转、AB分区切换、协议设计都有非常具体的认知。以后再去看Android的A/B升级或者汽车ECU的OTA方案脑子里就能浮现出对应的硬件行为。如果后续想把方案做得更安全可以考虑在上位机和Bootloader之间加入对固件包的签名用非对称算法验签后再写入这样即使传输通道被监听也无法伪造固件。不过这些都属于进阶话题了先把AB分区这条主链路跑通比什么都重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价