【ESP32笔记】SDIO读写TF卡

本节目标:分清 SDIO 与 SPI 两种接法,学会用 FATFS 挂载 TF 卡,并用标准 C 文件 API 完成读写

一、本节要解决的问题

ESP32 自带几 MB Flash,装完程序后剩下的空间有限,而且数据不方便拿到电脑上。遇到记录日志、存图片、放音频等需求时,最方便的方案就是插一张 MicroSD/TF 卡。

要让 ESP32 用上 TF 卡,需要三层东西配合:

  1. 物理接口:用 SDIO 还是 SPI 方式与卡通信;
  2. 文件系统 :卡上存的是 FAT 文件系统,需要 FATFS 组件来解析;
  3. 统一接口 :ESP-IDF 通过 VFS(虚拟文件系统) 把 FATFS 挂到某个路径下,之后就能用标准 C 的 fopen/fread/fwrite 操作卡上文件,不必自己写扇区读写。

二、硬件基础

2.1 从 MMC 到 SD 卡

  • MMC(Multi-Media Card)1997 年由西门子和 SanDisk 推出,7 针引脚、体积小,支持 1bit 模式;
  • SD 卡在 MMC 基础上发展而来,向下兼容;
  • MMC 今天基本退出消费市场,衍生出的 eMMC(BGA 封装、焊接在板上)在嵌入式领域很常见;
  • SDIO(Secure Digital Input and Output,安全数字输入输出)在 SD 卡接口基础上增加了低速 I/O 标准,既能读卡,也能连接蓝牙、WiFi、GPS 等 SDIO 外设。

2.2 SDIO 与 SPI 两种接法

MicroSD 卡同时支持 SPI 和 SDIO 两种通信模式,本板/本工程用的是 SDIO 4bit 模式:

对比项 SDIO 模式 SPI 模式
信号线 CLK、CMD、D0~D3 共 6 根 CS、CLK、MOSI、MISO 共 4 根
数据宽度 1bit 或 4bit 并行 固定 1bit
速度 快,本工程 4bit、默认 20MHz 相对慢,受 SPI 时钟限制
引脚 使用 SDMMC 外设专用引脚性能最好 任意 GPIO,布线灵活
挂载 API esp_vfs_fat_sdmmc_mount() esp_vfs_fat_sdspi_mount()
特殊限制 D2/D0 等引脚与 ESP32 启动配置有关(见 2.4) 无此类限制

注意 :教程只实现了 SDIO 方式。如果以后引脚不够或布线不方便,改成 SPI 方式时要把挂载函数换成 esp_vfs_fat_sdspi_mount(),其余文件操作代码不用改------VFS 已经把底层差异屏蔽掉了。

2.3 ESP32 的 SDMMC 外设与卡槽

ESP32 的 SDMMC 主机外设有两个卡槽,每个卡槽都可以单独使用:

信号 卡槽 0 卡槽 1
CMD GPIO11 GPIO15
CLK GPIO6 GPIO14
D0 GPIO7 GPIO2
D1 GPIO8 GPIO4
D2 GPIO9 GPIO12
D3 GPIO10 GPIO13
D4~D7 GPIO16 / GPIO17 / GPIO5 / GPIO18 无

卡槽 0 的引脚(GPIO6~GPIO11)与 ESP32 模组内部 Flash 的引脚冲突,所以实际只能用卡槽 1 ;SDMMC_HOST_DEFAULT() 的默认配置就是卡槽 1。

注意:MicroSD 卡的 CD 插入检测与 D3 引脚公用,没有独立的 WP 写保护引脚,所以检测卡是否插入要另想办法(如定期尝试挂载)。

2.4 上拉与两个"坑"引脚

SD 协议要求 CMD、D0~D3 接上拉电阻(10K~90K)。ESP32 内部带可编程上拉,代码里用一个标志打开:

c 复制代码
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;

教程作者踩过的两个坑(板级设计要牢记):

  • D2 = GPIO12:如果外部接 10K 上拉,ESP32 上电时会误判 Flash 供电为 1.8V(GPIO12 是启动配置脚),程序直接起不来;官方解决办法是烧 eFuse 强制 3.3V,本板选择不焊这个上拉;
  • D0 = GPIO2:如果外部接 10K 上拉,会影响启动模式,导致无法烧录程序。

因此板子把 D0、D2 的外部上拉 NC 掉、改用内部上拉,作者实测工作正常。

注意:这是"作者实践结论",官方注释也写明内部上拉不够、"仅用于调试/示例"。正式产品设计时,上拉方案要结合自己的原理图和启动配置引脚一起评估,不要直接照抄。

