## 引言 在嵌入式产品中,数据存储需求日益增长,如日志记录、参数保存、音频文件存储等。SD 卡凭借大容量、低成本、可插拔等优势成为主流选择。然而,直接操作 SD 卡扇区繁琐且易出错,引入 FatFS 文件系统可让开发者像操作 PC 文件一样管理数据。本文将带你从零开始,在 STM32 上移植 FatFS,实现高效的 SD 卡读写。 ## FatFS 与 STM32 存储架构 FatFS 是一个开源的 FAT/exFAT 文件系统模块,专为小型嵌入式系统设计。它独立于底层硬件,通过统一的接口(disk_initialize、disk_read、disk_write 等)与存储介质通信。在 STM32 上,SD 卡可通过两种接口连接: - **SDIO 接口**:4 位并行传输,速度更快,适合高性能应用。 - **SPI 接口**:引脚占用少,速度较慢,但兼容性广。 本文以 SDIO 模式为例(STM32F407 自带 SDIO 外设),但移植思路同样适用于 SPI 模式。 ## 移植步骤 ### 1. 获取 FatFS 源码 从 [FatFS 官网](http://elm-chan.org/fsw/ff/) 下载最新版本(如 R0.15)。解压后,核心文件包括: - `ff.h` / `ff.c`:文件系统实现 - `diskio.h` / `diskio.c`:底层接口(需修改) - `ffconf.h`:配置文件 将源码添加到工程,并确保头文件路径正确。 ### 2. 配置 ffconf.h 根据需求修改关键宏: ```c #define FF_USE_LFN 1 // 支持长文件名 #define FF_FS_MINIMIZE 0 // 完整功能 #define FF_USE_STRFUNC 1 // 支持 f_printf #define FF_USE_MKFS 1 // 支持格式化 #define FF_USE_FASTSEEK 1 // 快速定位 #define FF_VOLUMES 1 // 卷数量 ``` 若使用 SDIO,还需定义 `FF_USE_LFN` 的缓冲区大小,建议设为 255。 ### 3. 实现底层磁盘 I/O 接口 在 `diskio.c` 中,需要实现 5 个函数:`disk_initialize`、`disk_status`、`disk_read`、`disk_write`、`disk_ioctl`。以下为基于 STM32 HAL 库的示例(简化版): ```c #include "ff.h" #include "diskio.h" #include "sdio.h" // 全局 SD 卡句柄(由 HAL 库管理) extern SD_HandleTypeDef hsd; // 初始化磁盘 DSTATUS disk_initialize(BYTE pdrv) { if (pdrv != 0) return STA_NOINIT; if (HAL_SD_Init(&hsd) != HAL_OK) return STA_NOINIT; // 配置 SDIO 时钟等(略) return 0; } // 读取扇区 DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv != 0) return RES_PARERR; if (HAL_SD_ReadBlocks(&hsd, buff, sector, count, HAL_MAX_DELAY) != HAL_OK) return RES_ERROR; // 等待操作完成 while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); return RES_OK; } // 写入扇区 DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (pdrv != 0) return RES_PARERR; if (HAL_SD_WriteBlocks(&hsd, (BYTE*)buff, sector, count, HAL_MAX_DELAY) != HAL_OK) return RES_ERROR; while (HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER); return RES_OK; } // 获取磁盘状态 DSTATUS disk_status(BYTE pdrv) { if (pdrv != 0) return STA_NOINIT; return 0; // 简单返回就绪 } // 控制命令(如获取扇区大小) DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { if (pdrv != 0) return RES_PARERR; switch (cmd) { case GET_SECTOR_SIZE: *(WORD*)buff = 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD*)buff = 1; return RES_OK; case GET_SECTOR_COUNT: *(DWORD*)buff = hsd.SdCard.BlockNbr; return RES_OK; default: return RES_PARERR; } } ``` 注意:`LBA_t` 类型在 FatFS R0.15 中定义为 64 位,需确保 HAL 库函数参数匹配。 ### 4. 挂载文件系统 在应用代码中,使用 `f_mount` 挂载 SD 卡。若卡未格式化,可调用 `f_mkfs` 创建 FAT 文件系统。 ```c FATFS fs; FRESULT res; // 挂载 res = f_mount(&fs, "", 1); if (res == FR_NO_FILESYSTEM) { // 格式化 SD 卡(需先卸载) f_mount(NULL, "", 0); res = f_mkfs("", FM_FAT32, 0, work, sizeof(work)); if (res != FR_OK) error_handler(); f_mount(&fs, "", 1); } ``` ### 5. 文件读写示例 以下代码演示创建文件、写入数据、读取数据并删除文件: ```c FIL file; UINT bytes_written, bytes_read; char write_buf[] = "Hello STM32 FatFS!\r\n"; char read_buf[64]; // 写入文件 if (f_open(&file, "test.txt", FA_CREATE_ALWAYS | FA_WRITE) == FR_OK) { f_write(&file, write_buf, sizeof(write_buf)-1, &bytes_written); f_close(&file); } // 读取文件 if (f_open(&file, "test.txt", FA_READ) == FR_OK) { f_read(&file, read_buf, sizeof(read_buf)-1, &bytes_read); read_buf[bytes_read] = '\0'; f_close(&file); printf("Read: %s", read_buf); } // 删除文件 f_unlink("test.txt"); ``` ## 注意事项 - **DMA 与中断**:使用 SDIO 时,建议启用 DMA 传输,并在中断回调中处理完成标志,避免阻塞 CPU。 - **电源稳定**:SD 卡对供电敏感,需在 VCC 引脚并联 10μF 和 0.1μF 电容,并确保 3.3V 稳压输出。 - **长文件名支持**:启用 `FF_USE_LFN` 后,需提供工作缓冲区(如 `FF_LFN_BUF`),否则会占用较多栈空间。 - **多卷管理**:若同时使用多个存储设备(如 SPI Flash),需在 `diskio.c` 中根据 `pdrv` 区分设备。 - **错误处理**:每次文件操作后检查返回值,避免因卡未插入或损坏导致死循环。 ## 总结 通过以上步骤,你已成功在 STM32 上移植 FatFS,实现了 SD 卡的文件读写。实际项目中,可根据需求扩展功能,如目录遍历、文件追加、日志轮转等。FatFS 的移植核心在于底层接口的适配,一旦完成,上层应用开发将变得高效便捷。希望本文能为你后续的存储应用开发提供有力支持。