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) | 高 | 高 | 高速读写、大文件传输 |
初始化流程其实不复杂,但顺序错了就容易卡住。我一般按这个步骤来:
- 配置GPIO——把SPI引脚设为复用功能,注意上拉电阻
- 初始化SPI外设——时钟、极性、相位,SD卡通常用模式0或模式3
- 拉低CS片选——告诉SD卡:我要跟你说话了
- 发送CMD0——让SD卡进入SPI模式
- 发送CMD8——检查卡是否支持SDHC
- 循环发送CMD55+ACMD41——直到卡返回就绪状态
- 读取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的关系理清楚了。
3.5 常见问题与调试方法
写驱动的时候,十有八九会遇到问题。我把自己踩过的坑整理了一下,你遇到类似情况可以直接对照排查。
问题一:初始化卡死在CMD0
原因:SPI时钟太快,或者CS引脚没拉低。
我曾经调了一整天,最后发现是GPIO初始化时把CS设成了推挽输出,但忘了拉低。嗯,这种低级错误最容易让人抓狂。
原因:SPI时钟太快,或者CS引脚没拉低。
我曾经调了一整天,最后发现是GPIO初始化时把CS设成了推挽输出,但忘了拉低。嗯,这种低级错误最容易让人抓狂。
问题二:读扇区返回全0xFF
原因:SD卡没进入SPI模式,或者CMD8发送失败。
解决办法:在CMD0之后加一段延时,等卡准备好再发CMD8。我习惯加10ms的延时。
原因:SD卡没进入SPI模式,或者CMD8发送失败。
解决办法:在CMD0之后加一段延时,等卡准备好再发CMD8。我习惯加10ms的延时。
问题三:FatFS挂载返回FR_NO_FILESYSTEM
原因:SD卡格式不对,或者扇区0被破坏了。
排查方法:用WinHex或类似工具读扇区0,看看有没有0x55 0xAA。如果没有,重新格式化SD卡,选FAT32。
原因:SD卡格式不对,或者扇区0被破坏了。
排查方法:用WinHex或类似工具读扇区0,看看有没有0x55 0xAA。如果没有,重新格式化SD卡,选FAT32。
最后说一句,调试SD卡驱动最有效的工具是逻辑分析仪。你想想看,SPI有四根线,用逻辑分析仪一抓,哪根线没波形、哪个时序不对,一目了然。我每次调新板子,第一件事就是把逻辑分析仪接上,省得瞎猜。