本文记录了在ESP32-P4开发板(配ST7703 LCD屏幕)上,将摄像头视频采集改为SD卡MJPEG视频播放的完整开发过程。整个过程历经多次技术选型和问题排查,最终实现了稳定的24fps多视频轮播系统。
开发环境:
初始方案:AVI容器 + MJPEG编码
最初选择了AVI容器格式,理由如下:
遇到的第一个问题:AVI文件解析
实现了基于内存搜索的AVI解析器:
// 搜索"movi"标识定位数据区
uint32_t movi_offset = search_fourcc(header_buf, read_size,
"movi");
// 逐帧读取00dc chunk
while (fread(chunk_header,
1,
8, fp) ==
8) {
if (chunk_id ==
0x63643030) {
// "00dc"
// 读取JPEG帧数据
fread(jpeg_data,
1, chunk_size, fp);
}
}
这部分基本顺利,能正确提取JPEG帧数据。
ESP32-P4内置硬件JPEG解码器,理论性能很高。按照官方文档配置:
// 创建解码器引擎
jpeg_decode_engine_cfg_t decode_eng_cfg = {
.intr_priority =
0,
.timeout_ms =
40,
};
ESP_ERROR_CHECK(jpeg_new_decoder_engine(&decode_eng_cfg, &decoder_handle));
// 分配输入/输出缓冲区
jpeg_decode_memory_alloc_cfg_t rx_mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
};
output_buf = jpeg_alloc_decoder_mem(width * height *
3, &rx_mem_cfg, &size);
运行后出现以下问题:
ESP_ERR_TIMEOUTout_size正确,但buffer内容是全0关键日志:
E (6392) jpeg.decoder: jpeg_decoder_process timeout
I (6392) video_player: Decoded frame
#1 output data:
I (6392) video_player: 00 00 00 00 00 00 00 00 00 00 00 00 ...
W (6392) video_player: JPEG decode timeout but data complete (out:691200 bytes)
猜测1:输入JPEG数据有问题?
验证JPEG数据完整性:
// 检查JPEG头尾标记
if (jpeg_data[
0] ==
0xFF && jpeg_data[
1] ==
0xD8 &&
jpeg_data[size
-2] ==
0xFF && jpeg_data[size
-1] ==
0xD9) {
ESP_LOGI(TAG,
"✓ JPEG frame is complete");
}
结果:✅ JPEG数据完整正确
猜测2:RGB字节序不对?
尝试切换
JPEG_DEC_RGB_ELEMENT_ORDER_BGR 和
RGB。
结果:❌ 无效,仍然是色块
猜测3:YUV色彩空间转换问题?
添加YUV到RGB转换配置:
.conv_std = JPEG_YUV_RGB_CONV_STD_BT601,
结果:❌ 无效
猜测4:Cache一致性问题?
这是问题的核心!尝试了多种Cache同步方案:
// 输入:CPU写入后,刷新到内存
esp_cache_msync(input_buf, size, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
// 输出:DMA写入后,失效CPU cache
esp_cache_msync(output_buf, size, ESP_CACHE_MSYNC_FLAG_DIR_M2C);
结果:各种对齐错误,数据仍然全0
关键发现:
对比代码发现:
结论:问题不在Cache同步本身,而在AVI容器格式的连续解码上。
找到乐鑫官方的MJPEG播放示例,使用的是 纯MJPEG格式(不是AVI容器):
纯MJPEG格式:
[FF D8 ... FF D9][FF D8 ... FF D9][FF D8 ... FF D9]...
JPEG帧1 JPEG帧2 JPEG帧3
AVI容器格式:
[AVI Header][LIST movi]
[00dc][size][JPEG数据]
[00dc][size][JPEG数据]
使用FFmpeg转换:
# 错误的方式(强制YUV422p)
ffmpeg -i input.avi -pix_fmt yuvj422p -f mjpeg output.mjpeg
# ❌
# 正确的方式(让FFmpeg自动选择)
ffmpeg -i input.mp4 -q:v 3 -f mjpeg output.mjpeg
# ✅
关键差异:
yuvj422p:某些YUV变体,ESP32-P4可能不完全兼容yuv420p,标准格式,完全兼容复制官方的
esp_mjpeg_decode组件:
typedef
struct {
FILE *input;
uint8_t *mjpeg_buf;
uint8_t *output_buf;
jpeg_decoder_handle_t decoder_engine;
int16_t w, h;
// ...
}
esp_mjpeg_decode_t;
// 读取一帧
esp_mjpeg_decode_read_mjpeg_buf(&mjpeg);
// 解码
esp_mjpeg_decode_jpg(&mjpeg);
// 显示
esp_lcd_panel_draw_bitmap(..., esp_mjpeg_decode_get_out_buf(&mjpeg));
结果:✅ 立即成功!视频正常播放,无超时,无色块!
使用纯MJPEG格式后:
发现参考代码的LCD配置有一个关键参数:
esp_lcd_dpi_panel_config_t dpi_config = {
// ...
.flags.use_dma2d =
true,
// ★ 关键!
};
效果:帧率从 16fps 飙升到 70-82 FPS!
原理:
对比参考代码的sdkconfig,发现关键差异:
# 你的配置(失败时) CONFIG_CACHE_L2_CACHE_128KB=y CONFIG_CACHE_L2_CACHE_LINE_64B=y # 参考代码(成功) CONFIG_CACHE_L2_CACHE_256KB=y CONFIG_CACHE_L2_CACHE_LINE_128B=y
更大的Cache和Cache Line能提升DMA传输的稳定性。
发现:不同SD卡速度差异巨大!
教训:硬件性能对整体体验影响巨大,不要忽视SD卡的选择。
全速播放是70-82 FPS,但源视频是24 FPS。如何精确控制到24fps?
失败的尝试1:固定延迟
vTaskDelay(pdMS_TO_TICKS(
41));
// 固定延迟41ms
// 结果:18-19 FPS(太慢)
// 原因:FreeRTOS tick粒度问题,延迟不精确
失败的尝试2:动态延迟
elapsed_time = 实际处理时间;
delay = target_time - elapsed_time;
vTaskDelay(pdMS_TO_TICKS(delay));
// 结果:仍然18-19 FPS
// 原因:累积误差,每帧处理时间不同
核心思想:基于绝对时间而非相对延迟
int64_t next_frame_time_us = esp_timer_get_time();
// 初始时间
int64_t frame_interval_us =
1000000 /
24;
// 41667微秒
while (read_frame()) {
// 等待到预定时间
int64_t now = esp_timer_get_time();
int64_t wait_us = next_frame_time_us - now;
if (wait_us >
1000) {
vTaskDelay(pdMS_TO_TICKS(wait_us /
1000));
}
// 解码并显示
decode_and_display();
// 更新下一帧时间(累加,不是重新计算)
next_frame_time_us += frame_interval_us;
}
效果:帧率精确控制在 23.9-24.1 FPS,误差 < 0.5%
优点:
| 格式 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| AVI容器 | 包含元数据 | 解析复杂,Cache问题 | ⭐⭐ |
| 纯MJPEG | 简单高效 | 无元数据 | ⭐⭐⭐⭐⭐ |
转换命令:
ffmpeg -i video.mp4 -vf
"scale=480:480" -r 24 -q:v 3 -f mjpeg video.mjpeg
注意:
-f mjpeg 输出纯MJPEG-pix_fmt yuvj422p(可能不兼容)正确方式:
// 输入和输出都使用 jpeg_alloc_decoder_mem
jpeg_decode_memory_alloc_cfg_t tx_mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER,
};
input_buf = jpeg_alloc_decoder_mem(jpeg_size, &tx_mem_cfg, &alloc_size);
jpeg_decode_memory_alloc_cfg_t rx_mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
};
output_buf = jpeg_alloc_decoder_mem(w * h * bpp, &rx_mem_cfg, &alloc_size);
错误方式:
// ❌ 使用普通 heap_caps_malloc
input_buf = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA);
// 可能导致DMA访问问题
关键结论:
jpeg_alloc_decoder_mem 返回的内存是
DMA-coherent的,
不需要手动Cache同步!
如果你添加了
esp_cache_msync,反而可能导致问题:
正确做法:什么都不做,让库自动处理。
必须启用DMA2D:
esp_lcd_dpi_panel_config_t dpi_config = {
// ...
.flags.use_dma2d =
true,
// ★ 关键配置
};
效果:帧率从16fps → 70+fps
固定时间间隔法:
next_frame_time += frame_interval;
// 基于绝对时间
wait_until(next_frame_time);
// 等待到这个时间点
decode_and_display();
// 然后立即处理
优于动态延迟法(
delay = target - elapsed)。
可能原因:
解决方案:
jpeg_alloc_decoder_mem 分配内存原因:单次解码和连续解码的差异。
解决方案:
esp_mjpeg_decode 组件原因:
解决方案:
原因:FreeRTOS tick粒度(1ms)+ 动态延迟算法
解决方案:
esp_timer_get_time()(微秒级)
SD卡(SDMMC) → MJPEG文件读取 → JPEG硬件解码器
↓ ↓
40MHz → DMA输出缓冲区
↓
LCD(DMA2D加速) → 屏幕显示
✅ 推荐:纯MJPEG格式
-q:v 3(平衡质量和大小)❌ 不推荐:AVI容器(除非必须使用元数据)
关键诊断点:
// 1. 验证JPEG数据完整性
ESP_LOGI(TAG,
"JPEG header: %02x %02x", data[
0], data[
1]);
// 应该是 FF D8
// 2. 验证解码输出
ESP_LOGI(TAG,
"Decoded output: %02x %02x %02x ...",
output[
0], output[
1], output[
2]);
// 不应该全是00
// 3. 测量实际处理时间
int64_t start = esp_timer_get_time();
decode();
int64_t elapsed = (esp_timer_get_time() - start) /
1000;
ESP_LOGI(TAG,
"Decode took %lld ms", elapsed);
esp_err_t
init_sd_card
(
void) {
// LDO电源配置
esp_ldo_channel_config_t ldo_config = {
.chan_id =
4,
.voltage_mv =
3300,
};
ESP_ERROR_CHECK(esp_ldo_acquire_channel(&ldo_config, &ldo_handle));
// SDMMC主机配置
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.slot = SDMMC_HOST_SLOT_1;
host.max_freq_khz = SDMMC_FREQ_HIGHSPEED;
// 挂载
const
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed =
false,
.max_files =
10,
.allocation_unit_size =
64 *
1024
};
ESP_ERROR_CHECK(esp_vfs_fat_sdmmc_mount(
"/sdcard", &host,
&slot_config, &mount_config, &card));
return ESP_OK;
}
void
play_mjpeg
(
const
char *filename) {
// 初始化解码器
esp_mjpeg_decode_t mjpeg = {
.mjpeg_buffer_size =
480 *
480,
.output_buffer_size =
480 *
480 *
3,
.decode_cfg = {
.output_format = JPEG_DECODE_OUT_FORMAT_RGB888,
.rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_BGR,
}
};
esp_mjpeg_decode_setup(&mjpeg, filename);
// 帧率控制
int64_t next_frame_time = esp_timer_get_time();
int64_t frame_interval =
1000000 /
24;
// 24 fps
// 播放循环
while (esp_mjpeg_decode_read_mjpeg_buf(&mjpeg)) {
// 等待到预定时间
int64_t wait_us = next_frame_time - esp_timer_get_time();
if (wait_us >
1000) {
vTaskDelay(pdMS_TO_TICKS(wait_us /
1000));
}
// 解码
esp_mjpeg_decode_jpg(&mjpeg);
// 显示
esp_lcd_panel_draw_bitmap(panel, x, y, x+w, y+h,
esp_mjpeg_decode_get_out_buf(&mjpeg));
// 更新下一帧时间
next_frame_time += frame_interval;
}
esp_mjpeg_decode_close(&mjpeg);
}
esp_timer# PSRAM CONFIG_SPIRAM=y CONFIG_SPIRAM_SPEED_200M=y # Cache (重要!) CONFIG_CACHE_L2_CACHE_256KB=y CONFIG_CACHE_L2_CACHE_LINE_128B=y # FAT长文件名 CONFIG_FATFS_LFN_HEAP=y CONFIG_FATFS_MAX_LFN=255 # JPEG解码器 CONFIG_SOC_JPEG_DECODE_SUPPORTED=y
idf_component_register(SRCS "main.c" "app_lcd.c" "app_sdcard.c" REQUIRES esp_mjpeg_decode esp_driver_sdmmc esp_lcd esp_lcd_st7703 esp_timer fatfs driver)
components/
├── esp_mjpeg_decode/
# MJPEG解码组件
│ ├── esp_mjpeg_decode.c
│ ├── include/
│ │ └── esp_mjpeg_decode.h
│ └── CMakeLists.txt
main/
├── main.c
# 主程序(视频轮播)
├── app_lcd.c/h
# LCD初始化
├── app_sdcard.c/h
# SD卡管理
└── CMakeLists.txt
感谢乐鑫官方技术支持和开源社区的帮助。本项目的成功很大程度上得益于参考了官方示例代码和社区经验。
作者:拆技
日期:2025年11月25日
联系方式:78680321@qq.com
关键词:ESP32-P4, MJPEG, 视频播放, JPEG硬件解码, DMA2D, SD卡, Cache一致性, 帧率控制
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70048597/viewspace-3104571/ ,如需转载,请注明出处,否则将追究法律责任。