ITPub博客

首页 > 嵌入式/内核开发 > 嵌入式/内核开发 > ESP32-P4 MJPEG视频播放器开发实战:从摄像头到SD卡的完整解决方案

ESP32-P4 MJPEG视频播放器开发实战:从摄像头到SD卡的完整解决方案

原创 嵌入式/内核开发 作者:chaiji 时间:2025-12-05 22:27:51 0 删除 编辑

ESP32-P4 MJPEG视频播放器开发实战:从摄像头到SD卡的完整解决方案

项目背景

本文记录了在ESP32-P4开发板(配ST7703 LCD屏幕)上,将摄像头视频采集改为SD卡MJPEG视频播放的完整开发过程。整个过程历经多次技术选型和问题排查,最终实现了稳定的24fps多视频轮播系统。

开发环境:

  • 芯片:ESP32-P4
  • 屏幕:ST7703 MIPI-DSI (720x720)
  • ESP-IDF:v5.5.1
  • 视频格式:MJPEG (480x480 @ 24fps)

第一阶段:技术选型与初步实现

1.1 文件格式选择

初始方案:AVI容器 + MJPEG编码

最初选择了AVI容器格式,理由如下:

  • 成熟的格式,有现成的解析库
  • 包含完整的元数据(分辨率、帧率等)
  • 可以直接从已有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帧数据。

1.2 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);

第二阶段:问题爆发 - 解码失败与色块

2.1 现象描述

运行后出现以下问题:

  1. 每帧都超时: ESP_ERR_TIMEOUT
  2. 输出数据全0:即使 out_size正确,但buffer内容是全0
  3. 屏幕显示规则色块/网格:绿色、紫色、粉色相间的马赛克

关键日志:


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)

2.2 问题排查过程

猜测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

2.3 对比测试:单张照片 vs 视频

关键发现:

  • ✅ 单张JPEG照片能正常解码显示
  • ❌ AVI视频每帧都失败

对比代码发现:

  • 照片测试: 不调用任何Cache同步,却能正常工作
  • 视频播放:添加了各种Cache同步,反而失败

结论:问题不在Cache同步本身,而在AVI容器格式的连续解码上。


第三阶段:转折点 - 切换到纯MJPEG格式

3.1 发现参考代码

找到乐鑫官方的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数据]

3.2 视频格式转换

使用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,标准格式,完全兼容

3.3 集成参考代码

复制官方的 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));

结果:✅ 立即成功!视频正常播放,无超时,无色块!


第四阶段:性能优化

4.1 初始性能

使用纯MJPEG格式后:

  • 帧率:16-18 FPS
  • 瓶颈分析:
    • JPEG解码:~40ms
    • SD卡读取:~2ms
    • LCD刷新:~18ms
    • 总计:~60ms = 16.7 FPS

4.2 关键优化:启用DMA2D

发现参考代码的LCD配置有一个关键参数:



esp_lcd_dpi_panel_config_t dpi_config = {

     // ...
    .flags.use_dma2d =  true,   // ★ 关键!
};

效果:帧率从 16fps 飙升到 70-82 FPS!

原理:

  • 不启用DMA2D:CPU逐字节复制像素数据到LCD
  • 启用DMA2D:硬件DMA直接传输,CPU只需触发

4.3 Cache配置优化

对比参考代码的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传输的稳定性。

4.4 SD卡速度优化

发现:不同SD卡速度差异巨大!

  • 旧卡(SDSC):40 MHz → 16-18 fps
  • 新卡(SDHC):52 MHz → 70-82 fps

教训:硬件性能对整体体验影响巨大,不要忽视SD卡的选择。


第五阶段:帧率精确控制

5.1 问题

全速播放是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
// 原因:累积误差,每帧处理时间不同

5.2 成功的方案:固定时间间隔法

核心思想:基于绝对时间而非相对延迟



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%

优点:

  1. 消除累积误差
  2. 自动补偿慢帧
  3. 基于高精度定时器(微秒级)

核心技术要点总结

1. 文件格式选择

格式 优点 缺点 推荐度
AVI容器 包含元数据 解析复杂,Cache问题 ⭐⭐
纯MJPEG 简单高效 无元数据 ⭐⭐⭐⭐⭐

转换命令:


ffmpeg -i video.mp4 -vf 
"scale=480:480" -r 24 -q:v 3 -f mjpeg video.mjpeg

注意:

  • ✅ 使用 -f mjpeg 输出纯MJPEG
  • ✅ 让FFmpeg自动选择色彩空间(通常是yuv420p)
  • ❌ 不要强制 -pix_fmt yuvj422p(可能不兼容)

2. 内存分配

正确方式:



// 输入和输出都使用 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访问问题

3. Cache同步

关键结论: jpeg_alloc_decoder_mem 返回的内存是 DMA-coherent的, 不需要手动Cache同步!

如果你添加了 esp_cache_msync,反而可能导致问题:

  • C2M(Cache to Memory):会覆盖DMA写入的数据
  • M2C(Memory to Cache):可能有对齐错误

正确做法:什么都不做,让库自动处理。

4. LCD加速

必须启用DMA2D:



