ESP32-P4 MJPEG視頻播放器開發(fā)實(shí)戰(zhàn):從攝像頭到SD卡的完整解決方案

ESP32-P4 MJPEG視頻播放器開發(fā)實(shí)戰(zhàn):從攝像頭到SD卡的完整解決方案

項(xiàng)目背景

本文記錄了在ESP32-P4開發(fā)板(配ST7703 LCD屏幕)上,將攝像頭視頻采集改為SD卡MJPEG視頻播放的完整開發(fā)過程。整個(gè)過程歷經(jīng)多次技術(shù)選型和問題排查,最終實(shí)現(xiàn)了穩(wěn)定的24fps多視頻輪播系統(tǒng)。

開發(fā)環(huán)境:

芯片:ESP32-P4

屏幕:ST7703 MIPI-DSI (720x720)

ESP-IDF:v5.5.1

視頻格式:MJPEG (480x480 @ 24fps)

第一階段:技術(shù)選型與初步實(shí)現(xiàn)

1.1 文件格式選擇

初始方案:AVI容器 + MJPEG編碼

最初選擇了AVI容器格式,理由如下:

成熟的格式,有現(xiàn)成的解析庫

包含完整的元數(shù)據(jù)(分辨率、幀率等)

可以直接從已有AVI文件讀取

遇到的第一個(gè)問題:AVI文件解析

實(shí)現(xiàn)了基于內(nèi)存搜索的AVI解析器:

//?搜索"movi"標(biāo)識(shí)定位數(shù)據(jù)區(qū)

uint32_tmovi_offset?=?search_fourcc(header_buf,?read_size,"movi");

//?逐幀讀取00dc?chunk

while(fread(chunk_header,1,8,?fp)?==8)?{

if(chunk_id?==0x63643030)?{//?"00dc"

//?讀取JPEG幀數(shù)據(jù)

fread(jpeg_data,1,?chunk_size,?fp);

}

}

這部分基本順利,能正確提取JPEG幀數(shù)據(jù)。

1.2 JPEG硬件解碼器集成

ESP32-P4內(nèi)置硬件JPEG解碼器,理論性能很高。按照官方文檔配置:

//?創(chuàng)建解碼器引擎

jpeg_decode_engine_cfg_tdecode_eng_cfg?=?{

.intr_priority?=0,

.timeout_ms?=40,

};

ESP_ERROR_CHECK(jpeg_new_decoder_engine(&decode_eng_cfg,?&decoder_handle));

//?分配輸入/輸出緩沖區(qū)

jpeg_decode_memory_alloc_cfg_trx_mem_cfg?=?{

.buffer_direction?=?JPEG_DEC_ALLOC_OUTPUT_BUFFER,

};

output_buf?=?jpeg_alloc_decoder_mem(width?*?height?*3,?&rx_mem_cfg,?&size);

第二階段:問題爆發(fā) - 解碼失敗與色塊

2.1 現(xiàn)象描述

運(yùn)行后出現(xiàn)以下問題:

每幀都超時(shí):ESP_ERR_TIMEOUT

輸出數(shù)據(jù)全0:即使out_size正確,但buffer內(nèi)容是全0

屏幕顯示規(guī)則色塊/網(wǎng)格:綠色、紫色、粉色相間的馬賽克

關(guān)鍵日志:

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數(shù)據(jù)有問題?

驗(yàn)證JPEG數(shù)據(jù)完整性:

//?檢查JPEG頭尾標(biāo)記

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

}

結(jié)果:? JPEG數(shù)據(jù)完整正確

猜測2:RGB字節(jié)序不對(duì)?

嘗試切換JPEG_DEC_RGB_ELEMENT_ORDER_BGR和RGB。結(jié)果:? 無效,仍然是色塊

猜測3:YUV色彩空間轉(zhuǎn)換問題?

添加YUV到RGB轉(zhuǎn)換配置:

.conv_std?=?JPEG_YUV_RGB_CONV_STD_BT601,

結(jié)果:? 無效

猜測4:Cache一致性問題?

這是問題的核心!嘗試了多種Cache同步方案:

//?輸入:CPU寫入后,刷新到內(nèi)存

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

結(jié)果:各種對(duì)齊錯(cuò)誤,數(shù)據(jù)仍然全0

2.3 對(duì)比測試:單張照片 vs 視頻

關(guān)鍵發(fā)現(xiàn)

? 單張JPEG照片能正常解碼顯示

? AVI視頻每幀都失敗

對(duì)比代碼發(fā)現(xiàn):

照片測試:不調(diào)用任何Cache同步,卻能正常工作

視頻播放:添加了各種Cache同步,反而失敗

結(jié)論:問題不在Cache同步本身,而在AVI容器格式的連續(xù)解碼上。