三、知识点

3.1 FATFS + VFS:挂载点

挂载成功后,卡上文件出现在 VFS 的某个目录下(本工程是 /sdcard),后面所有路径都要带这个前缀,例如 /sdcard/hello.txt。挂载需要准备三样东西:

  • sdmmc_host_t:用哪个主机外设、哪个卡槽、频率(SDMMC_HOST_DEFAULT() 默认卡槽 1、20MHz);
  • sdmmc_slot_config_t:管脚配置与总线宽度(SDMMC_SLOT_CONFIG_DEFAULT() 默认 1bit,本工程改成 4bit);
  • esp_vfs_fat_sdmmc_mount_config_t:文件系统层面的参数。

3.2 挂载参数的取舍

c 复制代码
    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
        .format_if_mount_failed = true,     //挂载失败是否执行格式化
        .max_files = 5,                     //最大可打开文件数
        .allocation_unit_size = 16 * 1024   //执行格式化时的分配单元大小(分配单元越大,读写越快)
    };
  • format_if_mount_failed = true:挂载失败就格式化。实验阶段方便(空卡、格式不对都能自动处理),但会把卡上原有数据清掉;
  • max_files = 5:最多同时打开 5 个文件,每个打开的文件都占描述符和内存,够用即可;
  • allocation_unit_size = 16 * 1024:格式化时的分配单元(簇)大小,越大顺序读写越快,小文件多时越浪费空间。

四、代码分析

4.1 工程结构

复制代码
sdcard/
├── CMakeLists.txt
├── main/
│   ├── CMakeLists.txt      # SRCS "main.c"
│   └── main.c              # 挂载 + 写/读/重命名 + 卸载
└── sdkconfig               # 目标芯片 esp32

4.2 挂载:准备 host / slot / mount_config

c 复制代码
    sdmmc_card_t *card;
    const char mount_point[] = MOUNT_POINT;
    ESP_LOGI(TAG, "Initializing SD card");

    ESP_LOGI(TAG, "Using SDMMC peripheral");

    //默认配置,速度20MHz,使用卡槽1
    sdmmc_host_t host = SDMMC_HOST_DEFAULT();

    //默认的IO管脚配置,
    sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();

    //4位数据
    slot_config.width = 4;

    //不适用通过IO矩阵进行映射的管脚,只使用默认支持的SDMMC管脚,可以获得最大性能
    #if 0
    slot_config.clk = CONFIG_EXAMPLE_PIN_CLK;
    slot_config.cmd = CONFIG_EXAMPLE_PIN_CMD;
    slot_config.d0 = CONFIG_EXAMPLE_PIN_D0;
    slot_config.d1 = CONFIG_EXAMPLE_PIN_D1;
    slot_config.d2 = CONFIG_EXAMPLE_PIN_D2;
    slot_config.d3 = CONFIG_EXAMPLE_PIN_D3;
    #endif
    // Enable internal pullups on enabled pins. The internal pullups
    // are insufficient however, please make sure 10k external pullups are
    // connected on the bus. This is for debug / example purpose only.
    //管脚启用内部上拉
    slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
  • 被 #if 0 ... #endif 关住的是一段"自定义 SDIO 引脚"的示例(CONFIG_EXAMPLE_PIN_* 宏未定义,所以保持关闭);不经过 GPIO 交换矩阵、直接用 SDMMC 专用引脚才能获得最大性能;
  • slot_config.width = 4 表示启用 4bit 数据线,这是 SDIO 比 SPI 快的主要来源;
  • 挂载代码与错误处理:
c 复制代码
    ESP_LOGI(TAG, "Mounting filesystem");
    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). "
                     "Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret));
        }
        return;
    }
    ESP_LOGI(TAG, "Filesystem mounted");

    // Card has been initialized, print its properties
    sdmmc_card_print_info(stdout, card);

ESP_FAIL 一般表示卡能通信但文件系统有问题;其他错误码多是接线、上拉或卡本身的问题。

4.3 读写文件:标准 C API

写文件和读文件各封装成一个函数,用的全是标准 C 库接口:

c 复制代码
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;
}

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');
    if (pos) {
        *pos = '\0';
    }
    ESP_LOGI(TAG, "Read from file: '%s'", line);

    return ESP_OK;
}
  • 写:fopen(path, "w") → fprintf → fclose,fclose 之后数据才可靠落盘;
  • 读:fopen(path, "r") → fgets 读一行 → fclose,再把行尾的 \n 去掉后打印。

