2. NOR Flash基础:NOR与NAND Flash对比、存储单元结构、Page/Block/Sector概念、读/写/擦除特性

好,咱们正式开始NOR Flash的课程。这一节是基础中的基础,但也是很多新手容易混淆的地方。我个人习惯,在讲任何驱动之前,先把硬件的脾气摸透。NOR Flash和NAND Flash,虽然都叫Flash,但性格完全不同。你想想看,一个像内存一样可以随机访问,另一个像硬盘一样按块操作,这能一样吗?

2.1 NOR与NAND Flash的对比

先来张表,一目了然。我在项目中经常被问到“为什么不用NAND?便宜啊”。嗯,便宜是有代价的。

特性 NOR Flash NAND Flash
读取方式 随机访问,类似SRAM 按页读取,类似硬盘
读取速度 快(纳秒级) 较快(微秒级)
写入速度 慢(毫秒级) 快(微秒级)
擦除速度 慢(秒级) 快(毫秒级)
容量密度 低(通常128Mb以下) 高(可达Gb级)
位翻转 极少发生 常见,需要ECC
坏块管理 无需 必须
接口 并行/SPI 并行/SPI
典型用途 代码存储、启动加载 大容量数据存储

说白了,NOR Flash最大的优势就是“XIP(eXecute In Place)”,也就是代码可以直接在Flash上运行,不用搬到RAM里。这个特性在嵌入式系统里太重要了。我记得有一次做Bootloader,CPU上电后直接就从NOR Flash里取指令执行,省掉了拷贝的步骤,启动速度飞快。

而NAND Flash呢?它更像一个“数据仓库”。容量大、价格低,但读取方式受限。你不能像访问内存一样直接读某个地址,必须按页来。而且NAND天生就有坏块,就像硬盘有坏道一样,需要驱动层做坏块管理和ECC纠错。我刚开始接触NAND时,就被坏块折腾过,后来才明白这是它的“基因缺陷”。

核心区别一句话:NOR是“内存型”存储,适合存代码;NAND是“磁盘型”存储,适合存数据。

2.2 NOR Flash存储单元结构

咱们深入一点,看看NOR Flash里面到底长什么样。它的基本存储单元是一个浮栅晶体管(Floating Gate Transistor)。

你可以把它想象成一个“电子陷阱”。晶体管里有一个浮栅,被绝缘层包裹着。写入数据时,通过高电压把电子注入到浮栅里;擦除时,再把电子拉出来。电子被困在浮栅里,即使断电也不会消失,这就是非易失性的原理。

NOR Flash的存储单元是并联连接的。每个单元的一端接位线(Bit Line),另一端接地。这种结构的好处是:你可以通过地址线直接选中任意一个单元,读取它的状态。这就是“随机访问”的硬件基础。

我的经验:NOR Flash的读取时序非常像SRAM。你给它一个地址,它就能在几十纳秒内把数据吐出来。很多MCU的片内Flash其实就是NOR结构,所以CPU可以直接取指执行。

但并联结构也有代价——每个单元都需要一个独立的接触孔,这占用了大量芯片面积。所以NOR Flash的容量做不大,成本也降不下来。NAND Flash采用串联结构,单元密度高得多,但失去了随机访问能力。这就是工程上的取舍。

2.3 Page、Block、Sector概念

这三个概念,我敢说90%的初学者都搞混过。咱们一个一个理清楚。

  • Page(页):这是写入的最小单位。比如你写一个字节,实际上Flash内部会把整个Page都读出来,修改后再写回去。常见的Page大小是256字节或512字节。
  • Sector(扇区):这是擦除的最小单位。你不能只擦一个字节,必须擦掉整个Sector。常见大小是4KB或64KB。
  • Block(块):通常由多个Sector组成。有些芯片把Block作为擦除单位,有些则直接用Sector。具体要看数据手册。

为什么会这样?因为Flash的物理特性决定了:写入只能把1变成0(编程),而擦除只能把0变成1(恢复)。所以你要修改一个字节,必须先擦除整个Sector,再重新写入。这个“读-改-写”的过程,是Flash驱动设计的核心难点。

避坑指南:我曾经在一个项目里,直接往一个已经写过的地址再次写入数据,结果数据全乱了。后来才意识到,NOR Flash在写入前必须保证该地址处于“擦除态”(全1)。所以驱动里一定要做“写前擦除”检查。

2.4 读/写/擦除特性

好,咱们来聊聊实际操作。NOR Flash的三种操作,各有各的脾气。

读取(Read)

读取是最简单的。你只需要通过SPI接口发送“读命令”(通常是0x03或0x0B),然后跟上24位地址,Flash就会把数据吐出来。读取速度很快,通常能跑到几十MHz的SPI时钟。

// 伪代码示例:读取一个字节
uint8_t nor_read_byte(uint32_t addr) {
    spi_cs_low();
    spi_send_byte(0x03);      // 读命令
    spi_send_byte(addr >> 16); // 地址高8位
    spi_send_byte(addr >> 8);  // 地址中8位
    spi_send_byte(addr);       // 地址低8位
    uint8_t data = spi_recv_byte();
    spi_cs_high();
    return data;
}

嗯,这里要注意:有些Flash支持“快速读取”模式(0x0B),它会在地址后面加一个“dummy byte”,用来适应更高的时钟频率。我建议你在驱动里同时支持两种模式,根据时钟频率自动切换。

写入(Program)

写入就没那么痛快了。NOR Flash的写入是按Page进行的,而且写入前必须保证该Page已经被擦除。写入命令通常是0x02,后面跟上地址和数据。

写入时间比较长,一个Page通常需要几十到几百微秒。你可以通过读取状态寄存器(命令0x05)来查询写入是否完成。我个人习惯用“轮询”方式,而不是中断,因为写入操作本身就不频繁。

关键点:写入只能把1变成0。如果你想把0变成1,必须擦除。所以驱动里一定要有“写前检查”逻辑,避免在非擦除态下写入。

擦除(Erase)

擦除是最慢的操作,没有之一。一个Sector(4KB)的擦除时间可能长达几百毫秒,甚至几秒。擦除命令通常是0x20(Sector擦除)或0xC7(整片擦除)。

擦除操作会把整个Sector的所有位都变成1。所以擦除后,你读到的数据全是0xFF。这也是Flash的“初始状态”。

避坑指南:我曾经在擦除过程中突然断电,结果那个Sector的数据变得不伦不类,既不是旧数据,也不是全1。后来我学乖了,在擦除前先把重要数据备份到另一个Sector,擦除成功后再删除备份。这叫“掉电安全”设计。

总结一下NOR Flash的三大特性:

  • :随机访问,速度快,像内存一样用。
  • :按Page写入,只能1变0,写前必须擦除。
  • :按Sector擦除,只能0变1,速度最慢。

理解了这些,你就能明白为什么NOR Flash适合存代码,而不适合做频繁写入的数据存储。代码是“写一次,读多次”的典型场景,正好匹配NOR Flash的特性。而如果你要存日志文件,那还是乖乖用NAND或者SD卡吧。

好,这一节就到这里。下一节咱们开始实战,看看怎么用SPI接口驱动NOR Flash。到时候我会拿出一个实际项目里的代码,咱们一行一行地分析。