esp_lcd_dpi_panel_config_t dpi_config = {

     // ...
    .flags.use_dma2d =  true,   // ★ 关键配置
};

效果:帧率从16fps → 70+fps

5. 帧率控制

固定时间间隔法:


next_frame_time += frame_interval;  
// 基于绝对时间

wait_until(next_frame_time);         // 等待到这个时间点
decode_and_display();                // 然后立即处理

优于动态延迟法( delay = target - elapsed)。


常见问题与解决方案

Q1: JPEG解码器每帧都超时,输出全0

可能原因:

  1. 文件格式问题(AVI容器有兼容性问题)
  2. Cache一致性问题
  3. 内存分配不正确

解决方案:

  1. ✅ 改用纯MJPEG格式
  2. ✅ 使用 jpeg_alloc_decoder_mem 分配内存
  3. ✅ 不要手动Cache同步

Q2: 单张照片能解码,视频不行

原因:单次解码和连续解码的差异。

解决方案:

  • 使用参考代码的 esp_mjpeg_decode 组件
  • 确保视频格式是标准MJPEG(不是AVI)

Q3: 屏幕显示规则色块/网格

原因:

  1. 解码失败但返回了错误的成功状态
  2. 显示了未初始化的内存
  3. LCD DMA2D未启用

解决方案:

  1. 解决解码问题(参考Q1)
  2. 启用DMA2D

Q4: 帧率无法精确控制

原因:FreeRTOS tick粒度(1ms)+ 动态延迟算法

解决方案:

  • 使用固定时间间隔法
  • 基于 esp_timer_get_time()(微秒级)

最终实现效果

性能指标

  • JPEG解码能力:70-82 FPS(硬件极限)
  • 实际播放帧率:24.00-24.06 FPS(精确控制,误差<0.3%)
  • 视频切换:7个视频自动轮播,无缝切换
  • 稳定性:长时间运行85000+帧无崩溃

系统架构


SD卡(SDMMC) → MJPEG文件读取 → JPEG硬件解码器 

    ↓                               ↓
  40MHz              →        DMA输出缓冲区
                                    ↓
                           LCD(DMA2D加速) → 屏幕显示

资源使用

  • RAM:约20KB(栈+全局变量,使用堆分配避免栈溢出)
  • PSRAM:约2MB(JPEG缓冲区)
  • CPU占用:单核,约30%(大部分时间在等待DMA)

开发建议与最佳实践

1. 文件格式

✅ 推荐:纯MJPEG格式

  • 简单、高效、兼容性好
  • 使用FFmpeg转换,质量参数 -q:v 3(平衡质量和大小)

❌ 不推荐:AVI容器(除非必须使用元数据)

2. 开发流程

  1. 先测试单张JPEG解码:验证基本功能
  2. 再测试纯MJPEG播放:验证连续解码
  3. 最后优化性能和帧率:DMA2D、帧率控制

3. 调试技巧

关键诊断点:



// 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);

4. 性能优化清单

  • ✅ 使用纯MJPEG格式(避免容器解析开销)
  • ✅ 启用LCD DMA2D加速
  • ✅ 使用高速SD卡(Class 10或以上)
  • ✅ 适当调整L2 Cache大小(建议256KB)
  • ✅ 使用堆内存分配大对象(避免栈溢出)

完整代码示例

SD卡初始化




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;
}

MJPEG播放主循环




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);
}

经验教训

技术层面

  1. 不要过度优化:参考代码不做Cache同步也能工作,说明库已经处理好了
  2. 格式很重要:纯MJPEG比AVI容器简单可靠得多
  3. 硬件加速必须启用:DMA2D能带来4-5倍性能提升
  4. 精确延迟需要高精度定时器:FreeRTOS tick不够,要用 esp_timer

调试层面

  1. 对比测试法:单张照片 vs 视频,快速定位问题域
  2. 参考代码是金矿:官方示例代码已经踩过坑,直接使用最可靠
  3. 打印诊断信息:关键数据点(JPEG头、输出前16字节、地址)帮助快速定位
  4. 硬件也是变量:不要忽视SD卡等外设的影响

附录:完整配置清单

sdkconfig 关键配置

# 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

CMakeLists.txt

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

项目成果

  • 源代码:https://github.com/your-repo/esp32p4-mjpeg-player
  • 演示视频:[YouTube链接]
  • 性能测试:24fps稳定运行24小时+无崩溃

参考资料

  1. ESP-IDF JPEG编解码器文档
  2. SDMMC主机驱动文档
  3. ESP32-P4官方MJPEG示例代码
  4. FFmpeg官方文档

致谢

感谢乐鑫官方技术支持和开源社区的帮助。本项目的成功很大程度上得益于参考了官方示例代码和社区经验。


作者:拆技 日期:2025年11月25日
联系方式:78680321@qq.com


关键词:ESP32-P4, MJPEG, 视频播放, JPEG硬件解码, DMA2D, SD卡, Cache一致性, 帧率控制


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70048597/viewspace-3104571/ ,如需转载,请注明出处,否则将追究法律责任。

下一篇: 没有了~
请登录后发表评论 登录
全部评论

注册时间:2025-12-04

  • 博文量
    5
  • 访问量
    4442