Skip to content

3.9 SD 卡文件、图片显示与音频播放 ​

这节要搞懂:SD 卡在工程里存什么、怎么读写、怎么把图片显示出来、怎么放音频文件。 需要基础:3.8

一、工程里拿 SD 卡干什么 ​

用途负责的文件说明
网页管理文件app_wifi.c浏览器上传 / 下载 / 删除,还能直接点播放和显示
显示图片app_image_viewer.c读 BMP 图片画到屏幕上
播放音频app_audio_player.c放完整的音频文件

挂载和路径管理由 app_sd.c 统一负责。

和闹钟提示音不是一回事

3.7 的闹铃是代码实时算出来的方波,不需要 SD 卡。

这里说的是播放卡里的音频文件 —— 两条独立的路径。

二、挂载 SD 卡 ​

挂载(Mount)就是把 SD 卡的文件系统"接入"到程序里,之后就能像操作普通文件一样读写它。

c
#include "esp_vfs_fat.h"
#include "driver/sdmmc_host.h"

static sdmmc_card_t *s_card;

esp_err_t app_sd_mount(void)
{
    sdmmc_host_t host = SDMMC_HOST_DEFAULT();

    sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
    slot_config.width = 1;                            // ⭐ 1 线模式
    slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;

    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
        .format_if_mount_failed = false,              // ⚠️ 别设 true
        .max_files = 5,
    };

    return esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config,
                                   &mount_config, &s_card);
}

为什么用 1 线模式

SDMMC 有 1 线和 4 线两种接法:

1 线4 线
占用引脚少 3 个多
速度约 5MB/s约 20MB/s
稳定性飞线也很稳线一长就容易出错

放音频、看图片这点数据量,1 线完全够用,还省引脚、更稳定。

卡必须是 FAT32

64GB 以上的卡默认是 exFAT,ESP32 读不了。

买 32GB 或更小的,在电脑上格式化成 FAT32。

三、挂载之后就是普通文件操作 ​

c
// 写
FILE *f = fopen("/sdcard/log.csv", "a");          // "a" = 追加
if (f != NULL) {
    fprintf(f, "2026-09-04 15:30,26.5,62.1\n");
    fclose(f);
}

// 读
FILE *f = fopen("/sdcard/music/alarm.wav", "rb");
if (f != NULL) {
    uint8_t buf[4096];
    size_t n = fread(buf, 1, sizeof(buf), f);
    fclose(f);
}

// 列目录
DIR *dir = opendir("/sdcard");
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
    ESP_LOGI(TAG, "%s", entry->d_name);
}
closedir(dir);

// 删除
unlink("/sdcard/old.wav");

就是标准 C 库那一套,没有额外要学的。

四、网页管理文件 ​

浏览器打开 http://192.168.4.1/sd(或设备的局域网 IP)就能管理卡里的文件。

配网用的那个 HTTP Server(3.8)挂了这些接口:

方法路径干什么怎么实现
GET/sd文件列表页面遍历目录拼成 HTML
POST/sd/upload?name=xxx上传循环 httpd_req_recv(),边收边 fwrite
GET/sd/file?name=xxx下载边读边 httpd_resp_send_chunk()
POST/sd/delete删除unlink()
POST/sd/show屏幕显示图片读图片文件 → 推给 LCD
POST/sd/show-rgb565显示 RGB565 图片数据已是屏幕格式,直接推
POST/sd/audio-play播放音频文件读文件 → 喂 I2S
POST/sd/audio-pcm播放 PCM 裸数据没有文件头,直接喂 I2S
POST/sd/audio-stop停止播放置标志位让播放任务退出
POST/sd/exit退出显示 / 播放回到主界面

为什么图片和音频各有两个接口

因为文件格式不同,处理方式也不同:

需要解析吗谁更省 CPU
/sd/show(如 BMP)要读文件头、可能要转格式通用
/sd/show-rgb565不用,数据就是屏幕要的格式⭐ 直接推,最快
/sd/audio-play(如 WAV)要跳过 44 字节文件头通用
/sd/audio-pcm不用,就是裸采样数据⭐ 拿来就喂

规律是一样的:带文件头的通用格式方便管理,裸数据格式省事省 CPU。

设备端资源紧张时,提前在电脑上转好格式比在设备上解析划算。

上传的关键是分块处理(文件可能有几 MB,不可能一次读进内存):

c
static esp_err_t upload_handler(httpd_req_t *req)
{
    FILE *f = fopen("/sdcard/upload.tmp", "wb");
    if (f == NULL) {
        return ESP_FAIL;
    }

    char buf[2048];
    int remaining = req->content_len;

    while (remaining > 0) {
        int received = httpd_req_recv(req, buf,
                                      MIN(remaining, sizeof(buf)));
        if (received <= 0) {
            fclose(f);
            return ESP_FAIL;
        }

        fwrite(buf, 1, received, f);      // 边收边写,不占内存
        remaining -= received;
    }

    fclose(f);
    httpd_resp_sendstr(req, "upload ok");
    return ESP_OK;
}

为什么要分块

一个 3MB 的音频文件,芯片总共才 300KB 内存 —— 根本读不进来。

收一块、写一块,内存里永远只有 2KB。这是嵌入式处理大文件的通用套路。

五、显示 SD 卡里的图片 ​

app_image_viewer.c 负责把图片画到屏幕上。

为什么用 BMP 格式:

