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控制器 |
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
4.2.2 初始化流程
SPI模式的初始化,说白了就是发一串命令让卡「认主」。我习惯把流程画成下面这样:
你想想看,为什么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;
}
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的轮询方式省了多少事。
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传错了数据。