ESP32平台规避fseek实现SD卡文件同时读写的技术问询
解决ESP32同一文件同时读写的低延迟方案
针对ESP32音频存储+实时播放场景(写速250KB/s、读速16KB/s、最大文件100M、内存有限),以下是几种可行的优化方案:
方案1:优化单文件句柄读写,移除冗余fseek
你当前写端每次调用fseek(f, 0, SEEK_END)是核心性能瓶颈——大文件下FATFS需要遍历文件分配表定位末尾,耗时极高。通过修改文件打开模式可直接消除这个操作:
实现步骤:
- 初始化时用
"a+"模式打开文件(自动追加写入,无需手动定位到末尾):FILE *f = fopen(filename, "a+"); - 写端逻辑简化(加互斥锁避免读写竞争):
// WRITER TASK xSemaphoreTake(file_mutex, portMAX_DELAY); fwrite(data, 1, data_len, f); fflush(f); // 按需调用,允许延迟的话可批量flush降低IO次数 xSemaphoreGive(file_mutex); - 读端保留原逻辑但缩短锁持有时间:
// READER TASK xSemaphoreTake(file_mutex, portMAX_DELAY); fseek(f, rdpos, SEEK_SET); int bytes_read = fread(buf, 1, buf_len, f); rdpos += bytes_read; xSemaphoreGive(file_mutex);
优势:完全移除写端的fseek操作,大幅降低写延迟,代码改动最小。
方案2:正确使用多文件句柄,避免文件截断
你之前两次打开文件读取无效,核心原因是用"w+"模式打开写端——该模式会截断现有文件为0长度,导致读端拿到空文件。调整打开模式即可解决:
实现步骤:
- 初始化时分别打开读写句柄(确保FATFS开启多文件共享,ESP-IDF中需配置
FF_FS_SHARE为1):FILE *fwr = fopen(filename, "a+"); // 追加写,不截断文件 FILE *frd = fopen(filename, "r"); // 只读打开 - 写端直接追加写入(无需seek):
// WRITER TASK fwrite(data, 1, data_len, fwr); fflush(fwr); - 读端维护独立的读取位置,无需和写端互斥(文件系统会处理多句柄的一致性):
// READER TASK fseek(frd, rdpos, SEEK_SET); int bytes_read = fread(buf, 1, buf_len, frd); rdpos += bytes_read;
注意:读端若需要获取最新文件大小,可调用fseek(frd, 0, SEEK_END)后用ftell(frd)获取,但尽量减少该操作频率,避免额外耗时。
方案3:引入环形缓冲区做读写隔离(最优低延迟)
由于写速远高于读速,且音频播放对读取延迟敏感,可在SD卡和读端之间加一层环形缓冲区,彻底隔离SD卡IO延迟:
实现步骤:
- 创建一个环形缓冲区(大小建议256KB~512KB,平衡内存占用和延迟):
#define BUF_SIZE 262144 // 256KB uint8_t ring_buf[BUF_SIZE]; size_t buf_head = 0, buf_tail = 0; SemaphoreHandle_t buf_mutex, buf_not_empty; - 写端逻辑:先把音频数据写入环形缓冲区(供读端实时读取),再异步批量写入SD卡:
// WRITER TASK // 1. 写入环形缓冲区 xSemaphoreTake(buf_mutex, portMAX_DELAY); size_t copy_len = min(data_len, BUF_SIZE - (buf_head - buf_tail + BUF_SIZE) % BUF_SIZE); memcpy(&ring_buf[buf_head], data, copy_len); buf_head = (buf_head + copy_len) % BUF_SIZE; xSemaphoreGive(buf_mutex); xSemaphoreGive(buf_not_empty); // 2. 异步批量写入SD卡(比如每攒够4KB写一次) static uint8_t sd_buf[4096]; static size_t sd_buf_len = 0; sd_buf_len += copy_len; if (sd_buf_len >= sizeof(sd_buf)) { fwrite(sd_buf, 1, sizeof(sd_buf), fwr); fflush(fwr); sd_buf_len = 0; } - 读端逻辑:优先从环形缓冲区读取,缓冲区空时再从SD卡补数据:
// READER TASK xSemaphoreTake(buf_not_empty, portMAX_DELAY); xSemaphoreTake(buf_mutex, portMAX_DELAY); size_t avail = (buf_head - buf_tail + BUF_SIZE) % BUF_SIZE; size_t read_len = min(buf_len, avail); memcpy(buf, &ring_buf[buf_tail], read_len); buf_tail = (buf_tail + read_len) % BUF_SIZE; xSemaphoreGive(buf_mutex); // 缓冲区空时从SD卡补充数据 if (avail < buf_len) { fseek(frd, rdpos, SEEK_SET); int sd_read = fread(buf + read_len, 1, buf_len - read_len, frd); rdpos += sd_read; read_len += sd_read; }
优势:读端几乎无延迟(从内存缓冲区取数据),彻底解决SD卡IO延迟导致的解码器缓冲区空问题;写端批量写入SD卡,提升IO效率。
额外优化建议
- 调整SD卡IO块大小:设置读写块为SD卡扇区大小(512字节)或其倍数(如4KB),减少
fwrite/fread的调用次数。 - 启用SD卡DMA:ESP-IDF中配置
SDMMC_DMA_ENABLE,提升SD卡读写速度。 - 减少
fflush频率:写端无需每次fwrite都调用fflush,可每攒够几KB再执行,降低IO次数。
内容的提问来源于stack exchange,提问作者JakobJ
相关产品推荐
相关产品推荐

