You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ESP32平台规避fseek实现SD卡文件同时读写的技术问询

解决ESP32同一文件同时读写的低延迟方案

针对ESP32音频存储+实时播放场景(写速250KB/s、读速16KB/s、最大文件100M、内存有限),以下是几种可行的优化方案:

方案1:优化单文件句柄读写,移除冗余fseek

你当前写端每次调用fseek(f, 0, SEEK_END)是核心性能瓶颈——大文件下FATFS需要遍历文件分配表定位末尾,耗时极高。通过修改文件打开模式可直接消除这个操作:

实现步骤:

  1. 初始化时用"a+"模式打开文件(自动追加写入,无需手动定位到末尾):
    FILE *f = fopen(filename, "a+");
    
  2. 写端逻辑简化(加互斥锁避免读写竞争):
    // WRITER TASK
    xSemaphoreTake(file_mutex, portMAX_DELAY);
    fwrite(data, 1, data_len, f);
    fflush(f);  // 按需调用,允许延迟的话可批量flush降低IO次数
    xSemaphoreGive(file_mutex);
    
  3. 读端保留原逻辑但缩短锁持有时间:
    // 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长度,导致读端拿到空文件。调整打开模式即可解决:

实现步骤:

  1. 初始化时分别打开读写句柄(确保FATFS开启多文件共享,ESP-IDF中需配置FF_FS_SHARE为1):
    FILE *fwr = fopen(filename, "a+");  // 追加写,不截断文件
    FILE *frd = fopen(filename, "r");    // 只读打开
    
  2. 写端直接追加写入(无需seek):
    // WRITER TASK
    fwrite(data, 1, data_len, fwr);
    fflush(fwr);
    
  3. 读端维护独立的读取位置,无需和写端互斥(文件系统会处理多句柄的一致性):
    // 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延迟:

实现步骤:

  1. 创建一个环形缓冲区(大小建议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;
    
  2. 写端逻辑:先把音频数据写入环形缓冲区(供读端实时读取),再异步批量写入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;
    }
    
  3. 读端逻辑:优先从环形缓冲区读取,缓冲区空时再从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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 17:06:49