ESP32-S3 SPI 驱动 TF 卡与 FATFS 文件系统

5830 字
29 分钟
ESP32-S3 SPI 驱动 TF 卡与 FATFS 文件系统

ESP32-S3 SPI 驱动 TF 卡与 FATFS 文件系统#

本章重要 API 函数#

ESP-IDF SDSPI + FATFS API#

API功能参数说明头文件
SDSPI_HOST_DEFAULT()生成默认 SD SPI 主机配置void:无参数,返回 sdmmc_host_t 结构体,含 do_transactionset_card_clk 等函数指针"driver/sdspi_host.h"
SDSPI_DEVICE_CONFIG_DEFAULT()生成默认 SD SPI Slot 配置void:无参数,返回 sdspi_device_config_t 结构体,默认 CS=GPIO10、host_id=SPI2_HOST"driver/sdspi_host.h"
esp_vfs_fat_sdspi_mount()五合一:初始化 SPI 总线 + 识别 SD 卡 + 注册 VFS + 挂载 FAT 文件系统const char *base_path:挂载点路径(如 "/sdcard");
const sdmmc_host_t *host_config:从 SDSPI_HOST_DEFAULT() 获取;
const sdspi_device_config_t *slot_config:从 SDSPI_DEVICE_CONFIG_DEFAULT() 获取;
const esp_vfs_fat_mount_config_t *mount_config:FAT 挂载参数;
sdmmc_card_t **out_card:输出 SD 卡信息句柄。
返回 esp_err_tESP_OK 成功
"esp_vfs_fat.h"
esp_vfs_fat_sdcard_unmount()卸载 SD 卡并释放 SPI 总线const char *base_path:挂载点路径;
sdmmc_card_t *card:卡句柄。
返回 esp_err_t
"esp_vfs_fat.h"
sdmmc_card_print_info()通过串口打印 SD 卡信息(容量、速度等级等)FILE *stream:输出流(如 stdout);
const sdmmc_card_t *card:卡句柄
"driver/sdmmc_types.h"

esp_vfs_fat_mount_config_t 关键字段#

