ESP32-S3 FATFS文件系统开发教程

版本信息

  • 教程版本: v2.0
  • ESP-IDF版本: v5.1.6
  • 开发板: 立创实战派ESP32-S3
  • 最后更新: 2026-01-01

教程概述

本教程基于立创实战派ESP32-S3开发板,详细介绍如何使用ESP-IDF框架操作SD卡的FATFS文件系统。通过本教程,您将学习到SD卡的挂载、文件读写、目录操作、磁盘管理等完整的文件系统操作。

适用对象

  • ESP-IDF开发初学者
  • 需要在ESP32-S3上实现数据存储功能的开发者
  • 学习嵌入式文件系统操作的工程师

学习目标

  • 掌握ESP32-S3的SDMMC接口配置
  • 理解FATFS文件系统的挂载和卸载流程
  • 熟练使用标准C文件操作API
  • 掌握目录管理和磁盘空间查询

硬件准备

开发板信息

  • 开发板: 立创实战派ESP32-S3
  • 芯片: ESP32-S3-WROOM-1
  • 存储接口: SDMMC (1-line mode)
  • SD卡: 建议使用FAT32格式的Micro SD卡

引脚连接

功能ESP32-S3引脚SD卡引脚说明
CLKGPIO 47CLK时钟线
CMDGPIO 48CMD命令线
D0GPIO 21DAT0数据线0 (1线模式)
VCC3.3VVCC电源
GNDGNDGND地线

代码分段详解

第一部分:头文件和宏定义

#include <stdio.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include <dirent.h>
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "driver/sdmmc_host.h"

#define BSP_SD_CLK          (47)
#define BSP_SD_CMD          (48)
#define BSP_SD_D0           (21)

static const char *TAG = "main";

#define MOUNT_POINT              "/sdcard"
#define EXAMPLE_MAX_CHAR_SIZE    128

代码说明

  • 标准C库头文件stdio.h(标准输入输出)、string.h(字符串操作)
  • POSIX文件系统头文件sys/unistd.h(文件删除)、sys/stat.h(文件状态)、dirent.h(目录操作)
  • ESP-IDF专用头文件
    • esp_vfs_fat.h:FATFS虚拟文件系统接口
    • sdmmc_cmd.h:SD/MMC卡命令接口
    • driver/sdmmc_host.h:SDMMC主机驱动
  • 引脚定义:根据立创实战派ESP32-S3的硬件设计定义SD卡引脚
  • 挂载点:定义SD卡在文件系统中的挂载路径为 /sdcard

第二部分:文件写入函数

static esp_err_t s_example_write_file(const char *path, char *data)
{
    ESP_LOGI(TAG, "Opening file %s", path);
    FILE *f = fopen(path, "w");   // 以只写方式打开路径中文件
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for writing"); 
        return ESP_FAIL;
    }
    fprintf(f, data); // 写入内容
    fclose(f);  // 关闭文件
    ESP_LOGI(TAG, "File written");

    return ESP_OK;
}

功能说明

  • 以只写模式("w")打开文件,如果文件不存在则创建,存在则清空内容
  • 使用 fprintf() 将数据写入文件
  • 使用 fclose() 关闭文件,确保数据写入磁盘
  • 返回 ESP_OK 表示成功,ESP_FAIL 表示失败

关键点

  • 文件路径必须包含挂载点前缀(如 /sdcard/test.txt
  • 必须检查 fopen() 返回值,避免空指针操作
  • 必须调用 fclose() 释放资源并刷新缓冲区

第三部分:文件读取函数

static esp_err_t s_example_read_file(const char *path)
{
    ESP_LOGI(TAG, "Reading file %s", path);
    FILE *f = fopen(path, "r");  // 以只读方式打开文件
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for reading");
        return ESP_FAIL;
    }
    char line[EXAMPLE_MAX_CHAR_SIZE];  // 定义一个字符串数组
    fgets(line, sizeof(line), f); // 获取文件中的内容到字符串数组
    fclose(f); // 关闭文件

    // strip newline
    char *pos = strchr(line, '\n'); // 查找字符串中的"\n"并返回其位置
    if (pos) {
        *pos = '\0'; // 把\n替换成\0
    }
    ESP_LOGI(TAG, "Read from file: '%s'", line); // 把数组内容输出到终端

    return ESP_OK;
}

