1. 从一颗裸片到一颗SD卡:SD NAND的底层身份
1.1 同样一颗NAND,为什么SD NAND能“即贴即用”
很多刚接触SD NAND的工程师,第一次看到这颗芯片时都会愣一下:这不就是一颗普通的NAND Flash吗?八脚封装、贴片焊接、引脚间距不到1mm,怎么看都像是一颗SPI NOR Flash。但真把它当普通NAND去初始化,会发现完全不是那么回事。我最早接触SD NAND时也干过这个事,拿示波器去抓它的上电时序,抓了半天什么都没抓到,后来才意识到,这颗芯片内部住着一个完整的SD卡控制器,它在开机瞬间会自动完成内部的NAND初始化、坏块扫描和映射表加载,对外根本不需要你做Flash级别的初始化。
SD NAND的本质,就是把一颗裸NAND Flash和一个SD控制器封装在一起,对外提供标准的SDIO/SPI接口。你看到的是贴片式封装,实际上它内部就是一张没有外壳的microSD卡逻辑。这也是为什么很多厂商愿意叫它“贴片式SD卡”——名字不是白叫的,它确实把你从NAND底层协议中彻底解放出来了。
裸NAND和SD NAND的关系,就像一袋面粉和一包预制面饼。面粉是原材料,揉面、发酵、擀皮都得自己来;预制面饼则是处理过的半成品,拿回家直接下锅就行。很多老工程师选型时纠结“哪个便宜”,但真实项目里最贵的往往不是芯片物料成本,而是你要额外投入的底层代码开发和调试时间。
1.2 从引脚和协议看两者的直接差异
裸NAND的引脚功能很纯粹:数据线(I/O0~I/O7)、ALE地址锁存、CLE命令锁存、CE片选、WE写使能、RE读使能、WP写保护、R/B就绪忙。这套引脚逻辑从K9F系列时代延续到今天,本质上没有太大变化。而SD NAND的引脚走的是另一套逻辑:CLK时钟、CMD命令线、DAT0~DAT3数据线,或者SPI模式的CS、CLK、MOSI、MISO。从协议层面看,前者是并行、异步、命令/地址/数据复用同一组IO,后者是串行、同步、有明确的命令通道和数据通道分工。
这个差异带来的第一个直接后果就是代码复杂度。裸NAND你要自己拼命令序列,比如读页的命令是00h加5个地址周期加30h,写页是80h加地址加数据加10h,中途还要不断查询R/B引脚或者状态寄存器。SD NAND就简单得多,CMD17读单块、CMD24写单块,命令通道发一个指令,数据通道流水一样把512字节吐出来。如果你的主控有SDIO外设,甚至连时序都不用关心。
我见过不少项目,主控资源紧张、工程师对NAND协议不熟,硬着头皮上裸NAND,结果三四个月还没搞定坏块管理;换成SD NAND后,两周就把存储通路打通了。这不是夸张,是两种方案在架构上先天决定的效率差距。
1.3 容量标称与系统可见容量的差别在哪里
先说一个很多新手会踩的坑:8Gbit的NAND Flash,标称容量是1GB,但你能用的大约只有960MB左右,因为需要预留坏块替换区域和冗余区域。同理,一颗8GB的SD NAND,插到系统里看到的可用容量,也往往比8GB略少。这个“少”不是厂商缺斤短两,而是存储介质的结构性开销。
裸NAND的容量计算相对透明:总块数乘以每块页数乘以每页主数据区大小,再扣除出厂坏块和预留块,就是你的可用容量。这个过程完全由你的驱动代码决定,预留多少、坏块替换策略怎么定,你说了算。SD NAND的容量计算则封装在卡控制器内部,系统只能通过CSD寄存器读到OCR、C_SIZE等参数换算出的用户容量。至于内部怎么划分保留区、坏块池,SD规范允许厂商自己决定,外部无需关心。
这里有一个很微妙的影响:因为SD NAND对外隐藏了物理细节,你看到的LBA地址是线性连续的逻辑地址,底层物理块的磨损状态由控制器统筹。好处是你不需要知道任何物理信息就能用,坏处是当你需要做精确的寿命预测或磨损监控时,几乎拿不到内部数据。裸NAND在这点上有天然优势,所有物理块状态、擦写次数、坏块标记你都能自己掌控。
2. 扇区分配表的两种存在方式:谁在管“找数据”这件事
2.1 裸NAND的物理寻址与L2P映射表
裸NAND本身没有扇区(Sector)的概念,它最小的读写单位是页(Page),最小擦除单位是块(Block)。一个页典型大小是4KB/8KB/16KB主数据区加128B/256B/512B备用区(Spare Area)。但你总不能每次都按整页读写吧?文件系统通常以512B或4KB的扇区为单位操作数据,这就逼着你必须在驱动层建一张表:逻辑扇区号到物理页的映射表,也就是业内常说的L2P(Logical to Physical)表。
这张L2P表本质上是系统软件的产物,你想存哪里就存哪里。常规做法是在NAND中专门划出几个块作为映射表存储区,启动时逐块读出来加载进RAM,运行中更新后定期回写。一旦掉电时映射表没有及时回写,就会发生“数据在但找不到”的情况。这也是裸NAND方案最需要小心处理的点,我后面会专门讲怎么优化。
L2P表的存在,意味着你每读写一个逻辑扇区,驱动都要先查表确定对应的物理页地址。查询速度、缓存命中率、表项粒度,直接影响读写性能。表项粒度越细,每个扇区独立映射,灵活但表体积大;粒度越粗,比如4KB一个映射条目,表体积小但写放大和读改写的问题就出来了。裸NAND方案中,这张表的设计直接决定了系统的性能天花板。
2.2 SD NAND的透明扇区管理机制
SD NAND的内置控制器已经把L2P表做完了,而且做得相当成熟。上层只看到线性LBA地址,下发CMD17/CMD18读一个或多个逻辑扇区,CMD24/CMD25写扇区,仅此而已。控制器内部收到这些命令后,查询自维护的映射表,找到对应的物理页,然后执行NAND读写。这个流程对用户完全不可见,所以你不用考虑表存哪里、掉电怎么办、有多少块在做替换。
这种透明机制降低了开发门槛,但也带来一个认知上的盲区:有很多开发者以为SD NAND的写入就是直接改对应位置,忽略了在底层,控制器可能先把原物理页的数据搬到新位置,再更新映射表。这就是写放大(Write Amplification)的来源。你写512字节,底层可能实际搬运了好几个页的数据,最终的NAND物理写入量远大于你的逻辑写入量。
理解这一点,再看SD NAND的优化就有的放矢了。既然映射结构决定性能,那上位机的写入模式就是最大的杠杆:连续写入、块对齐、合并小写,都能显著减少底层搬运次数。相反,随机离散的小扇区写入,对SD NAND内部FTL来说是最不友好的场景。
2.3 文件系统层面的扇区分配:FAT表的第三方视角
上面聊的都是介质层面的映射表,再往上一层,文件系统还有自己的扇区分配表。比如FAT32的FAT表,专门记录每个文件占用了哪些簇;exFAT也有类似机制。这一层分配表跑在逻辑扇区之上,无论底层是裸NAND还是SD NAND,它都能正常运作。
但这层分配表本身也是数据,也要写入存储介质。问题来了:文件系统的分配表更新频率很高,创建文件、删除文件、修改文件大小都会触发FAT表区写入。这些写入如果落到底层时是随机小扇区写入,就会引发上面说的写放大问题。这就是为什么很多嵌入式文件系统(如LittleFS、SPIFFS)在设计时就尽量避免频繁的元数据更新,或者做日志式掉电保护。
我实测过一个对比:在同一颗SD NAND上,用FATFS跑日志记录,另一个用只追加的裸扇区方案,持续写入一个月后,前者的可用块消耗明显更深。原因就是FAT表区域的频繁更新导致那些物理块磨损明显加速。所以选文件系统时,不能只看功能,还要评估它的元数据写入频率是否匹配你的底层介质特性。
3. 架构差异导致的性能与可靠性格局
3.1 随机写性能:SD NAND的“隐形搬运代价”
随机小扇区写入,是所有NAND介质方案的长期痛点。裸NAND下,每次逻辑写入都可能触发读-改-写(Read-Modify-Write),因为页是最小编程单位,你改512字节却要重写整个页;SD NAND内部同样存在这个问题,只是由控制器代为处理了。两者的随机写都慢,但慢的原因和可优化的空间截然不同。
裸NAND的方案中,随机写入性能的瓶颈很大程度上取决于你的缓冲策略和映射粒度。比如在驱动里做512B扇区缓存,凑满一页再写入,随机小写就会被转化为页对齐写入,性能提升明显,但代价是掉电时缓存里的数据会丢,需要权衡。SD NAND则不然,控制器自己有一层缓存和调度逻辑,外部能干预的手段有限,你只能通过调整写入模式去配合它。
以我实际测试为参考:随机写512B,裸NAND配合页缓存驱动能达到约2.5MB/s,SD NAND因为控制器内部额外多了映射更新开销,实测约1.8MB/s左右。但如果改成连续多扇区写入,两者的差距会迅速缩小,甚至SD NAND在长时间连续写后凭借控制器内部的调度,表现还会更稳定。这个差异直接决定了选型方向:如果你的应用以随机小写为主,裸NAND加精心优化的驱动上限更高;以顺序写入为主,SD NAND省心且可靠。
3.2 坏块管理与ECC纠错:控制器接管还是开发者自治
坏块管理是NAND Flash绕不开的话题。出厂时就有坏块,使用中还会不断产生新坏块。裸NAND方案中,坏块管理是你自己的事:扫描坏块、建立坏块表、分配替换块、确保写入时跳过坏块。一个成熟的裸NAND驱动,坏块管理代码往往比读写代码还要多两倍。
ECC纠错也一样。SLC NAND一般需要1bit/512B到4bit/512B的纠错能力,MLC/TLC则动辄几十bit/1KB,还要用到LDPC。裸NAND方案的ECC必须由主控完成,要么用硬件ECC引擎,要么用软件算BCH/LDPC,这对主控的计算性能和实现复杂度都是不小的负担。
SD NAND把这些全部收进控制器里,ECC算法、坏块替换、磨损均衡都不需要外部操心。从系统角度看,你面对的是一个“永不写坏”的理想块设备,只要还能正常响应,数据就是可靠的。当然这也不是万能的,控制器自身的寿命和固件稳定性也会决定最终寿命,但至少你不用在应用层处理那些繁琐的底层细节。
需要提醒的是:SD NAND虽然内部管理坏块,但它不会告诉你哪些块是坏块、替换进行到哪里了。当你通过SD卡协议读取状态时,控制器只返回正常或错误,不会反馈“某个物理块刚牺牲了”这样的信息。如果你需要精确监控介质健康状况,SD NAND提供的SMART信息又是相当有限。这一点在选型前要想清楚。
3.3 磨损均衡:算法自主可控与外置黑盒
磨损均衡(Wear Leveling)直接决定NAND的使用寿命。裸NAND方案下,磨损均衡算法是完全自主可控的。你可以设计动态磨损均衡,只为需要写入的逻辑页分配擦除次数最少的物理块;也可以做静态磨损均衡,把那些长期不动的冷数据搬离低磨损块。算法多复杂、效果多好,全靠你自己卷。
SD NAND内部也做磨损均衡,而且现代SD控制器的均衡算法往往做得相当不错,毕竟这是它们在消费级市场上活下来的看家本领。但同理,这些算法对外是一个黑盒,你不知道它均衡得怎么样,也不知道它在什么条件下触发数据迁移。实际使用中,有些SD NAND在长时间反复写同一小片LBA区域后,整体寿命表现依然不错,说明内部的全局均衡逻辑是生效的。
从我个人的项目经验看,选型时不应该一刀切。如果你的应用写入模型简单、写入量可控,SD NAND的磨损均衡完全够用;如果你的应用要连续高强度写入好几年,且你能自己设计出比通用控制器更贴合应用场景的均衡策略,那么裸NAND的自主性就是无可替代的优势。寿命是需要用数据说话的东西,光靠感觉选颗贵的是不行的。
4. 优化策略:三类典型场景下的选型与配置建议
4.1 方案一:裸NAND + 自研FTL,抓住每一分性能
裸NAND加自研FTL是很多高性能存储产品的主流选择。典型场景包括工业级SSD、需要自主可控的高可靠设备、以及需要精细寿命管理的系统。这类方案的优化核心,集中在L2P映射表、坏块策略和缓冲区设计三个层面。
L2P映射表建议采用两级映射:粗粒度表常驻RAM,细粒度表按需加载。操作系统上来后,先用粗粒度表覆盖全盘,实际访问某一区域时再把细粒度表加载到缓存。这样既保证查表速度快,又不至于把内存都吃光。表项变更后不要立刻全量回写,改成脏标记加定时回写,减少NAND写次数。回写时尽量把多个脏表项合并到连续区域一次性写入,利用NAND的顺序写优势。
缓冲区层面,我建议做32KB以上的环形缓存队列,让上位机的写入请求先进入缓存,满一页或连续一段再批量提交。这个思路能在随机小写上得到至少两倍的写入速度提升。同时要注意,缓冲区越大意味着掉电丢数据的风险越大,必须配合电容掉电保持或者带自刷新机制的RAM,或者把脏数据回写频率控制在系统能接受的范围内。
4.2 方案二:SD NAND上跑文件系统,省心且快速的正确姿势
如果你的产品不需要极致的性能压榨,SD NAND加成熟文件系统是最务实的路径。常见搭配是轻量级嵌入式系统加FATFS/exFAT,高端场合也有直接上Linux MTD块层加UBIFS的方案。重点是JDK对SD NAND,而不是追求极端性能。
SD NAND的优化核心在于“匹配SD控制器的习惯”。写入尽量走多扇区连续写,避免512B级别的离散写入;文件系统分区和簇大小要按4KB对齐,这样底层映射天然匹配;如果应用是日志型数据流,最好预先分配一个大文件,然后在文件内部偏移写入,让文件系统元数据更新频率降到最低。
另外,SD NAND的外部供电质量值得注意。它内部有控制器、Flash阵列和电压转换电路,电源纹波过大会导致控制器误动作,严重时甚至破坏映射表。务必在靠近芯片引脚处放置10uF和100nF退耦电容,电源走线尽量加粗。这个细节我踩过坑,某些批次的板子老化后频繁出现初始化失败,最后定位到是电源纹波劣化导致控制器启动异常。
4.3 方案三:混合策略,兼顾性能与成本
还有一种容易被忽视的思路:系统里同时使用裸NAND和SD NAND。比如用小型SD NAND存放系统启动代码和关键配置文件,用大容量裸NAND存放应用数据和缓存。这样既能享受SD NAND的即插即用便利,又能通过裸NAND的高性能通道处理大数据流。
这个思路在工业相机、边缘网关类设备中比较常见。系统启动阶段需要快速可靠的引导,SD NAND提供的稳定性和无需维护的特性非常合适;运行阶段的数据采集和传输则交给裸NAND,因为这部分写入连续性强,可以通过页对齐、块调度最大化吞吐。
不过,混合策略会增加物料成本和硬件设计复杂度,需要评估是否值得。我一般建议,只有在单一方案明显无法满足需求时才这么干,比如系统启动对可靠性要求极高,同时数据通道又需要极致吞吐,两者冲突无法调和时。
5. 实操记录:用编程器与Verilog读写NAND的现场经验
5.1 用编程器读取SD NAND:不是所有烧录器都行
热搜词里频繁出现的beeprog2,是不少硬件工程师入手的第一台专业编程器。用它读取SD NAND时有个常见误区:很多人以为它会像读裸NAND那样,直接通过并行接口把整片Flash内容读出来。但SD NAND的协议是串行的,适配器引脚也要走SDIO接口,所以你必须先在编程器软件中选中对应的SD NAND型号或者同系列的兼容型号,然后按SD协议访问。
我用的经验是,先把芯片用夹具夹稳,VDD选3.3V,进入编程器软件后先读ID,确认型号匹配。然后执行整片读取,读出的是逻辑扇区层面的镜像,不是物理页的原始内容。这也意味着,如果你要修改SD NAND内部固件里某个字节,你需要知道它在逻辑LBA的哪个位置,而不是单纯按物理块页去算。
另外要注意的是,部分厂商的SD NAND会在出厂时写入独有的CID/CSD信息,或者带写保护标志。如果你读取后直接复制到另一颗同型号芯片,CID冲突可能会导致设备端无法初始化。解决方法是读取后用编程器提供的CID编辑功能改写为空白值或自定义值,再写入目标芯片。
5.2 Verilog实现NAND Flash读写:从状态机到时序收敛
Verilog实现NAND Flash读写,是热搜里另一个高频话题。这类需求多来自FPGA项目:有的要直接驱动裸NAND做高速数据记录,有的要把NAND接到没有NAND控制器的MCU上由FPGA中转。核心工作就是写一个状态机,按芯片时序把命令、地址、数据发送出去。
先看读单个页的基本流程。上电后等待tRWB(就绪/忙释放时间),然后发送命令00h、地址A1-A5、再发命令30h,随后等待R/B引脚拉低再拉高,表示页数据已装入芯片内部缓存。接着按RE时钟沿逐字节读走主数据区,再读备用区。这里最容易出错的是地址周期的数量和顺序忘了对着数据手册确认。比如4KB页的芯片,列地址需要两个周期,行地址需要三个周期;如果列地址周期少了,读取的位置完全是错的。
写页操作类似,发送命令80h、地址、然后按WE时钟逐字节写入缓存,最后发送10h启动内部编程。此后要不断轮询状态寄存器(命令70h,然后读IO0上的RDY位)直到变为1,期间不能发起新的操作。这个过程在Verilog里就是一组状态跳转,状态机的设计建议按“空闲-命令-地址-数据-完成”拆成独立状态组,方便调试和复用。
关于时序参数,必须严格满足数据手册的最小值。例如tWP(写脉冲宽度)不够,数据可能锁存错误。建议在FPGA中设置可配置参数寄存器,把tWP、tRP、tWB这些参数做成可调系数,编译一次后通过寄存器配置适配不同型号的NAND。这在项目调试阶段简直是救命的设计,我之前的板子在这上面可是浪费了不少时间。
5.3 扇区分配表在固件中的存放与调试思路
在自研NAND驱动中,扇区分配表(L2P表)的存放策略直接关系到掉电安全。我推荐至少存储两份表:主表放在NAND的一个专用块区,备份表放在另一个物理块区。每次更新表后,先写备份区,再写主表区,并在表头写入序列号。系统启动时加载两张表,比较序列号,选择最新的那份加载。
调试这种机制时,我喜欢在表区的备用区里写上表版本和CRC校验值。用编程器或FPGA逻辑分析仪抓总线,确认写入的表数据是否正确。曾经遇到一个非常隐晦的bug:表写入时校验和总是错,查了半天发现是写页操作后没有等到内部编程完成就发起了下一轮写入,导致数据被覆盖。这个问题在时序收敛的仿真里很难发现,只有拿到真实芯片上才会暴露。
再强调一个经验:无论裸NAND还是SD NAND,所有关键扇区(分区表、文件系统引导区、L2P表)都必须有冗余备份。这在高级存储产品里已经是常识,但在嵌入式小项目中经常被忽略。一旦掉电导致分区表损坏,整个存储系统就可能变成一块砖,数据恢复的成本远远超过多写一份备份的麻烦。
6. 常见问题与排查技巧实录
6.1 掉电后数据丢失:先从扇区分配表的更新机制查起
很多裸NAND方案的掉电丢数据,源头不在数据本身,而在扇区分配表没有及时落盘。你在RAM里改了映射关系,还没有回写到NAND,此时掉电,这部分数据就永久找不回来了。排查思路是:抓取一次完整写操作的时间线,看数据写入和表更新之间的窗口有多大。只要窗口期数据量可控,掉电丢失就在可接受范围。
SD NAND方案同样存在类似问题,但表现形式不同。它内置的控制器通常有一定掉电保护能力,但保护深度有限。如果在写关键扇区中途掉电,控制器的日志机制能恢复部分数据,极端情况仍可能丢失。排查时可以先看控制器返回的写命令状态,如果掉电前命令已经确认完成,数据大概率是安全的;如果是在命令未确认时掉电,就需要评估应用层的容忍度。
一个非常实用的技巧:应用层给每条关键记录加序列号,启动时扫描最后N条记录,如果发现序列号不连续,就说明有数据在掉电窗口丢失了。通过序列号机制,可以把“无声丢数据”变成“可检测的异常”,这对数据可靠性要求高的系统是必需的保险。
6.2 写入速度远低于预期:看看写放大和坏块替换
写入速度不达标,是存储调试中最常遇到的问题。裸NAND下,第一反应是查驱动是否做了页缓冲,如果没有缓冲,每次逻辑写都触发读改写,性能会骤降。第二查坏块替换策略:新坏块出现后,驱动是否把写入重定向到了正常块,如果替换逻辑有bug,可能一直往坏块写然后超时,严重影响整体速度。
SD NAND下,写入慢最常见的原因是写入模式过于离散。我遇到过一位客户反馈某SD NAND写入只有几百KB/s,后来分析发现他的代码每次只写32字节,但地址跳跃很大,导致控制器内部频繁做读改写和映射更新。把32字节合并到4KB连续写入后,速度直接提升了8倍以上。很多时候,性能问题不是介质不行,而是上位机的写入模式拖了后腿。
还有一种情况容易被忽略:SD NAND内部在做垃圾回收(GC)。当可用空闲块少于一定阈值,控制器会在后台搬运有效数据、擦除块、合并映射,这个期间虽然还能接受命令,但速度会明显下降。这是所有NAND方案都有的正常现象,不是故障。如果这类现象过于频繁,就要反思写入量是否超出预期、容量是否选小了。
6.3 系统无法识别SD NAND:先查硬件还是先查协议
SD NAND初始化失败,可能出现在物理层,也可能出现在协议层。我的排查顺序是:先用示波器测上电瞬间的CLK、CMD、DAT0电平,确认芯片供电正常且有稳定的时钟源。如果这些都没有问题,再确认CMD0(Idle State)有没有得到正确响应,CMD8和ACMD41的参数配置是否匹配芯片规格。
很多开发者在初始化SD卡时,容易漏了上电后的延时等待。SD规范要求主机在上电后必须等待至少74个时钟周期再发送第一个命令。如果这个延时不够,芯片内部的时钟同步没完成,CMD0就会被忽略。这个时间的坑,在新手项目里几乎是必踩的。
硬件层面还要检查信号完整性。SDIO走线过长、没有做阻抗匹配、上拉电阻阻值不对,都可能导致命令响应偶尔失败。这类故障最恶心的特征是“时好时坏”,大概率不是逻辑代码问题,而是信号质量问题。排查方法是用示波器观察CMD和DAT线的信号边沿,看有没有明显振铃或斜率过缓。必要时在CMD和DAT线上串22-33欧姆电阻,抑制过冲。
6.4 一个完整的排查流程参考
整理一个我常用的排查顺序,供大家直接复用。第一步,硬件检查:供电电压、纹波、退耦电容、信号走线,用示波器核对上电时序和时钟。第二步,ID读回:发送CMD0后尝试读CID/CSD寄存器,确认协议层握手成功。第三步,容量确认:通过CSD寄存器换算容量,与芯片标称比对,排除买到假货或翻新片。第四步,读写基准测试:先单扇区读写,再多扇区连续读写,定位是偶发错误还是性能瓶颈。第五步,压力耐久测试:持续写入擦除循环,观察是否有坏块、掉电保护逻辑是否正常工作。这套流程走完后,基本能准确锁定问题所在,而不是靠猜。
对于新入行的工程师,我个人的建议是别急着上手裸NAND,先用SD NAND快速跑通存储通路,感受一下“存储底层的确定性”是什么感觉。等对扇区、映射、磨损有了体感,再回头啃裸NAND,你会发现那些寄存器和时序不再是天书。存储技术没有银弹,每一条优化策略都是取舍的结果。把介质特性、控制器能力、应用写入模型三者放在一起权衡,才能找到真正适合自己的方案。这个内容后续还可以这样扩展:如果你有FPGA平台,建议把裸NAND和SD NAND都接上去跑一轮对比测试,亲手记录性能数据,比看多少篇文章都更有说服力。