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子模块,流程是这样的:
- 把整个扇区读到RAM缓冲区。
- 在RAM里修改那4KB数据。
- 发擦除命令,擦掉整个扇区。
- 把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);
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寿命的必需品。
- 坏块管理在NAND Flash中是强制要求,NOR可以酌情处理。
嗯,Flash驱动这块内容不少,但核心就这些。你只要把接口设计好,擦写流程走对,磨损均衡加上去,基本就稳了。我当年第一次做Bootloader时,就是忽略了磨损均衡,结果产品跑了半年就开始频繁报错。后来加上均衡策略,再也没出过问题。所以,别偷懒,该做的都得做。