格式解码难度文件大小
BMP几乎不用解码,像素数据直接排在文件里大
JPG / PNG要引入解码库,吃内存和 CPU小

对于本地 SD 卡(空间不值钱)BMP 是最省事的选择 —— 读出来直接就能往屏幕上推。

基本流程:

c
// 1. 读 BMP 文件头,拿到宽、高、色深、像素数据起始位置
// 2. 从像素数据起始位置开始,一行一行读
// 3. 每读一行,转成 RGB565,推给屏幕

uint16_t line_buf[240];                            // 一行的缓冲

for (int y = 0; y < height; y++) {
    fread(raw_line, 1, line_bytes, f);
    convert_to_rgb565(raw_line, line_buf, width);  // 转成屏幕认的格式

    LCD_Address_Set(0, y, width - 1, y);           // 圈定这一行的范围
    for (int x = 0; x < width; x++) {
        LCD_WR_DATA(line_buf[x]);                  // 一个像素一个像素推过去
    }
}

BMP 的两个坑

① 图像是倒着存的 —— 标准 BMP 从最后一行开始存,直接按顺序画会上下颠倒。

② 每行要 4 字节对齐 —— 一行像素数据末尾可能有填充字节,算行长度时要加上。

图片尺寸要匹配

屏幕是 240×320,图片也做成 240×320 最省事,不用做缩放。

缩放要么牺牲画质(丢采样点),要么很吃 CPU(插值算法)。

六、播放 SD 卡里的音频 ​

app_audio_player.c 负责,思路和 3.7 播放提示音一样,只是数据来源从"算出来"变成"读文件":

c
void audio_play_file(const char *path)
{
    FILE *f = fopen(path, "rb");
    if (f == NULL) {
        return;
    }

    fseek(f, 44, SEEK_SET);                  // 跳过 WAV 的 44 字节文件头

    uint8_t buf[4096];
    size_t n;

    while ((n = fread(buf, 1, sizeof(buf), f)) > 0 && s_playing) {
        size_t written;
        i2s_channel_write(s_tx_chan, buf, n, &written, portMAX_DELAY);
    }

    fclose(f);
}

必须放在独立任务里

这段代码里有两处会阻塞:

  1. fread() —— SD 卡读取偶尔会卡几十毫秒
  2. i2s_channel_write() —— DMA 缓冲满了就等

放在主界面循环里,屏幕会一顿一顿的。必须拆成独立任务(第一章 1.4 讲过)。

WAV 文件的格式要对得上

播放前要确认文件的采样率、位深、声道数和 I2S 的配置一致,否则声音会:

现象原因
速度不对(快进/慢放)采样率不匹配
全是噪声位深不匹配(16 位当 32 位读)
只有一个喇叭响声道数不匹配

统一做成 16kHz / 16 位 / 单声道最省事。

本节要点 ​

  • SD 卡在工程里有三个用途:网页管理、显示图片、播放音频
  • 网页地址 192.168.4.1/sd,十个接口覆盖上传下载删除、显示、播放
  • 卡必须是 FAT32,32GB 以下;用 1 线模式更稳更省引脚
  • 挂载后就是标准 C 文件操作
  • 上传大文件要边收边写,内存里只放一小块
  • 图片有两条路:通用格式(要解析)和 RGB565 裸数据(直接推,最快)
  • 播放音频和放提示音是同一套 I2S,只是数据来源不同
  • ⭐ 读卡和播放都会阻塞,必须放独立任务

操作过程 ​

目标:用网页往 SD 卡传一个音频,点一下让设备播出来。

第 1 步:准备卡 ​

要求说明
容量32GB 或更小
格式FAT32

在电脑上格式化好,插进开发板的卡槽。

64GB 以上的卡读不了

默认是 exFAT 格式,ESP32 认不出来。

第 2 步:准备一个音频文件 ​

用 Audacity 或在线转换工具,导出成:

格式:WAV
采样率:16000 Hz
位深:16 位
声道:单声道

文件名用英文,比如 test.wav。

第 3 步:确认卡挂上了 ​

烧录后看串口:

I (3400) sd: sd card mounted
Name: SD32G  Type: SDHC/SDXC  Size: 30436MB

挂不上就别往下走

网页管理、播放、显示图片全都依赖它。

第 4 步:打开网页管理 ​

浏览器访问(两个地址都行):

http://192.168.4.1/sd          ← 连设备热点时
http://192.168.1.107/sd        ← 连同一路由器时(IP 看串口)

应该能看到卡里的文件列表。

第 5 步:上传并播放 ​

  1. 在页面上选择 test.wav,上传
  2. 上传完刷新,列表里应该出现它
  3. 点它旁边的播放按钮
  4. 喇叭应该放出声音

串口:

I (60000) sd: upload test.wav, 320044 bytes
I (65000) audio: play /sdcard/test.wav

第 6 步:试试显示图片 ​

准备一张 240×320 的图片传上去,点显示,屏幕上应该出现它。

尺寸对上最省事

屏幕就是 240×320,图片做成一样大不用缩放,直接推最快。

动手试试 ​

  1. 挂载 SD 卡,用 readdir 把根目录的文件名全部打印出来。
  2. 打开 192.168.4.1/sd 上传一个文件,再用串口确认卡里确实多了它。
  3. 放一张 240×320 的 BMP 到卡里显示出来;故意不处理倒序,看看是什么效果。
  4. 放一个 44.1kHz 的 WAV(和 I2S 配置的 16kHz 不匹配),听听速度差多少。