3、硬件抽象层设计:Flash分区规划、存储驱动抽象、电源管理接口
好,咱们接着往下聊。上一章我们把OTA的总体架构搭起来了,现在要落地到硬件抽象层。说白了,这一层就是给上层应用一个「干净」的接口,让它们不用关心底层用的是啥Flash芯片、啥电源芯片。
我个人习惯,做嵌入式软件架构时,硬件抽象层是第一个要敲定的模块。为什么?因为它是整个系统的地基。地基不稳,上层再漂亮的逻辑都是空中楼阁。我在项目中遇到过好几次,因为Flash分区没规划好,导致OTA升级到一半空间不够,最后只能返厂重刷——那叫一个惨。
3.1 Flash分区规划:别等出事了再后悔
Flash分区,说白了就是把存储空间切成几块,每块干不同的事。TWS耳机里Flash通常不大,常见的是2MB到8MB。空间有限,所以每一块都要精打细算。
我建议的分区方案是这样的:
| 分区名称 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x000000 | 64KB | 启动引导,校验固件 |
| App_A | 0x010000 | 512KB | 主运行区(当前固件) |
| App_B | 0x090000 | 512KB | 备份区(新固件下载) |
| Factory | 0x110000 | 128KB | 出厂固件,救砖用 |
| Config | 0x130000 | 32KB | 配对信息、音量设置等 |
| OTA_Info | 0x138000 | 16KB | 升级状态、版本号 |
| Log | 0x13C000 | 64KB | 运行日志,调试用 |
嗯,这里要注意几个点:
- App_A和App_B必须大小一致。这是双备份OTA的核心。我曾经见过有人把两个区设成不同大小,结果升级时发现新固件放不下——这种低级错误,千万别犯。
- Factory分区要独立。它只读,不擦除。万一升级把两个App区都搞坏了,还能从Factory启动。
- OTA_Info分区要放在最后。因为它的擦写频率最高,放在最后可以避免磨损影响其他分区。
核心原则:分区规划要「一次定死,后期不改」。一旦产品量产,Flash分区就不能动了。所以前期一定要想清楚,留够余量。
3.2 存储驱动抽象:别让上层知道你在用啥芯片
存储驱动抽象,说白了就是给上层提供一套统一的读写接口。不管底层是W25Q64还是MX25L128,上层调用的函数名都一样。
我一般这样设计接口:
// 存储驱动抽象层接口
typedef struct {
int (*init)(void);
int (*read)(uint32_t addr, uint8_t *buf, uint32_t len);
int (*write)(uint32_t addr, const uint8_t *buf, uint32_t len);
int (*erase)(uint32_t addr, uint32_t len);
int (*sync)(void); // 确保数据落盘
} storage_driver_t;
你想想看,有了这个结构体,上层代码根本不需要知道底层是啥。初始化时传入一个具体的驱动实例就行:
// 具体芯片的驱动实现
static storage_driver_t g_w25q64_driver = {
.init = w25q64_init,
.read = w25q64_read,
.write = w25q64_write,
.erase = w25q64_erase,
.sync = w25q64_sync,
};
// 上层使用
storage_driver_t *drv = &g_w25q64_driver;
drv->init();
drv->read(0x010000, buf, 1024);
小技巧:我习惯在驱动层加一个「写前擦除」的自动处理。因为Flash写入前必须先擦除,这个逻辑放在驱动层,上层就不用操心了。但要注意,擦除操作很慢(一个扇区可能几十毫秒),别在中断里调用。
还有一个坑,我曾经踩过:Flash的写操作不是原子的。如果写一半断电,数据就坏了。所以驱动层要提供「写后校验」的机制,或者用双缓冲区策略。
3.3 电源管理接口:省电是TWS的命根子
TWS耳机最怕什么?没电。所以电源管理接口的设计,直接决定了产品的续航表现。
我建议的电源管理接口包含这几个核心功能:
- 电量检测:获取当前电池电压和百分比
- 充电状态:是否在充电、充电完成
- 低电量告警:低于阈值时触发回调
- 功耗模式切换:正常、待机、休眠、深度休眠
代码层面,我一般这样抽象:
// 电源管理接口
typedef enum {
POWER_MODE_ACTIVE, // 正常模式
POWER_MODE_IDLE, // 待机
POWER_MODE_SLEEP, // 休眠
POWER_MODE_DEEP_SLEEP // 深度休眠
} power_mode_t;
typedef struct {
int (*get_battery_level)(void); // 返回0-100
int (*get_charging_status)(void); // 0:未充电 1:充电中 2:已充满
int (*set_power_mode)(power_mode_t mode);
void (*register_low_battery_callback)(void (*cb)(void));
} power_manager_t;
注意:OTA升级过程中,千万不能进入深度休眠!我见过一个产品,升级到一半耳机自动休眠了,结果固件只写了一半,直接变砖。所以升级前一定要调用 set_power_mode(POWER_MODE_ACTIVE) 锁定功耗模式。
还有一个细节:电量检测要防抖。电池电压在负载变化时会波动,比如蓝牙发射时电压会瞬间下降。我习惯用滑动平均滤波,取最近10次采样的平均值,这样比较准。
嗯,说到这我想起一个项目。当时我们做一款TWS耳机,客户要求续航8小时。结果实测只有5小时。排查下来,发现是Flash擦除操作太频繁,每次擦除要消耗几十毫安电流。后来我们在驱动层加了「擦除合并」的优化——把多次小擦除合并成一次大擦除,功耗直接降了30%。
所以你看,硬件抽象层设计得好不好,直接影响到产品的性能和续航。别觉得这是「底层杂活」,它其实是整个系统的骨架。
总结一下:
- Flash分区要留够余量,App_A和App_B必须大小一致
- 存储驱动用结构体抽象,上层不关心具体芯片
- 电源管理要防抖、防休眠打断升级
- 所有接口都要考虑异常情况(断电、写失败等)
下一章,咱们聊聊传输层设计——蓝牙怎么高效、可靠地传固件数据。那也是个坑不少的地方。