4. 存储设备驱动:NAND Flash、eMMC/SD卡、SPI Flash与驱动裁剪策略
存储设备驱动,是Bootloader里最绕不开的一块硬骨头。你想想看,Bootloader的核心任务是什么?把内核镜像从存储介质里读出来,放到内存里,然后跳过去执行。如果驱动写不好,连数据都读不对,后面全是白搭。
我这些年接触过的嵌入式项目,存储方案基本就三大家:NAND Flash、eMMC/SD卡、SPI Flash。每种都有自己的脾气,驱动写法也完全不同。今天咱们就把它们挨个捋一遍,最后再聊聊怎么裁剪——毕竟Bootloader里不需要Linux内核那么完整的驱动栈。
核心观点:Bootloader里的存储驱动,不求功能全面,只求“刚好够用、稳定可靠”。能用简单轮询就别上中断,能用单块读写就别搞DMA。少一行代码,就少一个出bug的机会。
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驱动拆成三层:
- 底层硬件层:操作SD/MMC控制器寄存器,发送命令、接收响应、读写数据。
- 协议层:实现CMD0(复位)、CMD2(获取CID)、CMD3(设置RCA)、CMD7(选择卡)、CMD16(设置块大小)、CMD18(多块读)等关键命令。
- 块设备层:提供简单的读块/写块接口,屏蔽底层细节。
这里有个坑: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内核,不需要支持所有功能。驱动裁剪的核心原则就一句话:只实现启动路径上需要的功能。
我总结了一套裁剪策略,你可以参考:
- 砍掉写功能:如果Bootloader只需要读内核镜像,那就只实现读操作。写操作、擦除操作全部去掉。代码量直接减半。
- 砍掉错误恢复:Bootloader里遇到读错误,直接报错重启就行,不需要复杂的重试和恢复逻辑。
- 砍掉多分区支持:如果内核放在固定分区,那就硬编码分区号,不要写分区表解析代码。
- 砍掉高速模式:eMMC的HS200、SPI Flash的QPI模式,Bootloader里用不上,统统砍掉。
- 用条件编译隔离:用宏控制哪些驱动被编译进来。比如
#ifdef CONFIG_STORAGE_NAND只编译NAND驱动,其他驱动代码根本不会链接进来。
| 裁剪项 | 裁剪前代码量(估算) | 裁剪后代码量(估算) | 说明 |
|---|---|---|---|
| 写/擦除功能 | ~500行 | 0行 | 只读启动,不需要写 |
| 错误恢复逻辑 | ~200行 | ~20行 | 只保留报错重启 |
| 多分区支持 | ~300行 | ~30行 | 硬编码分区号 |
| 高速模式 | ~400行 | 0行 | 只用默认模式 |
注意:裁剪不是乱砍。砍之前,先搞清楚你的启动路径到底需要什么。比如,如果你的Bootloader支持从NAND和eMMC两种介质启动,那两种驱动都得保留,但每种驱动内部可以裁剪到最简。我曾经见过一个项目,为了省空间把NAND的ECC校验砍了,结果产品在高温下频繁启动失败——这就是裁剪过度了。
嗯,存储设备驱动这块,说到底就是理解硬件特性,抓住核心流程,然后大胆裁剪。NAND要管坏块和ECC,eMMC要处理好初始化序列,SPI Flash注意地址模式和写使能。把这些搞定了,Bootloader的存储驱动部分就稳了。