4. SD卡驱动实战:SPI模式与SDIO模式下的LVGL集成

SD卡,说白了就是嵌入式设备的「外置硬盘」。LVGL要加载高清图片、字体文件,光靠内部Flash根本不够用。我做过一个项目,UI里放了十几张800×480的背景图,每张都超过1MB——不用SD卡,连编译都过不了。

这一章,咱们就聊聊SD卡的两种主流驱动方式:SPI和SDIO。我会结合LVGL的集成要点,把坑都给你指出来。

4.1 SPI模式 vs SDIO模式:选哪个?

很多初学者会纠结这个问题。我直接说结论:没有绝对的好坏,只有合不合适的场景。

对比项 SPI模式 SDIO模式
引脚数量 4线(CS、SCK、MOSI、MISO) 6线(CMD、CLK、DAT0~DAT3)
最大速度 典型20~40MHz 典型50~100MHz(4位模式)
驱动复杂度 低,几乎所有MCU都支持 高,需要SD协议栈
LVGL读图性能 够用(小图/低帧率场景) 优秀(大图/高帧率场景)
硬件资源占用 少,一个SPI外设即可 多,需要专用SDIO控制器
我的建议:如果你的MCU没有SDIO外设,或者项目对图片加载速度要求不高(比如静态菜单、设置界面),用SPI模式完全够。但如果你要做视频播放、地图缩放、相册滑动这类操作,老老实实上SDIO。

4.2 SPI模式驱动:从底层到LVGL

SPI驱动SD卡,核心就三步:初始化、读写扇区、对接LVGL的文件系统接口。

4.2.1 硬件连接

嗯,这里要注意:SD卡的SPI模式有严格的初始化时序。我见过有人直接把SD卡插到SPI总线上,结果死活读不到数据——因为SD卡上电后默认是SD模式,需要先发一个特殊的复位命令才能切换到SPI模式。

标准接线如下:

MCU SPI          SD卡
CS    ---------> CS (片选)
SCK   ---------> CLK (时钟)
MOSI  ---------> DI (数据输入)
MISO  <--------- DO (数据输出)
3.3V  ---------> VCC
GND   ---------> GND
我曾经踩过的坑:SD卡必须用3.3V供电!5V会直接烧卡。而且SPI的片选CS不能一直拉低,每次读写操作完成后必须拉高,否则卡会进入「忙」状态,后续命令全部失效。

4.2.2 初始化流程

SPI模式的初始化,说白了就是发一串命令让卡「认主」。我习惯把流程画成下面这样:

SPI模式SD卡初始化流程 上电延时(至少74个时钟) 发送CMD0(进入SPI模式) 发送CMD8(检查电压范围) 循环发送CMD55+ACMD41 发送CMD16(设置块大小) 初始化完成,准备读写 超时则失败

你想想看,为什么CMD55+ACMD41要循环发?因为SD卡内部可能还在初始化,需要不断轮询直到返回0x00(空闲状态结束)。我遇到过一些低速卡,要循环几百次才能通过。

4.2.3 读写扇区代码示例

这是最核心的部分。LVGL不直接操作SD卡,它通过文件系统接口间接调用。但底层扇区读写必须自己实现:

// 读单个扇区(512字节)
uint8_t SD_SPI_ReadSector(uint32_t sector, uint8_t *buffer) {
    uint8_t cmd[6];
    uint8_t response;
    
    // 构造CMD17命令
    cmd[0] = 0x51;  // CMD17 = 0x40 | 17
    cmd[1] = (sector >> 24) & 0xFF;
    cmd[2] = (sector >> 16) & 0xFF;
    cmd[3] = (sector >> 8) & 0xFF;
    cmd[4] = sector & 0xFF;
    cmd[5] = 0xFF;  // CRC(SPI模式忽略)
    
    CS_LOW();
    SPI_Transfer(cmd, 6);
    
    // 等待响应
    do {
        response = SPI_Transfer(0xFF);
    } while(response == 0xFF);
    
    if(response != 0x00) {
        CS_HIGH();
        return 1;  // 错误
    }
    
    // 等待数据令牌0xFE
    while(SPI_Transfer(0xFF) != 0xFE);
    
    // 读取512字节数据
    for(uint16_t i = 0; i < 512; i++) {
        buffer[i] = SPI_Transfer(0xFF);
    }
    
    // 跳过2字节CRC
    SPI_Transfer(0xFF);
    SPI_Transfer(0xFF);
    
    CS_HIGH();
    return 0;
}
关键点:SPI模式下,每次读写后必须等待SD卡不再「忙」(即DO引脚为高电平)。我习惯加一个超时机制,比如等待100ms,超时直接返回错误,防止死循环。

4.3 SDIO模式驱动:性能与复杂度并存

SDIO模式比SPI复杂得多,但性能也翻倍。我记得第一次调SDIO驱动时,被CMD和DAT线的时序折腾了两天——后来发现是时钟相位没配好。

