目录
[2.1 文件系统](#2.1 文件系统)
[2.2 FAT文件系统](#2.2 FAT文件系统)
[2.3 POSIX API和VFS](#2.3 POSIX API和VFS)
[2.1 读写模式](#2.1 读写模式)
[2.2 组件依赖](#2.2 组件依赖)
[2.3 sdmmc.c](#2.3 sdmmc.c)
[2.4 sdmmc.h](#2.4 sdmmc.h)
[2.5 main.c](#2.5 main.c)
[2.6 代码解释](#2.6 代码解释)
[2.7 结果展示](#2.7 结果展示)
[3.1 flash.c](#3.1 flash.c)
[3.2 flash.h](#3.2 flash.h)
[3.3 main.c](#3.3 main.c)
[3.4 结果展示](#3.4 结果展示)
前言
本章基于第一章的模板工程,完成对flash设备和sd卡设备的读写,开发板是微雪的**ESP32-P4-Module-DEV-KIT。**本章代码实现比较简单,但涉及的知识概念比较多。
开发环境是VScode+ESP-IDF6.0,开发芯片是ESP32P4。
一、前提知识
2.1 文件系统
文件系统就是存储设备的管家,没有文件系统去管理存储设备,我们对存储设备的读写只能按地址去寻找数据,非常繁琐。有了文件系统的加持,就能像电脑的盘管理一样,具有文件夹、文件等格式存储数据,读写非常便捷。
主流的文件系统如下:
NTFS(Windows 电脑本地硬盘)
- 特点:支持大文件、权限加密、日志防损坏、压缩、快照
- 用处:电脑 C 盘、D 盘、内置机械硬盘、固态硬盘
exFAT(U 盘 / SD 卡大容量)
- FAT32 升级版
- 支持超过 4GB 单个文件,没有文件数量限制
- 用处:大 U 盘、移动硬盘、相机存储卡
- 兼容:Windows、Mac、手机、大部分数码设备
FAT32(老 U 盘 / 相机 / 单片机)
- 结构最简单,所有设备万能兼容
- 缺点:单个文件最大只能 4GB
- 用处:行车记录仪、TF 卡、STM32/ESP32 嵌入式、老相机、启动盘
APFS(Mac 苹果电脑专用)
ext4(Linux 主流文件系统)
F2FS(手机安卓内置存储)
ISO、UDF(光盘专用)
本文的SD存储管理要使用的就是FAT文件系统
2.2 FAT文件系统
FAT 是微软早期开发、结构极简、全平台通用 的老式磁盘文件系统,以一张文件分配链表管理所有存储簇,是 U 盘、SD 卡、相机、嵌入式设备通用格式。
主流版本:FAT12(软盘)→ FAT16(老硬盘)→ FAT32(U 盘 / 存储卡主流)
主要作用: 管理磁盘空间分配,让系统快速**读写、删除、新建文件;**超强跨设备兼容。
ESP-IDF 使用 FatFs 库来实现 FAT 文件系统。FatFs 是日本开源轻量FAT32/FAT16 软件库,纯 C 编写,无操作系统也能用,它的特点如下:
- 作用:解析 FAT 表、管理 SD 卡 / Flash 簇、文件读写、目录遍历
- 场景:STM32、ESP32、单片机裸机、RT-Thread、FreeRTOS,读写 SD/U 盘
FatFs是非常底层的FAT 文件系统驱动库,直接使用代码可移植性差、环境依赖度高、复杂性强。所以一般借助 C 标准库和 POSIX API 通过 VFS(虚拟文件系统)使用 FatFs 库的大多数功能。
2.3 POSIX API和VFS
POSIX 是全行业通用标准文件操作函数规范**,**Linux、安卓、Mac、服务器、RTOS 统一遵守这套规则。
文件操作API固定长这样:open() read() write() close()
目录操作API固定长这样:opendir() readdir()
使用POSIX API:一套代码,Linux、开发板、服务器通用,不用学各家乱七八糟私有函数。
VFS 是统一中转接口层,硬盘、SD 卡、Flash、U 盘,它们底层文件系统都不一样,那每个读写函数都不一样。VFS 做了一层统一外壳,不管底下是什么文件系统,上层调用格式永远一样。
它们之间的关系为:
虚拟文件系统VFS屏蔽实际文件系统差异,使不同文件系统的接口统一。接着POSIX去调用这些统一的接口完成你需要的读写等操作。这种方式下,所有不同存储设备在不同系统下都能使用同一套代码去运行。极大的提高了复用性。
当然直接使用FatFs 与 VFS可可以完成存储设备的驱动,只是无法使用类似这些open() read() write() close()函数,不过不同设备也会提供属于他的同功能的接口函数,这种用法在嵌入式中非常常见。
二、SD卡读写
2.1 读写模式
SD卡有SDMMC 模式和SPI 模式两种模式,如下:
- SDMMC 模式:SD 卡原生高速并行总线协议,ESP 专用硬件控制器
- SPI 模式:通用串行串口协议,把 SD 卡模拟成 SPI 从设备,所有 MCU 通用
| 对比维度 | SDMMC 4 线 / 1 线模式 | SPI 模式 |
|---|---|---|
| 引脚数量 | 4 线:CLK+CMD+4 条 DATA 1 线:CLK+CMD+1 条 DATA | 仅 4 根:SCK+MOSI+MISO+CS |
| 读写速度 | 极高:最高~20MB/s(UHS 模式可达 100MB/s) | 很低:最高~1~2MB/s,日常稳定 1MHz |
| 硬件依赖 | ESP 专属 SDMMC 硬件外设,必须对应芯片专用引脚 | 通用 SPI 外设,任意 GPIO 都能重映射 |
| 电压要求 | ≤20MHz:3.3V 超高速 UHS:必须 1.8V 电平 | 永远只用 3.3V |
| 信号稳定性 | 对线长、布线、供电、干扰极度敏感 | 超强抗干扰!面包板飞线随便接都稳 |
| SD 卡兼容性 | 只支持正规 SD/SDHC/SDXC 卡 | 几乎所有 TF/SD 卡全兼容 |
| 文件寿命 | 高频写入损耗更低 | 普通 |
我的开发板上有专用SD卡座,故可以使用sdmmc模式。
2.2 组件依赖
bash
idf_component_register(SRCS "msdmmc.c"
INCLUDE_DIRS "include"
PRIV_REQUIRES fatfs esp_driver_sdmmc sdmmc console)
2.3 sdmmc.c
cpp
#include <stdio.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "msdmmc.h"
#include "driver/sdmmc_host.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "sd_protocol_types.h"
#include "esp_log.h"
#include "esp_console.h"
static const char *TAG = "MSDMMC";
#define base_path "/sdcard"
static esp_err_t sdmmc_write_file(int argc, char **argv);
static esp_err_t sdmmc_read_file(int argc, char **argv);
void SDMMC_init(void)
{
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
slot_config.clk = sd_clk;
slot_config.cmd = sd_cmd;
slot_config.d0 = sd_d0;
slot_config.d1 = sd_d1;
slot_config.d2 = sd_d2;
slot_config.d3 = sd_d3;
slot_config.width = 4;
esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5,
.allocation_unit_size = 18 * 1024
};
sdmmc_card_t *card = NULL;
ESP_ERROR_CHECK(esp_vfs_fat_sdmmc_mount(base_path, &host, &slot_config, &mount_config, &card));
sdmmc_card_print_info(stdout, card);
//esp_vfs_fat_sdcard_unmount(base_path, card);
esp_console_cmd_t cmd = {
.command = "write",
.help = "write file",
.func = sdmmc_write_file,
};
esp_console_cmd_register(&cmd);
cmd.command = "read";
cmd.help = "read file";
cmd.func = sdmmc_read_file;
esp_console_cmd_register(&cmd);
}
static esp_err_t sdmmc_write_file(int argc, char **argv)
{
char path[64];
sprintf(path,"%s/%s" ,base_path ,argv[1]);
ESP_LOGI(TAG, "Opening file %s", path);
FILE *f = fopen(path, "w"); // C 标准库函数,只写模式打开或创建文件
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for writing");
return ESP_FAIL;
}
fprintf(f, "%s", argv[2]); // 向文件中写入字符串
fclose(f); // 关闭文件
ESP_LOGI(TAG, "File written");
return ESP_OK;
}
static esp_err_t sdmmc_read_file(int argc, char **argv)
{
char path[64];
sprintf(path,"%s/%s" ,base_path ,argv[1]);
ESP_LOGI(TAG, "Reading file %s", path);
FILE *f = fopen(path, "r"); // C 标准库函数,以只读模式打开文件
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for reading");
return ESP_FAIL;
}
char line[64];
fgets(line, sizeof(line), f); // C 标准库的按行读取字符串函数,从文件中读取sizeof(line)-1个字符,存储在line中
fclose(f);
char *pos = strchr(line, '\n'); // 查找换行符
if (pos) {
*pos = '\0'; // 如果换行符存在,则将换行符替换为空
}
ESP_LOGI(TAG, "Read from file: '%s'", line);
return ESP_OK;
}
2.4 sdmmc.h
cpp
#ifndef __MSDMMC_H__
#define __MSDMMC_H__
#define sd_clk 43
#define sd_cmd 44
#define sd_d0 39
#define sd_d1 40
#define sd_d2 41
#define sd_d3 42
void SDMMC_init(void);
#endif
2.5 main.c
cpp
#include <stdio.h>
#include "user.h"
#include "msdmmc.h"
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
ESP_LDOV4_SET(3300);
SDMMC_init();
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
2.6 代码解释
代码的实现比较简单,首先用函数esp_vfs_fat_sdmmc_mount一体化完成SD卡的sdmmc驱动和FAT挂载。注意对于结构体sdmmc_slot_config_t,要把预配置引脚编号换成自己实际的引脚编号。完成后可以打印一下sd卡信息。
然后我们使用前面学过的控制台命令创建函数创建sd卡的写命令和读命令,来看效果。
2.7 结果展示

sd卡是支持汉字字符存储的,可是控制台不支持,只要在控制台输出汉字便会卡死。
三、flash读写
ESP32连接flash芯片的方式有两种,一种是通过SPI总线去连接,这种称为外部spi flash,还有一种是通过flash专用管脚连接,这种可称为内部flash。
我使用的ESP32-P4内部没有自带片内flash,那么这个内部flash是必须要连接的,因为esp32有很多底层的参数和固件程序需要存储在flash上。
我们来看一下flash芯片的vfs一体挂载函数,比起上面的sdmmc函数,多了一个分区标签参数partition_label。这代表这想要flash使用FAT文件系统,需要先给flash创建一个分区。
esp_err_t esp_vfs_fat_spiflash_mount_rw_wl(const char *base_path, const char *partition_label, const esp_vfs_fat_mount_config_t *mount_config, wl_handle_t *wl_handle)
- 功能:类似esp_vfs_fat_sdmmc_mount,用于在 SPI flash中初始化 FAT 文件系统,并将其注册到 VFS 中。
- 输入参数:
- base_path :FATFS 注册所使用的路径前缀。
- partition_label :应使用的分区标签
- mount_config :指向带有额外参数的结构体的指针,用于挂载 FATFS 系统
- 输出参数:wl_handle :磨损补偿驱动器句柄
把Flash当成一整块空白硬盘(相当于电脑装的硬盘),分区就是把这块大硬盘,切成好几个独立小区域(相当于电脑的C盘、D盘等)。大家打开自己电脑的磁盘管理,会发现主硬盘会有好几个不同的盘符,每一个盘符就是分区。
如果是外部spi flash,创建分区得先初始化spi总线、总线添加flash设备、flash初始化、分区创建。如果是内部的flash,只需要分区创建即可。
创建好分区后,挂载在FAT系统,就和sdmmc一样读写即可。这里我偷个懒,目前用不到,就不写这部分程序了。
前面说过,对于ESP32-P4的内部falash,会有底层的参数和固件程序存储在上面,那么必然就已经创建了一些分区,那我们可以查看一下这些分区的信息。
先添加依赖:
bash
idf_component_register(SRCS "flash.c"
INCLUDE_DIRS "include"
REQUIRES spi_flash esp_partition)
3.1 flash.c
cpp
#include <stdio.h>
#include "flash.h"
#include "esp_flash.h"
#include "esp_partition.h"
#include "esp_log.h" // ESP32日志函数
static const char *TAG = "FLASH";
void flash_printf(void)
{
// 先判断驱动是否就绪
if (esp_flash_chip_driver_initialized(esp_flash_default_chip)) {
ESP_LOGI(TAG, "Flash 驱动初始化成功");
}
// 定义变量
uint32_t flash_id;
uint32_t curr_size, phys_size;
// 1. 读取 JEDEC ID
esp_flash_read_id(esp_flash_default_chip, &flash_id);
ESP_LOGI(TAG, "Flash JEDEC ID : 0x%06X", flash_id);
// 2. 实际使用大小 / 物理芯片容量
esp_flash_get_size(esp_flash_default_chip, &curr_size);
esp_flash_get_physical_size(esp_flash_default_chip, &phys_size);
ESP_LOGI(TAG, "💾 当前系统识别容量 : %u MB (%u 字节)", curr_size / 1024 / 1024, curr_size);
ESP_LOGI(TAG, "💾 芯片物理总容量 : %u MB (%u 字节)\n", phys_size / 1024 / 1024, phys_size);
esp_err_t flag;
esp_partition_iterator_t flash = NULL;
flag = esp_partition_find_err(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL, &flash);
if (flag == ESP_OK) {
while(flash != NULL)
{
const esp_partition_t *partition = esp_partition_get(flash);
ESP_LOGI(TAG, "💾 Flash 标签 : %s", partition->label);
ESP_LOGI(TAG, "💾 Flash 类型 : %d", partition->type);
ESP_LOGI(TAG, "💾 Flash 子类型 : %d", partition->subtype);
ESP_LOGI(TAG, "💾 Flash 地址 : 0x%06X", partition->address);
ESP_LOGI(TAG, "💾 Flash 分区大小 : %d", partition->size);
ESP_LOGI(TAG, "💾 Flash 擦除大小 : %d", partition->erase_size);
ESP_LOGI(TAG, "💾 Flash 加密 : %d", partition->encrypted);
ESP_LOGI(TAG, "💾 Flash 只读 : %d\n", partition->readonly);
flash = esp_partition_next(flash);
}
esp_partition_iterator_release(flash);
}
}
3.2 flash.h
cpp
#ifndef __FLASH_H__
#define __FLASH_H__
void flash_printf(void);
#endif
3.3 main.c
cpp
#include <stdio.h>
#include "user.h"
#include "flash.h"
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
//ESP_LDOV4_SET(3300);
flash_printf();
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
代码比较简单,都是读取信息,就不解释了
3.4 结果展示

通过flash的ID为0xC84018就能得到很多信息,
- 0xC8 → 制造商 = GD(兆易创新)
- 0x4018 → 型号 = GD25Q128(128Mbit = 16MB)
系统识别 = 物理容量 = 16MB,完全一致!说明 Flash 识别完美、接线正确、通讯正常!
系统自动初始化了3个分区
nvs 分区
Flash 标签 : nvs
类型 : 1(数据区)
子类型 : 2(NVS 格式)
地址 : 0x009000
大小 : 24576 字节(24KB)
作用:存 WiFi 密码、配置参数、设备状态
phy_init 分区
Flash 标签 : phy_init
类型 : 1(数据区)
子类型 : 1(PHY 射频参数)
地址 : 0x00F000
大小 : 4096 字节(4KB)
作用:存 WiFi 密码、配置参数、设备状态
factory 分区
Flash 标签 : factory
类型 : 0(APP 程序)
子类型 : 0(固件)
地址 : 0x010000
大小 : 1048576 字节(1MB)
作用:存放你写的固件程序
以上分区都是系统自动创建的,千万不要直接去读写,更不能在这些分区上挂载fat系统,防止系统识别失败。如果需要,一定要写入自己创建的分区中。
还有一种更方便的方法,通过创建partitions.csv,直接再里面写入分区信息即可。同样也可以在该分区文件中修改上面系统自定义分区的一些信息。 关于详情可以查看《分区表》