外观
3.9 SD 卡文件、图片显示与音频播放
这节要搞懂:SD 卡在工程里存什么、怎么读写、怎么把图片显示出来、怎么放音频文件。 需要基础:3.8
一、工程里拿 SD 卡干什么
| 用途 | 负责的文件 | 说明 |
|---|---|---|
| 网页管理文件 | app_wifi.c | 浏览器上传 / 下载 / 删除,还能直接点播放和显示 |
| 显示图片 | app_image_viewer.c | 读 BMP 图片画到屏幕上 |
| 播放音频 | app_audio_player.c | 放完整的音频文件 |
挂载和路径管理由 app_sd.c 统一负责。
二、挂载 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);
}必须放在独立任务里
这段代码里有两处会阻塞:
fread()—— SD 卡读取偶尔会卡几十毫秒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 步:上传并播放
- 在页面上选择
test.wav,上传 - 上传完刷新,列表里应该出现它
- 点它旁边的播放按钮
- 喇叭应该放出声音
串口:
I (60000) sd: upload test.wav, 320044 bytes
I (65000) audio: play /sdcard/test.wav第 6 步:试试显示图片
准备一张 240×320 的图片传上去,点显示,屏幕上应该出现它。
尺寸对上最省事
屏幕就是 240×320,图片做成一样大不用缩放,直接推最快。
动手试试
- 挂载 SD 卡,用
readdir把根目录的文件名全部打印出来。 - 打开
192.168.4.1/sd上传一个文件,再用串口确认卡里确实多了它。 - 放一张 240×320 的 BMP 到卡里显示出来;故意不处理倒序,看看是什么效果。
- 放一个 44.1kHz 的 WAV(和 I2S 配置的 16kHz 不匹配),听听速度差多少。