1. Flash硬件基础:Flash存储器类型、内部结构、读写擦除原理、时序参数解读
做嵌入式底层开发,Flash 是绕不开的硬骨头。我刚开始接触 Bootloader 时,总觉得 Flash 就是个能存数据的黑盒子,直到被坑了几次才老老实实啃完数据手册。今天咱们就把 Flash 的底裤扒干净。
1.1 NOR Flash 与 NAND Flash:两种截然不同的性格
Flash 存储器分两大派系:NOR 和 NAND。名字来源于逻辑门电路的结构,但咱们搞工程的不需要背这个,记住它们的脾气就行。
NOR Flash,说白了就是「随机访问」的王者。它的地址线和数据线是独立引出的,CPU 可以直接映射到内存空间,像读 RAM 一样读它。我最早做的一款工业控制器,就是用 NOR Flash 存代码,上电直接执行,连拷贝到 RAM 的步骤都省了。
NAND Flash 呢?它走的是「块设备」路线。地址线复用,数据以页为单位访问,不能直接寻址到字节。你想想看,U 盘、SD 卡、手机里的存储,基本都是 NAND。为什么?因为便宜,容量大。
我整理了一个对比表,方便你快速把握:
| 特性 | NOR Flash | NAND Flash |
|---|---|---|
| 读取方式 | 随机访问,类似 RAM | 页读取,类似硬盘 |
| 写入速度 | 慢(字节编程) | 快(页编程) |
| 擦除速度 | 慢(扇区擦除) | 快(块擦除) |
| 容量密度 | 低(通常 512KB~256MB) | 高(通常 1GB~1TB) |
| 坏块管理 | 不需要 | 必须处理 |
| 典型应用 | 代码存储、Bootloader | 文件系统、大容量数据 |
核心结论:做 Bootloader 开发,NOR Flash 是首选。因为它支持 XIP(就地执行),上电就能跑代码。NAND 虽然便宜,但需要额外的坏块管理和 ECC 校验,不适合做启动介质。
1.2 内部结构:从浮栅晶体管说起
Flash 存储单元的核心,是一个浮栅晶体管。嗯,这里要注意,它比普通 MOS 管多了一层浮栅,夹在控制栅和沟道之间。
为什么能存数据?因为浮栅可以「困住」电子。写入时,通过热电子注入把电子打进浮栅;擦除时,通过 Fowler-Nordheim 隧穿把电子拉出来。电子困住了,阈值电压就变了,读的时候就能区分 0 和 1。
我个人习惯把 Flash 的存储层次记成:芯片 → 块(Block) → 扇区(Sector) → 页(Page) → 字节(Byte)。不同厂家叫法略有差异,但逻辑是一样的。
- 块(Block):擦除的最小单位。NOR 的块大小通常是 64KB 或 128KB,NAND 的块是 128KB 或 256KB。
- 扇区(Sector):NOR 里扇区是擦除子单位,比如 4KB。NAND 里扇区通常指 512 字节。
- 页(Page):NAND 的读写单位,通常是 2KB、4KB 或 8KB。NOR 没有页的概念。
避坑指南:我曾经在项目里把 NOR 的扇区大小搞错了,擦除时传了个 64KB 的参数,结果把相邻扇区的数据也抹了。后来我养成了一个习惯——每次写 Flash 驱动前,先把数据手册的「Memory Organization」章节打印出来贴在工位上。
1.3 读写擦除原理:三个操作,三种脾气
Flash 的操作就三种:读、写(编程)、擦除。但它们的脾气完全不同。
读操作最简单。NOR 直接给地址,数据就出来了,跟读 SRAM 一样。NAND 呢?得先发命令、发地址、再读数据,流程复杂一些。
写操作,也叫编程。注意,Flash 只能把 1 写成 0,不能把 0 写成 1。所以写之前必须先擦除,把整个块变成全 1 状态。我刚开始做的时候不理解,为什么不能直接覆盖写?后来想明白了——浮栅里的电子只能通过擦除才能拉出来,写操作只能注入电子。
擦除操作最耗时。NOR 擦除一个扇区可能要几百毫秒,NAND 擦除一个块也要几毫秒。为什么这么慢?因为擦除是批量操作,要同时把整个块里所有单元的电子都拉出来。
来看一个典型的 NOR Flash 编程时序(以 SST39VF160 为例):
// 写一个字节到 NOR Flash
void NOR_WriteByte(uint32_t addr, uint8_t data)
{
// 第一步:发送编程命令序列
*(volatile uint16_t*)0x5555 = 0xAA; // 解锁
*(volatile uint16_t*)0x2AAA = 0x55; // 解锁
*(volatile uint16_t*)0x5555 = 0xA0; // 编程命令
// 第二步:写入数据
*(volatile uint16_t*)addr = data;
// 第三步:等待完成(轮询 DQ7 或 DQ6)
while(!(*(volatile uint16_t*)addr & 0x80)); // 等待 DQ7 变 1
}
警告:上面的代码是简化版。实际项目中,一定要加超时处理。我曾经遇到过 Flash 老化后编程时间变长,没有超时导致系统死锁。建议超时时间设为最大编程时间的 3 倍。
1.4 时序参数解读:数据手册里藏着魔鬼
数据手册里的时序参数,是 Flash 驱动开发的「圣经」。但很多人一看那些波形图和表格就头大。我教你一个方法——抓住几个关键参数就行。
读时序的关键参数:
- tRC(读周期时间):两次连续读操作的最小间隔。NOR 通常是 70ns~100ns。
- tACC(地址访问时间):从地址有效到数据有效的时间。这个决定了你的 CPU 能不能直接连。
- tOE(输出使能时间):从 OE 有效到数据输出的时间。
写/编程时序的关键参数:
- tBP(字节编程时间):写一个字节需要的时间。NOR 通常是 10μs~20μs。
- tWC(写周期时间):两次连续写操作的最小间隔。
擦除时序的关键参数:
- tSE(扇区擦除时间):擦除一个扇区的时间。NOR 通常是 25ms~100ms。
- tBE(块擦除时间):擦除一个块的时间。NAND 通常是 2ms~10ms。
我整理了一个实际项目中常用的时序参数速查表:
| 参数 | 典型值(NOR) | 典型值(NAND) | 说明 |
|---|---|---|---|
| tRC | 70ns | 25ns(页模式) | 读周期时间 |
| tBP | 10μs | 200μs(页编程) | 编程时间 |
| tSE/tBE | 50ms(扇区) | 3ms(块) | 擦除时间 |
| 擦除次数 | 10万次 | 1万~10万次 | 寿命 |
个人经验:我习惯在驱动初始化时,把 Flash 的 ID 读出来,然后根据 ID 查表加载对应的时序参数。这样同一套驱动可以兼容不同厂家的 Flash。有一次客户换了 Flash 型号,我改个表就搞定了,省了不少事。
1.5 避坑指南:那些年我踩过的 Flash 坑
做 Flash 驱动开发,有几个坑是新人必踩的。我帮你列出来,省得你走弯路。
- 擦除后直接写,没等擦除完成——我曾经在擦除命令发出后,没等状态寄存器返回就写数据,结果数据全写飞了。记住,擦除完成后一定要检查状态。
- 写操作没做超时处理——Flash 老化后,编程时间会变长。不加超时,系统可能永远卡在 while 循环里。
- 忽略了写保护——有些 Flash 有写保护寄存器,默认是锁住的。不解锁,写操作直接失败。
- 跨扇区写数据——NOR 的扇区边界不能跨过去写。你写一个字节,如果地址跨了扇区,数据可能写到错误的位置。
- NAND 的坏块处理——NAND 出厂就有坏块,使用过程中还会产生新坏块。不做坏块管理,数据迟早会丢。
好了,Flash 硬件基础就讲到这里。下一章咱们开始动手写 Flash 驱动,从最简单的读 ID 开始。记住,数据手册是你最好的朋友,别偷懒。