4. 常见Bootloader对比
存储驱动这块,说实在的,是Bootloader移植里最绕不开的硬骨头。你想想看,Bootloader的第一使命是什么?就是把内核从存储介质里读出来,放到内存里跑。如果存储驱动没写好,后面全是白搭。
我个人习惯把存储驱动分成三大类来搞:原始Flash(NOR/NAND)、块设备(eMMC/SD卡)、SPI接口Flash。这三类的驱动模型完全不同,但底层逻辑是相通的。今天咱们就一个一个掰开揉碎了讲。
4.1 NOR Flash驱动移植:简单但别大意
NOR Flash的特点是XIP(片上执行),说白了就是代码可以直接在Flash里跑,不用搬到RAM里。这在早期Bootloader里很常见。
驱动移植的核心就三个操作:读、写、擦除。读最简单,直接地址映射就行。写和擦除需要发命令序列。
NOR Flash操作要点:
- 读操作:直接内存访问,注意总线宽度(8/16/32位)
- 写操作:先擦后写,发解锁命令(通常是0xAA+0x55序列)
- 擦除操作:扇区擦除(4KB/64KB)或整片擦除
- 状态检测:轮询DQ7(数据翻转位)或DQ6(超时标志)
我在项目中遇到过一个问题:某款国产NOR Flash的擦除命令时序和标准Jedec规范差了那么几个时钟周期。结果Bootloader死活擦不干净,每次写进去的数据都是乱的。后来用逻辑分析仪抓了波形才发现——嗯,驱动里的延时参数得按数据手册重新调。
/* NOR Flash扇区擦除示例 */
int nor_flash_erase(uint32_t addr)
{
/* 发送解锁命令 */
*(volatile uint16_t *)(0x555 << 1) = 0xAA;
*(volatile uint16_t *)(0x2AA << 1) = 0x55;
*(volatile uint16_t *)(0x555 << 1) = 0x80;
*(volatile uint16_t *)(0x555 << 1) = 0xAA;
*(volatile uint16_t *)(0x2AA << 1) = 0x55;
*(volatile uint16_t *)(addr) = 0x30; /* 扇区擦除命令 */
/* 等待擦除完成 */
while (*(volatile uint16_t *)(addr) != 0xFFFF);
return 0;
}
4.2 NAND Flash驱动移植:坏块管理与ECC校验
NAND Flash就复杂多了。它不像NOR那样可以直接寻址,而是以页(Page)为单位读写,以块(Block)为单位擦除。而且NAND天生就有坏块,这是工艺决定的,不是质量问题。
重要提醒:NAND Flash出厂时就可能有坏块,使用过程中还会产生新的坏块。Bootloader必须实现坏块管理机制,否则系统随时可能启动失败。
4.2.1 NAND驱动核心流程
驱动移植时,你需要关注这几个关键点:
- 硬件接口:通常是8位或16位并行总线,也有SPI接口的NAND
- 命令集:读ID、读页、写页、块擦除、状态读取
- 时序参数:tR(读时间)、tPROG(编程时间)、tBERS(擦除时间)
- ECC引擎:硬件ECC还是软件ECC?每页校验多少字节?
我曾经在一个项目里踩过坑:某款NAND Flash的页大小是2KB+64B OOB,但Bootloader默认配置的是512B+16B。结果读出来的数据全是错的,查了两天才发现是页大小配置不对。所以移植时一定要仔细看数据手册的页结构。
/* NAND Flash页读取流程 */
int nand_read_page(uint32_t page_addr, uint8_t *buf, int page_size)
{
/* 1. 发送读命令 */
nand_cmd(NAND_CMD_READ0);
/* 2. 发送列地址(低2字节) */
nand_addr(0x00);
nand_addr(0x00);
/* 3. 发送行地址(页地址) */
nand_addr(page_addr & 0xFF);
nand_addr((page_addr >> 8) & 0xFF);
nand_addr((page_addr >> 16) & 0xFF);
/* 4. 发送读确认命令 */
nand_cmd(NAND_CMD_READSTART);
/* 5. 等待RB引脚变高 */
while (!nand_rb_status());
/* 6. 读取数据 */
for (int i = 0; i < page_size; i++)
buf[i] = nand_read_byte();
return 0;
}
4.2.2 坏块管理策略
坏块管理说白了就两件事:标记坏块和跳过坏块。标准做法是在每个块的第一个页的OOB区域(偏移0或偏移5)写入非0xFF的值来标记坏块。
| 策略 | 描述 | 适用场景 |
|---|---|---|
| 跳过坏块 | 遇到坏块直接跳过,不分配逻辑块号 | 简单Bootloader,分区固定 |
| 坏块替换 | 用保留块替换坏块,维护映射表 | 需要高可靠性的系统 |
| BCH/RS纠错 | 通过ECC纠正位翻转,容忍部分坏块 | MLC/TLC NAND,位错误率高 |
我的经验:Bootloader阶段不需要实现完整的FTL(闪存转换层),但至少要做到:启动时扫描坏块表,写入时检查目标块是否坏块,遇到坏块就跳到下一个好块。我曾经见过一个产品,Bootloader没做坏块检查,结果出厂时有个坏块正好在U-Boot分区,整批产品都起不来——那叫一个惨。
4.2.3 ECC校验实现
ECC校验是NAND Flash的保命符。常用的算法有汉明码(1位纠错)和BCH码(多位纠错)。
硬件ECC的话,你只需要配置好控制器,读写时自动计算校验值。软件ECC就得自己实现了。我建议Bootloader阶段用硬件ECC,省心省力。
/* 软件汉明码ECC计算(简化版) */
uint32_t ecc_hamming_calc(uint8_t *data, int len)
{
uint32_t ecc = 0;
for (int i = 0; i < len; i++) {
uint8_t byte = data[i];
/* 对每个bit位置计算奇偶校验 */
for (int bit = 0; bit < 8; bit++) {
if (byte & (1 << bit)) {
ecc ^= (i << 3) | bit;
}
}
}
return ecc;
}
4.3 eMMC/SD卡驱动适配
eMMC和SD卡本质上是块设备,内部有控制器管理坏块和磨损均衡。Bootloader这边只需要通过SD/MMC协议跟它通信就行。
驱动适配的核心是初始化序列和读写命令。初始化序列包括:
- 发送CMD0(GO_IDLE_STATE)复位卡
- 发送CMD8(SEND_IF_COND)检测电压范围
- 发送ACMD41(SD_SEND_OP_COND)初始化
- 发送CMD2(ALL_SEND_CID)获取CID
- 发送CMD3(SEND_RELATIVE_ADDR)获取RCA
- 发送CMD9(SEND_CSD)获取CSD寄存器
- 设置总线宽度和时钟频率
关键参数:
- 初始化时钟:100-400KHz(所有卡都支持)
- 工作时钟:0-50MHz(取决于卡和控制器)
- 总线宽度:1位/4位/8位(eMMC支持8位)
- 数据传输模式:单块/多块/流式
我记得有一次移植eMMC驱动,卡初始化总是失败。用示波器看CMD线,发现命令发送后卡没响应。查了半天,原来是上电时序问题——eMMC的VCC和VCCQ必须按顺序上电,而且间隔不能小于5ms。改完电源管理代码,问题就解决了。
/* eMMC初始化核心代码 */
int mmc_init(struct mmc *mmc)
{
/* 1. 复位卡 */
mmc_send_cmd(mmc, CMD0, 0);
/* 2. 检测SDIO/MMC/SD卡类型 */
if (mmc_send_cmd(mmc, CMD8, 0x1AA) == 0) {
/* SDHC/SDXC卡 */
mmc->type = MMC_TYPE_SD;
} else {
/* MMC卡或SDSC卡 */
mmc->type = MMC_TYPE_MMC;
}
/* 3. 发送操作条件 */
uint32_t ocr = 0;
do {
if (mmc->type == MMC_TYPE_SD)
mmc_send_cmd(mmc, ACMD41, 0x40FF8000);
else
mmc_send_cmd(mmc, CMD1, 0x40FF8000);
mmc_send_cmd(mmc, CMD55, 0);
mmc_send_cmd(mmc, CMD41, &ocr);
} while (!(ocr & (1 << 31)));
/* 4. 获取CID和CSD */
mmc_send_cmd(mmc, CMD2, 0);
mmc_send_cmd(mmc, CMD3, 0);
mmc_send_cmd(mmc, CMD9, mmc->rca);
/* 5. 设置总线宽度和时钟 */
mmc_set_bus_width(mmc, 4);
mmc_set_clock(mmc, 50000000);
return 0;
}
4.4 SPI Flash与QSPI Flash驱动
SPI Flash在嵌入式系统里用得越来越多,尤其是QSPI(四线SPI),速度快、引脚少,非常适合Bootloader存储。
驱动移植的核心是SPI控制器初始化和Flash命令集。常见的命令包括:
| 命令 | 代码 | 功能 |
|---|---|---|
| READ | 0x03 | 单线读取 |
| FAST_READ | 0x0B | 快速读取(带dummy周期) |
| QUAD_READ | 0x6B | 四线读取(QSPI模式) |
| PAGE_PROGRAM | 0x02 | 页编程(通常256字节) |
| SECTOR_ERASE | 0x20 | 扇区擦除(4KB) |
| CHIP_ERASE | 0xC7 | 整片擦除 |
| RDID | 0x9F | 读ID |
注意:QSPI Flash的驱动跟普通SPI Flash不一样。QSPI模式下,数据线是双向的,需要配置控制器的IO模式。而且不同厂家的QSPI Flash,进入四线模式的命令可能不同——有的发0x38,有的发0x35。移植时一定要看数据手册的QE位设置。
我建议在Bootloader里实现一个SPI Flash抽象层,把读、写、擦除、读ID这些操作封装成标准接口。这样换Flash型号时,只需要改底层的命令表和时序参数,上层代码不用动。
/* SPI Flash抽象层接口 */
struct spi_flash_ops {
int (*read)(uint32_t addr, void *buf, size_t len);
int (*write)(uint32_t addr, const void *buf, size_t len);
int (*erase)(uint32_t addr, size_t len);
int (*read_id)(uint8_t *id);
};
/* QSPI Flash四线读取示例 */
int qspi_flash_read(uint32_t addr, void *buf, size_t len)
{
uint8_t cmd[5];
cmd[0] = 0x6B; /* QUAD_READ命令 */
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
cmd[4] = 0x00; /* dummy周期 */
/* 切换到四线模式 */
qspi_set_mode(QSPI_MODE_QUAD);
/* 发送命令和地址 */
qspi_send(cmd, 5);
/* 读取数据 */
qspi_read(buf, len);
/* 切回单线模式 */
qspi_set_mode(QSPI_MODE_SINGLE);
return 0;
}
4.5 存储驱动知识体系总览
下面这张图把本章的知识结构串起来了,你可以对照着看:
4.6 移植实战建议
最后,我总结几条实战经验:
- 先读ID,再干活:任何Flash驱动,第一步都是读设备ID,确认型号和参数。这能避免很多低级错误。
- 时序参数要留余量:数据手册上的时序是最小值/典型值,实际移植时建议加20%的余量。温度变化、电压波动都会影响时序。
- 做好错误重试:NAND Flash的位翻转是常态,读写操作失败后重试2-3次,成功率会高很多。
- 抽象层一定要做:把存储操作封装成标准接口,换硬件时只需要改底层驱动,上层Bootloader代码不用动。
我的习惯:每移植一种新的存储芯片,我都会写一个自测函数——先读ID,再写一个pattern,读回来对比,最后擦除验证。这个函数在调试阶段能帮你快速定位问题。我曾经靠这个函数,半小时就发现了一颗Flash的地址线虚焊问题。