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驱动核心流程

驱动移植时,你需要关注这几个关键点:

  1. 硬件接口:通常是8位或16位并行总线,也有SPI接口的NAND
  2. 命令集:读ID、读页、写页、块擦除、状态读取
  3. 时序参数:tR(读时间)、tPROG(编程时间)、tBERS(擦除时间)
  4. 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协议跟它通信就行。

驱动适配的核心是初始化序列和读写命令。初始化序列包括:

  1. 发送CMD0(GO_IDLE_STATE)复位卡
  2. 发送CMD8(SEND_IF_COND)检测电压范围
  3. 发送ACMD41(SD_SEND_OP_COND)初始化
  4. 发送CMD2(ALL_SEND_CID)获取CID
  5. 发送CMD3(SEND_RELATIVE_ADDR)获取RCA
  6. 发送CMD9(SEND_CSD)获取CSD寄存器
  7. 设置总线宽度和时钟频率

关键参数:

  • 初始化时钟: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 存储驱动知识体系总览

下面这张图把本章的知识结构串起来了,你可以对照着看:

存储驱动知识体系 原始Flash 块设备 SPI/QSPI Flash NOR Flash NAND Flash eMMC SD卡 SPI Flash QSPI Flash NOR操作 读/写/擦除 XIP支持 NAND操作 坏块管理 ECC校验 eMMC/SD操作 初始化序列 块读写 SPI/QSPI操作 命令集适配 四线模式 移植核心要点 时序参数配置 | 命令集适配 | 硬件抽象层设计 | 错误处理与重试机制 避坑指南 1. 上电时序:eMMC的VCC和VCCQ必须按顺序上电 2. 坏块扫描:NAND启动时必须扫描并记录坏块表 3. QE位:QSPI Flash的四线模式需要先设置状态寄存器

4.6 移植实战建议

最后,我总结几条实战经验:

  • 先读ID,再干活:任何Flash驱动,第一步都是读设备ID,确认型号和参数。这能避免很多低级错误。
  • 时序参数要留余量:数据手册上的时序是最小值/典型值,实际移植时建议加20%的余量。温度变化、电压波动都会影响时序。
  • 做好错误重试:NAND Flash的位翻转是常态,读写操作失败后重试2-3次,成功率会高很多。
  • 抽象层一定要做:把存储操作封装成标准接口,换硬件时只需要改底层驱动,上层Bootloader代码不用动。

我的习惯:每移植一种新的存储芯片,我都会写一个自测函数——先读ID,再写一个pattern,读回来对比,最后擦除验证。这个函数在调试阶段能帮你快速定位问题。我曾经靠这个函数,半小时就发现了一颗Flash的地址线虚焊问题。