先讲一件现场的事:代码能跑,不代表Flash用对了
我带过不少做Bootloader和固件升级的开发者,大家在把程序从片内Flash迁到外挂NOR Flash时,第一反应往往都是“找一份Demo代码抄一抄”。结果倒是很相似:程序能跑,ID能读,写进去的数据一复位就丢,或者干脆写半天返回成功、读回来全是0xFF。这时候再去查原理图、查手册,才发现问题出在电源去耦、写保护引脚、擦除边界这些基础环节上。
第十五课(1)这篇,我就把NOR Flash的原理和硬件介绍放在一起讲:先讲清楚存储单元凭什么能记住数据,再对照NAND Flash说清两者的分工,然后落到实际板子上的接线和驱动命令,最后用几个真实调试现场收尾。适合刚接触片外存储、正准备做固件升级或参数存储方案的读者,也适合那些已经踩过坑但还没想明白根因的人。内容确实有点多,但把这一课啃下来,后面的教程会顺很多。
1. 浮栅存储单元:NOR Flash读写擦背后的电压逻辑
1.1 一个晶体管如何记住一位数据
我们可以把NOR Flash的一个存储单元,想象成一个“有记忆功能的开关”。它本质上是一个带浮栅的MOSFET,浮栅是包裹在氧化层里的一个导电层。氧化层是很好的绝缘体,电子一旦进入浮栅,即使断电也能待很久,这就是“非易失”的根本原因。
数据的存在用阈值电压Vt表达。浮栅里的电子数量多,Vt就高,晶体管不容易导通;浮栅里的电子少,Vt就低,晶体管容易导通。逻辑电平的约定各家略有差异,但行业习惯里,已擦除状态通常读出来是1,编程写入之后变成0。也就是说,出厂状态或者擦除后的Flash,某一位读出来一般是0xFF里的那个1;当你往里写0时,是在往浮栅里注入电子。
读取操作并不复杂:给控制栅施加一个介于低Vt和高Vt之间的参考电压,根据位线上有没有电流流过,就能判断这个单元保存的是1还是0。整个过程和DRAM需要不断刷新完全不同,它不需要外部供电来维持电荷,所以断电后数据依旧存在。
1.2 读、写、擦三种操作的电压博弈
既然浮栅被绝缘层包裹,想把电子弄进去或弄出来,就不能靠常规电压,得“借力”。写入和擦除,本质上是在氧化层两端制造一个很强的电场,让电子发生量子隧穿效应。你可以把氧化层想象成一道很厚的墙,平时没人能翻过去,但两端电压差足够大时,电子会被“硬拉”过去。
写入时,控制栅加高电压,漏极和源极之间建立导电沟道,电子获得足够能量后翻越势垒进入浮栅,这就是沟道热电子注入,也有部分工艺用Fowler-Nordheim隧穿直接实现编程。写入是逐字节或逐页进行的,速度比读操作慢不少。
擦除时恰好相反,把浮栅里的电子强行拉出来,让单元回到高电平状态。擦除操作施加的是另一组电压偏置,所有参与擦除的存储单元会同时放电。所以擦除不能只针对某一位,只能按扇区、块甚至整片进行,这是NOR Flash结构上的硬约束。
这里面有个容易混淆的点:写入和编程虽然名词不同,但硬件动作是一样的——都是往浮栅里塞电子;擦除才是往外拉电子。嵌入式里经常说“先擦后写”,就是因为不擦掉旧数据,写入结果会和已有状态叠加在一起,根本得不到预期内容。
1.3 为什么擦除粒度必须这么大
NOR Flash的存储阵列里,许多单元共享源极和位线。为了擦除,芯片需要在单位面积上承受足够高的电场,如果允许任意单个单元单独擦除,就需要给每个单元都铺设一套高压开关电路,成本、面积和功耗都无法接受。因此设计上把阵列切成扇区、块,每次擦除按最小擦除单位进行。
常见的扇区大小有4KB、32KB、64KB,也有整片擦除命令。擦除时间通常比写入长不少,一个小扇区要几十毫秒甚至几百毫秒,整片擦除可能达到几十秒。这直接决定了上位机或MCU在擦写时的处理策略:必须先发命令,再轮询状态寄存器里的忙标志,而不是盲等固定延时。
理解了这一层,你才会明白为什么NOR Flash容量普遍做不大、做大了成本也高:存储单元多、高压电路多、工艺要求又高,容量翻倍后芯片面积和良率压力都会显著增长。这也自然引出第二节的对比问题:那要不要换成NAND?
2. NOR与NAND的分工:为什么代码放在NOR里,文件放在NAND里
2.1 随机读取能力和XIP属性
NOR Flash最突出的优势是随机读取快、寻址灵活。它允许外部控制器按照任意字节地址直接读数据,读取延迟低,时序稳定。正因如此,它可以做XIP,也就是片上执行,MCU把NOR Flash映射到自己的地址空间后,CPU可以直接从Flash里取指执行,不必先把代码拷贝到RAM。
XIP的意义在做Bootloader时特别明显。启动阶段内存可能还没有初始化,SDRAM/DDR还不可用,但代码已经得跑起来了。只要SoC支持SPI NOR的XIP映射模式,就能直接在外挂Flash上运行启动代码,后续再逐步初始化内存、搬运其他程序。
与之对比,NAND Flash是页式读取,随机访问延迟高,内部结构本质上就不适合字节级寻址。把NAND直接映射成执行地址空间会非常困难,性能也糟糕。所以行业里形成了清晰分工:代码放NOR,大容量文件放NAND。
2.2 NAND的坏块与ECC复杂度,为何与NOR不同
NAND Flash工艺上追求高容量、低成本,允许出厂时就存在坏块,也允许使用过程中逐渐出现新坏块。要正常使用,软件层必须维护坏块表、做坏块跳过,还需要配合汉明码、BCH码甚至LDPC做纠错,复杂度明显上了一个台阶。
NOR Flash虽然有寿命限制,但在常见的工业应用里,出厂坏块率要低得多,多数情况下不需要建立复杂的坏块表和映射层。你可以直接按地址读写,除非扇区寿命耗尽,一般不用担心某一块完全不可用。这正是很多轻量级产品选择NOR Flash的原因:软件简单,不需要文件系统,不用管ECC,开发周期短。
当然,简单也意味着没有过多防御能力。NOR Flash仍有擦写寿命上限,一般几千次到十万次,取决于工艺和选择的型号,如果软件反复擦写同一扇区,照样会提前报废。从这个角度说,NOR不是无限寿命,只是寿命管理比NAND容易。
2.3 选型边界:容量、成本与可靠性权衡
我整理了一个对比表格,在做方案选型时可以快速过一眼:
| 对比项 | NOR Flash | NAND Flash |
|---|---|---|
| 随机读取 | 快,支持字节寻址和XIP | 慢,按页顺序读取 |
| 最大容量 | 常见1Mb~512Mb,再大成本飙升 | 可达Gb级别甚至更高 |
| 写入/擦除速度 | 写慢、擦除慢 | 写快、擦除快,但需页对齐 |
| 坏块管理 | 基本不需要,出厂坏块极少 | 必须管理坏块和映射 |
| ECC需求 | 通常不需要,或依赖外部校验 | 经常需要硬件ECC或软件纠错 |
| 软件复杂度 | 低,适合裸机直接擦写 | 高,通常配文件系统或FTL |
| 成本 | 单位字节成本高 | 单位字节成本低 |
| 典型应用 | Bootloader、固件备份、参数存储 | U盘、SD卡、大容量文件系统 |
我个人的选型习惯是:容量需求在256Mb以内,或者需要让CPU直接执行代码,优先考虑NOR;如果是要存几十Gb的视频或日志,那就老老实实上NAND。中间地带如果只有几Mb参数存储需求,但希望擦写频繁、管理简单,也是NOR更稳。
3. 硬件接线实战:电源、写保护、复位与四线QSPI
3.1 SPI与QSPI引脚定义基本功
串行NOR Flash最小的封装用SPI接口就够,常见引脚包括CS#、CLK、DI、DO、VCC、GND,以及WP#和HOLD#。支持Quad SPI的芯片会在此基础上扩展IO2、IO3,实现四线数据传输。
CS#是片选,低电平有效。整个命令序列期间CS#必须始终保持低,任何一次不必要的拉高都可能让Flash认为命令结束或状态错乱,这是硬件调试里最常见的抖动源。CLK是时钟输入,DI负责主机到从机的命令和地址,DO负责数据返回。
用QSPI模式时,IO0和IO1既做输入也做输出,IO2、IO3参与数据。硬件上需要确认主控有没有四线功能的引脚,不能随便拿普通GPIO替代,因为QSPI是有严格时序的,普通引脚在高速翻转时容易多出毛刺。
3.2 电源去耦与噪声处理
千万不要因为NOR Flash“只是个小芯片”,就忽略它的电源设计。读操作电流只有几毫安,但擦除和编程的瞬间电流可能冲到几十毫安,如果电源轨上噪声大,内部高压泵工作就不稳定,表现出来就是擦写超时、状态寄存器一直显示忙。
最基本的要求是给VCC加一个0.1uF陶瓷电容,尽量贴近Flash的电源引脚放置,走线要短而粗。如果Flash靠近大功率器件或开关电源,建议再增加一颗10uF电容,并在原理图的电源入口留一个小磁珠或RC滤波。快速开关的时钟线、数据线不要太靠近电源脚走线,免得耦合噪声影响内部逻辑。
上电和下电时序也值得注意。芯片在VCC爬升过程中,如果CS#恰好被拉低,Flash可能把随机的CLK信号当成命令解析,最坏情况会误触发擦写操作。稳妥做法是给CS#加上拉电阻,或者确保主控在复位释放前把CS#拉高。
3.3 WP、HOLD和复位引脚的坑
WP#是写保护输入,低电平时会禁止某些区块写入。HOLD#是保持输入,低电平会暂停当前操作。这两个引脚在大多数应用里都应该固定拉高,不允许处于不确定状态。很多主控的GPIO在上电瞬间是浮空输入,如果WP#被下拉到低电平,写操作会被静默忽略,返回成功但数据没写进去。
我见过一个很典型的案例:板子用的NOR Flash支持单线SPI,设计者把空余的IO2、IO3直接悬空,又把WP#接到主控一个开机时默认拉低的GPIO上。结果是ID能读、写命令发了、状态寄存器也清了,就是读回数据不变。后来查原因,正是WP#在上电阶段处于低电平,Flash全局写保护没有被解除。
方案有两种:一是硬件上给WP#和HOLD#各加一个10kΩ上拉到VCC,确保默认高电平;二是如果走GPIO控制,必须在初始化代码里最早阶段把它们配置为高电平输出。我建议两种都做,硬件兜底加软件确认,避免某些主控管脚配置顺序不一致。
4. 驱动命令序列:读、写、擦三大操作的状态机
4.1 读命令、JEDEC ID和状态寄存器轮询
SPI NOR Flash的命令体系遵循JEDEC标准,虽然不同厂家的细节有所差异,但第一件该做的事永远是读ID。常见的读ID命令是0x9F,发送命令字后连续读取若干个字节,会得到厂商ID和存储类型等参数。在读ID之前,最简单的硬件检查清单是:VCC正常、CS#能正常拉低、CLK有信号、MISO/MOSI接线不反。
读数据最常用的是0x03命令,后面跟3个字节的目标地址,芯片就会从该地址开始连续输出数据。因为NOR支持任意地址读取,0x03可以一次性读任意长度,直到CS#拉高为止。
状态寄存器用来查询芯片是否忙,一般用0x05命令读取一个字节,最低位是WIP(Write In Progress),1表示忙,0表示空闲。后面做写、擦操作时必须循环轮询WIP,不能靠延时拍脑袋,否则时序一紧就容易在上一操作未结束时就发新命令。
4.2 写使能与页编程:为什么步骤不能省
写操作前必须先发送0x06写使能命令。这个步骤非常容易漏,因为部分开发者在Demo代码里看到过,但换一颗芯片或者换一种封装,又忘了把写使能串进去。没有写使能的写命令会被芯片直接忽略,擦了也没用。
标准页编程命令是0x02,按地址发送数据。SPI NOR一般按页编程,每页通常是256字节,有些新款芯片支持512字节甚至更大。页编程的一个关键点是地址不能跨页,否则数据会回卷覆盖到同一页的开头,这并不是符合预期的顺序写入,而是硬件限制。
更稳的驱动写法是:先按页边界切分数据,一段数据不能超过当前页剩余空间,每写一页前重新发WREN,再发页编程命令,然后轮询状态寄存器,等WIP清零后再写下一页。最后全部写完,要再读一遍对比数据,不要偷懒。
4.3 擦除命令与超时处理
擦除命令分为扇区擦除、块擦除和整片擦除。常见扇区擦除是0x20,擦4KB;0x52擦32KB、0xD8擦64KB;0xC7或0x60是整片擦除。整片擦除虽然方便,但时间很长,一些大容量芯片可能要几十秒,期间如果系统重启或掉电,数据完整性风险很高,量产阶段要谨慎用。
擦除动作保护逻辑更强:除了要发WREN,目标区域还可能受BP位和SRP位控制,如果状态寄存器里设置了区块保护,擦除命令会被拒绝。很多开发者改代码半天找不出原因,最后发现是之前调试时把状态寄存器保护位写成了1,导致后续擦除始终失败。
我建议驱动的擦除流程做成这样:读状态寄存器确认没有保护位阻碍,发WREN,发擦除命令,轮询WIP直到空闲,再读回该区域确认全部为0xFF。如果哪个字节不是0xFF,不要急着重擦,先排查是不是地址对齐错误或命令被截断。
5. 厂家手册之外的实际调试:掉电保持、扇区边界和误用坑
5.1 案例一:写进去读回来全是0xFF
有一块板子,同样的代码在开发板上正常,换到目标板上写不进数据。排查过程大概分了三步。第一步量电压,VCC稳定,没发现异常;第二步用逻辑分析仪抓SPI波形,发现命令、地址、数据都发了;第三步才注意到WP#引脚量出来是0V。
查原理图发现WP#被主控的GPIO控制,而这个GPIO上电后默认是低电平,并且直到应用代码运行到第几百行时才被配置成高电平输出。Flash的全局写保护在整个启动早期一直生效,所以所有写操作都被拒绝。解决方式很简单:把WP#硬件直接上拉,软件初始化时也显式输出高电平。
这个案例说明,不要只查驱动代码,硬件引脚状态会直接影响芯片内部保护逻辑。
5.2 案例二:扇区边界与数据分区错位
有个做参数存储的模块,地址规划从0x10000开始存参数A,从0x10050存参数B。开发者以为两个地址靠得近,方便后续统一管理,结果在升级功能时想擦除整个0x10000扇区重新写入,把参数B也一块擦掉了。问题的根子,是扇区边界还没有对齐。
NOR的扇区擦除是一次性擦掉整个4KB区域,如果你把不同逻辑意义的数据放在同一扇区,那么任意一个数据要重写,必然殃及其他数据。正确做法是按数据更新频率来划分扇区:频繁更新的单独放一个扇区,很少更新的放另一个扇区,一个扇区最好只存一条逻辑数据或者一组同步更新的数据。
我在做分区规划时还会预留几个空扇区做磨损均衡。比如日志区有三个扇区,轮流写,写入次数从3倍寿命起跳,远比死磕一个扇区可靠。
5.3 掉电保持、安全写入与量产校验建议
最后说生产中最容易踩的掉电问题。如果系统在擦写过程中突然断电,NOR Flash可能处于中间状态:部分扇区擦掉了,部分数据还是旧值,甚至个别单元写入了一半。对于Bootloader来说,这就是变砖的经典原因之一。
要防这个坑,最有效的不是硬件加超级电容,而是软件里做二次分区加回滚机制。例如固件升级时先把新固件写入备份区,校验通过后更新一个启动标志位,再切换启动分区;一旦目标区数据不完整,启动标志位还能指示加载旧的备份区。NOR Flash虽然本身不具备掉电保护事务能力,但通过这套“影子变量”和“双区”方案,可以把风险控制在可恢复范围。
量产阶段我也建议加一道回读校验,不是简单读一遍,而是在读回数据时逐字节比对,遇到不一致立即报错并记录地址。这样可以快速区分是芯片批次问题、焊接问题还是驱动时序问题。我见过太多产品因为省略回读,导致不良品流出到现场,返修成本远高于生产时多花的那几秒钟。
真正把NOR Flash用明白,靠的不是把命令背熟,而是把存储单元的原理、硬件引脚的状态、命令序列的先后关系和掉电场景下的数据流全部串起来。希望这一课能帮你把外挂Flash从“能用”推到“用得踏实”。