功能说明

  • 以只读模式("r")打开文件
  • 使用 fgets() 读取一行内容到缓冲区
  • 使用 strchr() 查找换行符并替换为字符串结束符
  • 输出读取的内容到日志

关键点

  • fgets() 会读取包括换行符在内的一行内容
  • 需要手动处理换行符以获得纯文本内容
  • 缓冲区大小要足够容纳读取的内容

第四部分:文件追加函数

static esp_err_t s_example_append_file(const char *path, char *data)
{
    ESP_LOGI(TAG, "Appending to file %s", path);
    FILE *f = fopen(path, "a");  // Open in append mode
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for appending");
        return ESP_FAIL;
    }
    fprintf(f, data);
    fclose(f);
    ESP_LOGI(TAG, "Content appended");
    return ESP_OK;
}

功能说明

  • 以追加模式("a")打开文件
  • 新内容会添加到文件末尾,不会覆盖原有内容
  • 如果文件不存在,会自动创建

应用场景

  • 日志记录系统
  • 数据采集追加记录
  • 事件记录

第五部分:文件重命名函数

static esp_err_t s_example_rename_file(const char *old_path, const char *new_path)
{
    ESP_LOGI(TAG, "Renaming file %s to %s", old_path, new_path);
    if (rename(old_path, new_path) != 0) {
        ESP_LOGE(TAG, "Rename failed");
        return ESP_FAIL;
    }
    ESP_LOGI(TAG, "File renamed successfully");
    return ESP_OK;
}

功能说明

  • 使用 rename() 函数重命名文件或移动文件
  • 可以在同一目录内重命名,也可以移动到其他目录
  • 返回0表示成功,非0表示失败

注意事项

  • 新路径的目录必须存在
  • 如果新路径已存在同名文件,行为取决于文件系统实现

第六部分:文件删除函数

static esp_err_t s_example_delete_file(const char *path)
{
    ESP_LOGI(TAG, "Deleting file %s", path);
    if (unlink(path) != 0) {
        ESP_LOGE(TAG, "Delete failed");
        return ESP_FAIL;
    }
    ESP_LOGI(TAG, "File deleted successfully");
    return ESP_OK;
}

功能说明

  • 使用 unlink() 函数删除文件
  • 删除后文件无法恢复
  • 只能删除文件,不能删除目录

安全建议

  • 删除前确认文件路径正确
  • 重要文件删除前做好备份

第七部分:创建目录函数

static esp_err_t s_example_create_dir(const char *path)
{
    ESP_LOGI(TAG, "Creating directory %s", path);
    if (mkdir(path, 0775) != 0) {
        ESP_LOGE(TAG, "mkdir failed");
        return ESP_FAIL;
    }
    ESP_LOGI(TAG, "Directory created successfully");
    return ESP_OK;
}

功能说明

  • 使用 mkdir() 创建目录
  • 第二个参数 0775 是权限模式(在嵌入式系统中通常不严格使用)
  • 返回0表示成功

注意事项

  • 父目录必须存在
  • 如果目录已存在,会返回失败

第八部分:目录遍历函数

