Flash 存储器深度解析:NAND 与 NOR 的区别

说到单片机里的非易失存储,Flash 绝对是主角。很多初学者搞不清 NAND 和 NOR 到底差在哪,我当年也踩过这个坑——第一次用 NAND Flash 做代码存储,结果发现不能直接寻址执行,项目差点延期。今天咱们就把这事彻底说透。

NAND vs NOR:核心差异

这两种 Flash 的架构完全不同。NOR 像一本书,你可以直接翻到任意一页读内容;NAND 像一卷胶片,你得先找到那一段,再整段读出来。说白了,NOR 支持随机访问,NAND 只支持页访问。

特性 NOR Flash NAND Flash
读取方式 随机访问,类似 RAM 页读取,类似硬盘
执行代码 支持 XIP(原地执行) 不支持,需拷贝到 RAM
写入速度 慢(字节编程) 快(页编程)
擦除速度 慢(块擦除) 相对较快
擦写寿命 约 10 万次 约 1~10 万次(视工艺)
容量密度 低(1~512Mb) 高(1Gb~1Tb)
成本 高 低

关键结论:NOR 适合存代码(直接跑),NAND 适合存数据(大容量)。STM32 内部 Flash 就是 NOR 架构,所以才能上电直接执行程序。

STM32 内部 Flash 的组织结构

STM32 的 Flash 不是一整块大内存,它分成了扇区、页、块三个层级。我习惯把扇区比作「抽屉」,页是「文件夹」,块是「文件柜」。不同型号的 STM32,这些层级的大小和数量都不一样。

以 STM32F103 为例

F103 的 Flash 分成了多个扇区,每个扇区 1KB 或 2KB。但更常见的 F4/H7 系列,扇区大小就不一样了。咱们看个典型结构:

// STM32F407 的 Flash 扇区划分(1MB 容量)
// 扇区 0~3:16KB 每个
// 扇区 4:64KB
// 扇区 5~11:128KB 每个

// 擦除时最小单位是扇区
// 写入时最小单位是半字(16位)或字(32位)
FLASH_EraseSector(FLASH_Sector_5, VoltageRange_3);
FLASH_ProgramWord(0x08020000, 0x12345678);

我的经验:做 IAP(在线升级)时,一定要搞清楚扇区大小。我曾经把 128KB 的扇区当 16KB 擦,结果把隔壁的代码也擦掉了——那次调试到凌晨三点。

读写寿命与擦除次数

Flash 不是永生的。每次擦写都会对浮栅晶体管造成损伤,就像反复折叠一根铁丝,总有一天会断。STM32 官方给出的寿命指标是:

  • 擦写次数:典型值 10,000 次(数据手册保证)
  • 数据保持:在 85°C 环境下可保持 20 年
  • 读操作:无限次(不影响寿命)

等等,10,000 次听起来很少?其实大多数应用根本用不完。但如果你做数据记录仪,每秒写一次 Flash,那 10,000 秒就废了——不到 3 小时!

避坑指南:我曾经接手过一个项目,设备运行半年后频繁死机。查到最后发现是 Flash 的一个扇区被写爆了——那个扇区存的是系统配置,每次上电都擦写一次。客户每天断电重启好几次,半年就超了寿命。解决方案:用磨损均衡算法,把配置数据分散到多个扇区。

磨损均衡与寿命管理

既然 Flash 有寿命限制,那怎么延长?核心思路就一条:别总盯着一个扇区擦写。

我常用的方法:

  1. 扇区轮换:维护一个索引表,每次写数据换一个扇区
  2. 写次数计数:记录每个扇区的擦写次数,优先用次数少的
  3. 缓存合并:小数据先攒到 RAM,凑够一页再写
// 简单的磨损均衡示例
#define SECTOR_COUNT 10
uint8_t wear_level[SECTOR_COUNT];

uint32_t get_next_sector(void) {
    uint32_t min_wear = wear_level[0];
    uint32_t sector = 0;
    for (int i = 1; i < SECTOR_COUNT; i++) {
        if (wear_level[i] < min_wear) {
            min_wear = wear_level[i];
            sector = i;
        }
    }
    return sector;
}

Flash 操作的关键时序

写 Flash 不是瞬间完成的。STM32 的 Flash 控制器有固定的时序要求:

  • 编程时间:约 20~50μs 每半字(16位)
  • 扇区擦除时间:约 20~40ms(视扇区大小)
  • 质量擦除时间:约 100~200ms(整片擦除)

嗯,这里要注意:擦除期间 CPU 会被阻塞。如果你有实时性要求,得考虑用 DMA 或者把擦除操作放到空闲时间做。

实战建议:做 Flash 驱动时,一定要加超时处理。我见过一个案例,Flash 擦除时电压不稳导致卡死,系统直接挂在那。加个 500ms 超时重试机制,问题就解决了。

Flash 结构图:从扇区到比特

下面这张图展示了 STM32 Flash 的层级关系,从芯片级到存储单元级:

STM32 Flash 层级结构图 STM32 Flash 整体(1MB / 512KB / 256KB) 扇区 0(16KB) 扇区 1(16KB) 扇区 2(16KB) ... 更多扇区(64KB / 128KB) 页 0(1KB) 页 1(1KB) 页 2(1KB) 页 3(1KB) ... 共 16 页 存储单元:每个页包含 1024 个字节,每个字节 8 个浮栅晶体管 擦除最小单位:扇区 | 编程最小单位:半字(16位) 注意:不同 STM32 系列的扇区大小不同,F1 系列多为 1KB/2KB,F4/H7 系列为 16KB/64KB/128KB 擦除前必须确保该扇区无正在执行的代码(不能擦自己)

常见故障与排查

Flash 出问题,症状往往很诡异。我总结了几种典型情况:

故障现象 可能原因 排查方法
程序跑飞 代码区被意外擦写 检查写保护寄存器,确认擦除地址范围
数据读出全 0xFF 扇区未编程或已被擦除 检查擦除操作是否误触发
写入失败 Flash 控制器忙或电压不足 检查 BSY 标志位,测量 VDD 是否稳定
数据偶尔出错 擦写寿命耗尽 读取擦写计数,检查磨损均衡

调试技巧:我习惯在 Flash 驱动里加一个调试接口,可以读出每个扇区的擦写次数和最后写入时间。这样现场出问题时,远程就能定位是不是 Flash 寿命问题。