3、文件系统驱动实现:基于FatFS的SD卡驱动编写实战

好,咱们进入实战环节。前面聊了那么多理论,说白了都是为了这一刻——让LVGL能从SD卡里把图片读出来。我个人习惯把这一步拆成两个阶段:先搞定SD卡的底层读写,再挂上FatFS文件系统。今天咱们就手把手走一遍。

3.1 硬件接口与初始化流程

先看看硬件连接。大部分MCU跟SD卡通信用的是SPI或者SDIO。我建议初学者先用SPI,原因很简单——引脚少,调试方便。我在项目里踩过SDIO的坑,布线稍微长一点就跑不稳,换成SPI立马消停。

接口类型 引脚数 速度 调试难度 推荐场景
SPI 4(CS、SCK、MOSI、MISO) 中 低 初学者、引脚紧张的项目
SDIO 6(CMD、CLK、DAT0-3) 高 高 高速读写、大文件传输

初始化流程其实不复杂,但顺序错了就容易卡住。我一般按这个步骤来:

  1. 配置GPIO——把SPI引脚设为复用功能,注意上拉电阻
  2. 初始化SPI外设——时钟、极性、相位,SD卡通常用模式0或模式3
  3. 拉低CS片选——告诉SD卡:我要跟你说话了
  4. 发送CMD0——让SD卡进入SPI模式
  5. 发送CMD8——检查卡是否支持SDHC
  6. 循环发送CMD55+ACMD41——直到卡返回就绪状态
  7. 读取CSD寄存器——获取卡容量等信息
小技巧:初始化时如果卡没反应,先检查SPI时钟频率。我建议初始阶段把时钟降到400kHz以下,等初始化完成后再切到高速。很多卡对高频初始化很敏感。

3.2 FatFS底层接口实现

FatFS是一个轻量级的文件系统库,它不直接操作硬件,而是通过一组底层接口函数跟你的SD卡驱动对接。说白了,你只需要实现6个函数:

  • disk_initialize——初始化磁盘
  • disk_status——获取磁盘状态
  • disk_read——读取扇区
  • disk_write——写入扇区
  • disk_ioctl——控制命令(比如获取扇区数)
  • get_fattime——获取当前时间(可选)

嗯,这里要注意。FatFS的读写单位是扇区,通常是512字节。你不能只读一个字节,必须整扇区操作。我刚开始做的时候没注意这个,结果读出来的数据全是乱的。

下面是一个典型的disk_read实现:

DRESULT disk_read (
    BYTE pdrv,      // 物理驱动器号
    BYTE *buff,     // 数据缓冲区
    LBA_t sector,   // 起始扇区号
    UINT count      // 扇区数量
)
{
    if (pdrv != 0) return RES_PARERR;

    // 选中SD卡
    SD_CS_LOW();

    // 发送CMD17(单扇区读取)或CMD18(多扇区读取)
    if (count == 1) {
        if (SD_SendCmd(CMD17, sector) != 0) {
            SD_CS_HIGH();
            return RES_ERROR;
        }
    } else {
        if (SD_SendCmd(CMD18, sector) != 0) {
            SD_CS_HIGH();
            return RES_ERROR;
        }
    }

    // 读取数据块
    for (UINT i = 0; i < count; i++) {
        if (SD_ReadBlock(buff + (i * 512), 512) != 0) {
            SD_CS_HIGH();
            return RES_ERROR;
        }
    }

    // 多扇区读取需要发送停止命令
    if (count > 1) {
        SD_SendCmd(CMD12, 0);
    }

    // 取消选中
    SD_CS_HIGH();

    return RES_OK;
}
避坑指南:我曾经在disk_write里忘记处理写保护状态,结果往一张锁定的SD卡里写数据,卡没坏,但数据全丢了。后来我养成了一个习惯——每次写操作前先调用disk_status检查写保护引脚。

3.3 文件系统挂载与测试

底层接口写好了,挂载FatFS就简单了。代码量很少,但这一步最容易出问题。

FATFS fs;          // 文件系统对象
FIL fil;           // 文件对象
FRESULT res;       // 返回值

// 挂载文件系统
res = f_mount(&fs, "0:", 1);
if (res != FR_OK) {
    printf("挂载失败: %d\n", res);
    return -1;
}

// 打开文件
res = f_open(&fil, "0:/image.bin", FA_READ);
if (res != FR_OK) {
    printf("打开文件失败: %d\n", res);
    return -1;
}

// 读取文件内容
UINT bytes_read;
BYTE buffer[1024];
res = f_read(&fil, buffer, sizeof(buffer), &bytes_read);
if (res == FR_OK) {
    printf("成功读取 %d 字节\n", bytes_read);
}

// 关闭文件
f_close(&fil);

你想想看,如果挂载失败,最常见的原因是什么?我列个排查清单:

错误码 含义 常见原因
FR_DISK_ERR 底层磁盘错误 SPI通信不稳定、时钟太快
FR_NOT_READY 磁盘未就绪 SD卡没插好、初始化失败
FR_NO_FILESYSTEM 没有有效文件系统 SD卡未格式化、格式不对
FR_INVALID_DRIVE 无效驱动器号 挂载时路径写错了
核心要点:挂载失败时,别急着怀疑FatFS。先确认disk_read能不能正确读出扇区0的数据。如果能读出MBR或DBR的签名(0x55 0xAA),那问题就不在底层。

3.4 知识结构总览

为了让你对整个流程有个全局认识,我画了一张图。这张图把SD卡驱动、FatFS和LVGL的关系理清楚了。

SD卡驱动与FatFS集成架构 LVGL 图片加载请求 FatFS 文件系统 FatFS 底层接口函数 disk_initialize | disk_read | disk_write | disk_ioctl | disk_status SD卡 SPI 驱动 SPI初始化 | CMD发送 | 数据块读写 | CRC校验 SD卡硬件 调试要点 1. 先测裸扇区读写 2. 确认0x55 0xAA签名 3. 检查SPI时钟极性 4. 验证CRC开关状态 5. 用逻辑分析仪抓波形

3.5 常见问题与调试方法

写驱动的时候,十有八九会遇到问题。我把自己踩过的坑整理了一下,你遇到类似情况可以直接对照排查。

问题一:初始化卡死在CMD0
原因:SPI时钟太快,或者CS引脚没拉低。
我曾经调了一整天,最后发现是GPIO初始化时把CS设成了推挽输出,但忘了拉低。嗯,这种低级错误最容易让人抓狂。
问题二:读扇区返回全0xFF
原因:SD卡没进入SPI模式,或者CMD8发送失败。
解决办法:在CMD0之后加一段延时,等卡准备好再发CMD8。我习惯加10ms的延时。
问题三:FatFS挂载返回FR_NO_FILESYSTEM
原因:SD卡格式不对,或者扇区0被破坏了。
排查方法:用WinHex或类似工具读扇区0,看看有没有0x55 0xAA。如果没有,重新格式化SD卡,选FAT32。

最后说一句,调试SD卡驱动最有效的工具是逻辑分析仪。你想想看,SPI有四根线,用逻辑分析仪一抓,哪根线没波形、哪个时序不对,一目了然。我每次调新板子,第一件事就是把逻辑分析仪接上,省得瞎猜。