3. 硬件选型与存储规划:Flash分区设计、双备份(A/B)系统原理、外部存储(SD/NAND)与内部Flash的选择
做OTA升级,硬件选型是第一步。说白了,选错了芯片,后面代码写得再漂亮也白搭。我这些年踩过的坑,有一半都跟存储规划有关。今天咱们就把这块掰开揉碎了讲清楚。
3.1 内部Flash vs 外部存储:到底怎么选?
很多新手上来就问:“我该用内部Flash还是外挂一个SD卡?” 这个问题其实没有标准答案。我个人的习惯是:先看你的固件有多大。
内部Flash,就是MCU自带的Flash。优点是快、稳、不用额外布线。缺点是容量有限。像STM32F103这种经典芯片,内部Flash通常只有256KB到512KB。如果你的固件只有100KB出头,那内部Flash完全够用。
外部存储,常见的有SPI Flash、SD卡、NAND Flash。容量从几MB到几GB都有。我曾在项目里用过W25Q64(8MB的SPI Flash),配合一个低成本的MCU,效果很好。
这里有个避坑指南:千万别把大固件硬塞进小Flash里。我曾经见过一个团队,为了省成本选了128KB Flash的芯片,结果固件编译出来120KB,加上OTA升级需要的备份区,根本放不下。最后只能砍功能,得不偿失。
我的建议:
- 固件 < 256KB:优先考虑内部Flash,省事
- 固件 256KB ~ 2MB:用外部SPI Flash,性价比高
- 固件 > 2MB:上NAND Flash或eMMC,但要注意坏块管理
3.2 Flash分区设计:别等到写代码时才想
分区设计,是OTA升级的基石。很多工程师喜欢先写代码,回头再想分区。嗯,这是大忌。我习惯在硬件选型阶段就把分区图画好。
一个典型的OTA分区方案长这样:
| 分区名称 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64KB | 启动引导、固件校验、升级入口 |
| App_A(当前运行) | 0x08010000 | 512KB | 主应用程序 |
| App_B(备份/待升级) | 0x08090000 | 512KB | 新固件存放区 |
| 参数区 | 0x08110000 | 64KB | 设备配置、升级状态标志 |
| 日志区 | 0x08120000 | 128KB | 升级日志、错误记录 |
你想想看,为什么参数区要单独划出来?因为升级过程中,用户的配置不能丢。我遇到过最惨的一次,升级后所有WiFi密码都丢了,客户直接打电话骂人。从那以后,参数区我都是单独放在一个扇区,升级时绝对不动它。
小技巧:分区大小最好是Flash扇区大小的整数倍。比如STM32的扇区是2KB或16KB,那你的分区就按2KB的倍数来规划。否则会浪费空间,而且擦写操作会很麻烦。
3.3 双备份(A/B)系统原理:为什么是“黄金标准”?
双备份系统,也叫A/B系统。原理很简单:Flash里同时存两份固件,一份运行,一份备用。升级时,新固件写到备用区,校验通过后切换启动。
为什么会这样设计?因为OTA升级最怕什么?最怕升级到一半断电了,或者网络断了,固件写了一半。这时候设备就变砖了。双备份系统可以完美解决这个问题。
具体流程是这样的:
- 设备当前在A区运行
- 下载新固件,写入B区
- 校验B区固件的CRC或哈希值
- 校验通过,设置启动标志为B区
- 重启,Bootloader从B区启动
- 如果B区运行正常,标记为“成功”
- 如果B区启动失败,回滚到A区
我记得有个项目,客户要求升级成功率必须达到99.99%。我们就是用A/B系统,配合Bootloader里的回滚机制。上线一年,几千台设备,没有一台变砖。
注意:双备份系统需要至少2倍固件大小的Flash空间。如果你的Flash只有512KB,固件却有300KB,那就没法做A/B了。这时候可以考虑“压缩升级包”或者“增量升级”的方案。
3.4 外部存储(SD/NAND)的选择与注意事项
当内部Flash不够用时,就得请外援了。常见的外部存储有三种:SPI Flash、SD卡、NAND Flash。
SPI Flash:我最常用的。比如W25Q64、W25Q128这些。优点是接口简单(SPI),驱动成熟,价格便宜。缺点是容量有限,最大也就64MB左右。适合固件在几MB以内的场景。
SD卡:容量大,便宜,而且可以插拔。但要注意,SD卡不适合频繁擦写。我有个项目用SD卡存日志,结果三个月就坏了一张卡。后来换成SPI Flash,稳如老狗。
NAND Flash:容量大,速度快,但有个大坑——坏块。NAND出厂时就可能有坏块,使用过程中还会产生新的坏块。你需要自己实现坏块管理和ECC校验。说实话,如果不是特别需要大容量,我不建议新手碰NAND。
我的选择优先级:
- 容量 < 16MB:SPI Flash,首选
- 容量 16MB ~ 128MB:SPI NAND(带坏块管理的),或者SD卡
- 容量 > 128MB:eMMC,省心但贵
3.5 实战中的分区代码示例
最后,给一段我常用的分区定义代码。这是基于STM32的HAL库写的,其他平台思路一样。
// flash_partition.h
#ifndef __FLASH_PARTITION_H__
#define __FLASH_PARTITION_H__
// 假设Flash起始地址为0x08000000,大小为1MB
#define FLASH_BASE_ADDR 0x08000000
#define FLASH_SIZE 0x00100000 // 1MB
// 分区定义
#define BOOTLOADER_ADDR 0x08000000
#define BOOTLOADER_SIZE 0x00010000 // 64KB
#define APP_A_ADDR 0x08010000
#define APP_A_SIZE 0x00080000 // 512KB
#define APP_B_ADDR 0x08090000
#define APP_B_SIZE 0x00080000 // 512KB
#define PARAM_ADDR 0x08110000
#define PARAM_SIZE 0x00010000 // 64KB
#define LOG_ADDR 0x08120000
#define LOG_SIZE 0x00020000 // 128KB
// 升级状态标志
typedef enum {
UPGRADE_IDLE = 0,
UPGRADE_IN_PROGRESS,
UPGRADE_SUCCESS,
UPGRADE_FAILED
} upgrade_status_t;
#endif // __FLASH_PARTITION_H__
这段代码我几乎每个项目都会复用。你只需要根据实际Flash大小调整地址和大小就行。嗯,这里要注意:地址一定要对齐到扇区边界,否则擦写时会出问题。
3.6 总结一下
硬件选型和存储规划,说白了就是三个字:算清楚。算清楚固件多大,算清楚Flash够不够,算清楚分区怎么划。别等到代码写完了才发现空间不够,那时候改起来就痛苦了。
我个人经验是:多留30%的余量。比如固件预计300KB,那就按500KB来规划分区。因为后续功能迭代、日志增加、参数扩展,都会吃掉空间。留点余量,后面你会感谢自己的。
下一章,咱们聊聊Bootloader的具体实现。那是OTA升级的“守门员”,写不好,前面所有努力都白费。