2. Flash存储器详解:Flash物理结构(NOR与NAND)、STM32 Flash组织架构、读操作与预取缓冲
说到单片机里的Flash,很多同学第一反应就是“存代码的地方”。这话没错,但Flash本身的门道其实不少。我最早接触Flash时,以为它就是个能反复擦写的ROM,后来在项目里踩过几次坑,才真正开始研究它的物理结构和访问方式。
这一节,我们就把Flash掰开揉碎了讲清楚。从NOR和NAND这两种主流架构的区别,到STM32内部Flash的具体布局,再到读操作和预取缓冲的机制。嗯,内容有点多,但都是硬货。
2.1 NOR Flash与NAND Flash:两种截然不同的“性格”
Flash存储器主要分两大类:NOR和NAND。名字听起来差不多,但内部结构和应用场景差别很大。
我个人习惯把NOR Flash比作“内存的远亲”——它支持随机访问,CPU可以直接在上面执行代码。而NAND Flash更像“硬盘的亲戚”——它按页读写,按块擦除,适合做大容量数据存储。
| 对比项 | NOR Flash | NAND Flash |
|---|---|---|
| 存储单元连接方式 | 并联(每个单元独立连接位线) | 串联(多个单元共享位线) |
| 读取方式 | 随机访问,支持XIP(片上执行) | 页读取,不支持直接XIP |
| 写入速度 | 较慢(字节/字编程) | 较快(页编程) |
| 擦除速度 | 较慢(块擦除) | 较快(块擦除) |
| 容量密度 | 低(通常几MB到几十MB) | 高(几百MB到几TB) |
| 坏块管理 | 无需(出厂无坏块) | 需要(出厂可能有坏块) |
| 典型应用 | MCU内部Flash、BIOS存储 | U盘、SSD、大容量存储卡 |
为什么NOR能随机访问?因为它的存储单元是并联的,每个单元都有一条独立的位线连接到数据总线。CPU发一个地址,就能直接选中对应的存储单元。而NAND是串联结构,多个单元串在一起,要读某个单元的数据,得先把整串的数据都读出来再筛选。
关键点:STM32内部集成的Flash,属于NOR Flash架构。这意味着CPU可以直接从Flash中取指令执行,不需要先把代码拷贝到RAM里。这就是所谓的“片上执行”(XIP,eXecute In Place)。
2.2 STM32 Flash组织架构:三块“地盘”各司其职
STM32的Flash内部并不是一整块“大平地”,而是划分成了三个功能区域:主存储器、系统存储器和选项字节。我刚开始用STM32时,一直以为Flash就是存用户代码的那一块,后来调试一个启动问题,才发现系统存储器和选项字节的存在感有多强。
2.2.1 主存储器(Main Memory)
这就是我们平时烧录程序的地方。它按“页”(Page)或“扇区”(Sector)组织,不同型号的STM32,页大小和扇区大小不一样。
- 小容量型号(如STM32F103C8T6):每页1KB,共64页,总计64KB。
- 中容量型号(如STM32F103RBT6):每页1KB,共128页,总计128KB。
- 大容量型号(如STM32F103ZET6):每页2KB,共256页,总计512KB。
擦除操作的最小单位是页(或扇区),编程操作的最小单位是半字(16位)或字(32位)。写Flash之前必须先擦除,这是Flash的物理特性决定的。
注意:我曾经在一个项目中,因为没注意页大小,写了一个跨页的数据结构。结果擦除时把相邻页的数据也抹掉了,导致系统跑飞。后来我养成了一个习惯——每次规划Flash存储布局时,先查手册确认页大小,然后按页对齐分配空间。
2.2.2 系统存储器(System Memory)
系统存储器是出厂时固化的一段只读区域,里面存放的是ST官方的Bootloader程序。这段程序通常用于通过串口(USART)、USB或CAN等接口下载用户程序。
怎么进入系统存储器?把BOOT0引脚拉高、BOOT1引脚拉低,然后复位或上电,芯片就会从系统存储器启动,运行内置的Bootloader。
小技巧:如果你手头的板子没有调试器(比如ST-Link),可以用USB转串口模块,配合ST官方的Flash Loader Demonstrator工具,通过系统存储器里的Bootloader烧录程序。我早期做原型验证时经常用这招,省去了买调试器的钱。
2.2.3 选项字节(Option Bytes)
选项字节是一组特殊的配置位,用来控制芯片的一些硬件行为。它们存储在Flash的一个独立区域,通常位于最高地址段。
常见的配置项包括:
- 读保护(RDP):防止别人通过调试接口读取你的程序。等级0(无保护)、等级1(禁止读)、等级2(永久禁止读,不可逆)。
- 看门狗硬件使能(IWDG_SW):选择独立看门狗是软件控制还是硬件自动启动。
- 启动配置(nBOOT0、nBOOT1):某些型号可以通过选项字节覆盖BOOT引脚的电平设置。
- Flash待机模式(STDBY_WKUP):配置待机唤醒后的Flash状态。
修改选项字节需要调用HAL库的HAL_FLASH_OB_Program()函数,操作完成后必须复位才能生效。
避坑指南:我曾经在调试一个产品时,不小心把RDP等级设成了2(永久保护),结果芯片彻底锁死,再也无法通过调试接口连接。那批芯片直接报废了。所以,除非你确定永远不再调试这块芯片,否则千万别设成等级2。
2.3 Flash读操作与预取缓冲:CPU为什么能跑得那么快?
Flash的读取速度,直接决定了CPU的执行效率。STM32的CPU主频可以跑到72MHz甚至更高,但Flash本身的访问速度通常只有几十纳秒级别。如果CPU每次取指令都直接访问Flash,那CPU就得等——这就是所谓的“等待状态”(Wait State)。
2.3.1 等待状态(WS,Wait State)
CPU主频越高,Flash访问需要的等待周期就越多。STM32F103系列的数据手册里有一张表,明确给出了不同主频下需要的等待状态数:
| CPU主频(HCLK) | Flash访问等待状态(WS) |
|---|---|
| 0 ~ 24 MHz | 0 WS(零等待) |
| 24 ~ 48 MHz | 1 WS |
| 48 ~ 72 MHz | 2 WS |
等待状态越多,CPU执行效率越低。比如在72MHz下需要2个等待周期,意味着CPU每读一次Flash,就要空转2个时钟周期。这显然是个性能瓶颈。
2.3.2 预取缓冲(Prefetch Buffer)
为了解决等待状态带来的性能损失,STM32在Flash控制器里内置了一个预取缓冲器。它的工作原理很简单:CPU执行程序时,指令地址通常是连续的。预取缓冲器会提前把当前指令后面的几条指令读出来,放到缓冲区里。CPU下次取指令时,如果指令已经在缓冲区里,就直接从缓冲区拿,不需要再访问Flash。
说白了,就是用“空间换时间”。预取缓冲器的大小通常是64位或128位,可以缓存2到4条16位指令。
核心机制:预取缓冲器只有在CPU连续取指令时才有效。如果程序中有大量的跳转(比如if-else、switch-case、函数调用),预取缓冲器的命中率会下降,性能提升就不明显了。
2.3.3 预取缓冲的使能与配置
在STM32的标准库或HAL库中,预取缓冲的使能是通过修改Flash控制寄存器(FLASH_ACR)来实现的。
// 使能预取缓冲(HAL库示例)
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
// 设置等待状态(以72MHz为例,需要2个WS)
__HAL_FLASH_SET_LATENCY(FLASH_LATENCY_2);
配置时要注意顺序:先使能预取缓冲,再设置等待状态。如果顺序反了,可能会在切换时钟频率时导致Flash访问出错。
经验之谈:我习惯在系统时钟初始化函数(比如SystemClock_Config())里,先把Flash的等待状态和预取缓冲配好,再切换系统时钟。这样能保证从低速时钟切换到高速时钟的过程中,Flash访问始终是稳定的。
2.4 本章知识结构图
下面这张图把Flash存储器的核心知识点串了起来。你可以把它当作一个快速索引,复习时对照着看。
这张图从Flash的两种物理架构出发,延伸到STM32内部的三个功能区域,最后落到读操作的性能优化机制上。你可以把它当作一个快速索引,复习时对照着看。
2.5 小结
这一节我们聊了Flash的物理结构、STM32的内部组织方式,以及读操作和预取缓冲的配合机制。核心就三件事:
- NOR vs NAND:NOR能随机访问、支持XIP,适合做MCU内部Flash;NAND容量大、成本低,适合做大容量存储。
- STM32 Flash三区域:主存储器存代码、系统存储器存Bootloader、选项字节存硬件配置。每个区域都有自己的用途和操作方式。
- 预取缓冲:通过提前读取连续指令,减少CPU等待Flash的时间。配置时注意先使能预取缓冲,再设置等待状态。