static esp_err_t s_example_list_dir(const char *path)
{
    ESP_LOGI(TAG, "Listing directory: %s", path);
    DIR *dir = opendir(path);
    if (dir == NULL) {
        ESP_LOGE(TAG, "Failed to open directory");
        return ESP_FAIL;
    }

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        char full_path[512];
        snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
        
        struct stat st;
        if (stat(full_path, &st) == 0) {
            if (S_ISDIR(st.st_mode)) {
                ESP_LOGI(TAG, "  [DIR]  %s", entry->d_name);
            } else {
                ESP_LOGI(TAG, "  [FILE] %s (size: %ld bytes)", entry->d_name, st.st_size);
            }
        }
    }
    closedir(dir);
    return ESP_OK;
}

功能说明

  • 使用 opendir() 打开目录
  • 使用 readdir() 循环读取目录中的每个条目
  • 使用 stat() 获取文件/目录的详细信息
  • 使用 S_ISDIR() 宏判断是否为目录
  • 使用 closedir() 关闭目录

关键数据结构

  • struct dirent:目录条目结构,包含文件名
  • struct stat:文件状态结构,包含大小、时间、权限等信息

第九部分:获取文件状态函数

static esp_err_t s_example_file_stat(const char *path)
{
    ESP_LOGI(TAG, "Getting file status: %s", path);
    struct stat st;
    if (stat(path, &st) != 0) {
        ESP_LOGE(TAG, "stat failed");
        return ESP_FAIL;
    }
    
    ESP_LOGI(TAG, "File size: %ld bytes", st.st_size);
    ESP_LOGI(TAG, "Last modified: %s", ctime(&st.st_mtime));
    return ESP_OK;
}

功能说明

  • 使用 stat() 获取文件的元数据信息
  • st.st_size:文件大小(字节)
  • st.st_mtime:最后修改时间
  • ctime() 将时间戳转换为可读字符串

应用场景

  • 检查文件是否存在
  • 获取文件大小用于内存分配
  • 检查文件修改时间

第十部分:磁盘空间信息函数

static esp_err_t s_example_disk_info(const char *path)
{
    ESP_LOGI(TAG, "Getting disk space info");
    
    FATFS *fs;
    DWORD fre_clust;
    
    // Get volume information and free clusters
    if (f_getfree("0:", &fre_clust, &fs) != FR_OK) {
        ESP_LOGE(TAG, "Failed to get disk info");
        return ESP_FAIL;
    }
    
    // Calculate total and free space (in bytes)
    uint64_t total_bytes = (uint64_t)(fs->n_fatent - 2) * fs->csize * fs->ssize;
    uint64_t free_bytes = (uint64_t)fre_clust * fs->csize * fs->ssize;
    
    ESP_LOGI(TAG, "Total space: %llu KB", total_bytes / 1024);
    ESP_LOGI(TAG, "Free space:  %llu KB", free_bytes / 1024);
    ESP_LOGI(TAG, "Used space:  %llu KB", (total_bytes - free_bytes) / 1024);
    
    return ESP_OK;
}

功能说明

  • 使用 f_getfree() 获取FATFS卷的空闲簇数量
  • 通过文件系统参数计算总空间和剩余空间
  • 计算公式:
    • 总空间 = (FAT表项数 - 2) × 每簇扇区数 × 扇区大小
    • 剩余空间 = 空闲簇数 × 每簇扇区数 × 扇区大小

关键参数

  • fs->n_fatent:FAT表项总数
  • fs->csize:每簇包含的扇区数
  • fs->ssize:每扇区的字节数
  • fre_clust:空闲簇数量

第十一部分:文件定位操作函数

static esp_err_t s_example_file_seek(const char *path)
{
    ESP_LOGI(TAG, "Testing file seek operations on %s", path);
    FILE *f = fopen(path, "r");
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file");
        return ESP_FAIL;
    }
    
    // Get file size
    fseek(f, 0, SEEK_END);
    long file_size = ftell(f);
    ESP_LOGI(TAG, "File size: %ld bytes", file_size);
    
    // Read from beginning
    fseek(f, 0, SEEK_SET);
    char buffer[64];
    fgets(buffer, sizeof(buffer), f);
    ESP_LOGI(TAG, "Content from beginning: %s", buffer);
    
    // Read from middle
    if (file_size > 10) {
        fseek(f, 5, SEEK_SET);
        fgets(buffer, sizeof(buffer), f);
        ESP_LOGI(TAG, "Content from offset 5: %s", buffer);
    }
    
    fclose(f);
    return ESP_OK;
}