第三階段:轉(zhuǎn)折點(diǎn) - 切換到純MJPEG格式

3.1 發(fā)現(xiàn)參考代碼

找到樂鑫官方的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數(shù)據(jù)]

[00dc][size][JPEG數(shù)據(jù)]

3.2 視頻格式轉(zhuǎn)換

使用FFmpeg轉(zhuǎn)換:

#?錯(cuò)誤的方式(強(qiáng)制YUV422p)

ffmpeg?-i?input.avi?-pix_fmt?yuvj422p?-f?mjpeg?output.mjpeg#??

#?正確的方式(讓FFmpeg自動(dòng)選擇)

ffmpeg?-i?input.mp4?-q:v?3?-f?mjpeg?output.mjpeg#??

關(guān)鍵差異

yuvj422p:某些YUV變體,ESP32-P4可能不完全兼容

自動(dòng)選擇:通常是yuv420p,標(biāo)準(zhǔn)格式,完全兼容

3.3 集成參考代碼

復(fù)制官方的esp_mjpeg_decode組件:

typedefstruct{

FILE?*input;

uint8_t*mjpeg_buf;

uint8_t*output_buf;

jpeg_decoder_handle_tdecoder_engine;

int16_tw,?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));

結(jié)果:? 立即成功!視頻正常播放,無超時(shí),無色塊!

第四階段:性能優(yōu)化

4.1 初始性能

使用純MJPEG格式后:

幀率:16-18 FPS

瓶頸分析:

JPEG解碼:~40ms

SD卡讀?。簙2ms

LCD刷新:~18ms

總計(jì):~60ms = 16.7 FPS

4.2 關(guān)鍵優(yōu)化:啟用DMA2D

發(fā)現(xiàn)參考代碼的LCD配置有一個(gè)關(guān)鍵參數(shù):

esp_lcd_dpi_panel_config_tdpi_config?=?{

//?...

.flags.use_dma2d?=true,//?★?關(guān)鍵!

};

效果:幀率從16fps 飆升到 70-82 FPS!

原理

不啟用DMA2D:CPU逐字節(jié)復(fù)制像素?cái)?shù)據(jù)到LCD

啟用DMA2D:硬件DMA直接傳輸,CPU只需觸發(fā)

4.3 Cache配置優(yōu)化

對(duì)比參考代碼的sdkconfig,發(fā)現(xiàn)關(guān)鍵差異:

# 你的配置(失敗時(shí))

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傳輸?shù)姆€(wěn)定性。

4.4 SD卡速度優(yōu)化

發(fā)現(xiàn):不同SD卡速度差異巨大!

舊卡(SDSC):40 MHz → 16-18 fps

新卡(SDHC):52 MHz → 70-82 fps

教訓(xùn):硬件性能對(duì)整體體驗(yàn)影響巨大,不要忽視SD卡的選擇。

第五階段:幀率精確控制

5.1 問題

全速播放是70-82 FPS,但源視頻是24 FPS。如何精確控制到24fps?

失敗的嘗試1:固定延遲

vTaskDelay(pdMS_TO_TICKS(41));//?固定延遲41ms

//?結(jié)果:18-19?FPS(太慢)

//?原因:FreeRTOS?tick粒度問題,延遲不精確

失敗的嘗試2:動(dòng)態(tài)延遲

elapsed_time?=?實(shí)際處理時(shí)間;

delay?=?target_time?-?elapsed_time;

vTaskDelay(pdMS_TO_TICKS(delay));

//?結(jié)果:仍然18-19?FPS

//?原因:累積誤差,每幀處理時(shí)間不同

5.2 成功的方案:固定時(shí)間間隔法

核心思想:基于絕對(duì)時(shí)間而非相對(duì)延遲

int64_tnext_frame_time_us?=?esp_timer_get_time();//?初始時(shí)間

int64_tframe_interval_us?=1000000/24;//?41667微秒

while(read_frame())?{

//?等待到預(yù)定時(shí)間

int64_tnow?=?esp_timer_get_time();

int64_twait_us?=?next_frame_time_us?-?now;

if(wait_us?>1000)?{

vTaskDelay(pdMS_TO_TICKS(wait_us?/1000));

}

//?解碼并顯示

decode_and_display();

//?更新下一幀時(shí)間(累加,不是重新計(jì)算)

next_frame_time_us?+=?frame_interval_us;

}

效果:幀率精確控制在23.9-24.1 FPS,誤差 < 0.5%

優(yōu)點(diǎn)

消除累積誤差

自動(dòng)補(bǔ)償慢幀

基于高精度定時(shí)器(微秒級(jí))

核心技術(shù)要點(diǎn)總結(jié)

