2、固件升级的硬件基础:MCU/SoC的存储架构
做固件升级,说白了你得先搞清楚芯片里那点存储空间是怎么安排的。我见过不少工程师,上来就写升级逻辑,结果发现Flash不够用,或者Bootloader把自己给覆盖了——嗯,那场面挺尴尬的。
这一节,咱们就把MCU/SoC的存储架构掰开揉碎讲清楚。你理解了这些,后面讲升级流程的时候,才能明白为什么要有那么多“小心翼翼”的设计。
2.1 三大存储元件:Flash、RAM、BootROM
一个典型的MCU或应用级SoC,内部通常有三个关键的存储角色。它们各司其职,缺一不可。
| 元件 | 特性 | 主要用途 | 掉电后数据 |
|---|---|---|---|
| Flash | 非易失、可擦写、读快写慢 | 存放固件代码、参数、文件系统 | 保留 |
| RAM | 易失、读写极快、容量通常较小 | 运行时堆栈、变量、数据缓存 | 丢失 |
| BootROM | 非易失、只读、出厂固化 | 存放一级启动代码(不可更改) | 保留 |
核心认知:固件升级的本质,就是把新固件写入Flash的指定区域。而BootROM和RAM,则是保证这个写入过程能安全、可靠执行的基础。
2.1.1 Flash:固件的“家”
Flash是存储固件的主体。我个人习惯把它想象成一个“格子本”,每个格子(Sector/Page)可以单独擦除和写入。但有个坑:Flash写入前必须先擦除,而且擦除操作很慢,通常需要几十到几百毫秒。
我在项目中遇到过一个问题:升级过程中突然断电,导致Flash里一个Sector只擦了一半。重新上电后,那片区域读出来的数据全是乱的。所以后来我设计升级逻辑时,一定会加“擦除校验”和“写后回读”的步骤。
⚠️ 注意:Flash的擦写寿命是有限的(通常1万到10万次)。频繁升级或日志写入,会加速Flash老化。设计参数存储区时,建议使用磨损均衡算法。
2.1.2 RAM:升级的“工作台”
RAM是临时存储区。升级过程中,它主要干三件事:
- 缓存接收到的固件包:从通信接口(UART、SPI、Wi-Fi)收来的数据,先放到RAM里攒够一包,再写入Flash。
- 运行升级程序:Bootloader或升级代理程序本身就在RAM里运行(或者从Flash拷贝到RAM里执行)。
- 存放临时变量和状态机:升级进度、校验结果、错误码等。
你想想看,如果RAM太小,一次只能缓存512字节,那升级一个大固件就得频繁擦写Flash,效率极低。所以我建议,RAM至少要有2KB以上的空闲空间给升级流程用,否则很容易出现“内存不足”的尴尬。
2.1.3 BootROM:芯片的“第一口气”
BootROM是芯片出厂时固化的一段只读代码。芯片上电后,CPU第一条指令就是从BootROM里取的。它的任务很简单:初始化硬件,然后决定从哪个地址启动。
BootROM通常支持多种启动模式:从Flash启动、从UART下载、从USB下载等。这给固件升级提供了“最后一道防线”——如果Flash里的Bootloader坏了,你还可以通过BootROM强制进入下载模式,重新烧录。
💡 经验之谈:我习惯在BootROM阶段就开启看门狗,防止升级过程中程序跑飞。虽然BootROM代码改不了,但你可以通过配置字(Option Bytes)来设置看门狗的行为。
2.2 存储分区规划:谁该待在哪?
有了Flash、RAM、BootROM,接下来就是怎么划分它们。分区规划做得好,升级就成功了一半。做不好?嗯,你可能得经常拿着编程器去现场救火。
一个典型的固件升级分区结构如下:
2.2.1 Bootloader区:升级的“守门员”
Bootloader区存放的是二级启动代码。芯片上电后,BootROM会跳转到Bootloader区执行。Bootloader的任务是:
- 检查升级标志:看看有没有“需要升级”的标记。
- 如果有升级请求:进入升级模式,接收新固件,写入App区。
- 如果没有升级请求:校验App区的完整性,然后跳转过去执行。
我曾经犯过一个错误:把Bootloader做得太大,占用了128KB。结果App区只剩256KB,客户要加新功能,死活塞不进去。最后只能重新分区,把Bootloader压缩到48KB。所以我的建议是:Bootloader能小就小,够用就行。通常32KB~64KB就足够了。
⚠️ 关键:Bootloader区在升级过程中绝对不能写!如果升级程序把自己所在的Flash区域给擦了,那芯片就变砖了。所以Bootloader区必须写保护,或者通过硬件引脚来锁定。
2.2.2 App区:主固件的“家”
App区就是存放你写的业务代码的地方。升级时,新固件会直接覆盖这个区域。
这里有个设计要点:App区起始地址必须固定。Bootloader跳转到App区时,是直接跳到一个绝对地址。如果你改了App区的起始地址,Bootloader也得跟着改,那升级就变成“先升级Bootloader,再升级App”的套娃问题了。
我个人习惯在App区的头部预留一个“向量表重定位”区域。这样即使App区起始地址变了,也能通过修改向量表来适配。嗯,这个技巧在Cortex-M系列芯片上特别有用。
2.2.3 参数区:升级的“记事本”
参数区虽然小,但作用很大。它通常存放:
- 升级标志:0xAA55 表示“请求升级”,0x0000 表示“正常启动”。
- 固件版本号:用于判断是否需要升级。
- 校验值:App区的CRC或哈希值,用于启动时校验完整性。
- 校准数据:设备出厂时的校准参数,升级时不能丢。
💡 避坑指南:我曾经遇到过一个问题:升级过程中参数区被意外写入了错误数据,导致设备重启后一直认为“需要升级”,陷入了死循环。后来我加了一个“参数区备份”机制——在Flash里存两份参数,一份主本,一份副本。启动时比较两者,如果一致才使用,不一致则用默认值恢复。
2.3 分区规划实战建议
说了这么多,到底怎么规划才合理?我根据经验总结了一个参考方案:
| Flash总容量 | Bootloader区 | App区 | 参数区 | 备注 |
|---|---|---|---|---|
| 256KB | 32KB | 216KB | 8KB | 适合简单IoT设备 |
| 512KB | 64KB | 432KB | 16KB | 适合中等复杂度设备 |
| 1MB | 64KB | 944KB | 16KB | 适合带文件系统的设备 |
| 2MB | 128KB | 1.8MB | 32KB | 适合带双备份(A/B升级)的设备 |
你可能会问:“为什么参数区只给这么点?” 嗯,因为参数区通常只需要存几个标志和校验值,8KB已经绰绰有余了。而且参数区擦写频繁,给它太大反而浪费Flash寿命。
好了,存储架构和分区规划就讲到这里。你理解了Flash、RAM、BootROM各自的作用,也知道了Bootloader区、App区、参数区该怎么划分,后面讲升级流程的时候,就能明白每一步操作背后的硬件逻辑了。