4. 存储设备驱动:NAND Flash、eMMC/SD卡、SPI Flash与驱动裁剪策略

存储设备驱动,是Bootloader里最绕不开的一块硬骨头。你想想看,Bootloader的核心任务是什么?把内核镜像从存储介质里读出来,放到内存里,然后跳过去执行。如果驱动写不好,连数据都读不对,后面全是白搭。

我这些年接触过的嵌入式项目,存储方案基本就三大家:NAND Flash、eMMC/SD卡、SPI Flash。每种都有自己的脾气,驱动写法也完全不同。今天咱们就把它们挨个捋一遍,最后再聊聊怎么裁剪——毕竟Bootloader里不需要Linux内核那么完整的驱动栈。

核心观点:Bootloader里的存储驱动,不求功能全面,只求“刚好够用、稳定可靠”。能用简单轮询就别上中断,能用单块读写就别搞DMA。少一行代码,就少一个出bug的机会。

Bootloader 存储设备驱动知识体系 Bootloader 存储驱动 NAND Flash 驱动 eMMC / SD 卡驱动 SPI Flash 驱动 坏块管理 / ECC校验 页读写 / 擦除操作 OOB区与标记识别 MMC协议 / 初始化序列 块读写 / 分区切换 HS200/HS400 模式 SPI协议 / 命令集 4字节地址模式 写使能 / 状态轮询 驱动裁剪策略:最小功能集 + 条件编译

4.1 NAND Flash 驱动:坏块与ECC是绕不过的坎

NAND Flash,便宜、容量大,但质量嘛……说实话,出厂就带坏块是常态。我最早做NAND驱动时,天真地以为所有块都能用,结果读到坏块直接卡死。从那以后,我养成了一个习惯:驱动初始化第一件事,扫描坏块表。

NAND驱动的核心就三件事:

  • 页读写:NAND的最小读写单位是页(通常2KB/4KB/8KB),不是字节。读一页发一个命令,等芯片准备好,再读数据。写之前必须先擦除块。
  • 坏块管理:每个块的第一页或第二页的OOB区,会标记是否为坏块。Bootloader里不需要复杂的坏块迁移,直接跳过就行。
  • ECC校验:NAND的bit翻转是家常便饭。Bootloader里我建议用硬件ECC,如果芯片不支持,至少用软件Hamming码做1bit纠错。

我的习惯:在NAND驱动的初始化函数里,加一个坏块扫描的调试打印。开发阶段能看到哪些块被跳过了,心里踏实。量产时关掉这个打印就行。

下面是一个简化的NAND页读取流程,去掉了错误处理,只保留主干:

int nand_read_page(uint32_t page_addr, uint8_t *buf, int len)
{
    // 1. 发送读命令 0x00
    nand_cmd(0x00);
    // 2. 发送列地址(低2字节)和行地址(页号)
    nand_addr(page_addr & 0xFF);
    nand_addr((page_addr >> 8) & 0xFF);
    nand_addr((page_addr >> 16) & 0xFF);
    // 3. 发送读确认命令 0x30
    nand_cmd(0x30);
    // 4. 等待芯片就绪(R/B引脚或状态寄存器)
    while (!nand_is_ready());
    // 5. 读取数据
    for (int i = 0; i < len; i++)
        buf[i] = nand_read_byte();
    return 0;
}

注意:NAND的时序非常敏感。我曾经在一个项目里,因为GPIO的驱动强度没配好,导致数据线信号质量差,读出来的数据每隔几页就错一位。排查了两天才找到原因。所以,硬件上先确认信号完整性,再调软件。

4.2 eMMC/SD卡驱动:协议栈虽厚,Bootloader只取一瓢

eMMC和SD卡,本质上都是MMC协议的变种。协议栈从物理层到应用层,厚厚一大本。但在Bootloader里,我们只需要实现最核心的部分:初始化、块读写、分区切换。

我个人习惯把eMMC驱动拆成三层:

  1. 底层硬件层:操作SD/MMC控制器寄存器,发送命令、接收响应、读写数据。
  2. 协议层:实现CMD0(复位)、CMD2(获取CID)、CMD3(设置RCA)、CMD7(选择卡)、CMD16(设置块大小)、CMD18(多块读)等关键命令。
  3. 块设备层:提供简单的读块/写块接口,屏蔽底层细节。

这里有个坑:eMMC的初始化序列比SD卡复杂,尤其是要切换到HS200/HS400高速模式时,需要调整时钟频率和时序。Bootloader里我建议只用默认的HS模式(50MHz),别折腾高速模式。为什么?因为Bootloader运行时间短,高速模式带来的收益有限,但引入的稳定性风险不小。

