本节目标:分清 SDIO 与 SPI 两种接法,学会用 FATFS 挂载 TF 卡,并用标准 C 文件 API 完成读写
一、本节要解决的问题
ESP32 自带几 MB Flash,装完程序后剩下的空间有限,而且数据不方便拿到电脑上。遇到记录日志、存图片、放音频等需求时,最方便的方案就是插一张MicroSD/TF 卡。
要让 ESP32 用上 TF 卡,需要三层东西配合:
- 物理接口:用 SDIO 还是 SPI 方式与卡通信;
- 文件系统:卡上存的是 FAT 文件系统,需要FATFS组件来解析;
- 统一接口: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 内部带可编程上拉,代码里用一个标志打开:
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 挂载参数的取舍
esp_vfs_fat_sdmmc_mount_config_tmount_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 # 目标芯片 esp324.2 挂载:准备 host / slot / mount_config
sdmmc_card_t*card;constcharmount_point[]=MOUNT_POINT;ESP_LOGI(TAG,"Initializing SD card");ESP_LOGI(TAG,"Using SDMMC peripheral");//默认配置,速度20MHz,使用卡槽1sdmmc_host_thost=SDMMC_HOST_DEFAULT();//默认的IO管脚配置,sdmmc_slot_config_tslot_config=SDMMC_SLOT_CONFIG_DEFAULT();//4位数据slot_config.width=4;//不适用通过IO矩阵进行映射的管脚,只使用默认支持的SDMMC管脚,可以获得最大性能#if0slot_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 快的主要来源;- 挂载代码与错误处理:
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 propertiessdmmc_card_print_info(stdout,card);ESP_FAIL一般表示卡能通信但文件系统有问题;其他错误码多是接线、上拉或卡本身的问题。
4.3 读写文件:标准 C API
写文件和读文件各封装成一个函数,用的全是标准 C 库接口:
staticesp_err_ts_example_write_file(constchar*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");returnESP_FAIL;}fprintf(f,data);fclose(f);ESP_LOGI(TAG,"File written");returnESP_OK;}staticesp_err_ts_example_read_file(constchar*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");returnESP_FAIL;}charline[EXAMPLE_MAX_CHAR_SIZE];fgets(line,sizeof(line),f);fclose(f);// strip newlinechar*pos=strchr(line,'\n');if(pos){*pos='\0';}ESP_LOGI(TAG,"Read from file: '%s'",line);returnESP_OK;}- 写:
fopen(path, "w")→fprintf→fclose,fclose之后数据才可靠落盘; - 读:
fopen(path, "r")→fgets读一行 →fclose,再把行尾的\n去掉后打印。
主流程依次演示了 写、重命名、读、再写、再读、卸载:
constchar*file_hello=MOUNT_POINT"/hello.txt";chardata[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);constchar*file_foo=MOUNT_POINT"/foo.txt";// Check if destination file exists before renamingstructstatst;if(stat(file_foo,&st)==0){// Delete it if it existsunlink(file_foo);}// Rename original fileif(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 卸载
// All done, unmount partition and disable SDMMC peripheralesp_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() | 判断文件是否存在、删除、重命名 |
六、编译与现象验证
cdesp32-board/sdcard# 工程当前配置的目标就是 esp32idf.py build idf.py-pCOM<端口>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两个文件。