3、Flash存储技术:NAND Flash与NOR Flash区别、Flash磨损均衡算法、坏块管理

说到嵌入式系统的数据存储,Flash 是绕不开的话题。我这些年做过的项目,从智能电表到工业控制器,几乎都在跟 Flash 打交道。今天咱们就聊聊 Flash 存储技术里的几个核心问题:NAND 和 NOR 到底怎么选?磨损均衡怎么做?坏块怎么管?

3.1 NAND Flash 与 NOR Flash 的区别

先说说这两种 Flash 的本质区别。你想想看,NOR Flash 就像一本可以随机翻开的书,你想读哪一页都行,速度很快。而 NAND Flash 更像一卷胶片,你得从头到尾卷过去才能找到想要的那一帧。

为什么会这样?这跟它们的内部结构有关。NOR Flash 的存储单元是并联的,每个单元都有独立的地址线,所以支持随机访问。NAND Flash 的单元是串联的,只能按页(Page)和块(Block)来操作。

我在一个数据采集器项目里就吃过这个亏。当时选了 NOR Flash 做代码存储,因为 CPU 可以直接在 NOR 上执行代码(XIP,eXecute In Place),省了拷贝到 RAM 的步骤。但后来发现容量不够,想换 NAND,结果整个软件架构都得改——因为 NAND 不能直接执行代码。

特性 NOR Flash NAND Flash
读取速度 快(随机读取,约 80-100ns) 较快(顺序读取,约 20-30μs 首字节)
写入速度 慢(约 1-5MB/s) 快(约 10-20MB/s)
擦除速度 慢(块擦除约 0.5-1s) 快(块擦除约 1-2ms)
容量密度 低(通常 ≤ 512Mb) 高(可达 1Tb 以上)
位成本
可靠性 高(位翻转率低) 较低(需要 ECC 纠错)
典型应用 代码存储、BIOS、FPGA 配置 大容量数据存储、文件系统

核心结论:NOR 适合存代码,NAND 适合存数据。如果项目需要大容量(>64MB),基本只能选 NAND。如果对随机读取速度要求高,或者需要 XIP,那就选 NOR。

3.2 Flash 磨损均衡算法

Flash 有个要命的特性:擦写次数有限。NOR 一般能撑 10 万次,NAND 的 SLC 约 10 万次,MLC 只有 1 万次左右,TLC 更惨,大概 3000-5000 次。你想想看,如果系统老是往同一个块里写数据,那个块很快就报废了。

磨损均衡(Wear Leveling)就是来解决这个问题的。说白了,就是把写操作均匀地分散到所有块上,别让某个块「累死」。

我做过一个车载黑匣子项目,要求连续记录 72 小时的数据。一开始没做磨损均衡,结果用了三个月就出问题了——日志文件所在的块全坏了。后来加了磨损均衡,设备跑了两年多都没事。

3.2.1 动态磨损均衡

动态磨损均衡只在写入时做均衡。每次要写数据时,算法会挑一个擦写次数最少的块来写。实现起来比较简单,但有个问题:那些长期不更新的「冷数据」块,永远没机会被均衡到。

// 动态磨损均衡的简化实现
uint32_t find_least_worn_block(void) {
    uint32_t min_erase_count = 0xFFFFFFFF;
    uint32_t target_block = 0;
    
    for (uint32_t i = 0; i < TOTAL_BLOCKS; i++) {
        if (block_info[i].erase_count < min_erase_count) {
            min_erase_count = block_info[i].erase_count;
            target_block = i;
        }
    }
    return target_block;
}

3.2.2 静态磨损均衡

静态磨损均衡更狠一些。它不光管写入,还会定期把那些长期不动的「冷数据」搬走,让它们的块也能被擦写。代价就是多了数据搬移的开销,而且算法复杂得多。

我的建议:如果项目容量不大(比如 16MB 以下),用动态磨损均衡就够了。要是容量大、寿命要求长,比如工业设备,那就得上静态磨损均衡。我曾经在一个工控项目里,因为偷懒用了动态均衡,结果冷数据块寿命还剩 90%,热数据块已经挂了——嗯,后来老老实实改成了静态均衡。

3.3 坏块管理

坏块是 NAND Flash 的「原罪」。出厂时就可能有坏块,使用过程中还会不断产生新坏块。NOR Flash 基本没这个问题,但 NAND 不行,你必须得管。

坏块管理说白了就两件事:标记坏块跳过坏块

3.3.1 坏块标记

NAND Flash 出厂时,每个块的第一页(Page 0)的某个字节(通常是第 2048 字节,即 spare area 的第一个字节)会被标记为 0x00,表示这是个坏块。使用过程中,如果发现某个块擦写失败,也要手动标记。

// 检查块是否为坏块
int is_bad_block(uint32_t block_addr) {
    uint8_t bad_block_flag;
    // 读取块的第一页的 spare area
    nand_read_spare(block_addr, 0, &bad_block_flag, 1);
    // 0x00 表示坏块,0xFF 表示好块
    return (bad_block_flag == 0x00) ? 1 : 0;
}

// 标记坏块
void mark_bad_block(uint32_t block_addr) {
    uint8_t bad_flag = 0x00;
    nand_write_spare(block_addr, 0, &bad_flag, 1);
}

3.3.2 坏块跳过策略

常见的坏块跳过策略有两种:

  • 块地址映射(BBT,Bad Block Table):在 Flash 里维护一张坏块表,记录所有坏块的地址。读写时先查表,跳过坏块。这是最常用的方法。
  • 块偏移法:遇到坏块就直接跳过,把后面的块地址往前挪。简单粗暴,但地址会乱,不适合大容量场景。

注意:坏块表本身也要存到 Flash 里,而且得存两份,防止坏块表自己所在的块也坏了。我见过一个项目,坏块表只存了一份,结果那个块坏了,整片 Flash 的数据都读不出来了——这就是典型的「看门人自己先倒了」。

3.3.3 ECC 纠错

NAND Flash 还有个毛病:位翻转(Bit Flip)。好好的数据,读出来可能就变了。所以必须加 ECC(Error Correction Code)。

常用的 ECC 算法有:

  • 汉明码(Hamming Code):能纠 1 位错,检 2 位错。适合 SLC NAND。
  • BCH 码:能纠多位错。MLC 和 TLC 基本都得用这个。
  • LDPC 码:纠错能力更强,但计算量大。现在 3D NAND 基本都靠它。
// 简单的汉明码 ECC 计算(每 256 字节生成 3 字节 ECC)
void calculate_ecc(uint8_t *data, uint8_t *ecc) {
    // 这里省略了具体的汉明码计算过程
    // 实际项目中建议用硬件 ECC 引擎,软件算太慢
    // 我在一个 STM32 项目里试过软件 ECC,写 1MB 数据要卡 3 秒...
}

避坑指南:我曾经在一个项目里,为了省成本选了没有硬件 ECC 的 MCU,结果软件 ECC 算得飞慢,写入速度直接砍半。后来换了带硬件 ECC 的芯片,速度才上来。所以我的建议是——能用硬件 ECC 就别用软件,除非你的数据量极小。

3.4 小结

嗯,Flash 存储这块,说白了就是三个核心问题:选对类型(NOR 还是 NAND)、做好磨损均衡、管好坏块。我做了这么多年嵌入式,发现很多项目出问题,都是在这三个点上栽了跟头。

你想想看,一个设备如果因为 Flash 坏了而报废,那多冤啊。所以设计阶段就把这些考虑进去,后面能省很多麻烦。

下一章咱们聊聊文件系统,看看怎么在 Flash 上高效地管理数据。