2. 硬件架构设计:双区Flash布局

做固件升级,第一件事不是写代码,而是先把Flash怎么分想清楚。我见过太多项目,代码写了一半才发现Flash不够用,或者升级失败后设备变砖——说白了,都是布局没规划好。

这一章,我们就来聊聊双区备份架构下的Flash分区方案。我会结合自己踩过的坑,把Bootloader区、App A区、App B区、参数区这四块怎么划分、怎么对齐、怎么预留,一次讲透。

2.1 为什么需要双区备份?

先问一个问题:如果你的设备在升级过程中突然断电了,会发生什么?

答案是——程序被擦了一半,新的没写完,旧的也没了。设备直接变砖。

双区备份就是为了解决这个问题的。说白了,就是准备两份App空间:一份在跑,一份用来接收新固件。升级时只写备份区,主区不动。等新固件校验通过,再切换过去。

核心思想:任何时候,至少有一份可运行的固件在Flash里。

我记得2018年做一个工业控制器项目,客户要求OTA升级成功率99.9%以上。当时我用了单区方案,结果现场升级失败率高达5%。后来改成双区备份,失败率直接降到0.1%以下。从那以后,但凡涉及远程升级的项目,我必用双区。

2.2 四区划分方案

一个典型的双区Flash布局,分为四个区域:

区域名称 起始地址 大小 作用
Bootloader区 0x08000000 64 KB 启动引导、升级入口、故障检测
App A区 0x08010000 384 KB 主运行区,当前固件
App B区 0x08070000 384 KB 备份区,接收新固件
参数区 0x080D0000 64 KB 升级标志、版本号、配置参数

这个布局基于STM32F4系列,Flash总容量1MB。实际项目中,你需要根据芯片容量和应用大小灵活调整。

2.3 Bootloader区设计要点

Bootloader是系统的看门人。它负责三件事:

  • 上电检测:检查参数区的升级标志,决定启动哪个App
  • 固件接收:通过CAN或USB接收新固件,写入备份区
  • 故障回滚:如果App启动失败,自动切回旧版本

我个人习惯把Bootloader控制在64KB以内。为什么?因为Bootloader很少改动,写大了浪费空间。而且Bootloader越精简,出bug的概率越低。

小技巧:Bootloader里不要放复杂的协议栈。CAN驱动和USB驱动够用就行,别把TCP/IP协议栈塞进去——我见过有人这么干,结果Bootloader比App还大,得不偿失。

2.4 App A区和App B区的对称设计

App A和App B的大小必须完全一致。这不是随便定的,有两个原因:

  1. 地址映射方便:切换时只需要修改向量表偏移,代码不用重定位
  2. 擦写时间可控:两个区大小一样,升级耗时可以预估

我曾经在一个项目中偷懒,把App B区设得比App A区小。结果新固件功能增加,体积超标,写不进备份区。最后只能重新分区,所有设备返厂升级——教训深刻。

另外,两个区的起始地址必须按Flash扇区对齐。以STM32F4为例,扇区大小是16KB、64KB、128KB不等。App A从0x08010000开始,正好是第4个扇区(64KB对齐),这样擦写操作不会越界。

2.5 参数区的关键作用

参数区虽然只有64KB,但它是整个升级系统的决策中心。里面存着:

  • 升级标志:0xAA55表示需要升级,0x55AA表示升级完成
  • 当前运行区:0表示App A,1表示App B
  • 固件版本号:主版本号+次版本号+修订号,共3字节
  • 校验值:整个App区的CRC32或SHA256
  • 回滚计数:记录回滚次数,防止反复回滚
注意:参数区必须使用独立的Flash页,不能和App混在一起。否则擦写App时会误删参数。

我建议参数区至少预留2个备份页。为什么?因为Flash写入有风险,万一写参数时断电,至少还有一个备份可用。我曾经在量产时遇到过参数区写坏的情况,就是因为只用了1页,没有冗余。

2.6 整体架构图

下面这张图展示了完整的Flash布局和升级流程。你可以看到,Bootloader在中间协调,App A和App B对称分布,参数区独立存放。

双区Flash布局与升级流程 Bootloader区 (64KB) 0x08000000 - 0x0800FFFF App A区 (384KB) — 当前运行 0x08010000 - 0x0806FFFF App B区 (384KB) — 备份/升级目标 0x08070000 - 0x080CFFFF 参数区 (64KB) — 升级标志/版本/校验 0x080D0000 - 0x080DFFFF 正常启动 升级切换 读取标志 💡 关键流程: 上电 → Bootloader读取参数区 → 判断升级标志 → 启动App A或App B → 运行中接收新固件写入备份区

2.7 分区对齐的硬规则

分区时,有几个硬性规则必须遵守:

  • 扇区对齐:每个区的起始地址必须是Flash扇区大小的整数倍
  • 大小对齐:App A和App B大小必须一致,且是扇区大小的整数倍
  • 地址连续:四个区在地址空间上连续,不要留空洞
  • 参数区独立:参数区必须独占一个或多个扇区,不能和其他区共用
曾经踩过的坑:有一次我把App A区设成384KB,App B区设成400KB,想着多留点空间。结果切换时发现向量表偏移计算错误,App B跑不起来。后来老老实实改成对称大小,再没出过问题。

2.8 不同芯片的适配建议

不同厂商的Flash扇区大小不一样,分区方案也要跟着调整:

芯片系列 Flash总容量 扇区大小 推荐分区方案
STM32F103 512KB 1KB/2KB Bootloader 32KB + App A 224KB + App B 224KB + 参数 32KB
STM32F407 1MB 16KB/64KB/128KB Bootloader 64KB + App A 384KB + App B 384KB + 参数 64KB
GD32F450 2MB 32KB/128KB Bootloader 128KB + App A 896KB + App B 896KB + 参数 64KB
NXP LPC54606 512KB 32KB Bootloader 32KB + App A 224KB + App B 224KB + 参数 32KB

嗯,这里要注意:扇区越大,擦写时间越长。128KB的扇区擦一次可能要几百毫秒。如果你的升级流程对时间敏感,建议选扇区小的芯片,或者用双Bank架构的芯片。

2.9 本章小结

双区Flash布局是固件升级的基石。Bootloader管启动,App A和App B互为备份,参数区存决策信息。四者配合,才能实现可靠的升级和回滚。

我个人习惯在项目初期就把Flash布局画出来,贴在白板上。每次改代码前先看一眼,确保不会越界。你想想看,如果连Flash怎么分都没想清楚,后面写再多代码也是白搭。