FATFS文件系统
缘起
因为心电数据的存储要求,想要用STM32来与SD卡数据进行读写,然后接触到了FAT32。来做知识总结
文件系统:从计算机磁盘开始
操作系统操作文件系统,来管理硬盘。
文件系统提供一种统一格式,抽象为目录和文件。
硬盘的工作原理
CPU控制盘面上的读写头来写入和读取扇区上的数据
文件系统管理磁盘
文件系统提供接口:文件和目录
window

- FAT12 /FAT16 /FAT32
- exFAT
- NTFS
linux
- ext2/ext3
- NFS
- jffs2
- yaffs
FAT32的存储原理
- 簇:文件系统存储的最小单元
文件放在簇中,不同大小的存储器,其簇的大小也不同。不同的管理方案都以簇作为最小的存储单元 - 文件属性
- 文件名,大小,占用空间,创建日期,修改日期,读写属性
- 文件表
FATFS文件系统
FatFs 是一个通用的 FAT 文件系统模块,专门为嵌入式系统设计。
它让我们可以在 SD 卡上创建、读取、写入和删除文件,就像在电脑上操作一样。
特点
- 完全用 C 语言编写,与硬件无关
- 支持 FAT12/FAT16/FAT32/exFAT
- 占用资源少(ROM < 30KB, RAM < 4KB)
- 可裁剪(通过 ffconf.h 配置)
FATFS 的三层架构
+------------------------------------------+
| 应用程序 (Application) |
| main.c - SD_Diagnostic_Test() |
+------------------------------------------+
| FatFs 文件系统层 (ff.c) |
| f_mount, f_open, f_read, f_write... |
+------------------------------------------+
| 底层存储接口 (diskio.c) |
| disk_initialize, disk_read, disk_write |
+------------------------------------------+
| 硬件驱动层 (user_diskio.c + SD.c) |
| SD_disk_initialize, SD_disk_read/write |
+------------------------------------------+
| MCU 外设层 (HAL + SPI2) |
| HAL_SPI_TransmitReceive(&hspi2, ...) |
+------------------------------------------+
| SD 卡硬件 |
| PB10 SCK, PC1 MOSI, PC2 MISO, PC3 CS |
+------------------------------------------+
2.1 应用层
你的代码(main.c)调用 FatFs API 来读写文件。
例如:f_open -> f_write -> f_close。
2.2 文件系统层(FatFs 核心)
FatFs 库(Middlewares/Third_Party/FatFs/src/ff.c)实现了:
- FAT 表的读写和管理
- 目录项的创建、查找、删除
- 文件的碎片管理(簇链)
- 长文件名支持(LFN)
- 不需要了解 SD 卡的扇区如何读写,它只调用底层接口
2.3 底层存储接口(diskio.c)
FatFs 定义了 5 个底层函数,你只需要实现它们:
| 函数 | 作用 | 你项目中的实现 |
|---|---|---|
| disk_initialize | 初始化磁盘 | 调用 SD_disk_initialize(0) |
| disk_status | 获取磁盘状态 | 调用 SD_disk_status(0) |
| disk_read | 读取扇区 | 调用 SD_disk_read(...) |
| disk_write | 写入扇区 | 调用 SD_disk_write(...) |
| disk_ioctl | 设备控制 | 调用 SD_disk_ioctl(...) |
2.4 硬件驱动层(SD.c)
项目的 Headware/src/SD.c 实现了上述 5 个 SD_disk_* 函数。
它直接操作 SPI2 外设,发送 SD 卡协议命令(CMD0/CMD8/CMD17/CMD24 等)
来完成扇区的读写。
关键流程:
- SD_disk_initialize() — SPI2 初始化时序 → CMD0 → CMD8 → ACMD41 → CMD58
- SD_disk_read() — CMD17(读单扇区)/ CMD18(读多扇区)
- SD_disk_write() — CMD24(写单扇区)/ CMD25(写多扇区)
FATFS 核心数据结构
3.1 FATFS(文件系统对象)
static FATFS fs; /* 必须 static!内部有 512 字节 win[] 缓冲区 */
每个挂载的驱动器需要一个 FATFS 对象。它保存:
- 文件系统类型(FAT12/16/32/exFAT)
- FAT 表的位置和大小
- 簇大小和总数
- win[FF_MAX_SS] — 512 字节的扇区缓冲区(这是栈溢出的原因)
3.2 FIL(文件对象)
static FIL fil; /* 必须 static!内部有 512 字节 buf[] 缓冲区 */
每个打开的文件需要一个 FIL 对象。它保存:
- 当前读写位置(fptr)
- 文件大小(fsize)
- 起始簇号(sclust)
- 当前簇号(clust)
- buf[FF_MAX_SS] — 512 字节的数据缓冲区
3.3 为什么必须加 static
这两个结构体内部各有 512 字节的数组。如果不加 static:
void func(void) {
FATFS fs; // 512+ 字节在栈上
FIL fil; // 512+ 字节在栈上
char buf[128]; // 128 字节
// 总共 ~1200+ 字节栈空间
}
加上函数调用链中的栈消耗(printf、HAL_SPI、SD_SendCmd 等),
容易超出 STM32H7 默认的 5120 字节栈空间,导致栈溢出。
加 static 后:
void func(void) {
static FATFS fs; // 在 .bss 段,不占栈
static FIL fil; // 在 .bss 段,不占栈
}
FATFS 核心函数详解
4.1 f_mount — 注册/卸载工作区
FRESULT f_mount(FATFS* fs, const TCHAR* path, BYTE opt);
功能:将 FATFS 对象注册到 FatFs 内部,或卸载已注册的驱动器。
参数:
- fs: FATFS 对象指针。NULL = 卸载
- path: 逻辑驱动器号,如 "0:" 或 ""(默认)
- opt: 0=延迟挂载, 1=立即挂载(会调用 disk_initialize)
实现思想:
FatFs 内部维护一个驱动器表,f_mount 把你的 FATFS 对象注册进去。
当 opt=1 时,会立即调用 disk_initialize → SD_disk_initialize 来初始化硬件,
然后读取 SD 卡的 0 号扇区(MBR/DBR)来解析 FAT 文件系统参数。
4.2 f_open — 打开/创建文件
FRESULT f_open(FIL* fp, const TCHAR* path, BYTE mode);
功能:打开一个文件用于读写,或创建新文件。
常用 mode 标志:
- FA_READ: 读权限
- FA_WRITE: 写权限
- FA_CREATE_NEW: 创建新文件(已存在则失败)
- FA_CREATE_ALWAYS: 创建新文件(已存在则覆盖)
- FA_OPEN_ALWAYS: 打开文件(不存在则创建)
- FA_OPEN_APPEND: 追加模式
实现思想:
FatFs 遍历目录项查找文件名。如果找到,初始化 FIL 对象的各个字段;
如果需要创建,在目录中分配一个新的目录项,并分配一个簇。
4.3 f_close — 关闭文件
FRESULT f_close(FIL* fp);
功能:刷新文件缓冲区(将未写入的数据写回 SD 卡),释放资源。
实现思想:
如果文件有写操作,f_close 会把 FIL 内部缓冲区中的脏数据写回 SD 卡,
更新目录项中的文件大小和时间戳。
4.4 f_read — 读文件
FRESULT f_read(FIL* fp, void* buff, UINT btr, UINT* br);
功能:从文件的当前位置读取 btr 字节到 buff。
br 返回实际读取的字节数(小于 btr 表示文件结束)。
实现思想:
- 计算当前簇中的偏移和剩余字节
- 如果 FIL 内部缓冲区有数据(来自上次读),直接拷贝
- 否则调用 disk_read 从 SD 卡读一个扇区到内部缓冲区
- 从缓冲区拷贝到用户 buff
- 更新 fptr(文件指针)
- 如果跨簇边界,通过 FAT 表找到下一个簇
4.5 f_write — 写文件
FRESULT f_write(FIL* fp, const void* buff, UINT btw, UINT* bw);
功能:从 buff 向文件当前位置写入 btw 字节。
bw 返回实际写入的字节数。
实现思想:
- 如果文件还没有分配簇,分配第一个簇
- 数据先写入 FIL 的内部缓冲区(扇区大小对齐)
- 缓冲区满了就调用 disk_write 刷到 SD 卡
- 更新 FAT 表(分配新簇)
- 更新 fptr 和 fsize
4.6 f_gets / f_puts — 文本读写
char* f_gets(char* buff, int len, FIL* fp);
int f_puts(const TCHAR* str, FIL* fp);
f_gets:读一行文本(遇到 '\n' 停止),自动添加 '\0'。
f_puts:写一个字符串到文件。
实现思想:
f_gets 逐个字符调用 f_read,直到遇到换行符或缓冲区满。
f_puts 逐个字符调用 f_write。
效率较低,适合少量文本;大数据用 f_read/f_write。
4.7 f_getfree — 获取空闲空间
FRESULT f_getfree(const TCHAR* path, DWORD* fre_clust, FATFS** fatfs);
功能:获取驱动器上的空闲簇数量和 FATFS 对象指针。
计算容量:
DWORD total_sectors = (pfs->n_fatent - 2) * pfs->csize;
DWORD total_kb = total_sectors / 2; // 每个扇区 512 字节
DWORD free_kb = fre_clust * pfs->csize / 2;
实现思想:
FatFs 读取 FAT 表中空闲簇的数量(在 FAT32 中可能缓存在 FSInfo 扇区)。
n_fatent = FAT 表项总数(= 簇总数 + 2)。csize = 每簇扇区数。
4.8 f_unlink — 删除文件
FRESULT f_unlink(const TCHAR* path);
功能:删除文件或空目录。
实现思想:
- 标记目录项的第一个字节为 0xE5(已删除)
- 将文件占用的所有簇在 FAT 表中标记为 0(空闲)
数据读写流程(以写入为例)
当你调用 f_write(&fil, data, 512, &bw) 时,数据流如下:
用户 data[512]
|
v
[1] FatFs 检查文件是否可写,fptr 是否在文件范围内
|
v
[2] 如果文件还没分配簇,通过 FAT 表分配一个簇
|
v
[3] 数据写入 FIL.buf[] 内部缓冲区
|
v
[4] 缓冲区满(512 字节),调用 disk_write
|
v
[5] disk_write 调用 SD_disk_write
|
v
[6] SD_disk_write 发送 CMD24(写单扇区)
| 发送 512 字节数据 + CRC
| 等待 SD 卡写完成
v
[7] 数据写入 SD 卡 NAND 闪存
读操作类似:f_read → 检查缓存 → disk_read → SD_disk_read → CMD17 → SD卡返回数据
FATFS 配置选项(ffconf.h)
| 宏 | 项目中的值 | 说明 |
|---|---|---|
| FF_MAX_SS | 512 | 扇区大小,决定了 FATFS/FIL 内部缓冲区大小 |
| FF_USE_LFN | 0 | 长文件名支持(0=关闭, 1~3=开启) |
| FF_FS_TINY | 0 | 小模式(1=FATFS 带缓冲区, FIL 不带) |
| FF_FS_READONLY | 0 | 只读模式 |
| FF_USE_STRFUNC | 1 | 允许 f_gets/f_puts |
| FF_VOLUMES | 1 | 支持的驱动器数量 |
如果开启 FF_FS_TINY=1,FIL 就不带 512 字节缓冲区,
而是复用 FATFS.win[] 缓冲区,可以节省栈空间。
实际使用示例
7.1 初始化
#include "SD.h"
#include "fatfs.h"
static FATFS fs;
static FIL fil;
void SD_Init(void) {
printf("SD_disk_initialize...\r\n");
uint8_t res = SD_disk_initialize(0);
if (res != 0) { printf("FAIL\r\n"); return; }
FRESULT fr = f_mount(&fs, "0:", 1);
if (fr != FR_OK) { printf("Mount FAIL: %d\r\n", fr); return; }
printf("SD card ready!\r\n");
}
7.2 写入数据
void SD_WriteData(const uint8_t* data, uint32_t len) {
UINT bw;
FRESULT fr = f_open(&fil, "ECG.DAT", FA_WRITE | FA_OPEN_APPEND);
if (fr != FR_OK) { printf("Open FAIL: %d\r\n", fr); return; }
fr = f_write(&fil, data, len, &bw);
if (fr != FR_OK || bw != len)
printf("Write FAIL: %d (wrote %u/%lu)\r\n", fr, bw, len);
f_close(&fil);
}
7.3 读取数据
void SD_ReadData(void) {
uint8_t buf[512];
UINT br;
FRESULT fr = f_open(&fil, "ECG.DAT", FA_READ);
if (fr != FR_OK) { printf("Open FAIL\r\n"); return; }
while (1) {
fr = f_read(&fil, buf, sizeof(buf), &br);
if (fr != FR_OK || br == 0) break;
// 处理 buf 中的 br 字节数据
}
f_close(&fil);
}
7.4 连续数据记录(ECG 场景)
static FATFS fs;
static FIL fil;
static uint8_t sd_buf[4096]; // 4KB 写入缓冲区
static uint32_t sd_buf_pos = 0;
/* 初始化 SD 卡并创建新文件 */
void SD_StartLogging(void) {
SD_disk_initialize(0);
f_mount(&fs, "0:", 1);
f_open(&fil, "ECG_LOG.DAT", FA_CREATE_ALWAYS | FA_WRITE);
sd_buf_pos = 0;
}
/* 每得到一个 ECG 采样点,调用此函数 */
void SD_LogSample(int16_t ch1, int16_t ch2) {
if (sd_buf_pos + 4 > sizeof(sd_buf)) {
/* 缓冲区满,刷到 SD 卡 */
UINT bw;
f_write(&fil, sd_buf, sd_buf_pos, &bw);
sd_buf_pos = 0;
}
/* 打包两个通道数据(每个 16 位) */
sd_buf[sd_buf_pos++] = ch1 & 0xFF;
sd_buf[sd_buf_pos++] = (ch1 >> 8) & 0xFF;
sd_buf[sd_buf_pos++] = ch2 & 0xFF;
sd_buf[sd_buf_pos++] = (ch2 >> 8) & 0xFF;
}
/* 停止记录 */
void SD_StopLogging(void) {
/* 刷掉最后一点数据 */
if (sd_buf_pos > 0) {
UINT bw;
f_write(&fil, sd_buf, sd_buf_pos, &bw);
}
f_close(&fil);
f_mount(NULL, "", 0);
}
关键优化思想:批量写入。每次 f_write 调用都有开销(解析 FAT 表、
更新目录项等)。攒够 4KB 再一次性写入,能大幅提高吞吐量。
注意事项
-
static 关键字:FATFS 和 FIL 必须在函数外定义或加 static,
避免栈溢出 -
及时关闭文件:f_close 会刷新缓冲区,确保数据完整写入。
意外断电可能导致最后几 KB 数据丢失 -
SD 卡格式:建议格式化为 FAT32,簇大小选默认(4KB)
-
写放大:即使只写 1 字节,SD 卡也需要擦写整个扇区(512 字节)
-
文件大小限制:FAT32 单个文件最大 4GB - 1 字节
-
并发访问:FatFs 不是线程安全的,不要在中断中调用 FatFs 函数
调试技巧
如果 SD 卡读写失败:
- 检查 SD_disk_initialize 返回值(非 0 表示硬件初始化失败)
- 检查 f_mount 返回值(常见错误见 ff.h 中 FR_* 定义)
- 用读测试验证:f_open + f_read,打印读到的内容
- 用容量测试验证:f_getfree 检查能否读取 FAT 表
- 如果卡在某个函数,检查 FATFS/FIL 是否加了 static
参考
- FatFs 官方文档: http://elm-chan.org/fsw/ff/00index_e.html
- STM32H7 SPI HAL 文档
- SD 卡物理层规范 v3.01

浙公网安备 33010602011771号