功能说明

  • 使用 fseek() 移动文件指针到指定位置
  • 使用 ftell() 获取当前文件指针位置
  • 三种定位模式:
    • SEEK_SET:从文件开头偏移
    • SEEK_CUR:从当前位置偏移
    • SEEK_END:从文件末尾偏移

应用场景

  • 获取文件大小(移动到末尾后使用ftell)
  • 随机访问文件内容
  • 跳过文件头部信息

第十二部分:主函数 - SD卡初始化和挂载

void app_main(void)
{
    esp_err_t ret;

    // 配置挂载参数
    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
        .format_if_mount_failed = true,   // 如果挂载不成功是否需要格式化SD卡
        .max_files = 5, // 允许打开的最大文件数
        .allocation_unit_size = 16 * 1024  // 分配单元大小
    };
    
    sdmmc_card_t *card;
    const char mount_point[] = MOUNT_POINT;
    ESP_LOGI(TAG, "Initializing SD card");
    ESP_LOGI(TAG, "Using SDMMC peripheral");

    // 配置SDMMC主机
    sdmmc_host_t host = SDMMC_HOST_DEFAULT();
    
    // 配置SDMMC插槽
    sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
    slot_config.width = 1;  // 设置为1线SD模式
    slot_config.clk = BSP_SD_CLK; 
    slot_config.cmd = BSP_SD_CMD;
    slot_config.d0 = BSP_SD_D0;
    slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP; // 打开内部上拉电阻

    ESP_LOGI(TAG, "Mounting filesystem");
    // 挂载SD卡
    ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &card);

    if (ret != ESP_OK) {
        if (ret == ESP_FAIL) {
            ESP_LOGE(TAG, "Failed to mount filesystem. ");
        } else {
            ESP_LOGE(TAG, "Failed to initialize the card (%s). ", esp_err_to_name(ret));
        }
        return;
    }
    ESP_LOGI(TAG, "Filesystem mounted");
    sdmmc_card_print_info(stdout, card); // 打印SD卡信息

配置说明

  1. 挂载配置 (esp_vfs_fat_sdmmc_mount_config_t):

    • format_if_mount_failed:挂载失败时是否自动格式化
    • max_files:同时打开的最大文件数
    • allocation_unit_size:分配单元大小,影响性能和空间利用率
  2. 主机配置 (sdmmc_host_t):

    • 使用 SDMMC_HOST_DEFAULT() 获取默认配置
    • 配置SDMMC外设的工作参数
  3. 插槽配置 (sdmmc_slot_config_t):

    • width:数据线宽度(1线或4线模式)
    • clk/cmd/d0:引脚分配
    • flags:启用内部上拉电阻
  4. 挂载函数 (esp_vfs_fat_sdmmc_mount):

    • 将SD卡挂载到VFS(虚拟文件系统)
    • 挂载后可以使用标准C文件操作函数

