嵌入式存储选型这件事,最怕的不是芯片本身难用,而是你按老经验把新芯片焊上去,结果发现引脚对不上、驱动找不到、启动流程完全不是一回事。我最近刚把一个跑了很久的 Nor Flash 方案切到米客方德的 SD NAND,整个过程踩了不少坑,也积累了一些在数据手册里翻不到的经验。这篇文章就把接口差异、驱动适配、启动流程改造这三块完整拆一遍,给正在做类似迁移的朋友一个可复现的参考路径。
1. 为什么会有从 Nor Flash 切到 SD NAND 这个需求
1.1 Nor Flash 的舒适区与它的天花板
Nor Flash 在嵌入式圈子里属于那种"用惯了就不想换"的器件。它最大的优势是支持 XIP(eXecute In Place),CPU 可以直接从 Flash 里取指令执行,不需要先把代码搬到 RAM。这意味着系统架构可以做得非常简单:上电、跳转到 Flash 地址、跑代码,整个链路短得不能再短。SPI Nor Flash 更是把引脚压缩到 4 根(CS、CLK、MOSI、MISO),随便一个 MCU 都能挂。
但 Nor Flash 的问题也很明显。第一是容量,市面上常见的 SPI Nor 大多在 1MB 到 32MB 之间,超过 64MB 的选择就很少了,而且价格曲线陡得厉害。第二是写入速度,Nor Flash 的擦除粒度小(通常 4KB 扇区),但擦除和写入都慢,尤其是大块数据写入时,等待时间让人抓狂。第三是寿命,Nor Flash 的擦写次数一般在 10 万次左右,如果你有频繁记录日志或者存储配置的需求,很快就到瓶颈了。
我之前的项目就是卡在容量上。系统需要存储固件、字库、音频提示音,还要留一块区域做数据记录,16MB 的 Nor Flash 捉襟见肘。想换 64MB 的 Nor,一问价格,比同容量的 SD NAND 贵出一大截,而且封装也大,PCB 布局要重新调。
1.2 SD NAND 到底是个什么东西
米客方德的 SD NAND 本质上是一颗把 NAND Flash 颗粒和控制器封装在一起的存储芯片,对外呈现的是标准 SD 接口。你可以把它理解成"一张焊在板子上的 SD 卡"——它内部有 FTL(Flash Translation Layer)做坏块管理和磨损均衡,主控这边只需要按 SD 协议发命令就行,不需要自己处理 NAND 的坏块、ECC 这些麻烦事。
这个设计带来的好处很直接:容量大(常见 1GB、2GB、4GB 甚至更大)、单位容量成本低、接口标准化(SDIO 或 SPI 都能驱动)、寿命比裸 NAND 好很多(因为有控制器做磨损均衡)。代价是它不像 Nor Flash 那样支持 XIP,代码必须先加载到 RAM 里才能执行,这对启动流程提出了新要求。
1.3 迁移决策的核心权衡点
在决定迁移之前,我列了一张对比表,把几个关键维度摆出来看:
| 维度 | SPI Nor Flash | 米客方德 SD NAND |
|---|---|---|
| 典型容量 | 1MB - 32MB | 1GB - 8GB |
| 接口 | SPI(4线) | SDIO(6线)或 SPI |
| XIP 支持 | 支持 | 不支持 |
| 擦写寿命 | 约 10 万次 | 控制器管理,显著更高 |
| 单位容量成本 | 高 | 低 |
| 驱动复杂度 | 低(SPI 驱动) | 中(SDIO 协议栈) |
| 启动方式 | 可直接执行 | 需 Bootloader 搬运 |
这张表里最关键的一行是"XIP 支持"。如果你的系统是那种上电就要跑、RAM 又小得可怜的架构,迁移到 SD NAND 意味着你必须引入一个 Bootloader 阶段,先把代码从 SD NAND 搬到 RAM,再跳转执行。这个改动不是换个驱动那么简单,它会影响整个启动链路的设计。
2. 接口层面的差异:从四线 SPI 到 SDIO 的硬件改造
2.1 引脚定义与硬件连接的重新规划
SPI Nor Flash 的引脚非常固定:CS、CLK、MOSI、MISO,加上 VCC 和 GND,六根线搞定。有些封装还会带 WP(写保护)和 HOLD 引脚,但大多数场景下直接拉高就行。
SD NAND 就复杂一些。它支持两种模式:SDIO 模式和 SPI 模式。SDIO 模式下需要 CLK、CMD、DAT0-DAT3 共 6 根信号线,加上 VCC 和 GND,总共 8 根。SPI 模式下可以只用 CS、CLK、MOSI、MISO 四根,但性能会打折扣。
我这次选的是 SDIO 模式,原因是速度。SDIO 四线并行传输,理论带宽比 SPI 单线高得多。实测下来,SDIO 模式下的连续读取速度能到 20MB/s 以上,而 SPI 模式即使跑到 50MHz 时钟,实际吞吐也就 5MB/s 左右。对于需要快速加载固件的场景,这个差距很致命。
硬件改造时要注意几个点。第一,SDIO 的 CLK 线要走等长,尤其是时钟频率超过 25MHz 之后,走线不等长会导致采样错误。第二,CMD 和 DAT 线需要上拉电阻,通常 10K 到 47K 之间,具体值参考主控手册。第三,电源去耦要到位,SD NAND 在写入时电流波动比较大,建议在 VCC 引脚附近放一个 10uF 的钽电容加一个 0.1uF 的陶瓷电容。
2.2 SDIO 总线的初始化时序与 Nor Flash 的本质区别
Nor Flash 的初始化非常简单:上电、拉低 CS、发命令、读 ID,确认芯片在位就行。整个过程是同步的、确定的,你甚至可以用 GPIO 模拟 SPI 时序来驱动它。
SDIO 的初始化就完全是另一回事了。它是一个状态机驱动的过程,需要经过一系列命令交互才能进入数据传输模式。大致流程是这样的:
- 上电后发送 CMD0,让卡进入 IDLE 状态
- 发送 CMD8,检查电压范围,确认是 SD 2.0 还是更早的协议
- 发送 CMD55 + ACMD41,进行初始化协商,这一步可能需要反复发送直到卡就绪
- 发送 CMD2,获取 CID 寄存器
- 发送 CMD3,获取 RCA(相对卡地址)
- 发送 CMD9,读取 CSD 寄存器,获取容量等信息
- 发送 CMD7,选中卡
- 发送 CMD16,设置块长度
- 发送 ACMD6,设置总线宽度为 4 位
这一套流程走下来,如果中间任何一步超时或者响应错误,初始化就会失败。我在调试时就遇到过 ACMD41 一直返回 busy 的情况,后来发现是电源上电斜率太慢,卡还没准备好就被主控开始轮询了。解决办法是在上电后加一个 10ms 的延时,等电源稳定后再开始发命令。
注意:SDIO 初始化对时序非常敏感,尤其是 CMD 线的响应超时。如果主控的 SDIO 控制器支持可配置超时,建议把初始化的超时时间设长一些,等卡进入传输模式后再收紧。
2.3 信号完整性与 PCB 布局的实战经验
从 SPI 切到 SDIO,PCB 布局的难度上了一个台阶。SPI 跑个 20MHz 基本不用太讲究,线走通了就行。SDIO 如果跑 50MHz 甚至更高,信号完整性问题就会冒出来。
我总结了几条实操经验。第一,CLK 线尽量短,并且远离 DAT 线,避免时钟串扰到数据线上。第二,如果板子空间允许,在 CLK 线串联一个 22 欧姆到 33 欧姆的电阻,可以抑制过冲。第三,DAT 和 CMD 线的上拉电阻要靠近主控端放置,而不是靠近 SD NAND 端,这样可以在信号反射回来之前就把电平拉高。第四,如果走线比较长(超过 5cm),考虑在 SD NAND 端也加上拉,形成分压式端接。
还有一点容易被忽略:SD NAND 的电源引脚旁边一定要放去耦电容,而且地线要短。我第一版板子为了省空间,把去耦电容放在离芯片 1cm 远的地方,结果写入大文件时偶尔出现数据校验错误。后来把电容挪到紧贴引脚的位置,问题就消失了。
3. 驱动适配:从 SPI 驱动到 SDIO 协议栈的跨越
3.1 主控 SDIO 控制器的配置要点
不同主控的 SDIO 控制器差异很大。有的主控(比如 STM32 系列)自带 SDMMC 外设,硬件帮你处理了大部分协议细节,你只需要配置时钟、总线宽度、DMA 就行。有的主控(比如一些低端 MCU)没有专用 SDIO 控制器,只能用 SPI 模式驱动 SD NAND,那就退回到类似 Nor Flash 的驱动方式了。
我这次用的是带 SDMMC 外设的主控,配置时主要关注几个参数:
- 时钟分频:初始化阶段用低速时钟(比如 400KHz),进入传输模式后再切到高速时钟(比如 25MHz 或 50MHz)。这个切换时机很关键,切早了卡还没准备好,切晚了性能上不去。
- 总线宽度:初始化时用 1 位模式,ACMD6 之后再切到 4 位模式。
- DMA 配置:SDIO 的数据传输最好用 DMA,否则 CPU 会被中断拖死。DMA 的 burst 长度要和 SDIO 控制器的 FIFO 深度匹配,不然会出现 overrun 或者 underrun。
- 超时设置:命令响应超时和数据传输超时都要设,而且要根据实际时钟频率计算。比如 400KHz 下,一个命令的响应可能需要几百微秒,超时就要设到毫秒级。
3.2 块设备驱动层的对接方式
在 Linux 或者 RTOS 环境下,SD NAND 通常会被注册成一个块设备(block device)。上层文件系统(比如 FATFS、LittleFS、ext4)通过块设备接口来读写扇区,不需要关心底层是 SD 协议还是 NAND 协议。
这里有一个关键点:SD NAND 的扇区大小通常是 512 字节,而 Nor Flash 的擦除粒度可能是 4KB。如果你之前的代码是直接操作 Nor Flash 的物理地址,迁移到 SD NAND 后必须改成通过块设备接口读写。这意味着你需要引入一个文件系统或者至少一个简单的块管理层。
我在 RTOS 环境下用的是 FatFS,对接 SD NAND 只需要实现几个底层函数:disk_initialize、disk_status、disk_read、disk_write、disk_ioctl。其中 disk_ioctl 里要正确处理 CTRL_SYNC 和 GET_SECTOR_COUNT 这两个命令,否则文件系统可能挂载失败。
DSTATUS disk_initialize(BYTE pdrv) { if (SD_Init() != SD_OK) { return STA_NOINIT; } return 0; } DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { if (SD_ReadBlocks((uint8_t *)buff, sector, count) != SD_OK) { return RES_ERROR; } return RES_OK; }这段代码看起来简单,但实际调试时要注意:SD_ReadBlocks 的 sector 参数是块地址还是字节地址,不同驱动库的定义可能不一样。我用的那个库是按块地址来的,但文档里没写清楚,害我调了半天才发现读出来的数据偏移了 512 倍。
3.3 性能调优:从理论带宽到实际吞吐
SDIO 的理论带宽很好算:4 位总线 × 50MHz 时钟 = 200Mbps = 25MB/s。但实际跑下来,连续读取能到 20MB/s 就算不错了,写入通常只有一半左右。
影响性能的因素有几个。第一是命令开销,每次读写都要发 CMD17/CMD18 或者 CMD24/CMD25,这些命令本身占用的时间不可忽略。第二是块大小,SD NAND 支持 512 字节和 1024 字节两种块大小,用 1024 字节可以减少命令次数,但有些主控的 FIFO 只有 512 字节深,用大块反而会触发多次 DMA 传输。第三是文件系统的开销,FatFS 在写入时会频繁更新 FAT 表,如果 FAT 表所在的扇区被反复擦写,性能会下降。
我的调优做法是:把块大小设成 512 字节(兼容性最好),开启多块读写(CMD18/CMD25),并且在文件系统层开启写缓存。这样调整后,连续写入速度从 5MB/s 提升到了 9MB/s 左右。
4. 启动流程的重构:从 XIP 到 Bootloader 搬运
4.1 为什么 Nor Flash 可以直接启动而 SD NAND 不行
Nor Flash 支持 XIP,是因为它的接口是随机访问的,CPU 可以通过地址总线直接读取任意地址的数据。你只要把复位向量指向 Nor Flash 的起始地址,CPU 上电后就能直接取指令执行。
SD NAND 不行,因为它是块设备,数据是按扇区读取的,不能随机访问单个字节。CPU 没法直接从 SD NAND 取指令,必须先通过 SDIO 控制器把数据读到 RAM 里,再从 RAM 执行。这就要求系统里必须有一段"搬运代码",也就是 Bootloader。
4.2 Bootloader 的分级设计与搬运策略
Bootloader 通常分两级。第一级(一级引导)固化在芯片内部的 ROM 里,或者放在一个小容量的 Nor Flash 里,它的任务非常简单:初始化 SDIO 控制器,从 SD NAND 的固定位置读取第二级 Bootloader 到 RAM,然后跳转过去执行。
第二级 Bootloader 就复杂一些,它要负责初始化时钟、DDR、外设,然后把操作系统或者应用程序从 SD NAND 搬到 RAM 的指定位置,最后跳转执行。
这里有一个关键决策:第二级 Bootloader 放在哪里。如果放在 SD NAND 里,那一级引导必须知道它的确切位置(通常是第一个分区或者某个保留扇区)。如果放在 Nor Flash 里,那一级引导就简单了,但你就还得保留一颗 Nor Flash,失去了完全替换的意义。
我选的是全部放在 SD NAND 里。具体做法是在 SD NAND 上划分一个隐藏分区,存放第二级 Bootloader,一级引导从固定扇区读取。这个隐藏分区不挂载文件系统,直接按扇区读写,避免文件系统损坏导致无法启动。
4.3 启动时间的实测与优化
从 Nor Flash 的 XIP 启动切到 SD NAND 的搬运启动,启动时间肯定会变长。我实测了一下,Nor Flash 方案从上电到应用跑起来大约 200ms,SD NAND 方案一开始要 1.2 秒,优化后降到了 600ms 左右。
优化手段有几个。第一,一级引导只搬必要的部分,不要一次性把整个固件都读进来。第二,第二级 Bootloader 里开启 SDIO 的高速模式和 4 位总线,搬运速度能快不少。第三,如果主控支持,可以用 DMA 搬运,CPU 同时做其他初始化工作。第四,压缩固件,减少搬运的数据量,但解压也需要时间,要权衡。
还有一个坑:SD NAND 的初始化时间本身就不短,尤其是第一次上电或者刚复位后,卡内部要做一些自检。如果一级引导没有等卡就绪就开始发命令,会一直失败。我的做法是在一级引导里加一个重试机制,CMD0 发完之后轮询 CMD13 直到卡返回 ready 状态,最多重试 100 次,每次间隔 1ms。
5. 迁移过程中踩过的坑与排查思路
5.1 初始化失败:从电源到时序的逐项排查
第一次上电,SD NAND 完全没反应,CMD0 发出去没有任何响应。排查过程是这样的:
先量电源,3.3V 正常,纹波也在可接受范围内。然后量时钟,CLK 线上有波形,频率也对。再量 CMD 线,发现上电后一直是高电平,发 CMD0 时也没有拉低。这说明主控的 SDIO 控制器可能没有正确使能。
查了主控手册,发现 SDIO 控制器的时钟使能位在 RCC 寄存器里,而我初始化时只使能了 GPIO 时钟,忘了使能 SDIO 时钟。补上之后,CMD 线有波形了,但卡还是不响应。
继续查,发现 CMD 线的上拉电阻没焊。SDIO 协议要求 CMD 和 DAT 线在空闲时保持高电平,靠的就是上拉电阻。补焊之后,初始化终于通过了。
这个坑的教训是:SDIO 的硬件检查清单一定要逐项过,电源、时钟、上拉、地线,一个都不能少。
5.2 数据读写错误:信号完整性与电源噪声的联合诊断
初始化通过后,读写测试开始出问题。小数据量读写正常,但连续读写超过 1MB 后就会出现校验错误。错误位置不固定,有时候在前半段,有时候在后半段。
先用逻辑分析仪抓波形,发现 DAT 线上的信号在高速传输时有明显的振铃。把时钟频率从 50MHz 降到 25MHz,错误率下降但没完全消失。然后在 CLK 线上串了一个 33 欧姆的电阻,振铃明显改善,但偶尔还是出错。
最后用示波器看电源纹波,发现写入时 VCC 上有大约 100mV 的波动。在 SD NAND 的 VCC 引脚旁边加了一个 10uF 的钽电容,问题彻底解决。
这个坑说明:SD NAND 在高速写入时对电源的要求比 Nor Flash 高得多。Nor Flash 写入电流小,电源波动不明显;SD NAND 内部有控制器和 NAND 阵列,写入时电流脉冲很大,去耦不到位就会导致数据错误。
5.3 启动失败:Bootloader 搬运地址与中断向量的对齐问题
Bootloader 写好后,第一次启动直接跑飞了。用调试器看,发现 PC 指针跳到了一个非法地址。
排查后发现是两个问题叠加。第一,搬运的目标地址没有对齐。ARM Cortex-M 系列要求中断向量表按 128 字节或 256 字节对齐,我选的地址是 0x20001000,虽然 4 字节对齐了,但没满足向量表的对齐要求。改成 0x20002000 后,向量表能正确加载了。
第二,跳转前没有关闭中断和复位外设。一级引导在跳转到第二级之前,如果还有中断挂着,跳转后中断向量表变了,中断服务程序就会跑到错误的地址。解决办法是在跳转前执行 __disable_irq(),并且把 SysTick 关掉。
__disable_irq(); SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; // 设置新的向量表偏移 SCB->VTOR = APP_ADDRESS; // 跳转 typedef void (*pFunction)(void); pFunction JumpToApp = (pFunction)(*(volatile uint32_t *)(APP_ADDRESS + 4)); __set_MSP(*(volatile uint32_t *)APP_ADDRESS); JumpToApp();这段代码是 Bootloader 跳转的标准写法,但有几个细节容易出错:APP_ADDRESS 必须是 4 字节对齐的,栈顶指针要从 APP_ADDRESS 处读取,复位向量要从 APP_ADDRESS + 4 处读取。如果固件生成时没有把向量表放在最前面,这里读出来的就是错误的值。
6. 迁移后的收益与适用场景判断
6.1 容量、成本与维护性的实际改善
迁移完成后,最直观的收益是容量。原来 16MB 的 Nor Flash 要精打细算,现在 2GB 的 SD NAND 随便用。固件、字库、音频、日志全部放进去,还剩一大半空间。
成本方面,2GB 的 SD NAND 单颗价格比 64MB 的 Nor Flash 还便宜,而且封装更小,PCB 面积省了不少。对于量产项目来说,这个成本差异很可观。
维护性也提升了。以前 Nor Flash 写日志要自己做磨损均衡,现在 SD NAND 内部控制器帮你做了,应用层直接写文件就行。固件升级也简单了,以前要擦除扇区再写入,现在直接替换文件,配合 Bootloader 做双备份,升级失败还能回滚。
6.2 什么情况下不建议迁移
SD NAND 不是万能的。如果你的系统满足以下条件,建议继续用 Nor Flash:
- RAM 非常小(比如小于 64KB),放不下 Bootloader 和搬运过来的固件
- 启动时间要求极高(比如小于 100ms),无法接受搬运带来的延迟
- 系统可靠性要求极高,不能接受块设备可能出现的坏块或者文件系统损坏
- 只需要几百 KB 的存储空间,Nor Flash 完全够用且更简单
我个人的判断标准是:如果存储需求超过 32MB,或者需要频繁写入数据,就值得考虑 SD NAND;如果只是存个固件和少量配置,Nor Flash 依然是更省心的选择。
6.3 混合方案的可行性探讨
还有一种折中方案:保留一颗小容量 Nor Flash 用于启动,SD NAND 用于大容量存储。这样一级引导可以从 Nor Flash 直接 XIP 执行,不需要 SDIO 初始化,启动速度快;应用程序和数据放在 SD NAND 里,容量大、成本低。
这个方案的代价是 BOM 上多了一颗芯片,PCB 面积也大了。但对于那些启动时间敏感、又需要大容量存储的项目,这是一个很实用的选择。我在另一个项目里就是这么做的,Nor Flash 只放一级引导和紧急恢复固件,SD NAND 放主固件和用户数据,兼顾了启动速度和存储容量。
整个迁移过程走下来,最大的感受是:Nor Flash 和 SD NAND 虽然都是存储芯片,但设计哲学完全不同。Nor Flash 是"简单直接",SD NAND 是"用复杂度换容量和寿命"。迁移的关键不在于驱动怎么写,而在于理解这个哲学差异,然后重新设计启动流程和存储架构。希望这篇总结能帮到正在做类似迁移的朋友,少踩几个我已经踩过的坑。