链接: B站Up

ESP32S3读取写入SD卡

1.前置目的

语音识别项目中,麦克风录制的音频需存入 SD 卡,再通过电脑验证录制内容

2.工程搭建步骤

  1. 创建 drivers 目录存放驱动代码
  2. 在该目录下新建 sd_card.cpp(源文件)和 sd_card.h(头文件)
    在这里插入图片描述3. 修改 main 目录下的 CMakeLists.txt:
    在这里插入图片描述
  • 用 set 命令手动列出所有 .cpp 源文件和头文件(不建议通配符,避免匹配备份文件导致编译异常)
  • 新增文件时直接在列表中追加一行

3.头文件 关键内容

  1. 使用 C++ 简洁写法替代 C 语言的头文件保护宏:#pragma once
    C 语言传统写法
#ifndef SD_CARD_H  // 如果没定义这个宏
#define SD_CARD_H  // 定义这个宏,标记头文件已包含
// 头文件内容...
#endif  // 结束判断
  1. 定义 SD 类,包含公有的构造函数、析构函数,以及三个核心方法:
  • 写文件:传入文件名 + 内容,支持覆盖 / 追加模式
  • 读文件:传入文件名 + 缓冲区,读取指定长度内容
  • 读一行:可选方法,按需使用
#pragma once

#include "esp_err.h"

#define TAG "SDCARD"
#define SD_CARD_PIN_CLK  GPIO_NUM_47
#define SD_CARD_PIN_CMD  GPIO_NUM_48
#define SD_CARD_PIN_D0   GPIO_NUM_21
#define MOUNT_POINT "/sdcard"
class SdCard
{
private:
    /* data */
public:
    SdCard();
    ~SdCard();
    esp_err_t sdWriteFile(char *path, char *data,bool isAppend);

    esp_err_t sdCardReadFile(const char *filename, char *output, size_t output_size);

    esp_err_t sdCardReadLine(const char *filename, int line_num, char *output, size_t output_size);
};

4.源文件核心实现

  1. 初始化逻辑:在构造函数中完成 SD 卡初始化(也可独立写初始化函数)
  2. 总线配置:采用 SDIO 总线,支持一线模式(占用引脚少,仅用 DA0)和四线模式(传输快,占用 DA0-DA3)
  3. 引脚配置:需根据硬件原理图确定,示例引脚:
  • SD_COMMAND → ESP32-S3 GPIO48
  • SD_CLOCK → GPIO47
  • SD_DATA → GPIO21
  1. 挂载与校验:调用系统函数挂载 SD 卡,初始化成功则打印卡信息(容量等),失败则打印错误信息
#include "sd_card.h" 
#include "esp_log.h"
#include "esp_err.h"
#include "driver/sdmmc_host.h" 
#include "sdmmc_cmd.h"
#include "esp_vfs_fat.h" 
#include <cstdio>
#include <cstring>

SdCard::SdCard()
{
    esp_err_t ret;
    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
        .format_if_mount_failed = true,
        .max_files = 5,
        .allocation_unit_size = 16 * 1024
    };
    sdmmc_card_t *card;
    const char mount_point[] = MOUNT_POINT;
    sdmmc_host_t host = SDMMC_HOST_DEFAULT();
    sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
    slot_config.width = 1;
    slot_config.cmd = SD_CARD_PIN_CMD;
    slot_config.clk = SD_CARD_PIN_CLK;
    slot_config.d0 = SD_CARD_PIN_D0;
    slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
    ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &card);

    if (ret != ESP_OK)
    {
        ESP_LOGE(TAG, "Failed to mount SD card VFAT filesystem.");
        ESP_LOGD(TAG, "SD card error: %s", esp_err_to_name(ret));
        /* code */
        ESP_LOGI(TAG, "SD card initialized failed.");
    }
    
    ESP_LOGI(TAG, "SD card initialized.");

    //打印SD卡信息
    sdmmc_card_print_info(stdout, card);

    // return ESP_OK;

}

SdCard::~SdCard()
{
}


/** 读取指定行数据
 * file_path : 文件路径
 * line_num : 行号
 * output : 输出缓冲区
 * output_size : 输出缓冲区大小
 */
