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存储器知识体系 Flash存储器 NOR Flash NAND Flash XIP(原地执行) 随机访问 页/块操作 高密度 NAND Flash 三大核心问题 坏块管理 跳过/替换策略 ECC校验 BCH/LDPC/汉明码 驱动接口 抽象/对齐/重试 Bootloader 中的 Flash 应用 Bootloader 通常放在 NOR 中 XIP,内核和文件系统放在 NAND 中

嗯,Flash这块内容不少,但都是Bootloader绕不过去的硬知识。理解了NAND和NOR的区别,掌握了坏块管理和ECC校验,再设计一套干净的驱动接口,后面做启动链联调时就会顺手很多。我个人觉得,花时间把Flash驱动调稳了,整个Bootloader项目就成功了一半。