博客 / 詳情

返回

ESP32-P4 MJPEG視頻播放器開發實戰:從攝像頭到SD卡的完整解決方案

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 現象描述
運行後出現以下問題:

每幀都超時:ESP_ERR_TIMEOUT
輸出數據全0:即使out_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)
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]...
JPEG幀1 JPEG幀2 JPEG幀3
AVI容器格式:

AVI Header
00dc[JPEG數據]
00dc[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. 文件格式選擇
    格式 優點 缺點 推薦度
    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(可能不兼容)

  1. 內存分配
    正確方式:

// 輸入和輸出都使用 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訪問問題

  1. Cache同步
    關鍵結論:jpeg_alloc_decoder_mem 返回的內存是DMA-coherent的,不需要手動Cache同步!

如果你添加了 esp_cache_msync,反而可能導致問題:

C2M(Cache to Memory):會覆蓋DMA寫入的數據
M2C(Memory to Cache):可能有對齊錯誤
正確做法:什麼都不做,讓庫自動處理。

  1. LCD加速
    必須啓用DMA2D:

esp_lcd_dpi_panel_config_t dpi_config = {

// ...
.flags.use_dma2d = true,  // ★ 關鍵配置

};
效果:幀率從16fps → 70+fps

  1. 幀率控制
    固定時間間隔法:

next_frame_time += frame_interval; // 基於絕對時間
wait_until(next_frame_time); // 等待到這個時間點
decode_and_display(); // 然後立即處理
優於動態延遲法(delay = target - elapsed)。

常見問題與解決方案
Q1: JPEG解碼器每幀都超時,輸出全0
可能原因:

文件格式問題(AVI容器有兼容性問題)
Cache一致性問題
內存分配不正確
解決方案:

✅ 改用純MJPEG格式
✅ 使用 jpeg_alloc_decoder_mem 分配內存
✅ 不要手動Cache同步
Q2: 單張照片能解碼,視頻不行
原因:單次解碼和連續解碼的差異。

解決方案:

使用參考代碼的 esp_mjpeg_decode 組件
確保視頻格式是標準MJPEG(不是AVI)
Q3: 屏幕顯示規則色塊/網格
原因:

解碼失敗但返回了錯誤的成功狀態
顯示了未初始化的內存
LCD DMA2D未啓用
解決方案:

解決解碼問題(參考Q1)
啓用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容器(除非必須使用元數據)

  1. 開發流程
    先測試單張JPEG解碼:驗證基本功能
    再測試純MJPEG播放:驗證連續解碼
    最後優化性能和幀率:DMA2D、幀率控制
  2. 調試技巧
    關鍵診斷點:

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

  1. 性能優化清單
    ✅ 使用純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);
    }
    經驗教訓
    技術層面
    不要過度優化:參考代碼不做Cache同步也能工作,説明庫已經處理好了
    格式很重要:純MJPEG比AVI容器簡單可靠得多
    硬件加速必須啓用:DMA2D能帶來4-5倍性能提升
    精確延遲需要高精度定時器:FreeRTOS tick不夠,要用 esp_timer
    調試層面
    對比測試法:單張照片 vs 視頻,快速定位問題域
    參考代碼是金礦:官方示例代碼已經踩過坑,直接使用最可靠
    打印診斷信息:關鍵數據點(JPEG頭、輸出前16字節、地址)幫助快速定位
    硬件也是變量:不要忽視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小時+無崩潰
參考資料
ESP-IDF JPEG編解碼器文檔
SDMMC主機驅動文檔
ESP32-P4官方MJPEG示例代碼
FFmpeg官方文檔
致謝
感謝樂鑫官方技術支持和開源社區的幫助。本項目的成功很大程度上得益於參考了官方示例代碼和社區經驗。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.