第十三部分:主函数 - 文件操作测试

    // 基础文件写入和读取
    const char *file_hello = MOUNT_POINT"/你好hello.txt";
    char data[EXAMPLE_MAX_CHAR_SIZE];
    snprintf(data, EXAMPLE_MAX_CHAR_SIZE, "%s %s!\n", "你好hello先生", card->cid.name);
    ret = s_example_write_file(file_hello, data);
    if (ret != ESP_OK) {
        return;
    }

    ret = s_example_read_file(file_hello);
    if (ret != ESP_OK) {
        return;
    }

    // Test 1: 文件追加
    ESP_LOGI(TAG, "\n=== Test 1: Append to file ===");
    ret = s_example_append_file(file_hello, "Appended line 1\n");
    if (ret == ESP_OK) {
        ret = s_example_append_file(file_hello, "Appended line 2\n");
    }
    if (ret == ESP_OK) {
        s_example_read_file(file_hello);
    }

    // Test 2: 文件定位操作
    ESP_LOGI(TAG, "\n=== Test 2: File seek operations ===");
    s_example_file_seek(file_hello);

    // Test 3: 获取文件状态
    ESP_LOGI(TAG, "\n=== Test 3: File status ===");
    s_example_file_stat(file_hello);

    // Test 4: 文件重命名
    ESP_LOGI(TAG, "\n=== Test 4: Rename file ===");
    const char *file_renamed = MOUNT_POINT"/renamed_file.txt";
    ret = s_example_rename_file(file_hello, file_renamed);
    if (ret == ESP_OK) {
        s_example_read_file(file_renamed);
    }

    // Test 5: 创建目录
    ESP_LOGI(TAG, "\n=== Test 5: Create directory ===");
    const char *test_dir = MOUNT_POINT"/test_dir";
    s_example_create_dir(test_dir);

    // Test 6: 在目录中创建文件
    ESP_LOGI(TAG, "\n=== Test 6: Create files in directory ===");
    const char *file_in_dir1 = MOUNT_POINT"/test_dir/file1.txt";
    const char *file_in_dir2 = MOUNT_POINT"/test_dir/file2.txt";
    s_example_write_file(file_in_dir1, "Content of file 1\n");
    s_example_write_file(file_in_dir2, "Content of file 2\n");

    // Test 7-8: 目录遍历
    ESP_LOGI(TAG, "\n=== Test 7: List root directory ===");
    s_example_list_dir(MOUNT_POINT);
    
    ESP_LOGI(TAG, "\n=== Test 8: List test_dir directory ===");
    s_example_list_dir(test_dir);

    // Test 9: 磁盘空间信息
    ESP_LOGI(TAG, "\n=== Test 9: Disk space information ===");
    s_example_disk_info(MOUNT_POINT);

    // Test 10: 删除文件和目录
    ESP_LOGI(TAG, "\n=== Test 10: Delete files ===");
    s_example_delete_file(file_in_dir1);
    s_example_delete_file(file_in_dir2);
    s_example_delete_file(file_renamed);
    
    ESP_LOGI(TAG, "Removing empty directory");
    if (rmdir(test_dir) == 0) {
        ESP_LOGI(TAG, "Directory removed successfully");
    } else {
        ESP_LOGE(TAG, "Failed to remove directory");
    }

    // 最终目录列表
    ESP_LOGI(TAG, "\n=== Final: List root directory after cleanup ===");
    s_example_list_dir(MOUNT_POINT);

    // 卸载SD卡
    esp_vfs_fat_sdcard_unmount(mount_point, card);
    ESP_LOGI(TAG, "\nCard unmounted");
}

测试流程说明

本例程包含10个完整的测试步骤,全面演示FATFS文件系统的各项功能:

  1. 基础读写:创建文件并写入内容,然后读取验证
  2. 文件追加:向已有文件追加新内容
  3. 文件定位:使用fseek/ftell进行文件指针操作
  4. 文件状态:获取文件大小和修改时间
  5. 文件重命名:重命名文件并验证
  6. 创建目录:创建新目录
  7. 目录文件:在目录中创建多个文件
  8. 目录遍历:列出目录内容,区分文件和目录
  9. 磁盘信息:查询磁盘总空间和剩余空间
  10. 清理操作:删除文件和空目录

ESP-IDF库函数学习要点

一、SD卡挂载和管理函数

1. esp_vfs_fat_sdmmc_mount()

功能:将SD卡挂载到VFS(虚拟文件系统)

函数原型