esp_err_t SdCard::sdCardReadLine(const char *filename, int line_num, char *output, size_t output_size)
{
    char line[1000]; // 假设每行最大长度为 1000 字符
    int current_line = 0;

    char  file_path[50];
    char data1[100];
    sprintf(file_path, "%s/%s", MOUNT_POINT, filename);

    if (output_size>(sizeof(line)-1))
    {
        return ESP_FAIL;
    }
    // 检查输出缓冲区是否足够大
    if (output_size < 1) {
        ESP_LOGE(TAG, "Output buffer size is too small");
        return ESP_FAIL;
    }

    FILE *f = fopen(file_path, "r");
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for reading");
        return ESP_FAIL;
    }

    // 逐行读取文件
    while (fgets(line, sizeof(line), f) != NULL) {
        current_line++; 
        // 如果当前行是目标行
        if (current_line == line_num) {
            // 去掉行末的换行符(如果有)
            size_t len = strlen(line);
            if (len > 0 && line[len - 1] == '\n') {
                line[len - 1] = '\0'; // 去掉换行符
            } 
            // 将目标行内容复制到输出缓冲区
            strncpy(output, line, output_size - 1);
            output[output_size - 1] = '\0'; // 确保字符串以 null 结尾
            fclose(f); 
            return ESP_OK;
        }
    }

    fclose(f);

    // 如果文件行数不足
    if (current_line < line_num) {
        ESP_LOGE(TAG, "File has only %d lines, requested line %d", current_line, line_num);
        return ESP_FAIL;
    }

    return ESP_FAIL;
}


/**
 * 读取指定文件内容
 * @param filename : 文件路径
 * @param output : 输出缓冲区
 * @param output_size : 输出缓冲区大小
 */
esp_err_t SdCard::sdCardReadFile(const char *filename, char *output, size_t output_size) {


    char  file_path[50];
    char data1[100];
    sprintf(file_path, "%s/%s", MOUNT_POINT, filename);


    FILE *f = fopen(file_path, "r");
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for reading");
        return ESP_FAIL;
    }

    // 获取文件大小
    fseek(f, 0, SEEK_END); // 移动到文件末尾
    long file_size = ftell(f); // 获取文件大小
    fseek(f, 0, SEEK_SET); // 回到文件开头

    if (file_size < 0) {
        ESP_LOGE(TAG, "Failed to get file size");
        fclose(f);
        return ESP_FAIL;
    }

    // 检查缓冲区是否足够大
    if (output_size < (size_t)(file_size + 1)) { // +1 用于字符串结束符
        ESP_LOGE(TAG, "Output buffer is too small (required: %ld, provided: %d)", file_size + 1, output_size);
        fclose(f);
        return ESP_FAIL;
    }

    // 读取文件内容
    size_t bytes_read = fread(output, 1, file_size, f);
    if (bytes_read != file_size) {
        ESP_LOGE(TAG, "Failed to read file content");
        fclose(f);
        return ESP_FAIL;
    }

    // 添加字符串结束符
    output[file_size] = '\0';

    fclose(f);
    ESP_LOGI(TAG, "Read entire file: %ld bytes", file_size);
    return ESP_OK;
}

 

/**
 * 像指定文件写入数据
 * @param filename : 文件路径
 * @param data : 要写入的数据
 * @param isAppend : 是否为追内容,true:追加内容  false:覆盖内容
 */
