AIGC标识 FATFS文件系统

缘起

因为心电数据的存储要求,想要用STM32来与SD卡数据进行读写,然后接触到了FAT32。来做知识总结

文件系统:从计算机磁盘开始

操作系统操作文件系统,来管理硬盘。
文件系统提供一种统一格式,抽象为目录和文件。

硬盘的工作原理

\[\text { 硬盘容量 }=\text { 盘面数 } \text { × } \text { 柱面数 } \text { × } \text { 扇区数 } \text { × } 512 \text { 字节(扇区容量) } \]

CPU控制盘面上的读写头来写入和读取扇区上的数据

文件系统管理磁盘

文件系统提供接口:文件目录

window

image

  • 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 等)
来完成扇区的读写。

关键流程:

  1. SD_disk_initialize() — SPI2 初始化时序 → CMD0 → CMD8 → ACMD41 → CMD58
  2. SD_disk_read() — CMD17(读单扇区)/ CMD18(读多扇区)
  3. 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 表示文件结束)。

实现思想:

  1. 计算当前簇中的偏移和剩余字节
  2. 如果 FIL 内部缓冲区有数据(来自上次读),直接拷贝
  3. 否则调用 disk_read 从 SD 卡读一个扇区到内部缓冲区
  4. 从缓冲区拷贝到用户 buff
  5. 更新 fptr(文件指针)
  6. 如果跨簇边界,通过 FAT 表找到下一个簇

4.5 f_write — 写文件

FRESULT f_write(FIL* fp, const void* buff, UINT btw, UINT* bw);

功能:从 buff 向文件当前位置写入 btw 字节。
bw 返回实际写入的字节数。

实现思想:

  1. 如果文件还没有分配簇,分配第一个簇
  2. 数据先写入 FIL 的内部缓冲区(扇区大小对齐)
  3. 缓冲区满了就调用 disk_write 刷到 SD 卡
  4. 更新 FAT 表(分配新簇)
  5. 更新 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 = 每簇扇区数。

FRESULT f_unlink(const TCHAR* path);

功能:删除文件或空目录。

实现思想:

  1. 标记目录项的第一个字节为 0xE5(已删除)
  2. 将文件占用的所有簇在 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 再一次性写入,能大幅提高吞吐量。


注意事项

  1. static 关键字:FATFS 和 FIL 必须在函数外定义或加 static,
    避免栈溢出

  2. 及时关闭文件:f_close 会刷新缓冲区,确保数据完整写入。
    意外断电可能导致最后几 KB 数据丢失

  3. SD 卡格式:建议格式化为 FAT32,簇大小选默认(4KB)

  4. 写放大:即使只写 1 字节,SD 卡也需要擦写整个扇区(512 字节)

  5. 文件大小限制:FAT32 单个文件最大 4GB - 1 字节

  6. 并发访问:FatFs 不是线程安全的,不要在中断中调用 FatFs 函数


调试技巧

如果 SD 卡读写失败:

  1. 检查 SD_disk_initialize 返回值(非 0 表示硬件初始化失败)
  2. 检查 f_mount 返回值(常见错误见 ff.h 中 FR_* 定义)
  3. 用读测试验证:f_open + f_read,打印读到的内容
  4. 用容量测试验证:f_getfree 检查能否读取 FAT 表
  5. 如果卡在某个函数,检查 FATFS/FIL 是否加了 static

参考

posted @ 2026-07-19 10:13  loong2525  阅读(18)  评论(0)    收藏  举报