1. NOR Flash 到底是什么?先搞清楚它在存储世界中的位置
嵌入式系统里有一类芯片,平时存在感极低,但每次上电都靠它才能把 CPU 拉起来。这个芯片就是 NOR Flash,很多人都把它叫做 Nor Flash,甚至 NorFLash,都是一个东西。严格来说,NOR 指的是存储单元内部晶体管的一种排列逻辑,它的名字来源于“或非门”的英文缩写,跟“不是某个品牌”没有关系。这一点,很多刚接触底层硬件的同学容易搞混,以为 Nor 是某个厂商缩写,其实不是。
这一课的内容,主要解决三个问题:NOR Flash 为什么能存数据、它跟外部电路怎么接、硬件上挂接时有哪些坑。适合正在做嵌入式驱动开发、准备画板子选型或者刚开始读 Flash 芯片手册的工程师阅读。如果你只是写纯应用层软件,可能用不太到;但一旦涉及产品启动、固件升级、配置保存,那你迟早要跟它打交道。
NOR Flash 的定位非常有意思。它属于非易失性存储器,掉电不丢数据,这一点跟 EEPROM 很像。但它的容量通常比 EEPROM 大得多,读取速度也快很多,而且支持片内执行(XIP),也就是说 CPU 可以直接在 Flash 里取指运行代码,不需要先拷贝到 RAM。这一条特性,直接决定了它在嵌入式系统中最重要的用途——启动代码的载体。
和 NAND Flash 相比,NOR Flash 的容量通常偏小,成本相对偏高,但它的随机读取性能极好,按字节寻址,所有地址都可见,非常适合存放代码和关键参数。我见过很多第一次做硬件的人,把 16MB 的 NAND 和 8MB 的 NOR 放在一个选项里纠结,其实它们两个根本不是一个路子:NAND 是块设备思维,按页/块读写,适合大容量数据存储;NOR 是内存地址映射思维,直接挂在总线或 SPI 上,适合代码执行和小数据存储。选错方向,后续整个软件栈都会别扭。
那“第十五课”这个位置,放在整个嵌入式学习链路里其实很典型。前面学完 GPIO、UART、中断,后面要接触 Bootloader、驱动和系统启动,NOR Flash 正好是桥接底层硬件的关键一环。这一课先讲原理和硬件怎么接,下一课大概率就是写驱动读写它。如果你能把 NOR Flash 这片东西吃透,后面看 Bootloader 源码、分析 Flash 分区、定位启动失败,都会顺畅很多。
2. 存储原理:浮栅晶体管是怎么记住数据的
2.1 一个晶体管就是一个存储单元
NOR Flash 的存储核心,是一种叫做浮栅晶体管的特殊结构。普通 MOS 管只有控制栅极,而浮栅晶体管在控制栅和沟道之间,多了一层由多晶硅制作的浮栅层,这层浮栅被绝缘介质完全包裹,与外界没有直接电连接。电荷一旦进入浮栅,就被“关”在里面,即使断电,也能保持数年甚至更长时间,这个特性就是非易失性的物理根源。
你可以把浮栅想象成一个密封的口袋。口袋外面有一扇可以通过电压控制的“门”,当电压足够高,电子就能越过绝缘层进入口袋;口袋里的电子数量,决定了晶体管导通阈值电压的高低。读取的时候,我们并不直接触摸口袋,而是感应它的状态:口袋里有电荷,晶体管开启需要的阈值变高,给同样的读电压,管子不通;口袋里没电荷,阈值低,管子导通。这个通/断两种稳定状态,就对应数字世界的 0 和 1。
这里有一个容易混淆的概念:NOR 和 NAND 都叫 Flash,存储单元用的都是浮栅晶体管,但单元之间的连接方式不同。NOR 结构中,每个存储单元的一端都直接连接到 Bit Line,另一端连接到 Ground,形成一种“或非”逻辑关系;NAND 则是把几十个存储单元串联起来,像一串珠子一样接到 Bit Line。正是因为 NOR 每个单元都能独立和 Bit Line 相连,它才能做到按字节随机读取;而 NAND 要读某个字节,得先把整串单元的状态全部读出再筛选,所以天生按页访问。
2.2 编程(写入)动作是怎么发生的
NOR Flash 的写入动作不叫 write,规范叫 Program。原理是利用量子隧穿效应,让电子穿过绝缘层,注入浮栅。
具体过程是这样的:在控制栅极上施加一个较高的正电压,通常十几伏,同时让源极接地,漏极也接到一个中间电平。这时候,沟道里的电子在强电场作用下获得足够能量,一部分高能电子会越过绝缘层势垒,被浮栅捕获。这个过程叫热电子注入。注入完成后,撤掉高压,电子就被困在浮栅里无法逃出,单元状态从 1 变成 0。
有一点必须注意:闪存的编程,只能把 1 变成 0,不能把 0 变成 1。所有 Flash 都是这个特性,包括 NAND。要让一个单元从 0 变回 1,只能执行擦除操作。
所以,如果你在应用层对 Flash 做类似 RAM 的“覆盖写”,比如原来地址存的是 0x5A,你想改成 0x5B,那是做不到的。正确做法是,先把整个扇区擦除成 0xFF,再写入新数据。这个逻辑,是所有 Flash 驱动开发的基础。
2.3 擦除动作是怎么发生的
擦除是把浮栅里的电子全部清空,让整个存储区域恢复成全 1 状态。这个过程刚好和编程相反,它利用的是 F-N 隧穿效应。
在擦除时,控制栅极接地,而衬底或者源极施加一个高压,让浮栅里的电子在电场作用下,以隧穿方式穿过绝缘层,全部被抽出去。因为擦除不能只对单个单元操作,必须按块或扇区进行,所以一个扇区里的所有单元会同时被擦除,变成一个整块的 0xFF。
不同厂商的 NOR Flash,扇区划分略有不同,但主流的分法已经形成一种默契:4KB 小扇区、32KB 块、64KB 块,还有整片擦除。设计驱动的时候,凡是代码要存参数、存日志、存升级镜像,都必须结合扇区大小做好磨损均衡规划。这里有个细节,很多刚入行的朋友容易忽略:擦除虽然是按扇区,但编程却可以按字节或者按页,页大小一般是 256 字节,写的时候一个时序最多写一页。
2.4 为什么 NOR Flash 的读速度远高于写速度
这是 NOR Flash 最容易被初学者忽略的特性。因为每个存储单元都能独立经过译码电路访问,读操作只需要选中地址,把对应单元的导通状态,通过灵敏放大器转换成电信号就行,这个路径短、时序快,所以读速度很高,通常能够支持工作在几十 MHz 频率的 SPI 总线上。
而写操作要先擦后写,擦除本身要经历高电压充到浮栅、等待隧穿完成、再放电,整个过程耗时毫秒级;编程同样需要维持高电压注入电子,一个字节或者一个字结构不同,典型时间是几十微秒。所以说 NOR 的读快、写慢,这不算缺点,而是 Flash 这个技术方案的固有物理特征。如果产品需要频繁写数据,你得考虑是不是选 NAND 或者用外部 EEPROM 做数据冗余,而不是硬扛在 NOR 上。
3. 硬件接口与外围电路:串行 SPI 和并行两种阵营
3.1 串行 SPI NOR Flash 是当前绝对主力
现在绝大多数嵌入式系统用的 NOR Flash,都是串行 SPI 接口。芯片引出四根线:CS 片选、CLK 时钟、MOSI 主出从入、MISO 主入从出,某些型号还带 WP 写保护脚和 HOLD 保持脚。引脚少、封装小、PCB 布线简单,一颗 8Mbit 的芯片,SOP-8 封装就能搞定,这在小体积物联网设备里非常有吸引力。
SPI 接口又有几种模式,常见的是标准 SPI(四线)、双倍速率(Dual SPI)和四线(Quad SPI)。Quad SPI 模式下,把 WP 和 HOLD 脚复用成 IO2 和 IO3,数据线由一根变成四根,读速度能提升好几倍。很多需要快速启动的产品,比如带 GUI 的嵌入式设备、需要从 Flash 加载大量资源文件的系统,都会用 Quad SPI 模式。
如果你做的是跑裸机或者 RTOS 的小系统,标准 SPI 就够了;但如果要跑 Linux,尤其涉及核心启动镜像从 SPI NOR 加载,我建议优先选支持 Quad 读并且带 4 字节地址模式的芯片,这样后续性能余量更大。我在实际项目中就遇到过一个板子,前期选了只支持标准 SPI 的片子,后期想在 Flash 里放开机画面和字库,读速度直接成为瓶颈,最后不得不换型号,折腾了一轮。
3.2 并行 NOR Flash 还有哪些场景在坚守
并行 NOR Flash 是早年 PC BIOS 和嵌入式系统的主力,数据线 8 位或 16 位,地址线一大把,还带片选、读使能、写使能、输出使能等一堆控制信号。它的好处很明显,直接映射到 CPU 地址空间,读操作就像读 SRAM 一样,不需要额外切换状态,非常适合 FPGA 配置、高端 DSP 引导以及需要极高读取实时性的场合。
不过现在新设计里,并行 NOR 越来越少,原因很直接:占引脚、占 PCB 面积、良率成本都比 SPI 高。只有在一些工业控制板、老平台维护、或者对启动时间有极端要求的专用系统里,你还能看到它。作为一个工程师,了解并行 NOR 的引脚定义和读写时序是有价值的,因为你要能读懂老的原理图、维护存量项目,但新设计直接用 SPI NOR 基本够用。
3.3 引脚定义与最小硬件电路设计
我以一颗最常见的 8Mbit SPI NOR Flash 为例,梳理最小硬件电路需要哪些东西:
电源:大多数型号支持 1.8V 或 3.3V 供电,两个电压版本不能混用。电源引脚旁边必须放置两个去耦电容,一个 100nF,一个 10μF,靠近芯片电源引脚摆放。很多小批量样板画得太随意,电容离芯片两三厘米,高速 SPI 读的时候波形畸变严重,会出现偶发读错数据。
CS 片选:必须接 MCU 的 GPIO 或者 SPI 片选。如果片选脚悬空,芯片可能会误进入非预期的状态,尤其是上电瞬间,噪声可能导致 Flash 被误选中。建议在硬件上对 CS 做上拉,保证默认高电平。
CLK 时钟:SPI 时钟脚,MCU 侧配置成输出。时钟频率上限看芯片手册,不同厂商、不同电压域差别很大,从 20MHz 到 133MHz 都有。新手设计时容易犯的错,是不看芯片的最高时钟能力,直接往 SPI 控制器配置里填一个很高的分频,导致数据采样时序不满足。
MOSI/MISO:数据线。MOSI 走 MCU 到 Flash,MISO 走 Flash 到 MCU。这两根线一般不需要额外上拉,但如果你用的是 Quad SPI 模式,IO2/IO3 必须注意,它们复用自 WP 和 HOLD 引脚,这两个脚如果悬空,读取 ID 没问题,但一旦要读数据时,芯片检测到 WP/HOLD 电平不确定,可能会锁死操作。设计时要把这两个脚通过电阻上拉或下拉固定电平。
WP 写保护:低电平有效。如果这个脚被拉低,则受保护区域的写操作会被拒绝。设计上建议默认拉高,让芯片处于允许编程状态。如果要搞代码保护,才通过软件控制它拉低。
HOLD 保持:低电平有效。HOLD 拉低后,芯片暂停通信,时钟变化不影响内部状态。这个脚如果不使用,必须接高电平,很多随机读失败的板子,问题就出在 HOLD 悬空。
还有一个非常容易忽略的引脚是 RESET 复位脚,部分型号带硬件复位引脚。如果你的系统里 Flash 挂了 RESET,要确保上电时它能正常拉高,设备树里也要配置对应 GPIO。我在一个产品调试里就踩过这个坑,Flash 带 RESET,但驱动没有主动释放它,导致每次开机 Flash 都处于复位未完成状态,驱动读取 ID 超时。
3.4 硬件设计常见踩坑点
做 NOR Flash 相关硬件设计,有几个高频问题我基本每次评审原理图都要提醒一遍:
第一,电源稳压器选型时只看了电压,没看峰值电流。Flash 在擦除瞬间电流会有一个脉冲尖峰,如果稳压器输出能力不足,或者去耦不够,擦除状态下电源电压会跌落,导致擦除失败或擦除结果不完整。这类问题非常难排查,因为你看示波器时,故障是偶发的,而且擦除失败后 Flash 里的旧数据已经被半抹掉,非常尴尬。
第二,SPI 信号串联电阻的使用。对于短距离板内走线,一般不需要串电阻;但如果 Flash 离 MCU 比较远,或者走线经过带状线、过孔数多,反射会导致眼图恶化。常规做法是在 MOSI、CLK 上各串联 22Ω 或 33Ω 电阻,MISO 上视情况加。别小看这几个电阻,它能有效抑制过冲,降低信号完整性风险。
第三,多片 Flash 或 Flash 与其他 SPI 器件共享总线。两条 SPI 设备的片选不能同时有效,这要求驱动层严格管理片选时序,否则总线数据出现不确定状态。硬件设计上最好给每个 SPI 设备独立的片选 GPIO,并且不要共用 CS 信号。
第四,需要仔细阅读芯片手册里的上电时序要求。大多数 NOR Flash 对 VCC 上升时间有要求,如果太慢,芯片内部上电复位电路工作异常,可能导致第一次读 ID 失败,必须做二次读取或者软件复位。硬件上,如果电源上升时间可控,这个问题一般能规避。
4. 芯片选型与关键参数:容量、块大小、电压和速度怎么定
4.1 容量和地址模式
市面上常见的 SPI NOR Flash,容量从 512Kbit 到 256Mbit 都有,但嵌入式产品用得最多的是 4Mbit 到 32Mbit。选容量不是拍脑袋,而是看用途精确算出来的。
比如一个物联网网关,Bootloader 放 64KB,内核镜像放 2MB,文件系统放 6MB,再留一部分做参数备份和升级缓存,那总容量至少 10MB。有人可能会问,为什么不干脆选 16MB,省心。理论可以,但要注意,容量超过 16MB(也就是 2 字节地址无法覆盖)时,芯片必须切换到 4 字节地址模式。有些老驱动只写死了 3 字节地址,导致读取后半段地址空间时数据错乱。所以选容量的时候,驱动方案要同步评估,别只盯价格。
厂家手册里的容量常常写的是 bit 单位,比如 8Mbit,对应的实际容量是 1MB。很多新手在老工程师面前说“我的 Flash 是 8MB”,其实别人心里在换算成 1MB。为了减少这种误会,我在团队里统一要求,方案阶段全部用 Byte 单位沟通。
4.2 块大小与磨损均衡
SPI NOR Flash 的扇区大小直接决定驱动复杂度。最常见的扇区是 4KB,这也就是说,你每次改一个小参数,最小单位也得擦掉 4KB 的一片区域。如果你的程序每次写一个字节都先擦一个扇区,那这个扇区很快就会被磨损坏。
所以做参数存储的时候,常见的做法是:分配两个或多个扇区,轮转写入,每次写新值之前,先把旧值统一标记失效,写完一个扇区再切换下一个。这种方法能把寿命放大很多倍,代价是驱动逻辑稍微复杂一点。擦除次数上限一般标称 10 万次,但实际芯片在多次擦写后阈值漂移,数据保持时间会显著缩短。如果是产品样机,可能感觉不到,真正量产几年后,老批次芯片出问题的概率就会显现。
4.3 电压域和电平匹配
很多国产 MCU 使用 1.8V 或 3.3V 供电,Flash 的输入电平必须匹配。如果 MCU 是 3.3V,Flash 是 1.8V,直接连接会击穿 Flash 内部电路;反过来 MCU 1.8V Flash 3.3V,那 Flash 就无法正确识别高电平。需要做电平转换时,我一般倾向选择支持宽电压域的 Flash,比如 1.65V 到 3.6V 全域支持,这样一颗芯片在不同板卡之间复用更方便。
另外,SPI 信号的电平阈值并不只看标称电压,还要参考 VIH/VIL 参数。如果总线上有其他器件把高电平拉到 2.5V,而 Flash 的 VIH 最低是 0.7×VCC(约 2.31V 在 3.3V 供电下),那 2.5V 其实是可以被正确识别的。但这些边界余量小,高速状态不稳定,稳妥设计还是保证高电平接近电源电压。
4.4 时钟频率和温度等级
SPI 时钟频率不能一味追高。芯片标称 133MHz,那是读操作在一个特定电压范围和特定电容负载条件下的能力,实际 PCB 寄生电容、走线长度都会影响上限。我通常预留 30% 裕量,比如芯片标称 100MHz,驱动里配置成 70MHz 左右,牺牲一点性能换取稳定性。工业级温宽(-40 到 85 度)是基本要求,如果在户外设备或者高温环境,得选 AEC-Q100 等级或者指定工业级版本。消费级芯片在高温下,写入时间会变长、数据保持能力下降,这是物理特性决定的,不是玄学。
5. 实操记录:从原理图到驱动点亮一颗 NOR Flash
5.1 硬件连接示例
我最近调试的一块板子,MCU 主控用的是某国产 Cortex-M 系列,Flash 是一颗 16Mbit 的 SPI NOR,3.3V 供电,原理图连接如下:
- CS 接 MCU PB12,默认上拉 10K 到 3.3V
- CLK 接 MCU PB13
- MOSI 接 MCU PB15
- MISO 接 MCU PB14
- WP 引脚接 10K 上拉到 3.3V
- HOLD 引脚接 10K 上拉到 3.3V
- VCC 接 3.3V,旁边放 100nF 和 4.7μF 去耦电容
- GND 直连地平面
这几个引脚的上拉电阻非常关键。我之前调试时,WP 和 HOLD 直接用飞线悬空,芯片读 ID 一切正常,但执行写入命令时,状态寄存器里的 WIP 位一直不翻转,卡死在那里。量了半天电压,发现 HOLD 引脚在噪声干扰下低于阈值,芯片被拉进保持状态,数据全被忽略。
5.2 初始化与读取 JEDEC ID
上电后第一步不是读任何数据,而是发一个软件复位命令,确保芯片从任何异常状态中恢复。然后读 JEDEC ID,这个命令固定是 0x9F,芯片会回传厂商 ID、设备 ID 和额外 ID。
我用示波器抓过时序,确保 SPI 模式配置正确。大部分 SPI NOR 支持 Mode 0 或 Mode 3,两者区别是时钟极性 CPOL 和相位 CPHA 的不同。配置错了,读回来的数据全是 0xFF,看起来像芯片没焊接好。排查这类问题,示波器看 CLK 空闲电平和采样沿,比反复改代码更高效。
初始化代码的思路是:拉低 CS,发送 0x9F,连续读 3 个字节,最后拉高 CS。返回的第一个字节是厂商 ID,比如某厂商的 ID 是 0xEF,某国产厂商是 0xC8。读不到合法 ID,说明要么接线错误、要么芯片没进入正常状态、要么 SPI 外设配置不对,先别急着调驱动程序,把物理层搞定再说。
5.3 读数据的标准流程
读数据的核心命令是 0x03,普通读命令。地址可以是 3 字节,也可以是 4 字节,取决于容量和是否进入 4 字节模式。发送命令加地址后,主控连续发送时钟,Flash 从 MISO 串出数据,直到片选拉高。
这里有个性能优化点。普通读命令 0x03 没有最大频率限制,但每次读都需要先把地址发送出去,对于大块数据读取,地址发送时间占比很高。快速读命令 0x0B 增加了 dummy byte,让高频信号有时间稳定,同时支持更高的时钟频率。如果内核镜像比较大,建议用快速读命令做批量读取。
实际项目里,我遇到过读大文件时数据随机错误的情况。定位过程是:先看波形,发现 MISO 波形上冲明显,再测 MISO 信号到 MCU 引脚的走线长度,发现因为布局问题绕了一大圈,串联电阻没加。后来在 MISO 上加 33Ω 串联电阻,并且把 SPI 时钟从 80MHz 降到 50MHz,问题立刻消失,整包固件校验通过。
5.4 写入和擦除的标准流程
写入前必须先擦除。擦除命令 0x20 擦除 4KB 扇区,0xD8 擦除 64KB 块,0xC7 是整片擦除。发擦除命令之前,先发 0x06 写使能命令,把状态寄存器里的 WEL 位置为 1。如果 WEL 位是 0,任何写和擦除操作都会被忽略。
擦除命令完成后,状态寄存器里的 WIP 位为 1,表示芯片正忙。软件需要轮询读 0x05 命令,直到 WIP 位变成 0。注意,这期间不能对同一个芯片执行其他操作,片选时序和状态轮询的节奏必须配合好。
写入命令 0x02 是页编程,一次最多写 256 字节。如果数据跨越页边界,必须拆分成多次页编程。很多人第一次写 Flash 驱动,误以为可以连续发一长串数据,结果写到页尾就停了,后面的数据直接丢。因为 Flash 内部只能把数据先锁存在页缓冲区然后一次性写入,跨页数据必须分两次写。
正确的写流程是:写使能,发页编程命令,发地址,发不超过 256 字节的数据,拉高片选,轮询 WIP 位直到为 0,再继续下一页。这个流程做完,读回校验,数据一致才算写入成功。
6. 常见问题与排查技巧实录
启动时读 ID 超时:先确认电源电压到达芯片引脚的波形,看有没有跌落;然后确认 CS 信号默认高电平,CLK 极性配置正确;最后检查 HOLD 和 WP 引脚是否固定电平。这四步能解决 90% 的读 ID 问题。
读数据全是 0xFF:大概率是片选没拉低,或者 SPI 模式配置错。0xFF 全高,说明 Flash 没有被正确选中,或者数据线没有驱动。小概率是芯片本身没焊接好,用万用表测引脚对地阻值辅助判断。
写使能后 WIP 位不翻转:典型原因是 WP 引脚被拉低了,写保护生效。有的芯片写保护是块级配置,即使 WP 引脚拉高,如果状态寄存器里的保护位被设过,也需要先解除保护再执行擦写。具体要看芯片手册的 BP 位配置逻辑。
擦除后数据还是旧的:检查擦除地址是否对齐。擦除命令的地址必须落在扇区起始地址上,如果地址给的是扇区中间某个值,擦除操作不会生效,这是很多驱动刚开始调试时都会犯的错误。擦除之前先做地址掩码对齐,把低 12 位清零(4KB 扇区),再发命令。
数据写入后读回不一致:优先检查是否跨页。写入地址加数据长度如果超过页边界,后面的数据不会写进去。其次是电源稳定性,尤其是同时擦写多页时,VCC 跌落会导致编程电压不足,单元没有正确注入电荷,读回时表现为随机跳变。
时间长了数据丢失或启动不稳定:考虑是不是频繁操作同一扇区,磨损导致数据保持能力下降。我见过一块板子,日志系统每 10 秒写一次同一个扇区,一年不到就出问题。后来改成双扇区轮转,存活周期增长了十倍以上。
7. 最后分享一点个人体会
NOR Flash 这个器件,表面看起来非常简单,无非就是读、写、擦三个命令,但实际项目中牵扯到的物理特性、时序约束、硬件电路、驱动状态管理是一整套体系。很多人学习时只看驱动源码,忽略硬件信号完整性和芯片手册,出了问题之后只能反复盲调,效率很低。
我自己的建议是,拿到一块新板子,第一步先不要急着写业务代码,用逻辑分析仪或者示波器抓一遍读 ID 的时序,亲眼确认 CLK、CS、MOSI、MISO 的波形关系,这比任何驱动框架都更能帮你建立直觉。接着做一次扇区擦除和页写入,验证 Flash 的物理操作正常,再往上叠抽象层。
另外,做产品化设计时,别把 Flash 当成无限耐用的大仓库。它写次数有限、擦除时间较长、读写速度不对称,这些物理特性要在项目早期就纳入设计考量。我见过太多方案,代码写得很漂亮,但存储规划一团糟,最后量产阶段频繁返工。先把这些底层细节吃透,后续做 Bootloader、OTA 升级、文件系统,你都会感觉顺手很多。
这一课先把原理和硬件讲透,下一课自然会进入驱动代码实现和状态机控制。如果你也在调试 SPI NOR Flash,遇到硬件或者驱动上的问题,按照前面排查列表的顺序逐项确认,基本上都能定位到根因。