esp_err_t SdCard::sdWriteFile(char *filename, char *data,bool isAppend)
{
    esp_err_t ret; 
    char  file_path[50];
    char data1[100];
    sprintf(file_path, "%s/%s", MOUNT_POINT, filename); 
   
    FILE *f =NULL;
    if(isAppend)
    {
        f = fopen(file_path, "a");
    }else{
        f = fopen(file_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;
}

官方代码应该是
https://github.com/espressif/esp-idf/blob/master/examples/storage/sd_card/sdmmc/main/sd_card_example_main.c
在这里插入图片描述

5.功能测试流程

  1. 无卡测试:下载程序后,未插 SD 卡会触发初始化失败提示
    在这里插入图片描述

  2. 插卡测试:SD 卡字朝上插入卡槽(弹簧自锁),重新下载 / 复位,打印卡信息则初始化成功
    在这里插入图片描述

  3. 写文件测试:调用写文件方法,传入 test.txt 和 hello world,默认覆盖模式(false);通过默认参数简化调用

  4. 读文件测试:定义 100 字节缓冲区,调用读文件方法,通过 printf 打印读取内容

  5. 中文文件名支持:默认不支持中文,需通过 idf.py menuconfig 配置:

idf.py menuconfig
  • 进入 Component config → FAT Filesystem
  • 开启长文件名支持,设置编码格式为 UTF-8,勾选中文支持
  • 保存配置后重新编译下载,即可创建中文名称文件
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

SD 卡程序重构

1.编程思想转变

从面向过程编程转变为依赖倒置的面向对象编程思想
在这里插入图片描述
核心规则:上层模块不依赖底层模块的具体实现,二者都依赖抽象接口;抽象不依赖细节,细节依赖抽象。
类比理解:
抽象接口 = 灯头 / 产品经理(标准协议 / 需求对接层)
底层实现 = 灯泡 / 程序员(具体功能执行者)
上层调用 = 导线 / 客户(只对接标准接口,不关心底层实现)

2.创建抽象类

file_interface.h

// file_interface.h
#pragma once
#include <cstddef>
#include <esp_err.h> 

enum class SeekMode {
    SET = SEEK_SET,  // 从文件开头
    CUR = SEEK_CUR,  // 从当前位置
    END = SEEK_END   // 从文件末尾
};

class FileInterface {
public:
    virtual ~FileInterface() = default; // 基类的析构函数一定要是虚(virtual)函数,这样在销毁派生类的对象时才能正确调用派生类的析构函数
 
    virtual esp_err_t open(const char* filename, const char* mode) = 0;
    virtual esp_err_t close() = 0;


    // 原接口保留(可选,推荐改用新接口)
    virtual esp_err_t read_file( char* output, size_t output_size) = 0;
    virtual esp_err_t write_file( const char* data, size_t size) = 0;
    virtual esp_err_t read_line(int line_num, char *output, size_t output_size) = 0;//=0表示 纯虚函数(C++抽象方法)
    virtual esp_err_t seek(size_t offset,SeekMode mode) = 0; 

};

这段代码是C++ 抽象接口类的典型实现,核心目的是定义一套「所有存储设备(SD 卡 / Flash/U 盘)都必须遵循的文件操作标准协议」,是依赖倒置原则的核心载体。

一、先讲 virtual:为啥要加这个关键字?
当你写 FileInterface* file = new SdCard(); 时(用基类指针指向子类对象),如果 open 方法不加 virtual,调用 file->open() 只会执行 FileInterface 里的(空)逻辑;加了 virtual,才会执行 SdCard 里重写的 open 逻辑 —— 这就是 virtual 的核心:让基类指针能调用到子类的具体实现。
二、再讲 = 0:这玩意儿是干啥的?
核心作用:给子类 “定规矩”—— 这个方法你必须实现,不实现就不让你用(编译报错)。
三、最后回答:这些方法是不是继承的对象必须强制定义?
只要子类继承了 FileInterface,就必须把这些带 = 0 的方法(open/close/read_file/write_file/read_line/seek)全都重写一遍,少一个都不行 —— 编译器会直接报错,不让你创建这个子类的对象。

3.将 file_interface.h 和 sd_card 都放到 storage 目录下

在这里插入图片描述
按照下面的步骤就不会显示drivers\storage了
在这里插入图片描述

4.改写sd_card.h

#pragma once
#include <esp_err.h>
 
#include "file_interface.h"
 
#define TAG "SDCARD"
#define SD_CARD_PIN_CLK  GPIO_NUM_47
#define SD_CARD_PIN_CMD  GPIO_NUM_48
#define SD_CARD_PIN_D0   GPIO_NUM_21
#define MOUNT_POINT "/sdcard"


class SdCard : public FileInterface
{
private:
    FILE* m_file = nullptr;
public:
    SdCard();
    ~SdCard();
    esp_err_t open(const char* filename, const char* mode) override;
    esp_err_t close() override; 
    esp_err_t write_file(const char* data,size_t size ) override; 
    esp_err_t read_file( char *output, size_t output_size) override; 
    esp_err_t read_line(int line_num, char *output, size_t output_size) override;
    esp_err_t seek(size_t offset,SeekMode mode) override;
};
 

5.改写sd_card.cpp

#include "sd_card.h" 
#include "esp_log.h"
#include "esp_err.h"
#include "driver/sdmmc_host.h" 
#include "sdmmc_cmd.h"
#include "esp_vfs_fat.h" 
#include <cstdio>
#include <cstring>

SdCard::SdCard()
{
    esp_err_t ret;
    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
        .format_if_mount_failed = true,
        .max_files = 5,
        .allocation_unit_size = 16 * 1024
    };
    sdmmc_card_t *card;
    const char mount_point[] = MOUNT_POINT;
    sdmmc_host_t host = SDMMC_HOST_DEFAULT();
    sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
    slot_config.width = 1;
    slot_config.cmd = SD_CARD_PIN_CMD;
    slot_config.clk = SD_CARD_PIN_CLK;
    slot_config.d0 = SD_CARD_PIN_D0;
    slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
    ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &card);

    if (ret != ESP_OK)
    {
        ESP_LOGE(TAG, "Failed to mount SD card VFAT filesystem.");
        ESP_LOGD(TAG, "SD card error: %s", esp_err_to_name(ret));
        /* code */
        ESP_LOGI(TAG, "SD card initialized failed.");
        
    }
    
    ESP_LOGI(TAG, "SD card initialized.");

    //打印SD卡信息
    sdmmc_card_print_info(stdout, card);

    // return ESP_OK;

}