特性 eMMC SD卡
初始化复杂度 较高(需配置EXT_CSD) 较低(CMD0-ACMD41即可)
分区支持 支持(boot分区、RPMB等) 不支持
Bootloader常用模式 HS模式(50MHz) 默认模式(25MHz)
写保护 支持永久/临时写保护 支持(卡侧开关)

避坑指南:我曾经在一个项目里,eMMC初始化总是失败,卡在CMD2获取CID那一步。查了半天,发现是上电后没有等足够长的时间(至少1ms)就发第一条命令。eMMC内部需要时间完成上电复位,别急着发命令,先等一等。

4.3 SPI Flash驱动:简单但别轻视

SPI Flash,结构最简单,驱动也最好写。但简单不代表可以轻视。SPI Flash的驱动核心就是操作那几个命令:读ID、读数据、写使能、页编程、扇区擦除。

写SPI Flash驱动时,我特别强调一点:写操作之前必须先写使能(WREN命令)。这个步骤太容易被忽略了。我见过不止一个新手,直接发页编程命令,结果数据写不进去,还以为是硬件坏了。

另外,现在的SPI Flash容量越来越大,超过16MB的芯片需要用到4字节地址模式。驱动里要检测容量,自动切换地址模式。否则,地址超过16MB后,读写就会出错。

void spi_flash_read(uint32_t addr, uint8_t *buf, uint32_t len)
{
    // 使能片选
    spi_cs_low();
    // 发送读命令(0x03 或 0x0B 快速读)
    spi_tx_byte(0x03);
    // 发送地址(3字节或4字节)
    if (flash_size > 16 * 1024 * 1024) {
        spi_tx_byte((addr >> 24) & 0xFF);  // 4字节地址
    }
    spi_tx_byte((addr >> 16) & 0xFF);
    spi_tx_byte((addr >> 8) & 0xFF);
    spi_tx_byte(addr & 0xFF);
    // 读取数据
    for (uint32_t i = 0; i < len; i++)
        buf[i] = spi_rx_byte();
    // 禁用片选
    spi_cs_high();
}

我的建议:SPI Flash驱动里,读状态寄存器轮询写完成这个操作,一定要加超时。我曾经遇到过SPI Flash在极端温度下写操作变慢,没有超时的话,Bootloader就卡死了。加个500ms的超时,超时了就报错返回,至少系统还能继续跑。

4.4 驱动裁剪策略:少即是多

Bootloader不是Linux内核,不需要支持所有功能。驱动裁剪的核心原则就一句话:只实现启动路径上需要的功能。

我总结了一套裁剪策略,你可以参考:

  1. 砍掉写功能:如果Bootloader只需要读内核镜像,那就只实现读操作。写操作、擦除操作全部去掉。代码量直接减半。
  2. 砍掉错误恢复:Bootloader里遇到读错误,直接报错重启就行,不需要复杂的重试和恢复逻辑。
  3. 砍掉多分区支持:如果内核放在固定分区,那就硬编码分区号,不要写分区表解析代码。
  4. 砍掉高速模式:eMMC的HS200、SPI Flash的QPI模式,Bootloader里用不上,统统砍掉。
  5. 用条件编译隔离:用宏控制哪些驱动被编译进来。比如 #ifdef CONFIG_STORAGE_NAND 只编译NAND驱动,其他驱动代码根本不会链接进来。
裁剪项 裁剪前代码量(估算) 裁剪后代码量(估算) 说明
写/擦除功能 ~500行 0行 只读启动,不需要写
错误恢复逻辑 ~200行 ~20行 只保留报错重启
多分区支持 ~300行 ~30行 硬编码分区号
高速模式 ~400行 0行 只用默认模式

注意:裁剪不是乱砍。砍之前,先搞清楚你的启动路径到底需要什么。比如,如果你的Bootloader支持从NAND和eMMC两种介质启动,那两种驱动都得保留,但每种驱动内部可以裁剪到最简。我曾经见过一个项目,为了省空间把NAND的ECC校验砍了,结果产品在高温下频繁启动失败——这就是裁剪过度了。

嗯,存储设备驱动这块,说到底就是理解硬件特性,抓住核心流程,然后大胆裁剪。NAND要管坏块和ECC,eMMC要处理好初始化序列,SPI Flash注意地址模式和写使能。把这些搞定了,Bootloader的存储驱动部分就稳了。