1. 文件格式選擇

格式優(yōu)點(diǎn)缺點(diǎn)推薦度

AVI容器包含元數(shù)據(jù)解析復(fù)雜,Cache問題??

純MJPEG簡單高效無元數(shù)據(jù)?????

轉(zhuǎn)換命令:

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

注意

? 使用-f mjpeg輸出純MJPEG

? 讓FFmpeg自動(dòng)選擇色彩空間(通常是yuv420p)

? 不要強(qiáng)制-pix_fmt yuvj422p(可能不兼容)

2. 內(nèi)存分配

正確方式:

//?輸入和輸出都使用?jpeg_alloc_decoder_mem

jpeg_decode_memory_alloc_cfg_ttx_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_trx_mem_cfg?=?{

.buffer_direction?=?JPEG_DEC_ALLOC_OUTPUT_BUFFER,

};

output_buf?=?jpeg_alloc_decoder_mem(w?*?h?*?bpp,?&rx_mem_cfg,?&alloc_size);

錯(cuò)誤方式:

//???使用普通?heap_caps_malloc

input_buf?=?heap_caps_malloc(size,?MALLOC_CAP_SPIRAM?|?MALLOC_CAP_DMA);

//?可能導(dǎo)致DMA訪問問題

3. Cache同步

關(guān)鍵結(jié)論:jpeg_alloc_decoder_mem返回的內(nèi)存是DMA-coherent的,不需要手動(dòng)Cache同步!

如果你添加了esp_cache_msync,反而可能導(dǎo)致問題:

C2M(Cache to Memory):會(huì)覆蓋DMA寫入的數(shù)據(jù)

M2C(Memory to Cache):可能有對(duì)齊錯(cuò)誤

正確做法:什么都不做,讓庫自動(dòng)處理。

4. LCD加速

必須啟用DMA2D

esp_lcd_dpi_panel_config_tdpi_config?=?{

//?...

.flags.use_dma2d?=true,//?★?關(guān)鍵配置

};

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

5. 幀率控制

固定時(shí)間間隔法

next_frame_time?+=?frame_interval;//?基于絕對(duì)時(shí)間

wait_until(next_frame_time);//?等待到這個(gè)時(shí)間點(diǎn)

decode_and_display();//?然后立即處理

優(yōu)于動(dòng)態(tài)延遲法(delay = target - elapsed)。

常見問題與解決方案

Q1: JPEG解碼器每幀都超時(shí),輸出全0

可能原因

文件格式問題(AVI容器有兼容性問題)

Cache一致性問題

內(nèi)存分配不正確

解決方案

? 改用純MJPEG格式

? 使用jpeg_alloc_decoder_mem分配內(nèi)存

? 不要手動(dòng)Cache同步

Q2: 單張照片能解碼,視頻不行

原因:單次解碼和連續(xù)解碼的差異。

解決方案

使用參考代碼的esp_mjpeg_decode組件

確保視頻格式是標(biāo)準(zhǔn)MJPEG(不是AVI)

Q3: 屏幕顯示規(guī)則色塊/網(wǎng)格

原因

解碼失敗但返回了錯(cuò)誤的成功狀態(tài)

顯示了未初始化的內(nèi)存

LCD DMA2D未啟用

解決方案

解決解碼問題(參考Q1)

啟用DMA2D

Q4: 幀率無法精確控制

原因:FreeRTOS tick粒度(1ms)+ 動(dòng)態(tài)延遲算法

解決方案

使用固定時(shí)間間隔法

基于esp_timer_get_time()(微秒級(jí))

最終實(shí)現(xiàn)效果

性能指標(biāo)

JPEG解碼能力:70-82 FPS(硬件極限)

實(shí)際播放幀率:24.00-24.06 FPS(精確控制,誤差<0.3%)

視頻切換:7個(gè)視頻自動(dòng)輪播,無縫切換

穩(wěn)定性:長時(shí)間運(yùn)行85000+幀無崩潰

系統(tǒng)架構(gòu)

SD卡(SDMMC)?→?MJPEG文件讀取?→?JPEG硬件解碼器

↓???????????????????????????????↓

40MHz??????????????→????????DMA輸出緩沖區(qū)

LCD(DMA2D加速)?→?屏幕顯示

資源使用

RAM:約20KB(棧+全局變量,使用堆分配避免棧溢出)

PSRAM:約2MB(JPEG緩沖區(qū))

CPU占用:單核,約30%(大部分時(shí)間在等待DMA)

開發(fā)建議與最佳實(shí)踐

1. 文件格式

?推薦:純MJPEG格式

簡單、高效、兼容性好

