4. Flash存储器基础:NAND Flash与NOR Flash的区别、坏块管理、ECC校验、Flash驱动接口
做Bootloader,说白了就是跟Flash打交道。你写的代码要存到Flash里,启动时要把它读出来,运行时的数据也要找个地方放。我这些年调过的板子,十有八九的问题都出在对Flash的理解不够深上。这一章咱们就把Flash的底裤扒干净。
4.1 NAND Flash vs NOR Flash:核心差异
很多人刚接触嵌入式时,总觉得Flash就是Flash,能存数据就行。其实NAND和NOR完全是两种生物。我习惯用一个比喻来理解:NOR像内存,NAND像硬盘。
| 对比项 | NOR Flash | NAND Flash |
|---|---|---|
| 读取速度 | 快,支持随机访问(类似SRAM) | 慢,只能按页读取 |
| 写入速度 | 慢(按字节/字编程) | 快(按页编程,批量写入) |
| 擦除速度 | 慢(按扇区擦除,通常64KB~256KB) | 快(按块擦除,通常128KB~2MB) |
| 密度 | 低(通常≤512Mb) | 高(可达Tb级别) |
| 坏块 | 几乎没有坏块 | 出厂就有坏块,使用中还会产生 |
| 可靠性 | 高,位翻转率低 | 需要ECC纠错 |
| 接口 | SRAM-like并行/SPI | 复用I/O(地址/数据共用) |
| 典型用途 | 存放Bootloader、代码原地执行(XIP) | 存放文件系统、大容量数据 |
关键结论:Bootloader的早期阶段(比如从复位向量到初始化DDR)通常放在NOR里,因为可以XIP(原地执行)。而存放内核、文件系统这些大块头,NAND是更经济的选择。
为什么会这样?NOR的读取时序跟SRAM几乎一样,CPU可以直接拉片选、给地址、读数据。NAND呢?你得先发命令,再发地址,再等它准备好,最后才能读数据。我见过有人非要在NAND上XIP,结果折腾了两个月,最后还是老老实实把启动代码搬到了NOR上。
4.2 坏块管理:NAND绕不开的坑
NAND Flash出厂时就有坏块,这是工艺决定的。你买一片新的NAND,第一个块(Block 0)保证是好的,其他的?不好说。我曾在项目中遇到一批NAND,坏块率高达5%,当时差点把采购骂死。
4.2.1 坏块标记
坏块怎么识别?标准做法是:在块的第1个页(Page 0)的 spare area(带外数据区)的第1个字节,如果值为0x00,就表示这个块是坏的。有些厂商会在第2个字节也做标记,但0x00是共识。
/* 检查坏块的伪代码 */
int is_bad_block(uint32_t block_addr) {
uint8_t marker;
/* 读取块内第1个页的spare area第1字节 */
nand_read_spare(block_addr, 0, &marker, 1);
if (marker == 0x00) {
return 1; /* 坏块 */
}
return 0; /* 好块 */
}
注意:千万不要在擦除操作后检查坏块标记!擦除会把spare area也清掉,标记就丢了。我见过有人写驱动时先擦除再检查,结果坏块全被当成好块,数据写进去就再也读不出来了。
4.2.2 坏块管理策略
坏块管理说白了就两件事:跳过坏块和替换坏块。Bootloader里不需要像文件系统那样做复杂的磨损均衡,但至少要做到这两点:
- 扫描并建立坏块表:上电后遍历所有块,把坏块地址记下来。
- 逻辑地址到物理地址映射:写数据时跳过坏块,把数据写到下一个好块。
我曾经在一个项目里偷懒,没做坏块表,直接按物理地址写。结果烧了100片板子,有3片启动不了。排查了一整天,最后发现是坏块把Bootloader的第二阶段给覆盖了。从那以后,我再也不敢省这一步。
4.3 ECC校验:数据不出错才是硬道理
NAND Flash有个毛病:位翻转(Bit Flip)。读出来的数据偶尔会有一位或几位跟写进去的不一样。这不是硬件坏了,是NAND的物理特性决定的。ECC(Error Correction Code)就是用来纠正这些错误的。
4.3.1 常见的ECC算法
| 算法 | 纠错能力 | 开销 | 适用场景 |
|---|---|---|---|
| 汉明码(Hamming) | 1位纠错,2位检错 | 低 | SLC NAND,老式控制器 |
| BCH码 | 多位纠错(可配置) | 中等 | MLC/TLC NAND,主流方案 |
| RS码(Reed-Solomon) | 多位纠错,适合突发错误 | 高 | 高可靠性存储 |
| LDPC码 | 接近香农极限 | 高(需要硬件加速) | 3D NAND,大容量场景 |
我个人习惯在Bootloader里用硬件ECC。现在的NAND控制器基本都集成了BCH或LDPC引擎,你只需要配置好参数,读写时硬件自动计算和校验。如果非要软件实现,BCH是个不错的选择,但要注意性能——在低速CPU上算一个页的ECC可能要花好几毫秒。
4.3.2 ECC在Bootloader中的实现
Bootloader的ECC实现跟文件系统不一样。文件系统可以容忍偶尔的读错误,重试一次就好。但Bootloader是启动代码,读错了系统就起不来。所以我的建议是:对Bootloader所在的区域,使用更强的ECC配置。
/* 配置NAND控制器ECC的示例(伪代码) */
void nand_ecc_config(void) {
/* 设置ECC强度:每512字节纠正8位错误 */
nand_ctrl->ECC_CFG = (8 << ECC_STRENGTH_SHIFT) |
(512 << ECC_STEP_SIZE_SHIFT);
/* 使能硬件ECC */
nand_ctrl->ECC_CTRL |= ECC_ENABLE;
}
/* 带ECC校验的页读取 */
int nand_read_with_ecc(uint32_t page_addr, uint8_t *buf, int len) {
int ret;
/* 启动DMA读取 */
nand_ctrl->DMA_ADDR = (uint32_t)buf;
nand_ctrl->DMA_LEN = len;
nand_ctrl->CMD = NAND_CMD_READ | NAND_CMD_ECC_EN;
/* 等待完成 */
while (!(nand_ctrl->STATUS & DMA_DONE));
/* 检查ECC状态 */
ret = nand_ctrl->ECC_STATUS;
if (ret & ECC_UNCORRECTABLE) {
return -1; /* 不可纠正的错误 */
}
if (ret & ECC_CORRECTED) {
/* 可纠正的错误,记录日志 */
log_warn("ECC corrected %d bits",
(ret >> ECC_BIT_COUNT_SHIFT) & 0xFF);
}
return 0;
}
经验之谈:ECC纠正过的数据,虽然读出来是对的,但说明这个页已经不太稳定了。如果Bootloader的某个关键页频繁触发ECC纠正,我建议在下次固件升级时把这个块标记为坏块,把数据迁移到好块上。
4.4 Flash驱动接口设计
好的驱动接口应该是硬件无关的。不管底层是NOR还是NAND,是SPI还是并行接口,上层代码看到的应该是一组统一的API。我习惯这样设计:
/* 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;
/* NOR Flash驱动实例 */
flash_drv_t nor_drv = {
.init = nor_init,
.read = nor_read,
.write = nor_write,
.erase = nor_erase,
.sync = nor_sync,
};
/* NAND Flash驱动实例 */
flash_drv_t nand_drv = {
.init = nand_init,
.read = nand_read,
.write = nand_write,
.erase = nand_erase,
.sync = nand_sync,
};
为什么要加sync接口?你想想看,NAND写数据时是先写到页缓存,再真正编程到存储单元。如果写完后立刻断电,数据可能还没落盘。sync就是确保数据真正写进去了。我在一个电池供电的设备上吃过这个亏,断电测试时固件总是损坏,加了sync之后问题就解决了。
4.4.1 驱动接口实现要点
- 地址对齐:NAND的读写必须按页对齐,擦除必须按块对齐。接口内部要做好对齐检查,不对齐就返回错误。
- 写前擦除:NOR和NAND都不能直接覆盖写,必须先擦除再写。驱动里最好自动处理这个逻辑,上层不用操心。
- 错误重试:NAND的读写可能因为位翻转失败,驱动应该自动重试2~3次。
避坑指南:我曾经在实现write接口时,为了省事直接调用了erase+program。结果上层连续写两个相邻的页时,第一页的数据被第二页的erase给擦掉了。正确的做法是:只在写地址跨块时才擦除,否则直接program。
4.5 本章知识体系
下面这张图把Flash相关的核心概念串起来了,你可以对照着梳理自己的知识结构:
嗯,Flash这块内容不少,但都是Bootloader绕不过去的硬知识。理解了NAND和NOR的区别,掌握了坏块管理和ECC校验,再设计一套干净的驱动接口,后面做启动链联调时就会顺手很多。我个人觉得,花时间把Flash驱动调稳了,整个Bootloader项目就成功了一半。