ESP-IDF+vscode开发ESP32第七讲——存储设备读写

目录

前言

一、前提知识

[2.1 文件系统](#2.1 文件系统)

[2.2 FAT文件系统](#2.2 FAT文件系统)

[2.3 POSIX API和VFS](#2.3 POSIX API和VFS)

二、SD卡读写

[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 结果展示)

三、flash读写

[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,直接再里面写入分区信息即可。同样也可以在该分区文件中修改上面系统自定义分区的一些信息。 关于详情可以查看《分区表

相关推荐
米饭不加菜2 小时前
Visual Studio Code 的 MATLAB 扩展
vscode·matlab
被放养的研究生1 天前
Translate for Zotero
vscode
dLYG DUMS2 天前
vscode配置django环境并创建django项目(全图文操作)
vscode·django·sqlite
JAVA学习通2 天前
励志从零打造LeetCode平台之C端竞赛列表
java·vscode·leetcode·docker·状态模式
萑澈2 天前
vscode怎么关闭点击音效
ide·vscode·编辑器
NQBJT2 天前
[特殊字符] VS Code + Markdown 从入门到精通:写论文、技术文档的超实用指南
开发语言·vscode·c#·markdown
dyxal3 天前
VS Code 终端疑难杂症排查:为什么 PowerShell 无法启动?
vscode
【ql君】qlexcel3 天前
Visual Studio Code开发STM32设置头文件宏定义uint32_t报错
vscode·stm32·vs code·头文件宏定义·uint32_t报错·uint8_t报错·uint16_t报错
琉璃榴3 天前
Visual Studio Code连接远程服务器
服务器·vscode·github