4、Flash驱动开发:读写擦除操作、分区规划与磨损均衡

Flash 驱动,说白了就是固件升级系统的地基。地基没打好,上面盖的房子再漂亮也得塌。我在这个领域摸爬滚打十几年,见过太多因为 Flash 操作不当导致的“变砖”事故。今天咱们就把这块硬骨头啃下来。

4.1 Flash 的物理特性与基本操作

先聊聊 Flash 的脾气。它有三个核心操作:读、写、擦除。但有个坑你必须知道——写操作只能把 1 变成 0,不能把 0 变成 1。想把 0 变回 1?只能擦除。而擦除的最小单位是扇区(Sector),通常是 4KB 或 64KB。

⚠️ 我曾经踩过的坑: 有个项目,同事直接往已写过数据的地址写新数据,结果数据全乱套了。后来排查才发现,他忘了先擦除。记住:写之前必须擦除,擦除的最小单位是扇区。

来看一个标准的 Flash 驱动接口设计:

/* Flash 驱动接口定义 */
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);
} flash_drv_t;

我个人习惯把接口设计成这种结构体形式。好处很明显:换 Flash 芯片时,只需要换一套驱动函数指针,上层代码完全不用动。

4.2 分区规划:给固件安个家

分区规划是门学问。你想想看,一个固件升级系统,至少需要这几个区域:

分区名称 用途 大小建议 说明
Bootloader 启动引导 64KB 不可升级,出厂固化
App Primary 主固件运行区 512KB 正常运行时执行
App Backup 备份固件区 512KB 升级失败时回滚
Download 下载暂存区 512KB 接收新固件数据
Config 配置参数区 64KB 设备参数、升级标志
Log 日志记录区 128KB 运行日志、错误记录

嗯,这里要注意:分区大小必须是擦除扇区大小的整数倍。否则你擦除时会擦到隔壁分区的数据,那画面太美我不敢看。

💡 我的经验: 分区表建议放在 Bootloader 的固定偏移位置,用结构体定义。这样 Bootloader 和 App 都能读取同一份分区信息,避免硬编码地址带来的维护噩梦。

4.3 磨损均衡:让 Flash 活得更久

Flash 有擦写寿命限制——MLC 大约 3000-10000 次,SLC 能到 10 万次。如果频繁往同一个扇区写数据,那个扇区会先“累死”。磨损均衡就是让所有扇区“轮流上班”。

为什么会这样?因为固件升级时,配置参数、升级标志这些数据会频繁更新。如果不做均衡,Config 分区可能几个月就报废了。

我常用的策略是动态映射 + 日志结构:

/* 磨损均衡实现思路 */
typedef struct {
    uint32_t logical_addr;   /* 逻辑地址 */
    uint32_t physical_addr;  /* 物理地址 */
    uint32_t erase_count;    /* 擦除次数 */
    uint8_t  valid;          /* 是否有效 */
} wear_level_entry_t;

/* 写操作流程 */
int wear_level_write(uint32_t logic_addr, uint8_t *data, uint32_t len) {
    /* 1. 找一个擦除次数最少的空闲扇区 */
    uint32_t free_phy = find_min_erase_sector();
    
    /* 2. 擦除该扇区 */
    flash_erase(free_phy);
    
    /* 3. 写入数据 */
    flash_write(free_phy, data, len);
    
    /* 4. 更新映射表 */
    update_mapping(logic_addr, free_phy);
    
    /* 5. 标记旧扇区为无效 */
    invalidate_old_sector(logic_addr);
    
    return 0;
}

说白了,就是每次写数据都换个新地方,旧的标记为“垃圾”,等垃圾攒够了再统一回收。这思路跟 SSD 的 FTL 层有点像,只不过咱们在 MCU 上实现个简化版。

🔑 核心要点:
  • 磨损均衡的核心是避免热点——不让某个扇区被频繁擦写
  • 映射表要保存在 Flash 里,掉电不丢失
  • 垃圾回收要在系统空闲时做,不能阻塞升级流程

4.4 整体架构与流程

把上面这些串起来,就是一套完整的 Flash 管理层。我画了张图帮你理解:

Flash 驱动与磨损均衡架构图 Flash 驱动与磨损均衡架构 固件升级系统(上层应用) Flash 管理层 分区管理 磨损均衡 垃圾回收 Flash 驱动层(读/写/擦除) 物理 Flash 芯片 扇区0 | 扇区1 | ... | 扇区N 💡 关键点 写前必须擦除 分区对齐扇区大小 ⚠️ 避坑 避免频繁写同一扇区 掉电保护要做好

从图里能看出来,上层应用只管调用接口,不用关心底层怎么擦、怎么均衡。这就是分层设计的好处。

4.5 避坑指南与实战建议

最后,分享几个我这些年用真金白银换来的教训:

  • 掉电保护必须做:升级过程中突然断电,数据写到一半怎么办?我的做法是写前先写“升级中”标志,写完后改为“完成”。Bootloader 启动时检查这个标志,如果发现是“升级中”,就回滚到备份区。
  • 校验不能省:每次写完数据,读回来做 CRC 校验。别信 Flash 的“写入成功”返回值,我遇到过写成功但数据不对的奇葩情况。
  • 磨损均衡的映射表要备份:映射表坏了,整个 Flash 的数据都找不到了。我一般存两份,交叉校验。
  • 别在中断里做擦除操作:擦除一个扇区可能要几十毫秒,在中断里做会阻塞系统。放到任务里做,或者用 DMA。
⚠️ 我曾经踩过的坑: 有个量产项目,用了某品牌的 Flash,擦除时间比数据手册长了 3 倍。后来发现是批次问题。从那以后,我每次换 Flash 批次都会做全参数测试,尤其是擦除时间和写时间。

Flash 驱动开发,说白了就是“小心驶得万年船”。把基础打牢了,后面的升级系统才能跑得稳。记住:分区规划决定上限,磨损均衡决定寿命,驱动质量决定可靠性。这三样都做好了,你的固件升级系统就成功了一半。