【AI小智硬件程序(三)】
只要子类继承了 FileInterface,就必须把这些带 = 0 的方法(open/close/read_file/write_file/read_line/seek)全都重写一遍,少一个都不行 —— 编译器会直接报错,不让你创建这个子类的对象。
AI小智硬件程序(三)
链接: B站Up
ESP32S3读取写入SD卡
1.前置目的
语音识别项目中,麦克风录制的音频需存入 SD 卡,再通过电脑验证录制内容
2.工程搭建步骤
- 创建 drivers 目录存放驱动代码
- 在该目录下新建 sd_card.cpp(源文件)和 sd_card.h(头文件)
3. 修改 main 目录下的 CMakeLists.txt:
- 用 set 命令手动列出所有 .cpp 源文件和头文件(不建议通配符,避免匹配备份文件导致编译异常)
- 新增文件时直接在列表中追加一行
3.头文件 关键内容
- 使用 C++ 简洁写法替代 C 语言的头文件保护宏:#pragma once
C 语言传统写法
#ifndef SD_CARD_H // 如果没定义这个宏
#define SD_CARD_H // 定义这个宏,标记头文件已包含
// 头文件内容...
#endif // 结束判断
- 定义 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.源文件核心实现
- 初始化逻辑:在构造函数中完成 SD 卡初始化(也可独立写初始化函数)
- 总线配置:采用 SDIO 总线,支持一线模式(占用引脚少,仅用 DA0)和四线模式(传输快,占用 DA0-DA3)
- 引脚配置:需根据硬件原理图确定,示例引脚:
- SD_COMMAND → ESP32-S3 GPIO48
- SD_CLOCK → GPIO47
- SD_DATA → GPIO21
- 挂载与校验:调用系统函数挂载 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.功能测试流程
-
无卡测试:下载程序后,未插 SD 卡会触发初始化失败提示

-
插卡测试:SD 卡字朝上插入卡槽(弹簧自锁),重新下载 / 复位,打印卡信息则初始化成功

-
写文件测试:调用写文件方法,传入 test.txt 和 hello world,默认覆盖模式(false);通过默认参数简化调用
-
读文件测试:定义 100 字节缓冲区,调用读文件方法,通过 printf 打印读取内容
-
中文文件名支持:默认不支持中文,需通过 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 盘实现类,改一行代码,几乎无调试成本 |
更多推荐


所有评论(0)