esp_err_t esp_vfs_fat_sdmmc_mount(
    const char* base_path,                          // 挂载点路径
    const sdmmc_host_t* host_config,                // 主机配置
    const void* slot_config,                        // 插槽配置
    const esp_vfs_fat_sdmmc_mount_config_t* mount_config, // 挂载配置
    sdmmc_card_t** out_card                         // 输出卡信息
);

参数说明

  • base_path:挂载点,如 “/sdcard”
  • host_config:SDMMC主机接口配置
  • slot_config:SD卡插槽配置(引脚、位宽等)
  • mount_config:挂载选项(最大文件数、格式化选项等)
  • out_card:返回SD卡信息结构体指针

返回值

  • ESP_OK:挂载成功
  • ESP_FAIL:挂载失败
  • 其他错误码

使用示例

esp_vfs_fat_sdmmc_mount_config_t mount_config = {
    .format_if_mount_failed = true,
    .max_files = 5,
    .allocation_unit_size = 16 * 1024
};

sdmmc_card_t *card;
ret = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card);

2. esp_vfs_fat_sdcard_unmount()

功能:卸载SD卡

函数原型

esp_err_t esp_vfs_fat_sdcard_unmount(
    const char *base_path,    // 挂载点路径
    sdmmc_card_t *card        // SD卡信息
);

重要性

  • 确保所有数据写入磁盘
  • 释放系统资源
  • 避免数据丢失

3. sdmmc_card_print_info()

功能:打印SD卡详细信息

函数原型

void sdmmc_card_print_info(FILE* stream, const sdmmc_card_t* card);

输出信息

  • SD卡名称
  • 卡类型(SDHC/SDXC等)
  • 容量大小
  • 速度等级

二、标准C文件操作函数

4. fopen()

功能:打开文件

函数原型

FILE *fopen(const char *filename, const char *mode);

模式参数

  • "r":只读,文件必须存在
  • "w":只写,文件不存在则创建,存在则清空
  • "a":追加,文件不存在则创建,写入追加到末尾
  • "r+":读写,文件必须存在
  • "w+":读写,文件不存在则创建,存在则清空
  • "a+":读写追加

5. fclose()

功能:关闭文件

函数原型

int fclose(FILE *stream);

重要性

  • 刷新缓冲区到磁盘
  • 释放文件描述符
  • 避免资源泄漏

6. fprintf()

功能:格式化写入文件

函数原型

int fprintf(FILE *stream, const char *format, ...);

使用示例

fprintf(f, "Hello World!\n");
fprintf(f, "Number: %d, String: %s\n", 123, "test");

7. fgets()

功能:从文件读取一行

函数原型

char *fgets(char *str, int n, FILE *stream);

参数说明

  • str:存储读取内容的缓冲区
  • n:最大读取字符数(包括’\0’)
  • stream:文件指针

特点

  • 读取到换行符或EOF或达到n-1个字符时停止
  • 会保留换行符

8. fseek()

功能:移动文件指针

函数原型

int fseek(FILE *stream, long offset, int whence);

whence参数

  • SEEK_SET:从文件开头偏移
  • SEEK_CUR:从当前位置偏移
  • SEEK_END:从文件末尾偏移

使用示例

fseek(f, 0, SEEK_END);      // 移动到文件末尾
long size = ftell(f);        // 获取文件大小
fseek(f, 0, SEEK_SET);      // 移动到文件开头

9. ftell()

功能:获取当前文件指针位置

函数原型

long ftell(FILE *stream);

返回值:当前文件指针位置(字节偏移)

应用:配合fseek获取文件大小


三、文件和目录管理函数

10. rename()

功能:重命名或移动文件

函数原型

int rename(const char *old_name, const char *new_name);

返回值:成功返回0,失败返回-1


11. unlink()

功能:删除文件

函数原型

int unlink(const char *pathname);

注意:只能删除文件,不能删除目录


12. mkdir()

