1. 从一块板子的存储选型说起
去年年底接手一个工业网关的改版项目,主控还是那颗熟悉的 MCU,原本板子上挂的是一颗 16MB 的 Nor Flash,用来放 Bootloader、应用固件和一小块参数区。项目跑了一年多,问题不大,但客户提了两个新需求:一是固件体积要翻倍,二是要支持现场记录运行日志,至少得留 64MB 的空间。Nor Flash 在这个容量段价格开始不友好,而且擦写寿命和写入速度都撑不住频繁的日志落盘。于是我把目光转向了 SD NAND。
这个选型切换听起来只是"换个存储芯片",实际动手才发现,从 Nor Flash 到 SD NAND,接口层、驱动层、启动流程三个维度全都要重新捋一遍。Nor Flash 是典型的随机访问存储,CPU 可以直接按地址取指执行;SD NAND 本质上是把 NAND 颗粒和控制器封装在一起,对外走的是 SD 协议,必须通过 SDIO 或 SPI 接口、经过命令交互才能读写。这个差异直接决定了 Bootloader 能不能直接从它启动、驱动要写多厚、启动时间会拉长多少。
这篇文章就是把这套切换过程完整拆开讲。适合正在做存储选型、准备从 Nor Flash 迁移到 SD NAND 的嵌入式工程师,也适合想搞清楚 SDIO、SPI 两种接口在存储场景下到底怎么选的人。我会把接口差异、驱动架构、启动流程的坑一个个讲透,参数怎么算、代码怎么写、踩过哪些雷,都摊开说。
2. 先搞清楚两者到底差在哪
2.1 存储介质与访问方式的本质区别
Nor Flash 的存储单元是 NOR 结构,支持字节级随机访问,读操作可以直接寻址,这也是它能做 XIP(eXecute In Place,原地执行)的根本原因。CPU 通过地址总线和数据总线直接读,不需要任何控制器介入,读延迟通常在几十纳秒级别。写操作则必须先擦除再写入,擦除以扇区(常见 4KB)为单位,写入速度慢,典型值在几百 KB/s 到 1MB/s 量级。
SD NAND 内部是 NAND 颗粒加一颗控制器。NAND 颗粒本身不支持随机字节访问,读写以页(常见 2KB 或 4KB)为单位,擦除以块(通常 128KB 或 256KB)为单位。控制器负责坏块管理、ECC 纠错、磨损均衡,对外暴露的是标准 SD 协议接口。这意味着 CPU 不能直接寻址,所有读写都要通过命令(CMD)和数据线完成,读延迟在几十微秒到毫秒级别,比 Nor Flash 高了两三个数量级。
这个差异带来的直接后果是:Nor Flash 上可以直接跑代码,SD NAND 上不行,必须先加载到 RAM 再执行。这是启动流程改造的核心矛盾点。
2.2 接口层面的对比:SPI 与 SDIO
SD NAND 支持两种对外接口:SPI 和 SDIO。这两种接口的选择直接决定了驱动复杂度和吞吐性能。
SPI 接口只需要 4 根线(CS、CLK、MOSI、MISO),几乎所有 MCU 都有硬件 SPI 外设,接线简单,驱动实现门槛低。但 SPI 是单数据线传输,时钟频率通常限制在 25MHz 到 50MHz,实际吞吐在 3MB/s 到 6MB/s 之间。而且 SPI 模式下 SD 协议的命令响应格式会多一层封装,每次读写都要走完整的命令-响应-数据流程,协议开销大。
SDIO 接口用 4 根数据线(DAT0-DAT3)加 1 根时钟(CLK)和 1 根命令线(CMD),支持 1-bit 和 4-bit 模式。4-bit 模式下时钟 50MHz 时理论带宽 200Mbps,实际吞吐能到 20MB/s 以上。代价是引脚多、驱动复杂,需要处理命令线双向复用、CRC 校验、多块传输等细节。
| 对比项 | SPI 模式 | SDIO 4-bit 模式 |
|---|---|---|
| 引脚数 | 4 | 6 |
| 典型时钟 | 25-50MHz | 50MHz |
| 实测吞吐 | 3-6MB/s | 20-25MB/s |
| 驱动复杂度 | 低 | 中高 |
| MCU 外设要求 | 任意 SPI | 专用 SDIO 控制器 |
| 启动可用性 | 部分 MCU 支持 | 需 BootROM 支持 |
选哪个,取决于你的 MCU 有没有 SDIO 控制器、启动阶段需不需要从 SD NAND 加载、以及对吞吐的要求。工业网关这种要频繁写日志的场景,我最终选了 SDIO 4-bit,因为 SPI 的写入吞吐撑不住日志频率。
2.3 容量、寿命与成本的权衡
Nor Flash 常见容量在 1MB 到 64MB 之间,超过 64MB 后价格曲线陡增,128MB 的 Nor Flash 单价可能是同容量 SD NAND 的三到五倍。SD NAND 主流容量从 128MB 到 8GB,容量越大单位成本越低。
寿命方面,Nor Flash 的擦写次数典型值 10 万次,但擦除块小,适合频繁小数据更新。SD NAND 的 NAND 颗粒擦写次数只有 3000 到 10000 次(MLC 更低,SLC 更高),但控制器做了磨损均衡,实际可用寿命取决于写入放大和均衡策略。对于日志这种顺序追加写入的场景,SD NAND 反而更合适,因为顺序写能触发控制器的块合并优化,减少擦除次数。
注意:SD NAND 的磨损均衡是控制器内部行为,你无法直接控制。如果应用层做频繁的小块随机写,写入放大会很严重,寿命可能远低于预期。日志场景一定要做缓冲批量写,别一条一条写。
3. 驱动架构的迁移路径
3.1 从 MTD 到块设备:软件栈的重构
Nor Flash 在 Linux 下的软件栈是 MTD(Memory Technology Device)子系统,上层可以跑 JFFS2、UBIFS 这些专为 Flash 设计的文件系统,也可以直接 mtdblock 当块设备用。MTD 的特点是直接操作物理擦除块,没有中间层,读写接口是mtd_read、mtd_write、mtd_erase。
SD NAND 走的是 MMC 子系统,被识别为块设备(/dev/mmcblkX),上层跑的是标准块文件系统如 ext4、FAT32。软件栈从 MTD 换到 MMC,意味着文件系统也要换。JFFS2 在 SD NAND 上跑不了,因为它是为裸 Flash 设计的,依赖 MTD 接口。你得换成 ext4 或者 F2FS。
这个迁移在应用层的影响是:原来直接操作 /dev/mtdX 的代码全部要改成操作文件。参数存储如果原来用的是 mtd 分区,现在要么改成文件,要么单独留一颗小 Nor Flash 或 EEPROM 放参数。
3.2 SPI 模式驱动的实现要点
如果你的 MCU 没有 SDIO 控制器,只能走 SPI。SPI 模式下 SD NAND 的驱动实现有几个关键点。
初始化阶段,SD NAND 上电后默认处于 SD 模式,要发 CMD0 让它进入 SPI 模式。CMD0 在 SPI 模式下的格式是 0x40 0x00 0x00 0x00 0x00 0x95,前导字节 0x40 表示命令起始位加 CMD0 索引,最后 0x95 是 CRC。进入 SPI 模式后,所有命令的 CRC 都可以省略(除了 CMD0 和 CMD8),响应格式也变成 SPI 特有的 R1、R3、R7 格式。
时钟管理上,初始化阶段时钟不能超过 400kHz,等 CMD58 读到 OCR 寄存器确认电压范围后,才能切换到高速时钟。我见过有人一上来就给 25MHz,结果卡在 CMD0 一直超时,排查了半天才发现是时钟问题。
// SPI 模式 SD NAND 初始化关键步骤(伪代码) sd_spi_send_cmd(CMD0, 0, 0x95); // 进入 SPI 模式,需带 CRC delay_ms(10); sd_spi_send_cmd(CMD8, 0x1AA, 0x87); // 检查电压范围,需带 CRC // 循环发 CMD55 + ACMD41 直到卡就绪 do { sd_spi_send_cmd(CMD55, 0, 0); r1 = sd_spi_send_cmd(ACMD41, 0x40000000, 0); } while (r1 != 0x00); sd_spi_send_cmd(CMD58, 0, 0); // 读 OCR 确认 CCS 位 sd_spi_set_clock(25000000); // 切换到高速时钟数据读写用 CMD17(单块读)、CMD24(单块写)、CMD18(多块读)、CMD25(多块写)。多块传输时要注意 CMD12 停止命令的时机,写多块时如果中途出错,必须发 CMD12 终止传输,否则卡会一直等数据。
3.3 SDIO 模式驱动的实现要点
SDIO 模式驱动复杂度高一个档次,但性能提升明显。初始化流程和 SPI 类似,但走的是 SD 总线协议,命令通过 CMD 线发送,响应也通过 CMD 线返回,数据通过 DAT0-DAT3 传输。
关键差异在于:SDIO 模式下命令和响应都有 CRC 校验,硬件控制器通常会自动处理 CRC,但你要正确配置控制器的 CRC 使能位。4-bit 模式需要在 ACMD6 里设置总线宽度位,设置前要先发 CMD55 通知卡这是应用特定命令。
DMA 配置是 SDIO 驱动的性能关键。以 STM32 的 SDMMC 外设为例,IDMA 模式适合小数据量传输,但大数据量传输时 CPU 占用率高。我一般用外部 DMA 配合 SDMMC,配置成双缓冲模式,读写 512 字节扇区时 CPU 几乎不参与数据搬运。
// STM32 SDMMC + DMA 配置关键参数 hsdmmc.Instance = SDMMC1; hsdmmc.Init.ClockEdge = SDMMC_CLOCK_EDGE_RISING; hsdmmc.Init.ClockBypass = SDMMC_CLOCK_BYPASS_DISABLE; hsdmmc.Init.ClockPowerSave = SDMMC_CLOCK_POWER_SAVE_DISABLE; hsdmmc.Init.BusWide = SDMMC_BUS_WIDE_4B; // 4-bit 总线 hsdmmc.Init.HardwareFlowControl = SDMMC_HARDWARE_FLOW_CONTROL_ENABLE; hsdmmc.Init.ClockDiv = 2; // 时钟分频,50MHz 输入时得 25MHz提示:SDIO 的时钟分频要算清楚。STM32 的 SDMMC 时钟源通常是 48MHz 或 50MHz,ClockDiv 是分频系数,实际时钟 = 源时钟 / (2 * ClockDiv)。初始化阶段 ClockDiv 要设大(比如 118,得到 400kHz 以下),初始化完成后再改小。
3.4 驱动层的常见坑与规避
第一个坑是上电时序。SD NAND 上电后需要至少 1ms 的稳定时间才能接受命令,有些板子电源爬升慢,1ms 不够,得给到 5ms 甚至 10ms。我遇到过一批板子,冷启动时 SD NAND 识别失败,热启动正常,最后发现是电源上升时间太长,加了延时就好了。
第二个坑是 CMD 线的上拉电阻。SDIO 模式下 CMD 线是双向的,必须加上拉电阻(典型 10kΩ 到 50kΩ),否则命令响应会出错。SPI 模式下 MISO 线也要上拉。有些设计为了省料把上拉去掉,结果就是时好时坏,很难查。
第三个坑是热插拔检测。SD NAND 是焊在板子上的,不需要热插拔,但驱动里如果使能了卡检测功能,可能会误触发。要在设备树或驱动配置里把 CD(Card Detect)功能关掉,或者把 CD 引脚固定为有效电平。
4. 启动流程的改造是最大的坎
4.1 Nor Flash 的 XIP 启动为什么简单
Nor Flash 做启动介质时,Bootloader 的第一条指令可以直接从 Flash 地址取。以 STM32 为例,BOOT0/BOOT1 引脚配置成从 Flash 启动后,CPU 复位向量指向 0x08000000,这个地址映射到 Nor Flash,CPU 直接取指执行。整个启动链路是:复位 -> 取指 -> 初始化时钟 -> 初始化 RAM -> 加载应用 -> 跳转。
这个流程里,Nor Flash 不需要任何驱动,因为它是内存映射的。Bootloader 本身就在 Nor Flash 里跑,不需要先把它加载到 RAM。这就是 XIP 的威力,也是 Nor Flash 在启动场景下最大的优势。
4.2 SD NAND 启动的三种方案
SD NAND 不能 XIP,所以启动流程必须改造。常见方案有三种。
方案一:BootROM 直接支持。部分 MCU(如某些 i.MX 系列、部分国产 SoC)的 BootROM 内置了 SD/MMC 启动支持,上电后 BootROM 会初始化 SDIO 控制器,从 SD NAND 的第一个分区读取 Bootloader 到内部 RAM 执行。这个方案最省事,但依赖芯片支持,MCU 级别很少见。
方案二:二级启动。板子上保留一颗小容量 Nor Flash 或 SPI Flash,里面放一级 Bootloader(SPL),SPL 负责初始化 SDIO/SPI 控制器,从 SD NAND 读取二级 Bootloader(U-Boot 等)到 DDR,然后跳转。这是嵌入式 Linux 系统最常见的方案,RK3588、i.MX6/8 都是这个路子。
方案三:MCU 内部 Flash 启动。如果 MCU 有内部 Flash,把 Bootloader 放内部 Flash,Bootloader 负责从 SD NAND 加载应用固件到 RAM 执行。这是 MCU 场景下最实际的方案,也是我这次项目采用的。
| 启动方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| BootROM 直接支持 | SoC 级别 | 无需额外器件 | 芯片支持有限 |
| 二级启动 | Linux 系统 | 灵活,支持大固件 | 需额外 Nor/SPI Flash |
| 内部 Flash 启动 | MCU 场景 | 成本低,简单 | 受内部 Flash 容量限制 |
4.3 内部 Flash 启动方案的详细实现
我这次用的是方案三。MCU 内部有 512KB Flash,Bootloader 编译出来 80KB 左右,放得下。Bootloader 的职责是:初始化时钟和 SDIO、从 SD NAND 读取应用固件到外部 SDRAM、校验固件完整性、跳转到应用入口。
固件在 SD NAND 上的存储格式要自己定义。我用的是最简单的方案:从 SD NAND 的 LBA 0 开始,前 512 字节是固件头,包含魔数、固件长度、CRC32 校验值、版本号。固件头后面紧跟固件数据,按扇区对齐。
typedef struct { uint32_t magic; // 0x544F4F42 "BOOT" uint32_t fw_size; // 固件字节数 uint32_t fw_crc; // 固件 CRC32 uint32_t version; // 版本号 uint8_t reserved[496];// 填充到 512 字节 } fw_header_t;Bootloader 的加载流程:读 LBA 0 拿到固件头,校验魔数,按 fw_size 算出需要读多少个扇区,循环读扇区到 SDRAM 的目标地址,读完后算 CRC32 和头部记录的值比对,一致就跳转,不一致就报错并停在 Bootloader 里等待重新烧录。
跳转前要做的准备工作:关中断、关看门狗、设置栈指针到应用固件的栈顶、设置向量表偏移寄存器(SCB->VTOR)指向应用固件的向量表地址。这几步漏一个都可能跳过去跑飞。
void jump_to_app(uint32_t app_addr) { uint32_t stack_top = *(uint32_t*)app_addr; uint32_t reset_handler = *(uint32_t*)(app_addr + 4); __disable_irq(); HAL_DeInit(); SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; SCB->VTOR = app_addr; // 重定向向量表 __set_MSP(stack_top); // 设置主栈指针 ((void (*)(void))reset_handler)(); // 跳转 }4.4 启动时间与可靠性的实测数据
启动时间方面,Nor Flash XIP 启动到应用跑起来大概 200ms 到 500ms,主要花在外设初始化上。SD NAND 方案因为多了固件加载环节,启动时间会拉长。我实测的数据:SDIO 4-bit 模式,固件 1MB,加载耗时约 80ms,加上 SD NAND 初始化 30ms、CRC 校验 20ms,总共比 Nor Flash 方案多了约 130ms。如果走 SPI 模式,1MB 固件加载要 250ms 以上,总启动时间会到 600ms 左右。
可靠性方面,SD NAND 启动要额外考虑几个失效点:SD NAND 初始化失败、固件头损坏、CRC 校验失败、加载过程中断电。我的处理策略是:Bootloader 里做三次重试,三次都失败就进入 DFU 模式等待上位机重新烧录。固件头做双备份,LBA 0 和 LBA 1 各存一份,读的时候两份都读,取有效的那份。
注意:SD NAND 的固件更新一定要做原子性保护。我的做法是先写新固件到备用区域,写完校验通过后,再更新固件头指向新区域。这样即使更新过程中断电,旧固件头还指向旧区域,系统还能启动。
5. 实操过程中的问题排查实录
5.1 SD NAND 识别失败怎么查
识别失败是最常见的问题,排查要按顺序来。先量电源,SD NAND 的供电典型是 3.3V,纹波要控制在 50mV 以内。我遇到过一批板子,SD NAND 供电和 MCU 共用 LDO,MCU 跑起来后电流波动导致 SD NAND 供电不稳,识别时好时坏,后来单独给 SD NAND 加了一颗 LDO 就好了。
电源没问题就查时钟。SPI 模式下初始化时钟必须低于 400kHz,SDIO 模式下初始化时钟也要低于 400kHz。用示波器量 CLK 引脚,确认频率和占空比。占空比不对的话,检查时钟源配置和分频系数。
时钟没问题就查命令。用逻辑分析仪抓 CMD 线,看 CMD0 发出去后有没有响应。SPI 模式下 CMD0 的响应是 0x01,如果收到 0xFF 说明卡没响应,可能是接线问题或者卡没进入 SPI 模式。SDIO 模式下 CMD0 的响应是 R0,没有响应说明 CMD 线有问题,重点查上拉电阻和走线。
5.2 读写数据出错怎么定位
数据出错分两种情况:命令阶段出错和数据阶段出错。命令阶段出错通常是 CRC 校验失败,SDIO 模式下硬件会自动重试,如果一直失败,检查 CMD 线的信号质量,必要时降低时钟频率试试。
数据阶段出错要看是读出错还是写出错。读出错的话,先确认读的地址对不对,SD NAND 的地址是按块(LBA)算的,不是按字节。我见过有人把字节地址直接当 LBA 用,读出来的数据全是乱的。写出错的话,检查写保护位,SD NAND 的 CSD 寄存器里有写保护标志,如果卡被写保护了,写命令会返回错误。
还有一种隐蔽的错误是数据位翻转,读出来的数据个别 bit 不对。这通常是信号完整性问题,重点查 DAT 线的走线长度和阻抗匹配。SDIO 4-bit 模式下 DAT0-DAT3 的走线要等长,偏差控制在 5mm 以内。
5.3 启动跳转后跑飞怎么排查
跳转后跑飞的原因通常有三个。一是向量表没重定向,应用固件的中断向量还在 Bootloader 的向量表里,一进中断就跳到 Bootloader 的中断处理函数,行为不可预期。二是栈指针没设置对,应用固件的栈顶地址要从固件的第二个 32 位字读,不是随便设的。三是外设没复位干净,Bootloader 初始化过的外设(比如 SDIO、DMA)还处于活动状态,应用固件重新初始化时冲突。
排查方法:在跳转前把关键寄存器打印出来,确认 MSP、VTOR 的值对不对。跳转后在应用固件的入口点打个 GPIO 翻转,用示波器看有没有执行到。如果没执行到,说明跳转本身有问题;如果执行到了但很快跑飞,说明是外设或中断的问题。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 识别失败,CMD0 无响应 | 电源不稳、时钟不对、接线错 | 量电源纹波、量时钟频率、查接线 | 加 LDO、调分频、重新接线 |
| 识别成功但读写超时 | 时钟太快、信号质量差 | 降时钟测试、抓波形 | 降低时钟、加匹配电阻 |
| 读数据 CRC 错误 | 地址算错、信号完整性问题 | 核对 LBA 地址、查 DAT 走线 | 修正地址、等长走线 |
| 写数据失败 | 写保护、卡寿命耗尽 | 读 CSD 寄存器、换卡测试 | 解除写保护、更换卡 |
| 跳转后跑飞 | 向量表未重定向、栈指针错 | 打印 MSP/VTOR、GPIO 打点 | 重定向向量表、正确设栈 |
| 启动时间过长 | SPI 模式、固件太大 | 测各阶段耗时 | 换 SDIO、压缩固件 |
5.5 几个独家避坑技巧
第一个技巧:SD NAND 的固件烧录工具要自己做。市面上没有现成的工具能把固件按你的格式写到 SD NAND 的指定 LBA,我用 Python 写了个脚本,通过串口把固件传给 Bootloader,Bootloader 负责写到 SD NAND。脚本里做了分块传输和 CRC 校验,烧录 1MB 固件大概 15 秒。
第二个技巧:Bootloader 里加一个"安全模式"入口。上电时检测某个 GPIO 或者串口有没有特定字符输入,如果有就停在 Bootloader 里不跳转,方便现场调试。这个功能救过我好几次,现场设备启动不了,进安全模式一看就知道是固件问题还是硬件问题。
第三个技巧:SD NAND 的寿命监控。SD NAND 没有标准的寿命查询命令,但可以通过读 SMART 信息或者统计写入量来估算。我在应用层做了个简单的写入计数器,每次写日志累加,写到一定量就告警提示更换。虽然不精确,但比什么都不做强。
6. 迁移决策的几点个人体会
从 Nor Flash 转到 SD NAND,不是简单的替换,而是一次存储架构的升级。接口从内存映射变成命令交互,驱动从 MTD 变成 MMC,启动从 XIP 变成加载执行,每一层都要重新设计。
我的建议是:如果应用场景是代码存储加少量参数,Nor Flash 依然是最优解,简单可靠。如果场景涉及大容量数据存储、频繁写入、成本敏感,SD NAND 值得迁移,但要接受启动流程复杂化和驱动工作量的增加。SDIO 和 SPI 的选择上,有 SDIO 控制器就优先用 SDIO,性能差距是数量级的;没有的话 SPI 也能用,但要对吞吐有合理预期。
最后分享一个我在实际项目中验证过的参数:SD NAND 做日志存储时,应用层缓冲区设成 4KB 到 8KB,攒够一个擦除块再写,写入放大能控制在 1.5 以内,寿命比逐条写提升五倍以上。这个数字是我用不同缓冲大小跑了三个月实测出来的,供参考。