第二节 固件升级原理:升级流程、分区表设计、Bootloader与App分区

好,咱们进入正题。固件升级这事儿,说白了就是让设备在运行中把自己给换了。听起来有点玄乎,对吧?我刚开始做基站开发时也觉得这玩意儿挺神奇——一个正在跑的程序,怎么能把自己给覆盖掉?

嗯,这里的关键在于「分工」。我们把存储空间分成几个区域,各干各的活。就像盖房子,你不能一边住人一边拆承重墙,得先搭个临时板房,再慢慢拆旧房。

2.1 升级流程:三步走,稳得很

一个完整的固件升级流程,我习惯把它拆成三个阶段:

  1. 下载阶段:新固件从远程服务器拉下来,存到备用区域
  2. 校验阶段:检查下载的文件是否完整、是否被篡改
  3. 切换阶段:让Bootloader接管,把新固件搬到主运行区

你想想看,如果下载到一半断电了怎么办?如果校验不通过就启动新固件,设备变砖了怎么办?这些问题,都是我们在设计流程时必须考虑的。

核心原则:升级过程中,任何时候断电或异常,设备都必须能恢复到可运行状态。说白了,就是「不死机」。

我在项目中遇到过最典型的情况:某次基站批量升级,网络突然抖动,30%的设备只下载了60%的固件。幸好我们设计了「断点续传」和「版本回退」机制,否则那场面...嗯,你懂的。

2.2 分区表设计:存储空间的「房产证」

分区表,就是告诉芯片:哪块地址归谁管,多大面积,干什么用。我建议你把它想象成一张地图。

一个典型的基站固件分区表长这样:

分区名          起始地址      大小        用途
-----------------------------------------------
Bootloader      0x08000000   64KB       启动加载器
App_Primary     0x08010000   1MB        主运行区(当前固件)
App_Backup      0x08110000   1MB        备份区(新固件下载区)
Config          0x08210000   128KB      配置参数区
Log             0x08230000   256KB      日志存储区
Factory         0x08270000   64KB       出厂固件(救砖用)

为什么要搞两个App分区?我刚开始做时也觉得浪费空间。直到有一次升级到一半,发现新固件有严重bug...你想想看,如果没有备份区,设备就彻底废了。两个分区,一个跑、一个备,这叫「双保险」。

我的经验:分区大小最好留20%余量。固件只会越来越大,别到时候想加个功能,发现空间不够了。我曾经就因为少算了200KB,导致后续三个版本都得重新调整分区表,那叫一个折腾。

2.3 Bootloader与App分区:谁先谁后?

Bootloader,就是上电后第一个跑的程序。它的任务很简单:检查App分区是否有效,有效就跳过去执行,无效就进入升级模式。

App分区,就是咱们真正干活的地方。基站协议栈、信号处理、网络管理,全在这里。

这两个分区的关系,我打个比方:

  • Bootloader = 机场的塔台,负责引导飞机(App)起飞降落
  • App = 飞机本身,负责执行飞行任务

塔台不能跟着飞机一起飞走,对吧?所以Bootloader分区是只读的,升级时绝对不能碰它。一旦Bootloader坏了,设备就真成砖了。

警告:千万不要在App运行期间擦写Bootloader分区!我见过有人为了省空间,把Bootloader和App放在同一个可擦写区域,结果一次升级失败,整个设备变砖,只能拆机用编程器救。那酸爽...你懂的。

2.4 升级流程的完整时序

咱们把整个流程串起来,看看实际跑起来是什么样子:

  1. 设备上电 → Bootloader启动
  2. Bootloader检查:App分区是否有效?
  3. 有效 → 跳转到App,正常运行
  4. 无效 → 进入升级等待模式(比如亮红灯、开FTP服务)
  5. App运行中收到升级指令 → 下载新固件到App_Backup分区
  6. 下载完成 → 校验CRC/SHA256,确保文件完整
  7. 设置标志位 → 告诉Bootloader「下次启动用新固件」
  8. 重启设备 → Bootloader读取标志位,从App_Backup启动
  9. 新固件运行 → 如果运行正常,把App_Backup复制到App_Primary
  10. 如果新固件启动失败 → Bootloader自动回退到App_Primary

你看,第9步和第10步是关键。为什么要复制?因为下次升级时,App_Backup要被覆盖,如果新固件一直跑在备份区,那下次升级就没地方存了。

避坑指南:我曾经设计过一个系统,升级后直接跑在备份区,不复制到主区。结果第二次升级时,备份区被覆盖,主区还是旧版本。设备重启后,Bootloader发现主区版本太老,又跳回备份区...嗯,死循环了。从那以后,我坚持「升级完必须同步到主区」。

2.5 分区表设计的几个坑

说几个我踩过的坑,你记一下:

  • 对齐问题:分区起始地址必须是擦除块大小的整数倍。比如Flash的擦除块是64KB,那分区起始地址就得是0x10000、0x20000这样。不对齐的话,擦除时会误伤邻居。
  • 版本号管理:每个分区头部都要存版本号。我习惯用4字节,格式是「主版本.次版本.修订号.构建号」。别用字符串,解析起来慢。
  • 校验和冗余:分区表本身也要有校验。我见过有人把分区表放在Flash开头,结果一次写操作出错,整个分区表乱掉,设备直接变砖。后来我加了两个备份分区表,一个坏了用另一个。

嗯,说到校验,我再啰嗦一句:CRC32不够用,用SHA256。CRC32只能检测随机错误,防不了人为篡改。基站设备涉及网络安全,别在这种地方省钱。

2.6 小结

固件升级这事儿,说白了就是「稳」字当头。分区表设计好了,流程设计严谨了,剩下的就是代码实现的问题。我见过太多人把精力花在「怎么让升级更快」上,结果忽略了「怎么让升级不死机」。记住:基站设备可以慢,但不能死

下一节,咱们聊聊具体的代码实现——怎么在STM32上写一个可靠的Bootloader。到时候我会把实际项目中的代码片段拿出来,咱们一行一行地过。