功能:创建目录

函数原型

int mkdir(const char *pathname, mode_t mode);

参数

  • pathname:目录路径
  • mode:权限模式(如0775)

13. rmdir()

功能:删除空目录

函数原型

int rmdir(const char *pathname);

限制:只能删除空目录


14. opendir()

功能:打开目录

函数原型

DIR *opendir(const char *name);

返回值:目录流指针,失败返回NULL


15. readdir()

功能:读取目录条目

函数原型

struct dirent *readdir(DIR *dirp);

返回值:指向dirent结构的指针,到达末尾返回NULL

dirent结构

struct dirent {
    ino_t d_ino;           // inode编号
    char d_name[256];      // 文件名
};

16. closedir()

功能:关闭目录

函数原型

int closedir(DIR *dirp);

17. stat()

功能:获取文件状态

函数原型

int stat(const char *pathname, struct stat *statbuf);

stat结构重要字段

struct stat {
    mode_t st_mode;      // 文件类型和权限
    off_t st_size;       // 文件大小(字节)
    time_t st_mtime;     // 最后修改时间
};

判断文件类型宏

  • S_ISDIR(mode):是否为目录
  • S_ISREG(mode):是否为普通文件

四、FATFS特有函数

18. f_getfree()

功能:获取FATFS卷的空闲簇数量和文件系统信息

函数原型

FRESULT f_getfree(
    const TCHAR* path,    // 逻辑驱动器号
    DWORD* nclst,         // 返回空闲簇数量
    FATFS** fatfs         // 返回文件系统对象指针
);

使用示例

FATFS *fs;
DWORD fre_clust;

if (f_getfree("0:", &fre_clust, &fs) == FR_OK) {
    uint64_t total_bytes = (uint64_t)(fs->n_fatent - 2) * fs->csize * fs->ssize;
    uint64_t free_bytes = (uint64_t)fre_clust * fs->csize * fs->ssize;
    
    ESP_LOGI(TAG, "Total: %llu KB", total_bytes / 1024);
    ESP_LOGI(TAG, "Free: %llu KB", free_bytes / 1024);
}

FATFS结构关键字段

  • n_fatent:FAT表项总数
  • csize:每簇扇区数
  • ssize:扇区大小(字节)

函数分类速查表

分类函数名功能头文件
SD卡管理esp_vfs_fat_sdmmc_mount()挂载SD卡esp_vfs_fat.h
esp_vfs_fat_sdcard_unmount()卸载SD卡esp_vfs_fat.h
sdmmc_card_print_info()打印SD卡信息sdmmc_cmd.h
文件打开/关闭fopen()打开文件stdio.h
fclose()关闭文件stdio.h
文件读写fprintf()格式化写入stdio.h
fgets()读取一行stdio.h
文件定位fseek()移动文件指针stdio.h
ftell()获取当前位置stdio.h
文件管理rename()重命名文件stdio.h
unlink()删除文件unistd.h
stat()获取文件状态sys/stat.h
目录操作mkdir()创建目录sys/stat.h
rmdir()删除空目录unistd.h
opendir()打开目录dirent.h
readdir()读取目录项dirent.h
closedir()关闭目录dirent.h
磁盘管理f_getfree()获取磁盘空间ff.h (FATFS)

编译和运行

环境准备

# 激活ESP-IDF环境
get_idf

# 设置目标芯片
idf.py set-target esp32s3

编译项目

idf.py build

烧录程序

idf.py -p /dev/tty.wchusbserial1440 flash

查看运行日志

idf.py -p /dev/tty.wchusbserial1440 monitor

退出监视器

Ctrl + ]


运行输出示例

I (xxx) main: Initializing SD card
I (xxx) main: Using SDMMC peripheral
I (xxx) main: Mounting filesystem
I (xxx) main: Filesystem mounted

Name: SD32G
Type: SDHC/SDXC
Speed: 20 MHz
Size: 30436MB