字段类型含义
format_if_mount_failedbool挂载失败时是否自动格式化(首次使用设 true
max_filesint最大同时打开文件数(含目录遍历句柄,建议 ≥ 5)
allocation_unit_sizesize_t簇大小,填 0 则自动选择
disk_status_check_enablebool是否启用磁盘状态轮询(省 CPU 可设 false

项目封装 API(spi_sdcard.c#

API功能参数说明头文件
sd_spi_init()初始化 SD 卡并挂载到 /sdcardvoid:无参数,返回 esp_err_tESP_OK 成功)"spi_sdcard.h"

本章复用 SPI 封装 API(来自 spi.c#

API功能参数说明头文件
spi2_init()初始化 SPI2 总线(60 MHz,Mode 0,DMA 自动)void:无参数"spi.h"
spi2_write_data()通过 SPI2 批量写数据(轮询模式)uint8_t *data:数据缓冲;int len:字节数"spi.h"
spi2_transfer_byte()SPI2 全双工收发单字节uint8_t data:待发字节,返回 uint8_t:接收字节"spi.h"

本章目录#

点击条目可跳转到对应小节。

章节导航#


学习前后依赖#

依赖方向章节知识点
前置基础01_LEDGPIO 输出配置
前置基础14_IICBH1750双线通信协议原理、推挽/开漏
前置基础16_SPILCDSPI 四线通信、Mode 0 时序、CS 片选、DMA
前置基础0-1_under_Loop_stu轮询 vs 中断 vs DMA 三阶段递进
本章核心17_SPITF(本章)SD 卡容量体系 + FATFS + VFS + SPI 联动
后续承接后续项目日志存储、配置文件、OTA 固件升级、MQTT + SD 缓存

一、概述#

1.1 从 SD 卡到 TF 卡#

SD 卡和 TF 卡本质上是同一种东西的两个外观版本

SD 大卡(32×24×2.1mm) TF 卡 = microSD(15×11×1.0mm)
┌──────────────┐ ┌─────┐
│ ████████████│ │░░░░░│
│ ██ SD ██ │ │░░░░░│
│ ████████████│ └─────┘
└──────────────┘ ↑
9-pin 金手指 8-pin 金手指
对比维度SD 大卡TF 卡(microSD)
尺寸32 × 24 × 2.1 mm15 × 11 × 1.0 mm
引脚数9 根(含写保护检测)8 根(无写保护)
电气协议完全一致(SD 1.0/2.0/3.0)完全一致
转换卡套TF → SD 卡套(纯物理转接,无芯片)
容量上限SDUC 支持到 128 TB同左
ESP32 常用较少(体积大)主流(模块自带 TF 卡座)

关键结论:把 TF 卡插入 SD 卡套就变成 SD 卡。电气上完全相同,驱动代码完全共用。

1.2 为什么用 SPI 而非 SDIO?#

SD 卡支持两种通信模式:

模式信号线速度ESP32-S3 支持复杂度
SPI4 线(MOSI、MISO、CLK、CS)~25 MB/s(高速 SPI)任何 SPI2/3 通道
SDIO 1-bit5 线(CMD、CLK、DATA0)~25 MB/s需要专用 SDMMC 外设
SDIO 4-bit8 线(CMD、CLK、DATA0~3)~100 MB/s需要专用 SDMMC 外设

ESP32-S3 选择 SPI 的理由:

  • 引脚占用少(4 根 vs 8 根),屏幕已经占了一个 SPI2,SD 卡还可以复用同一条 SPI 总线
  • SD 卡上电默认进入 SPI 模式(CMD0 命令后),无需额外配置
  • ESP-IDF 的 esp_vfs_fat_sdspi_mount() 一行代码搞定初始化 + 挂载 + VFS 注册

二、TF 卡数据存储与容量体系#

2.1 NAND Flash 内部结构#

TF 卡内部是一颗 NAND Flash 芯片加一个微型控制器

┌─────────────────────────────────────────┐
│ TF 卡内部 │
│ ┌─────────┐ ┌───────────────┐ │
│ │ NAND │ ◄───► │ 微型控制器 │ │
│ │ Flash │ │ (FTL 层) │ │
│ │ 存储体 │ │ 坏块管理 │ │
│ │ │ │ 磨损均衡 │ │
│ │ │ │ ECC 纠错 │ │
│ └─────────┘ └───────┬───────┘ │
│ │ │
│ SPI / SDIO 接口 │
└───────────────────────────────┼──────────┘
MOSI MISO CLK CS

微型控制器做的事:

  • FTL(Flash Translation Layer):把逻辑扇区地址映射到 NAND 物理页
  • 磨损均衡(Wear Leveling):平均分配擦写次数,避免某个块过早损坏
  • 坏块管理:出厂即有坏块标记,使用中新增坏块自动替换
  • ECC 纠错:每页数据带校验码,读出时自动纠正单 bit 错误

这些你都不用管——微型控制器对主机透明,你的代码只管读写扇区。

2.2 三大容量规格:SDSC / SDHC / SDXC#

规格容量范围寻址模式文件系统典型标记
SDSC≤ 2 GBByte 寻址(CMD17/24)FAT16无标记或”SD”
SDHC2 GB ~ 32 GBBlock(512B)寻址FAT32标”HC”
SDXC32 GB ~ 2 TBBlock 寻址exFAT标”XC”
SDUC2 TB ~ 128 TBBlock 寻址exFAT标”UC”(极少见)

容量判断示意图:

2GB 32GB 2TB
SDSC │ SDHC │ SDXC │
─────────────────┼─────────────────────┼───────────────────────┼──►
Byte 寻址 │ 512B Block 寻址 │ 512B Block 寻址 │
FAT16 │ FAT32 │ exFAT │

SDHC/SDXC 强制要求 Block(512 字节)寻址——CMD17 的参数不再是”字节偏移”,而是”扇区号”。

2.3 顺序 vs 随机读写#

顺序读写(Sequential):
┌─┬─┬─┬─┬─┬─┬─┬─┐
│0│1│2│3│4│5│6│7│ → 连续扇区,速度最快
└─┴─┴─┴─┴─┴─┴─┴─┘
随机读写(Random):
┌──────────────────────────────────────────┐
│扇区0 │...│扇区73 │...│扇区512 │...│扇区8│ → 跳来跳去,慢
└──────────────────────────────────────────┘
操作类型Class 10 卡典型速度用途
顺序写10~20 MB/s日志追加、录视频
顺序读20~30 MB/s加载图片、回放
随机写 4KB0.5~2 MB/s数据库、配置文件
随机读 4KB2~5 MB/s检索小文件

实战启示:ESP32 写日志时,尽量用 fprintf 连续写入,不要频繁 fopen/fclosemount_config.max_files = 5 限制同时打开文件数的意义就在这里——避免大量随机寻道。


三、文件系统与 FATFS#

3.1 为什么需要文件系统?#

如果没有文件系统,你对 TF 卡的读写是这样的:

// 裸扇区读写:没有任何"文件"概念
uint8_t buf[512];
sd_read_sector(1000, buf); // 读第 1000 号扇区
sd_write_sector(2000, buf); // 写第 2000 号扇区

问题:

  • 你得手动记录”日志写在扇区 10009999、图片存在 1000019999”
  • 删除数据时不知道哪些扇区可以重用
  • 拔卡插电脑上,Windows 不认(没有 FAT 文件系统)

有了文件系统之后:

FILE *f = fopen("/sdcard/log.txt", "a");
fprintf(f, "temperature: %d\n", temp);
fclose(f);
// 拔卡插电脑——直接打开 log.txt 就能看

这就是文件系统存在的意义:把扇区抽象成文件,人类可读写,跨设备可识别。

3.2 FAT 文件系统结构#

FAT(File Allocation Table)是 SD 卡最通用的文件系统:

┌───────────┬───────────┬───────────┬───────────┬───────────┐
│ MBR │ FAT 表 │ 根目录 │ 数据区 │ 数据区 │
│ (扇区0) │ (簇链表) │ (文件索引) │ (簇2开始) │ ... │
└───────────┴───────────┴───────────┴───────────┴───────────┘

FAT 表的本质:一个扇区级的”链表”

假设 log.txt 占用了簇 5、簇 8、簇 12:
FAT 表: 簇5 → 簇8 → 簇12 → EOF
(5号位置 (8号位置 (12号位置
写"8") 写"12") 写结束标记)
文件即使碎片化分散在不同扇区,FAT 表能把它们串起来。
区域大小作用
MBR512 B启动记录 + 分区表(告诉系统这是一张 FAT 盘)
保留区若干扇区备份引导扇区
FAT 表取决于容量文件系统的”链表”,记录每个簇的下一簇号
根目录区FAT12/16 固定,FAT32 可变存放文件名、大小、创建时间、起始簇号
数据区剩余空间文件实际内容,按簇(Cluster)分配

3.3 FATFS 是什么?#

FATFS 是一个很轻量的开源 FAT 文件系统实现(纯 C 语言),专为嵌入式设计。它不依赖操作系统,只做一件事:解析 FAT 表、管理目录、读写文件

FATFS 在 ESP-IDF 中的位置:
┌────────────┐
│ 你的代码 │ fopen / fread / fwrite / fclose
└─────┬──────┘
│ C 标准库调用
┌─────▼──────┐
│ VFS │ ESP-IDF 虚拟文件系统(分发路由)
└─────┬──────┘
│ 路由到 /sdcard 路径
┌─────▼──────┐
│ FATFS │ 解析 FAT 表、计算扇区号、管理目录项
└─────┬──────┘
│ 块读写请求(扇区号 + 缓冲区)
┌─────▼──────┐
│ SDSPI 驱动 │ 把扇区号转换成 SPI CMD17/CMD24 命令
└─────┬──────┘
│ SPI 帧
┌─────▼──────┐
│ TF 卡 │
└────────────┘

FATFS 的特性:

  • 代码极小:最小配置下 < 10 KB ROM
  • 支持 FAT12 / FAT16 / FAT32 / exFAT
  • 支持长文件名(LFN)
  • 支持 RTOS 多任务(FATFS 内部有互斥锁)
  • ESP-IDF 已内置:路径 components/fatfs/

四、FATFS 与 C 标准库 I/O 的桥梁:VFS#

4.1 传统裸机读 SD 卡 vs VFS 模式#

这是一段不用 VFS 的裸 FATFS 代码

// 裸 FATFS:用 f_open / f_read / f_write,而不是 fopen / fread / fwrite
FATFS fs;
FIL file;
f_mount(&fs, "", 1); // 挂载文件系统
f_open(&file, "log.txt", FA_WRITE | FA_CREATE_ALWAYS);
f_write(&file, "hello", 5, &bw);
f_close(&file);

这是一段用了 VFS 后的代码

// VFS 模式:用 fopen / fread / fwrite / fprintf 等标准 C 函数
FILE *f = fopen("/sdcard/log.txt", "w");
fprintf(f, "hello\n");
fclose(f);

VFS 做的事:把 fopen("/sdcard/log.txt") 这个 call:

  1. 解析路径前缀 /sdcard → 找到注册的 FATFS 驱动
  2. 把剩余路径 log.txt + 标准 C 的 fopen 参数翻译成 FATFS 的 f_open 调用
  3. 返回一个 FILE * 指针给你——你感觉不到 FATFS 的存在

4.2 ESP-IDF VFS 注册流程#

esp_vfs_fat_sdspi_mount("/sdcard", &host, &slot, &mount, &card)
├─ spi_bus_initialize(SPI2_HOST, ...) 复制 SPI2 驱动初始化后的总线
│ 内部再次初始化 SPI2(如果已经初始化过,跳过)
├─ sdspi_host_init_device(&slot, &handle) 初始化 CS 引脚
├─ sdmmc_card_init(&host, card) 发送 CMD0→CMD8→ACMD41 序列
│ 识别 SDSC / SDHC / SDXC 类型
│ 读取 CID、CSD、OCR 寄存器
│ 确定扇区大小(512B or 1024B)
├─ esp_vfs_fat_sdmmc_mount("/sdcard", &host, card, &mount)
│ 注册 VFS:把 "/sdcard" 路径绑定到 FATFS 驱动
│ 读写请求 → FATFS → sdmmc_host.do_transaction() → SPI CMD17/24
└─ 返回 ESP_OK(card 指向已填充的卡信息结构体)

esp_vfs_fat_sdspi_mount() 实际上是 esp_vfs_fat_sdmmc_mount 的一个包装宏。


五、TF 卡与 ESP32-S3 的 SPI 联动#

5.1 硬件连接总览#

ESP32-S3 TF 卡模块(microSD)
┌──────────────────────┐ ┌────────────────────────┐
│ │ │ │
│ SPI2 │ │ CS ←── GPIO 2 │
│ ┌────────────────┐ │ │ MOSI ←── GPIO 11 │
│ │ MOSI (GPIO 11) ├──┼───────────┼─► MOSI │
│ │ MISO (GPIO 13) │◄─┼───────────┼── MISO │
│ │ CLK (GPIO 12) ├──┼───────────┼─► CLK │
│ │ CS (GPIO 2) ├──┼───────────┼─► CS │
│ └────────────────┘ │ │ │
│ │ │ 3.3V ←── ESP 3.3V │
│ SPI2 │ │ GND ←── ESP GND │
│ (复用给 LCD) │ └────────────────────────┘
│ ┌────────────────┐ │
│ │ CS_LCD(GPIO 48)│ │ ← LCD 用独立的 CS,与 TF 卡
│ └────────────────┘ │ 共享 SPI2 总线,分时复用
└──────────────────────┘

关键设计:LCD 和 TF 卡共享 SPI2(MOSI 11、MISO 13、CLK 12),通过两个独立的 CS 片选线(GPIO 48 给 LCD、GPIO 2 给 TF 卡)分时访问。

5.2 SPI 模式下的 SD 命令#

SD 卡命令是固定长度的 6 字节帧,通过 SPI MOSI 发送:

SD 命令帧(SPI 模式)
┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ Byte0 │ Byte1 │ Byte2 │ Byte3 │ Byte4 │ Byte5 │
│ 0 1 xxxxx│地址[31:24]│地址[23:16]│地址[15:8]│地址[7:0] │ CRC │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
命令索引 32-bit 参数(扇区号 / 字节偏移)

常用命令:

命令索引参数响应说明
CMD000R1复位到 Idle 状态(进入 SPI 模式)
CMD880x1AAR7查询电压范围(SDHC/SDXC 存在检测)
ACMD4141HCS 位R3初始化(含容量识别)
CMD1717扇区号R1 + 512B读单个扇区
CMD2424扇区号R1 + 512B写单个扇区

5.3 SD 卡初始化流程#

上电
空闲状态(Idle)
├─ 发送 CMD0 ──→ 复位,进入 SPI 模式
│ │
│ ▼ (R1 响应 ≠ 0x01 则失败)
├─ 发送 CMD8 ──→ 检查电压 + 识别 SDHC/SDXC
│ │ 参数 0x1AA,期望 R7=0x1AA
│ ▼
│ 收到 R7=0x01AA? ──Yes──► SD Ver 2.0+ (SDHC/SDXC)
│ │ No
│ ▼
│ 可能是 SDSC (≤2GB) 或极老的 MMC 卡
├─ 循环发送 ACMD41 ──→ 等待卡进入 Ready 状态
│ │ HCS=1 表示支持 SDHC/SDXC
│ │ 超时通常设 1~2 秒
│ ▼
│ OCR 的 bit30=1? ──Yes──► SDHC/SDXC (Block 寻址)
│ │ No
│ ▼
│ SDSC (Byte 寻址,≤2GB)
├─ 读取 CID / CSD ──→ 获取序列号、容量、速度等级
就绪(Ready)→ 可以 CMD17 读 / CMD24 写了

六、软件架构传递树#

app_main() [main\main.c]
├─ lcd_init() [components\LCD\lcd.c]
│ ├─ spi2_init() [components\SPI\spi.c]
│ │ └─ spi_bus_initialize() ESP-IDF
│ │ └─ 配置 SPI2: MOSI=11, MISO=13, CLK=12, DMA 自动
│ │
│ ├─ spi_bus_add_device() ESP-IDF
│ │ └─ 注册 LCD 设备: 60MHz, Mode 0, CS=GPIO48
│ │
│ ├─ gpio_config() × 3 ESP-IDF
│ │ └─ 配置 DC=GPIO47, BLK=GPIO40, RST=GPIO21
│ │
│ └─ lcd_hard_reset() → 初始化序列
├─ sd_spi_init() [components\SDIO\spi_sdcard.c]
│ ├─ SDSPI_HOST_DEFAULT() ESP-IDF 宏
│ │ └─ 生成 sdmmc_host_t:
│ │ flags = SPI, slot = SPI2_HOST
│ │ do_transaction → sdspi_host_do_transaction
│ │ set_card_clk → sdspi_host_set_card_clk
│ │
│ ├─ SDSPI_DEVICE_CONFIG_DEFAULT() ESP-IDF 宏
│ │ └─ 设置 CS = GPIO 2
│ │
│ ├─ esp_vfs_fat_mount_config_t ESP-IDF 结构体
│ │ └─ format=false, max_files=5
│ │
│ └─ esp_vfs_fat_sdspi_mount() ESP-IDF (五合一函数)
│ ├─ 内部 spi_bus_initialize() (如果 SPI2 未初始化则初始化)
│ ├─ 内部 sdspi_host_init_device() (注册 CS 信号)
│ ├─ 内部 sdmmc_card_init() (发 CMD0→CMD8→ACMD41)
│ ├─ 内部 esp_vfs_fat_sdmmc_mount() (注册 VFS /sdcard)
│ └─ 返回 card 句柄 → 全局 sdcard
└─ while(1)
├─ 标准 C fopen/fread/fwrite/fprintf("/sdcard/...")
│ └─ VFS 路由 → FATFS → sdmmc_host.do_transaction() → SPI2
└─ LCD 刷新界面
└─ spi2_write_data() → ESP-IDF SPI2 轮询发送

七、代码逐段解析#

7.1 sd_spi_init() 详解#

源文件:components/SDIO/spi_sdcard.c

#include "spi_sdcard.h"
#include "esp_vfs_fat.h"
sdmmc_card_t* sdcard; // ★ 全局卡信息句柄,挂载成功后指向卡信息

sdspi_host_init_device 挂载成功后可通过它获取卡信息:

if (sdcard != NULL) {
printf("SD Card Size: %llu MB\n",
((uint64_t)sdcard->csd.capacity * sdcard->csd.sector_size) / (1024 * 1024));
}

函数体逐行解析:

esp_err_t sd_spi_init(void)
{
esp_err_t ret;
// ① 获取默认 SPI 主机配置
const sdmmc_host_t host = SDSPI_HOST_DEFAULT();

SDSPI_HOST_DEFAULT() 展开为一个含 do_transactionset_card_clk 函数指针的结构体。这些函数 ESP-IDF 已经帮你写好了——初始化阶段用低速(~400 kHz),识别卡后自动切换到高速(~20 MHz)。

// ② 配置 Slot(主要是改 CS 引脚)
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.host_id = SPI2_HOST; // 明确用 SPI2
slot_config.gpio_cs = GPIO_NUM_2; // 覆盖默认 GPIO 10

SDSPI_DEVICE_CONFIG_DEFAULT() 内部设置了 host_id=SPI2_HOSTgpio_cs=10。这里重新设一遍是为了明确关系,并把 CS 从 GPIO 10 改成 GPIO 2。

// ③ 配置 FAT 文件系统参数
esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = false, // 挂载失败不格式化(保护数据)
.max_files = 5, // 最多同时打开 5 个文件
.allocation_unit_size = 0, // 自动选择簇大小
.disk_status_check_enable = false, // 不轮询磁盘状态
};
字段为什么这样设
format_if_mount_failed = falseSD 卡已有数据,不能抹掉
max_files = 5日志文件 + 配置文件 + 少量目录遍历,5 个够用
allocation_unit_size = 0让 FATFS 根据卡容量自动选最优簇大小
disk_status_check_enable = false省 CPU 时间,除非要实现热插拔检测
// ④ 一行搞定:SPI 初始化 + SD 识别 + FAT 挂载 + VFS 注册
ret = esp_vfs_fat_sdspi_mount("/sdcard", &host, &slot_config,
&mount_config, &sdcard);
return ret;
}

"/sdcard" 是挂载点。挂载成功后:

  • fopen("/sdcard/log.txt", "w") 可以写文件
  • opendir("/sdcard") 可以遍历目录
  • stat("/sdcard/config.ini", &st) 可以读文件属性
  • 拔卡插电脑,Windows 直接识别 FAT 文件系统

7.2 如何与 main.c 集成#

#include "spi_sdcard.h"
#include "lcd.h"
void app_main(void)
{
lcd_init(); // ① 先初始化 LCD(用 SPI2)
esp_err_t ret = sd_spi_init(); // ② 再挂载 SD 卡(复用 SPI2)
if (ret == ESP_OK) {
printf("SD card mounted at /sdcard\n");
} else {
printf("SD mount failed: %s\n", esp_err_to_name(ret));
// 即使挂载失败,LCD 仍然可用
}
// ③ 写一句日志到 SD 卡
FILE *f = fopen("/sdcard/hello.txt", "w");
if (f) {
fprintf(f, "ESP32-S3 SPI TF Card Test\n");
fclose(f);
}
// ④ LCD 照常显示
lcd_show_string(2, 1, "SD Card OK", BLUE, BLACK);
while(1) { vTaskDelay(1000); }
}

另外还要修改 components/CMakeLists.txt:把 SDIO 目录加入 src_dirsinclude_dirs,并在 requires 里加上 fatfs


八、完整通信时序图#

8.1 初始化阶段#

上电后 SD 卡处于 Idle(仅响应 CMD0)
├─ ESP32 发送 CMD0(复位)
│ 主机: CMD0
│ SD: R1(0x01) = In Idle State
├─ ESP32 发送 CMD8(查电压 + 判别 SDHC/SDXC)
│ 主机: CMD8(0x1AA)
│ SD: R7(0x01AA) → 是 SD 2.0+ 卡,支持 3.3V
├─ ESP32 循环发送 ACMD41(等 Ready)
│ 主机: ACMD41(HCS=1)
│ SD: R1(0x01) = still busy ... 循环 N 次 ...
│ SD: R1(0x00) = Ready!
├─ 读 OCR → bit30=1 → SDHC/SDXC,Block 寻址
├─ 读 CID(序列号)→ 读 CSD(容量、扇区大小)
│ 主机: CMD9(读 CSD)
│ SD: R1 + 16B CSD 数据
└─ esp_vfs_fat_sdmmc_mount("/sdcard")
注册 VFS,FAT 分区已可访问

8.2 读写阶段#

写文件流程:fopen("/sdcard/test.txt","w") → fprintf → fclose
应用层: fopen fprintf fclose
│ │ │
VFS: │ 路由 /sdcard │ 路由 /sdcard │ 路由 /sdcard
▼ ▼ ▼
FATFS: f_open f_write f_close
创建目录项 计算扇区号 更新目录项
分配首簇 攒满一个扇区 写回 FAT 表
写入 FAT 表 CMD24 写扇区
SDSPI: ─────────── CMD24 → 扇区号 → 512B 数据 → SPI2 → TF 卡
不涉及 SPI

容易踩坑#

序号表现原因与修正
1仅挂载失败esp_vfs_fat_sdspi_mount 返回 ESP_FAIL检查 TF 卡是否插好、是否 FAT32 格式(exFAT 需 CONFIG_FATFS_LFN
2挂载成功但 LCD 花屏LCD 显示乱码、白屏LCD 和 SD 卡共用 SPI2,SD 卡初始化期间会拉低 CS 线。确保 CS 硬件连接正确
3SDIO 代码编译不进去链接时找不到 sd_spi_init 符号components/CMakeLists.txt 里没加 SDIO 目录,也没加 fatfsrequires
4文件写入后电脑读不到SD 卡插入电脑找不到文件写入后记得 fclose(),否则数据还在 FATFS 内部缓冲区
5随机读写特别慢4KB 写入 > 50 msTF 卡随机写入本来就慢(~0.5 MB/s),尽量顺序写、攒大数据再 fwrite
6中文文件名乱码fopen 中文路径失败FATFS 默认只支持 ASCII,需启用 CONFIG_FATFS_CODEPAGE=936
7挂载后 SPI 总线频率太保守SD 读写带宽不足SDSPI_HOST_DEFAULT() 默认 ~20 MHz。手动改 host.max_freq_khz = 40000
8TF 卡是 SDSC(≤2GB)老卡但代码当 SDHC 处理读扇区数据偏移错误SDSC 用 Byte 寻址,SDHC 用 Block(512B)寻址
9第一次使用空白卡挂载失败ESP_FAILmount_config.format_if_mount_failed = true

九、本章小结#

知识点核心结论
SD 卡 vs TF 卡协议完全相同,仅物理尺寸不同,驱动代码通用
容量规格SDSC ≤ 2GB(Byte 寻址)、SDHC 232GB(Block 寻址)、SDXC 32GB2TB(Block 寻址)
FAT 文件系统MBR → FAT 表(簇链表)→ 根目录(文件索引)→ 数据区(实际内容)
FATFS开源轻量 FAT 实现,< 10 KB,支持 RTOS,ESP-IDF 已内置
VFS 桥接fopen/fread/fwrite → VFS 路由 → FATFS → SDSPI → TF 卡,开发者只写 C 标准库代码
SPI 联动LCD 和 TF 卡共享 SPI2 总线(MOSI 11、CLK 12、MISO 13),用独立 CS 分时切换
SD 命令CMD0 进 SPI 模式 → CMD8 查电压 → ACMD41 等就绪 → CMD17/CMD24 读写
初始化esp_vfs_fat_sdspi_mount() 一行完成 SPI 总线初始化 + 卡识别 + FAT 挂载 + VFS 注册
关键配置max_files=5(同时打开文件数)、format_if_mount_failed=false(保护数据)
容易踩坑fclose 数据未写回、CMakeLists 没加组件导致编译不到、SDSC 用错寻址模式

至此,你已经学完了从 GPIO 到 SPI 到 I2C 到 UART 到 DMA 再到 TF 卡文件系统这一整条 ESP32-S3 外设链路。


Tips#

VFS 的魔法——fopen 为什么能打开 SD 卡上的文件#

这是本章最值得深入理解的设计模式。当你写下 fopen("/sdcard/log.txt", "w") 时,这一行代码背后发生了 4 层转发:

你的代码: fopen("/sdcard/log.txt", "w")
1. C 标准库 (newlib): 解析路径前缀 "/sdcard"
│ 查找 VFS 注册表中匹配的驱动
2. ESP-IDF VFS 层: 匹配到 FATFS 驱动
│ 将剩余路径 "log.txt" 传入驱动
3. FATFS (ff.c): 解析 FAT 表,分配簇
│ 将"创建文件"翻译成"写 FAT 表 + 写目录项"
4. SDSPI 驱动: FATFS 说"写扇区 5000"
│ 转换为 CMD24 + SPI 帧 → TF 卡
TF 卡 NAND Flash 物理写入

这就是 VFS 的注册机制esp_vfs_fat_sdspi_mount("/sdcard", ...) 在初始化时会把 "/sdcard" 这个路径前缀和 FATFS 驱动绑定,存在一个全局的 VFS 注册表中。此后任何以 "/sdcard" 开头的文件操作(fopenfreadfwritefprintffclosestatopendir……)全部自动转发到 FATFS。

设计哲学:VFS 层让”硬件存储”对业务代码完全透明。你可以今天把日志写到 SD 卡(/sdcard),明天改写到 SPI Flash(/spiflash),业务的 fprintf 一行代码都不用改——只需改挂载路径。这就是分层架构的价值。

SD 卡容量规格的寻址陷阱#

这是 SD 卡驱动中最容易出隐蔽 bug 的地方:

// SDSC (≤2GB): 用 Byte 寻址
// 读第 1 个扇区: CMD17 的参数 = 512(字节偏移)
// 读第 2 个扇区: CMD17 的参数 = 1024
// SDHC/SDXC (>2GB): 用 Block 寻址
// 读第 1 个扇区: CMD17 的参数 = 0(扇区号)
// 读第 2 个扇区: CMD17 的参数 = 1

如果你拿着一张 16GB 的 SDHC 卡,却用 Byte 寻址去读写,结果是:你读的是正确的扇区号 × 512 的偏移位置,数据全错。更糟糕的是——卡不会报错,它只是从错误的地址返回了无意义的数据。

ESP-IDF 的 sdmmc_card_init() 会自动识别容量类型,存到 sdcard->ocr 寄存器中。FATFS 通过 sdcard->csd.sector_size 获取扇区大小(通常是 512),不会再换算。所以只要用 ESP-IDF 的标准 API,就不用关心这个问题。但如果你自己写裸驱动,这是第一个要检查的地方。

SPI 总线共享的设计智慧#

LCD 和 SD 卡共享 SPI2 是本章最优雅的工程决策:

SPI2 总线 = 一组 SCLK/MOSI/MISO(物理线缆)
┌ SPI2 ───┬─── LCD (CS=GPIO48) ← 拉低 CS48 → LCD 被选中
│ │
│ └─── SD (CS=GPIO2) ← 拉低 CS2 → SD 被选中
两个从机永远不会同时被选中(CS 互斥)

前提条件:LCD 和 SD 卡的速度兼容、SPI 模式兼容。本章 LCD 用 60MHz Mode 0,SD 卡初始化用 400kHz(识别后提至 20MHz),都工作在 Mode 0。两者兼容。

一个常见问题:如果在 SD 卡传输数据(CS2=低)的同时,LCD 也拉了 CS48——此时 SPI2 总线上的 SCLK/MOSI/MISO 会被两个从机同时监听。LCD 可能误把 SD 命令当成像素数据。解决方法是软件互斥锁——在操作 SD 卡前先加锁,完成后释放,LCD 刷新也加同一把锁。

踩坑实录#

  1. fopen 成功但 fprintf 失败mount_config.max_files = 5 是全局限制。如果已经打开了 5 个文件(包括目录遍历句柄),第 6 个 fopen 会失败。关闭不用的文件再试。

  2. 拔卡后程序卡死:FATFS 在读写时如果卡被拔出,会进入无限重试。解决方案:启用 disk_status_check_enable 或在任务中加入超时逻辑。

  3. exFAT 卡不识别:默认的 FATFS 只编译了 FAT12/16/32 支持。需要在 menuconfig 中启用 CONFIG_FATFS_LFN 和 exFAT 支持。

知识衔接#

  • 上一章03_SPI_LCD——SPI 总线和 CS 片选机制是理解本章”LCD + SD 卡共享 SPI2”的前提。
  • 下一章05_Camera——摄像头通过 DVP 并行接口高速传输图像数据。和 SD 卡一样,摄像头也用 DMA 做数据搬运,用 PSRAM 做帧缓冲。理解了 SPI+DMA+FS 的协作模式,阅读摄像头代码会更顺畅。
  • 高级扩展:基于 SD 卡做离线 OTA(esp_ota_write 从 SD 卡读取固件)、MQTT 日志上传(WiFi + SD 卡定时同步)。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

ESP32-S3 SPI 驱动 TF 卡与 FATFS 文件系统
https://mjzy.tech/posts/embedded/esp32-s3/04-spi-tf-sd-card/
作者
ENKIDU
发布于
2026-08-23
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
ENKIDU
深度学习 & 计算机底层原理 | 用代码理解世界
低语
天之锁永系天与地,而这里系着文字与记忆。欢迎来到乌鲁克的数字荒原。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
49
分类
6
标签
45
总字数
180,771
运行时长
0
最后活动
0 天前
站点信息
构建平台
Vercel
文章许可
CC BY-NC-SA 4.0

文章目录