SdCard::~SdCard()
{
}

esp_err_t SdCard::open(const char* filename, const char* mode) {

    char  file_path[50]; 
    sprintf(file_path, "%s/%s", MOUNT_POINT, filename);

    m_file = fopen(file_path, mode);
    return m_file ? ESP_OK : ESP_FAIL;
}

esp_err_t SdCard::close() {
    if (m_file) {
        fclose(m_file);
        m_file = nullptr;
    }
    return ESP_OK;
}


/** 读取指定行数据
 * file_path : 文件路径
 * line_num : 行号
 * output : 输出缓冲区
 * output_size : 输出缓冲区大小
 */
esp_err_t SdCard::read_line( int line_num, char *output, size_t output_size)
{
    char line[1000]; // 假设每行最大长度为 1000 字符
    int current_line = 0;

    char  file_path[50];
    char data1[100]; 

    if (output_size>(sizeof(line)-1))
    {
        return ESP_FAIL;
    }
    // 检查输出缓冲区是否足够大
    if (output_size < 1) {
        ESP_LOGE(TAG, "Output buffer size is too small");
        return ESP_FAIL;
    } 

    // 逐行读取文件
    while (fgets(line, sizeof(line), m_file) != NULL) {
        current_line++; 
        // 如果当前行是目标行
        if (current_line == line_num) {
            // 去掉行末的换行符(如果有)
            size_t len = strlen(line);
            if (len > 0 && line[len - 1] == '\n') {
                line[len - 1] = '\0'; // 去掉换行符
            } 
            // 将目标行内容复制到输出缓冲区
            strncpy(output, line, output_size - 1);
            output[output_size - 1] = '\0'; // 确保字符串以 null 结尾 
            return ESP_OK;
        }
    }
 

    // 如果文件行数不足
    if (current_line < line_num) {
        ESP_LOGE(TAG, "File has only %d lines, requested line %d", current_line, line_num);
        return ESP_FAIL;
    }

    return ESP_FAIL;
}


/**
 * 读取指定文件内容
 * @param filename : 文件路径
 * @param output : 输出缓冲区
 * @param output_size : 输出缓冲区大小
 */