I (xxx) main: Opening file /sdcard/你好hello.txt
I (xxx) main: File written
I (xxx) main: Reading file /sdcard/你好hello.txt
I (xxx) main: Read from file: '你好hello先生 SD32G!'

=== Test 1: Append to file ===
I (xxx) main: Appending to file /sdcard/你好hello.txt
I (xxx) main: Content appended

=== Test 2: File seek operations ===
I (xxx) main: File size: 85 bytes
I (xxx) main: Content from beginning: 你好hello先生 SD32G!

=== Test 3: File status ===
I (xxx) main: File size: 85 bytes
I (xxx) main: Last modified: Wed Jan 01 10:00:00 2026

=== Test 7: List root directory ===
I (xxx) main:   [FILE] 你好hello.txt (size: 85 bytes)
I (xxx) main:   [DIR]  test_dir

=== Test 9: Disk space information ===
I (xxx) main: Total space: 30436352 KB
I (xxx) main: Free space:  30435328 KB
I (xxx) main: Used space:  1024 KB

I (xxx) main: Card unmounted

常见问题和解决方案

1. SD卡挂载失败

可能原因

  • SD卡未正确插入
  • 引脚连接错误
  • SD卡格式不支持(建议FAT32)
  • 供电不足

解决方法

  • 检查硬件连接
  • 使用电脑格式化SD卡为FAT32
  • 确保3.3V供电稳定
  • 检查引脚定义是否与硬件匹配

2. 文件操作失败

注意事项

  • 文件路径必须包含挂载点前缀(如 /sdcard/
  • 打开文件后必须关闭,否则可能导致数据丢失
  • 检查 max_files 配置是否足够
  • 确保文件名不包含非法字符

3. 中文文件名支持

说明

  • FATFS支持UTF-8编码的中文文件名
  • 确保代码文件保存为UTF-8编码
  • 终端需要支持UTF-8显示

4. 编译错误:format-truncation

错误信息

error: 'snprintf' output may be truncated

解决方法
增大缓冲区大小,例如将256改为512:

char full_path[512];

5. 性能优化建议

提高读写速度

  • 增大 allocation_unit_size 可提高大文件写入速度
  • 使用4线SDMMC模式可提高传输速度
  • 批量操作时减少文件打开/关闭次数
  • 使用缓冲区批量读写

4线模式配置

slot_config.width = 4;  // 4线模式
slot_config.d1 = GPIO_NUM_XX;
slot_config.d2 = GPIO_NUM_XX;
slot_config.d3 = GPIO_NUM_XX;

扩展学习建议

1. 日志记录系统

  • 创建按日期命名的日志文件
  • 实现日志轮转机制(按大小或时间)
  • 添加时间戳和日志级别
  • 实现异步写入提高性能

2. 数据采集应用

  • 定时采集传感器数据
  • 保存为CSV格式便于分析
  • 实现数据缓冲和批量写入
  • 添加数据校验和错误恢复

3. 配置文件管理

  • 使用JSON格式存储配置
  • 实现配置读取和解析
  • 支持配置热更新
  • 添加配置备份机制

4. 固件升级

  • 从SD卡读取固件文件
  • 实现OTA升级功能
  • 添加固件校验(CRC/MD5)
  • 实现升级失败回滚

参考资源


总结

通过本教程,您已经学习了:

✅ ESP32-S3的SDMMC接口配置方法
✅ FATFS文件系统的挂载和卸载流程
✅ 18个核心函数的使用方法
✅ 标准C文件操作API的完整应用
✅ 目录管理和遍历技巧
✅ 磁盘空间查询方法
✅ 完整的代码实现和测试流程

这些知识可以应用于:

  • 数据记录和日志系统
  • 配置文件管理
  • 传感器数据存储
  • 固件升级包存储
  • 多媒体文件播放
  • 数据备份和恢复

更多推荐