主流程依次演示了 写、重命名、读、再写、再读、卸载:

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);

    const char *file_foo = MOUNT_POINT"/foo.txt";
    // Check if destination file exists before renaming
    struct stat st;
    if (stat(file_foo, &st) == 0) {
        // Delete it if it exists
        unlink(file_foo);
    }

    // Rename original file
    if (rename(file_hello, file_foo) != 0) {
        ESP_LOGE(TAG, "Rename failed");
        return;
    }

    ret = s_example_read_file(file_foo);
  • 先写 /sdcard/hello.txt,内容是 "Hello <卡的 CID 名字>!";
  • 重命名前先用 stat 判断目标文件是否存在,存在就 unlink 删掉,再 rename 成 /sdcard/foo.txt;
  • 读回并打印;
  • 再用同样方式写 /sdcard/nihao.txt(内容 "Nihao <卡名>!")并读回。

注意(教程与仓库代码的差异) :教程代码清单里没有给出 s_example_write_file() / s_example_read_file() 的实现,只调用了它们;仓库里两个函数完整存在(见本节代码)。另外仓库还多了一段被 #if 0 关掉的格式化示例 esp_vfs_fat_sdcard_format(),教程未提及。学习和引用时以仓库代码为准。

4.4 卸载

c 复制代码
    // All done, unmount partition and disable SDMMC peripheral
    esp_vfs_fat_sdcard_unmount(mount_point, card);
    ESP_LOGI(TAG, "Card unmounted");

卸载会把文件系统刷盘并释放 SDMMC 外设;卸载后 card 句柄失效,再访问 /sdcard/... 会失败。

五、关键 API 速查

API 作用
SDMMC_HOST_DEFAULT() 生成默认 host 配置:卡槽 1、20MHz
SDMMC_SLOT_CONFIG_DEFAULT() 生成默认管脚配置(默认 1bit)
esp_vfs_fat_sdmmc_mount() 初始化 SDMMC 并把 FATFS 挂到挂载点,返回卡对象
sdmmc_card_print_info() 打印卡信息(名称、容量、速度等)
esp_vfs_fat_sdcard_unmount() 卸载文件系统并释放外设
esp_vfs_fat_sdcard_format() 格式化卡(仓库中被 #if 0 关掉,未启用)
fopen() / fprintf() / fgets() / fclose() 标准 C 文件读写(VFS 转发给 FATFS)
stat() / unlink() / rename() 判断文件是否存在、删除、重命名

六、编译与现象验证

bash 复制代码
cd esp32-board/sdcard       # 工程当前配置的目标就是 esp32
idf.py build
idf.py -p COM<端口> flash monitor
  • 依次打印 Initializing SD card、Using SDMMC peripheral、Mounting filesystem、Filesystem mounted,然后是一段卡信息(sdmmc_card_print_info 的输出);
  • 接着 Opening file /sdcard/hello.txt、File written、Renaming file /sdcard/hello.txt to /sdcard/foo.txt、Read from file: 'Hello <卡名>!';
  • 再 Opening file /sdcard/nihao.txt、Read from file: 'Nihao <卡名>!';
  • 最后 Card unmounted。把卡插到电脑上,能看到 foo.txt 和 nihao.txt 两个文件。
相关推荐
幼儿园蛋小黄1 小时前
嵌入式学习总结:从 I2C 中断驱动到 DHT11 温湿度采集实战
单片机·嵌入式硬件·学习
万物智能信息科技1 小时前
气体传感器MQ-2芯片驱动开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
驱动开发·嵌入式硬件·华为·开源·harmonyos
星恒随风1 小时前
Linux进程间通信(二):深入理解System V共享内存、消息队列、信号量与内核IPC资源管理
linux·运维·服务器·笔记·学习·状态模式
商业白皮书1 小时前
企业通过API调用大模型,推荐哪些具备数据隐私保护能力的平台?
大数据·人工智能·笔记
可信计算1 小时前
从单视口渲染到分支击穿:关于概率、脑神经与“创世启动器”的一些构想
人工智能·笔记
Stenve1 小时前
UWB 标称厘米级,为什么一量产就翻车?四道坎逐个拆
物联网
优化Henry1 小时前
LTE基站传输割接后S1连接失败排查实录
运维·网络·笔记·学习·信息与通信
HZZD_HZZD2 小时前
写字楼空调分项怎么算?分相计量与峰值控制
大数据·物联网·腾讯云
fly_sunnn2 小时前
STM32-TIM输入捕获
stm32·单片机·嵌入式硬件