4.3.1 硬件差异

SDIO模式使用4位数据总线(DAT0~DAT3),理论上传输速度是SPI的4倍。但代价是:

  • 需要MCU有SDIO外设(STM32F4/F7/H7系列都有)
  • PCB布线要注意等长,尤其是高频下
  • 驱动代码量是SPI的3~5倍

4.3.2 初始化与读写

SDIO模式的初始化流程和SPI类似,但命令格式不同。这里我直接给出STM32 HAL库的典型配置:

// SDIO初始化(以STM32H743为例)
void SD_SDIO_Init(void) {
    hsd.Instance = SDMMC1;
    hsd.Init.ClockEdge = SDMMC_CLOCK_EDGE_RISING;
    hsd.Init.ClockPowerSave = SDMMC_CLOCK_POWER_SAVE_DISABLE;
    hsd.Init.BusWide = SDMMC_BUS_WIDE_4B;  // 4位模式
    hsd.Init.HardwareFlowControl = SDMMC_HARDWARE_FLOW_CONTROL_DISABLE;
    hsd.Init.ClockDiv = 0;  // 最大时钟
    
    HAL_SD_Init(&hsd);
    
    // 配置4位模式
    HAL_SD_ConfigWideBusOperation(&hsd, SDMMC_BUS_WIDE_4B);
}

读写操作直接用HAL库的HAL_SD_ReadBlocks和HAL_SD_WriteBlocks,底层DMA传输,CPU几乎不用管。你想想看,这比SPI的轮询方式省了多少事。

我曾经踩过的坑:SDIO的时钟分频系数不能随便设。有些MCU的SDIO外设要求时钟不能超过25MHz(标准卡)或50MHz(高速卡)。我试过把时钟设到60MHz,结果卡直接不响应——不是卡坏了,是时序不满足。

4.4 对接LVGL的文件系统接口

不管用SPI还是SDIO,最终都要让LVGL能通过文件路径读取图片。LVGL提供了lv_fs_drv_t结构体,我们只需要注册一个驱动:

// 注册SD卡文件系统驱动
void lv_sd_fs_init(void) {
    lv_fs_drv_t drv;
    lv_fs_drv_init(&drv);
    
    drv.letter = 'S';  // 盘符,例如 "S:/image.bmp"
    drv.open_cb = sd_fs_open;
    drv.close_cb = sd_fs_close;
    drv.read_cb = sd_fs_read;
    drv.write_cb = sd_fs_write;
    drv.seek_cb = sd_fs_seek;
    drv.tell_cb = sd_fs_tell;
    
    lv_fs_drv_register(&drv);
}

// 回调函数示例:打开文件
static void *sd_fs_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode) {
    FIL file;
    BYTE flags;
    
    if(mode == LV_FS_MODE_RD) flags = FA_READ;
    else if(mode == LV_FS_MODE_WR) flags = FA_WRITE;
    else flags = FA_READ | FA_WRITE;
    
    if(f_open(&file, path, flags) == FR_OK) {
        // 返回文件句柄
        return (void *)file;
    }
    return NULL;
}

注册完成后,LVGL就能用"S:/images/logo.png"这样的路径加载图片了。我个人习惯把图片放在SD卡的/images目录下,方便管理。

4.5 性能对比实测数据

下面是我在STM32H743平台上实测的数据,SD卡为Class 10,图片为320×240的JPEG:

模式 时钟频率 单张加载时间 连续加载(10张) CPU占用率
SPI 20MHz 约120ms 约1.3s 85%(轮询)
SDIO 1位 25MHz 约60ms 约650ms 30%(DMA)
SDIO 4位 50MHz 约25ms 约280ms 15%(DMA)

数据很直观:SDIO 4位模式比SPI快了近5倍,而且CPU占用率低得多。如果你的UI需要流畅滑动或动画,SDIO几乎是必选项。

4.6 避坑指南

最后,我把这些年踩过的坑总结一下:

  • 上电时序:SD卡上电后必须等待至少74个时钟周期才能发命令,否则卡不响应。我见过有人直接复制代码,结果卡死在初始化。
  • 热插拔:SPI模式不支持热插拔,SDIO模式部分MCU支持(需要检测CD引脚)。如果项目需要热插拔,务必加检测电路和去抖处理。
  • 文件系统选择:LVGL推荐搭配FatFS使用。注意FatFS的配置项FF_USE_FASTSEEK要开启,否则大文件读取会慢得离谱。
  • 缓存对齐:SDIO的DMA传输要求缓冲区地址4字节对齐。我遇到过图片加载花屏,最后发现是缓冲区没对齐,DMA传错了数据。
一个小技巧:调试SD卡驱动时,先用逻辑分析仪抓SPI/SDIO的波形。我习惯先看CMD0的响应是不是0x01,再看CMD8的响应是不是0x01+0xAA——这两个对了,后面基本没问题。