使用FFmpeg轉(zhuǎn)換,質(zhì)量參數(shù)-q:v 3(平衡質(zhì)量和大?。?/p>

?不推薦:AVI容器(除非必須使用元數(shù)據(jù))

2. 開發(fā)流程

先測試單張JPEG解碼:驗(yàn)證基本功能

再測試純MJPEG播放:驗(yàn)證連續(xù)解碼

最后優(yōu)化性能和幀率:DMA2D、幀率控制

3. 調(diào)試技巧

關(guān)鍵診斷點(diǎn)

//?1.?驗(yàn)證JPEG數(shù)據(jù)完整性

ESP_LOGI(TAG,"JPEG?header:?%02x?%02x",?data[0],?data[1]);//?應(yīng)該是?FF?D8

//?2.?驗(yàn)證解碼輸出

ESP_LOGI(TAG,"Decoded?output:?%02x?%02x?%02x?...",

output[0],?output[1],?output[2]);//?不應(yīng)該全是00

//?3.?測量實(shí)際處理時(shí)間

int64_tstart?=?esp_timer_get_time();

decode();

int64_telapsed?=?(esp_timer_get_time()?-?start)?/1000;

ESP_LOGI(TAG,"Decode?took?%lld?ms",?elapsed);

4. 性能優(yōu)化清單

? 使用純MJPEG格式(避免容器解析開銷)

? 啟用LCD DMA2D加速

? 使用高速SD卡(Class 10或以上)

? 適當(dāng)調(diào)整L2 Cache大?。ńㄗh256KB)

? 使用堆內(nèi)存分配大對(duì)象(避免棧溢出)

完整代碼示例

SD卡初始化

esp_err_tinit_sd_card(void){

//?LDO電源配置

esp_ldo_channel_config_tldo_config?=?{

.chan_id?=4,

.voltage_mv?=3300,

};

ESP_ERROR_CHECK(esp_ldo_acquire_channel(&ldo_config,?&ldo_handle));

//?SDMMC主機(jī)配置

sdmmc_host_thost?=?SDMMC_HOST_DEFAULT();

host.slot?=?SDMMC_HOST_SLOT_1;

host.max_freq_khz?=?SDMMC_FREQ_HIGHSPEED;

//?掛載

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

returnESP_OK;

}

MJPEG播放主循環(huán)

voidplay_mjpeg(constchar*filename){

//?初始化解碼器

esp_mjpeg_decode_tmjpeg?=?{

.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_tnext_frame_time?=?esp_timer_get_time();

int64_tframe_interval?=1000000/24;//?24?fps

//?播放循環(huán)

while(esp_mjpeg_decode_read_mjpeg_buf(&mjpeg))?{

//?等待到預(yù)定時(shí)間

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

//?更新下一幀時(shí)間

next_frame_time?+=?frame_interval;

}

esp_mjpeg_decode_close(&mjpeg);

}

經(jīng)驗(yàn)教訓(xùn)

技術(shù)層面

不要過度優(yōu)化:參考代碼不做Cache同步也能工作,說明庫已經(jīng)處理好了

格式很重要:純MJPEG比AVI容器簡單可靠得多

硬件加速必須啟用:DMA2D能帶來4-5倍性能提升

精確延遲需要高精度定時(shí)器:FreeRTOS tick不夠,要用esp_timer

調(diào)試層面

對(duì)比測試法:單張照片 vs 視頻,快速定位問題域

參考代碼是金礦:官方示例代碼已經(jīng)踩過坑,直接使用最可靠

打印診斷信息:關(guān)鍵數(shù)據(jù)點(diǎn)(JPEG頭、輸出前16字節(jié)、地址)幫助快速定位

硬件也是變量:不要忽視SD卡等外設(shè)的影響

附錄:完整配置清單

sdkconfig 關(guān)鍵配置

# 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)

組件結(jié)構(gòu)

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

項(xiàng)目成果

源代碼:https://github.com/your-repo/esp32p4-mjpeg-player

演示視頻:[YouTube鏈接]

性能測試:24fps穩(wěn)定運(yùn)行24小時(shí)+無崩潰

參考資料

ESP-IDF JPEG編解碼器文檔

SDMMC主機(jī)驅(qū)動(dòng)文檔

ESP32-P4官方MJPEG示例代碼

FFmpeg官方文檔

致謝

感謝樂鑫官方技術(shù)支持和開源社區(qū)的幫助。本項(xiàng)目的成功很大程度上得益于參考了官方示例代碼和社區(qū)經(jīng)驗(yàn)。

作者:拆技日期:2025年11月25日

聯(lián)系方式:78680321@qq.com

關(guān)鍵詞:ESP32-P4, MJPEG, 視頻播放, JPEG硬件解碼, DMA2D, SD卡, Cache一致性, 幀率控制

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容