4. Flash驱动开发:Flash存储器特性、扇区擦除与编程、Flash驱动接口设计、磨损均衡策略

Flash驱动,说白了就是Bootloader和硬件之间的翻译官。你发指令,它干活。但Flash这玩意儿脾气不小——写之前得先擦,擦又得按扇区来,而且还有寿命限制。我早年做第一个Bootloader项目时,就因为没摸透Flash的脾气,差点把整个产线搞停。嗯,今天咱们就把这些坑一个个填上。

4.1 Flash存储器特性

先搞清楚Flash的分类。嵌入式里最常见的就是NOR和NAND两种。我习惯用一张表来对比它们,这样最直观。

特性 NOR Flash NAND Flash
读取速度 快,支持XIP(片上执行) 较慢,需先拷贝到RAM
写入速度 慢 快
擦除速度 慢(秒级) 相对较快
擦除粒度 扇区(通常4KB~128KB) 块(通常128KB~1MB)
坏块管理 无需,出厂无坏块 必须,出厂就有坏块
寿命(擦写次数) 约10万次 约1万~10万次
典型用途 代码存储、Bootloader 大容量数据、文件系统

Bootloader通常放在NOR Flash里,因为要支持XIP,上电就能跑。但如果你用的是NAND,那就得在Bootloader里先初始化NAND控制器,再把代码搬到RAM里执行。我个人建议,能用NOR就别折腾NAND,除非你的代码实在太大。

4.2 扇区擦除与编程

Flash有个铁律:写之前必须先擦。而且擦的最小单位是扇区,不是字节。你想想看,如果我只想改一个字节,却要把整个扇区擦掉再重写,这效率多低?但没办法,硬件就是这么设计的。

我举个例子,假设你用的是某款常见的NOR Flash,扇区大小64KB。你要更新扇区里的一个4KB的Bootloader子模块,流程是这样的:

  1. 把整个扇区读到RAM缓冲区。
  2. 在RAM里修改那4KB数据。
  3. 发擦除命令,擦掉整个扇区。
  4. 把RAM里的数据写回扇区。

代码实现大致长这样:

// 伪代码:扇区擦除与编程
#define SECTOR_SIZE  (64 * 1024)
#define TARGET_ADDR  0x08010000

uint8_t buffer[SECTOR_SIZE];

// 1. 读取整个扇区到RAM
memcpy(buffer, (uint8_t*)TARGET_ADDR, SECTOR_SIZE);

// 2. 在RAM中修改数据
modify_bootloader_submodule(buffer, 0x1000, 4096);

// 3. 擦除扇区
flash_erase_sector(TARGET_ADDR);

// 4. 写入数据
flash_program(TARGET_ADDR, buffer, SECTOR_SIZE);
注意:擦除操作期间绝对不能断电。我曾经遇到过产线上擦到一半断电,结果整个扇区数据全乱,Bootloader直接变砖。后来我们加了擦除前的CRC校验和擦除后的回读验证,才彻底解决这个问题。

4.3 Flash驱动接口设计

驱动接口设计,我习惯遵循一个原则:上层调用者不需要知道底层Flash是什么型号。你想想看,Bootloader的刷新逻辑如果跟具体的Flash型号耦合在一起,换一颗Flash就得重写一遍,那多痛苦?

所以我会抽象出这样一组接口:

接口函数 功能描述 参数说明
flash_init() 初始化Flash控制器和GPIO 无
flash_read(addr, buf, len) 从指定地址读取数据 addr: 起始地址; buf: 缓冲区; len: 长度
flash_write(addr, buf, len) 写入数据(自动处理页对齐) 同上
flash_erase(addr, len) 擦除指定区域(自动对齐扇区) addr: 起始地址; len: 擦除长度
flash_erase_sector(addr) 擦除单个扇区 addr: 扇区起始地址
flash_get_info() 获取Flash参数(大小、扇区数等) 返回结构体指针

这里有个细节:flash_write() 内部要处理页对齐。很多Flash的编程页大小是256字节或512字节,如果你写入的地址不是页对齐的,就得先读后写。我一般在驱动层就把这个逻辑封装好,上层只管给地址和数据就行。

小技巧:在 flash_erase() 里加一个地址范围合法性检查。如果传入的地址不在Flash映射范围内,直接返回错误码。这能避免很多因为指针越界导致的奇怪问题。

4.4 磨损均衡策略

Flash的擦写次数是有限的。NOR Flash大概10万次,NAND更少。如果你的Bootloader频繁更新某个扇区,那个扇区很快就会报废。磨损均衡,说白了就是别可着一个扇区薅羊毛。

我参与过一个OTA项目,设备每天要更新一次配置参数。如果每次都写同一个扇区,按10万次寿命算,大概能用270年。听起来很多对吧?但问题是,Bootloader本身也会频繁写日志、存状态。如果日志和配置都在同一个扇区,那寿命会急剧缩短。

常用的磨损均衡策略有几种:

  • 静态磨损均衡:把数据分散到多个扇区,每次写操作都换一个扇区。适合日志、配置这类频繁写入的小数据。
  • 动态磨损均衡:根据每个扇区的擦写次数,动态选择擦写次数最少的扇区来写。适合大块数据更新。
  • 坏块替换:当某个扇区达到寿命上限时,标记为坏块,用备用扇区替换。NAND Flash必须做这个,NOR一般不需要。

我画了一张图,帮你理解静态磨损均衡的流程:

静态磨损均衡流程图 静态磨损均衡流程图 写入请求 查找擦写次数最少的扇区 该扇区是否达到寿命上限? 否 写入该扇区 是 标记坏块 使用备用扇区 说明: 静态磨损均衡通过记录每个扇区的擦写次数,优先选择擦写次数最少的扇区进行写入。 当扇区达到寿命上限时,标记为坏块并启用备用扇区,从而延长整体Flash寿命。

实现磨损均衡时,我建议在Flash的末尾保留一个磨损均衡信息区,专门存放每个扇区的擦写次数。每次写入前先查这个表,写入后更新它。注意,这个信息区本身也要做磨损均衡,不然它先挂了就尴尬了。

核心要点:
  • Flash驱动要抽象化,上层不关心底层型号。
  • 擦除是扇区级操作,写入是页级操作,必须遵守硬件约束。
  • 磨损均衡不是可选项,是延长Flash寿命的必需品。
  • 坏块管理在NAND Flash中是强制要求,NOR可以酌情处理。

嗯,Flash驱动这块内容不少,但核心就这些。你只要把接口设计好,擦写流程走对,磨损均衡加上去,基本就稳了。我当年第一次做Bootloader时,就是忽略了磨损均衡,结果产品跑了半年就开始频繁报错。后来加上均衡策略,再也没出过问题。所以,别偷懒,该做的都得做。