esp_err_t SdCard::read_file(char *output, size_t output_size) {
 
    if (m_file == nullptr) {
        ESP_LOGE(TAG, "File not opened");
        return ESP_ERR_INVALID_STATE;
    } 
    // 获取当前文件指针位置
    long current_pos = ftell(m_file);
    ESP_LOGD(TAG, "Current file position before read: %ld", current_pos); 
    // 获取文件大小
    if (fseek(m_file, 0, SEEK_END) != 0) {
        ESP_LOGE(TAG, "Failed to seek to end");
        return ESP_FAIL;
    } 
    long file_size = ftell(m_file);
    if (file_size < 0) {
        ESP_LOGE(TAG, "Failed to get file size");
        return ESP_FAIL;
    } 
    // 重置文件指针
    if (fseek(m_file, 0, SEEK_SET) != 0) {
        ESP_LOGE(TAG, "Failed to seek to start");
        return ESP_FAIL;
    } 
    // 检查缓冲区大小
    if (output_size < (size_t)(file_size + 1)) {
        ESP_LOGE(TAG, "Buffer too small (need %ld, have %d)", 
                file_size + 1, output_size);
        return ESP_ERR_INVALID_SIZE;
    } 
    // 读取文件内容
    size_t bytes_read = fread(output, 1, file_size, m_file);
    output[bytes_read] = '\0';  // 确保字符串终止
  

    ESP_LOGI(TAG, "Read %zu/%ld bytes: %.*s", 
             bytes_read, file_size, 
             (int)(bytes_read > 50 ? 50 : bytes_read), output);

    if (bytes_read != (size_t)file_size) {
        ESP_LOGW(TAG, "Partial read (expected %ld, got %zu)", 
                file_size, bytes_read);
    }

    return ESP_OK;
}

 

/**
 * 像指定文件写入数据
 * @param filename : 文件路径
 * @param data : 要写入的数据
 * @param isAppend : 是否为追内容,true:追加内容  false:覆盖内容
 */
esp_err_t SdCard::write_file(const char*  data,size_t size)
{ 
    
    return fwrite(data, 1, size, m_file) == size ? ESP_OK : ESP_FAIL;  
}

esp_err_t SdCard::seek(size_t offset,SeekMode mode) {
    if (!m_file) {
        return ESP_ERR_INVALID_STATE; // 文件未打开
    }

    int origin;
    switch (mode) {
        case SeekMode::SET: origin = SEEK_SET; break;
        case SeekMode::CUR: origin = SEEK_CUR; break;
        case SeekMode::END: origin = SEEK_END; break;
        default: return ESP_ERR_INVALID_ARG;
    }

    if (fseek(m_file, offset, origin) != 0) {
        return ESP_FAIL; // 移动失败
    }

    return ESP_OK;
}

6.改写CMakeList.txt

set(SOURCES "app.cpp"
    "drivers/storage/sd_card.cpp" 
)

set(INCLUDE_DIRS "." 
            "drivers"
            "drivers/storage"
)


idf_component_register(SRCS ${SOURCES} 
                INCLUDE_DIRS ${INCLUDE_DIRS} 
                )

7.测试代码

char buffer[100];

extern "C" void app_main(void)
{  
    auto sdFile = std::make_shared<SdCard>();
    ESP_ERROR_CHECK(sdFile->open("文件111111.txt", "w+")); // 改为追加模式  
    sdFile->write_file("Hello World!", strlen("Hello World!"));  
    sdFile->read_file(buffer, sizeof(buffer));
    std::printf("buffer = %s\n", buffer);
    sdFile->close(); 
}

8.小结

表面变化:新增file_interface.h抽象接口,sd_card类继承该接口,sd_card.cpp仅微调(读写不再传路径,用文件句柄);
核心变化:从「上层直接绑死 SD 卡」变成「上层只认通用接口,SD 卡只是接口的一个实现」,换存储介质(如 Flash)只需加新实现类,上层代码零修改。

有无总接口的开发场景对比

场景 无总接口(改写前) 有总接口(改写后)
新增 Flash 存储 1. 写 Flash 类;
2. 把上层所有调用SdCard的代码,逐行改成FlashCard
3. 还要检查 Flash 类的方法名/参数是否和 SD 卡一致,不一致还要改
1. 写 Flash 类(继承总接口);
2. 只改一行:new SdCard()new FlashFile()
3. 不用改任何上层业务代码
同时用 SD 卡 + Flash 上层代码要分别记 SD 卡和 Flash 的调用方式,容易混(比如一个要传路径,一个不用) 上层用同一套调用逻辑,只是换个实现类,不用记两套规则
后期改需求(比如加 U 盘) 又要逐行改上层代码,改一处错一处,调试成本高 只加 U 盘实现类,改一行代码,几乎无调试成本
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