Merge branch 'isp_dma_one_frame_v6.1' into 'release/v6.1'

feat(isp): support isp dma input and add example (v6.1)

See merge request espressif/esp-idf!50754
This commit is contained in:
morris
2026-08-28 10:05:35 +08:00
29 changed files with 1436 additions and 28 deletions

View File

@@ -6,10 +6,16 @@ set(public_include "include")
set(priv_requires "esp_driver_gpio")
set(requires "esp_hal_cam")
if(${target} STREQUAL "linux")
set(requires "")
else()
set(requires "esp_hal_cam" "esp_mm" "esp_driver_dma")
endif()
if(CONFIG_SOC_ISP_SUPPORTED)
list(APPEND srcs "src/isp_core.c"
"src/isp_dma.c"
"src/isp_af.c"
"src/isp_ccm.c"
"src/isp_awb.c"

View File

@@ -32,6 +32,7 @@ typedef struct {
uint32_t h_res; ///< Input horizontal resolution, i.e. the number of pixels in a line
uint32_t v_res; ///< Input vertical resolution, i.e. the number of lines in a frame
color_raw_element_order_t bayer_order; ///< Bayer order
uint32_t dma_burst_size; ///< DMA output burst length in units of 64-bit beats. Set to 0 to use default value 16
int intr_priority; ///< The interrupt priority, range 0~3, if set to 0, the driver will try to allocate an interrupt with a relative low priority (1,2,3)
struct {
uint32_t bypass_isp : 1; ///< Bypass ISP pipelines

View File

@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stddef.h>
#include "esp_err.h"
#include "driver/isp_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Process one ISP DMA frame: feed the input buffer through the ISP and wait for completion
*
* @note The driver synchronizes cacheable buffers before and after DMA access. Input buffers use
* an unaligned cache write-back, while cacheable output buffers and their derived frame
* sizes must be aligned to the cache line size.
* @note This function blocks until both input and output DMA channels finish. On timeout,
* the output buffer may still be owned by DMA and must not be accessed.
*
* @param[in] proc Processor handle
* @param[in] output_buffer Destination buffer for ISP output
* @param[in] input_buffer Source input buffer for RAW frame data
* @param[in] timeout_ms Timeout in milliseconds for waiting transfer completion
*
* @return
* - ESP_OK On success
* - ESP_ERR_INVALID_ARG Invalid argument
* - ESP_ERR_TIMEOUT Wait timeout
*/
esp_err_t esp_isp_dma_process_frame(isp_proc_handle_t proc, void *output_buffer, const void *input_buffer, uint32_t timeout_ms);
#ifdef __cplusplus
}
#endif

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -58,6 +58,8 @@ typedef enum {
/*---------------------------------------------------------------
Driver Context
---------------------------------------------------------------*/
typedef struct esp_isp_dma_frame_ctx_t esp_isp_dma_frame_ctx_t;
typedef struct isp_processor_t {
int proc_id;
isp_clk_src_t clk_src;
@@ -70,10 +72,14 @@ typedef struct isp_processor_t {
DECLARE_CRIT_SECTION_LOCK_IN_STRUCT(spinlock);
isp_color_t in_color_format;
isp_color_t out_color_format;
isp_input_data_source_t input_data_source;
uint32_t h_res;
uint32_t v_res;
color_raw_element_order_t bayer_order;
bool bypass_isp;
uint32_t dma_in_burst_len; // ISP DMA input burst length (64-bit beats), kept in sync with the input GDMA dst MSIZE
uint32_t dma_out_burst_len; // ISP DMA output burst length (64-bit beats)
esp_isp_dma_frame_ctx_t *dma_frame_ctx;
/* sub module contexts */
isp_af_ctlr_t af_ctlr[ISP_LL_AF_CTLR_NUMS];
isp_awb_ctlr_t awb_ctlr;
@@ -131,6 +137,45 @@ bool esp_isp_awb_isr(isp_proc_handle_t proc, uint32_t awb_events);
bool esp_isp_sharpen_isr(isp_proc_handle_t proc, uint32_t sharp_events);
bool esp_isp_hist_isr(isp_proc_handle_t proc, uint32_t hist_events);
/*---------------------------------------------------------------
DMA INPUT
---------------------------------------------------------------*/
/**
* @brief Configure the ISP DMA input path (frame size, burst length, data type)
*
* @note Internal helper invoked while creating a processor whose input source is DWGDMA.
*
* @param[in] proc Processor handle
*
* @return
* - ESP_OK On success
* - ESP_ERR_INVALID_ARG Invalid input color format or frame size
*/
esp_err_t isp_dma_configure_input(isp_proc_handle_t proc);
/**
* @brief Create the ISP DMA frame context
*
* @note Internal helper invoked while creating a processor whose input source is DWGDMA.
*
* @param[in] proc Processor handle
*
* @return
* - ESP_OK On success
* - ESP_ERR_INVALID_ARG Invalid processor, burst length, or frame size
* - ESP_ERR_INVALID_STATE Processor input source is not DWGDMA or the context already exists
* - ESP_ERR_NO_MEM Failed to allocate the context or synchronization objects
* - Other errors returned by the underlying DMA driver
*/
esp_err_t isp_dma_new_frame_ctx(isp_proc_handle_t proc);
/**
* @brief Delete the ISP DMA frame context
*
* @param[in] proc Processor handle
*/
void isp_dma_del_frame_ctx(isp_proc_handle_t proc);
#ifdef __cplusplus
}
#endif

View File

@@ -18,6 +18,7 @@
#include "esp_private/mipi_csi_share_hw_ctrl.h"
#include "hal/hal_utils.h"
#include "hal/color_hal.h"
#include "hal/mipi_csi_brg_ll.h"
#include "soc/mipi_csi_bridge_struct.h"
#include "hal/isp_periph.h"
#include "soc/soc_caps.h"
@@ -79,7 +80,17 @@ esp_err_t esp_isp_new_processor(const esp_isp_processor_cfg_t *proc_config, isp_
esp_err_t ret = ESP_FAIL;
ESP_RETURN_ON_FALSE(proc_config && ret_proc, ESP_ERR_INVALID_ARG, TAG, "invalid argument: null pointer");
ESP_RETURN_ON_FALSE(proc_config->h_res <= ISP_LL_HSIZE_MAX, ESP_ERR_INVALID_ARG, TAG, "invalid h_res");
ESP_RETURN_ON_FALSE(proc_config->input_data_source != ISP_INPUT_DATA_SOURCE_DWGDMA, ESP_ERR_NOT_SUPPORTED, TAG, "input source not supported yet");
if (proc_config->input_data_source == ISP_INPUT_DATA_SOURCE_DWGDMA) {
ESP_RETURN_ON_FALSE((proc_config->input_data_color_type == ISP_COLOR_RAW8) ||
(proc_config->input_data_color_type == ISP_COLOR_RAW10) ||
(proc_config->input_data_color_type == ISP_COLOR_RAW12),
ESP_ERR_INVALID_ARG, TAG, "dma input only supports RAW8/RAW10/RAW12");
uint32_t in_bits_per_pixel = color_hal_pixel_format_fourcc_get_bit_depth(proc_config->input_data_color_type);
uint64_t frame_bits = (uint64_t)proc_config->h_res * proc_config->v_res * in_bits_per_pixel;
ESP_RETURN_ON_FALSE((frame_bits % 64) == 0, ESP_ERR_INVALID_ARG, TAG, "frame bits should be 64-bit aligned");
ESP_RETURN_ON_FALSE((frame_bits / 64) <= ((1UL << 22) - 1), ESP_ERR_INVALID_ARG, TAG, "frame size exceeds hardware limit");
ESP_RETURN_ON_FALSE(proc_config->dma_burst_size <= 16, ESP_ERR_INVALID_ARG, TAG, "dma burst size out of range");
}
if (proc_config->flags.bypass_isp) {
ESP_RETURN_ON_FALSE(proc_config->input_data_color_type == proc_config->output_data_color_type, ESP_ERR_INVALID_ARG, TAG, "isp is bypassed, input and output data color type should be same");
}
@@ -173,7 +184,9 @@ esp_err_t esp_isp_new_processor(const esp_isp_processor_cfg_t *proc_config, isp_
isp_ll_yuv_set_range(proc->hal.hw, proc_config->yuv_range);
}
if ((out_color_format == ISP_COLOR_RGB888 || out_color_format == ISP_COLOR_RGB565) && proc_config->input_data_source == ISP_INPUT_DATA_SOURCE_DVP) {
if ((out_color_format == ISP_COLOR_RGB888 || out_color_format == ISP_COLOR_RGB565) &&
(proc_config->input_data_source == ISP_INPUT_DATA_SOURCE_DVP ||
proc_config->input_data_source == ISP_INPUT_DATA_SOURCE_DWGDMA)) {
isp_ll_color_enable(proc->hal.hw, true); // workaround for DIG-474
}
if (proc_config->flags.byte_swap_en) {
@@ -184,16 +197,23 @@ esp_err_t esp_isp_new_processor(const esp_isp_processor_cfg_t *proc_config, isp_
proc->in_color_format = in_color_format;
proc->out_color_format = out_color_format;
proc->input_data_source = proc_config->input_data_source;
proc->h_res = proc_config->h_res;
proc->v_res = proc_config->v_res;
proc->bayer_order = proc_config->bayer_order;
proc->bypass_isp = proc_config->flags.bypass_isp;
proc->dma_out_burst_len = proc_config->dma_burst_size;
if (proc->input_data_source == ISP_INPUT_DATA_SOURCE_DWGDMA) {
ESP_GOTO_ON_ERROR(isp_dma_configure_input(proc), err, TAG, "configure dma input failed");
ESP_GOTO_ON_ERROR(isp_dma_new_frame_ctx(proc), err, TAG, "create dma frame context failed");
}
*ret_proc = proc;
return ESP_OK;
err:
isp_dma_del_frame_ctx(proc);
if (proc->intr_hdl) {
esp_isp_deregister_isr(proc, ISP_SUBMODULE_GENERAL);
}
@@ -207,6 +227,8 @@ esp_err_t esp_isp_del_processor(isp_proc_handle_t proc)
ESP_RETURN_ON_FALSE(proc, ESP_ERR_INVALID_ARG, TAG, "invalid argument: null pointer");
ESP_RETURN_ON_FALSE(atomic_load(&proc->isp_fsm) == ISP_FSM_INIT, ESP_ERR_INVALID_STATE, TAG, "processor isn't in init state");
isp_dma_del_frame_ctx(proc);
//declaim first, then do free
ESP_RETURN_ON_ERROR(s_isp_declaim_processor(proc), TAG, "declaim processor fail");
#if SOC_ISP_SHARE_CSI_BRG
@@ -338,20 +360,26 @@ static void IRAM_ATTR s_isp_isr_dispatcher(void *arg)
do_dispatch = false;
}
if ((error_events & ISP_LL_EVENT_DATA_TYPE_ERR) || (error_events & ISP_LL_EVENT_DATA_TYPE_SETTING_ERR)) {
ESP_EARLY_LOGE(TAG, "data type error");
}
if ((error_events & ISP_LL_EVENT_ASYNC_FIFO_OVF) || (error_events & ISP_LL_EVENT_BUF_FULL)) {
ESP_EARLY_LOGE(TAG, "fifo overflow");
}
if ((error_events & ISP_LL_EVENT_HVNUM_SETTING_ERR) || (error_events & ISP_LL_EVENT_MIPI_HNUM_UNMATCH)) {
ESP_EARLY_LOGE(TAG, "hnum / vnum setting error");
}
if (error_events & ISP_LL_EVENT_GAMMA_XCOORD_ERR) {
ESP_EARLY_LOGE(TAG, "gamma xcoord error");
}
if (error_events & ISP_LL_EVENT_CROP_ERR) {
ESP_EARLY_LOGE(TAG, "crop error");
if (error_events) {
if ((error_events & ISP_LL_EVENT_DATA_TYPE_ERR) || (error_events & ISP_LL_EVENT_DATA_TYPE_SETTING_ERR)) {
ESP_EARLY_LOGE(TAG, "data type error");
}
if ((error_events & ISP_LL_EVENT_ASYNC_FIFO_OVF)) {
ESP_EARLY_LOGE(TAG, "fifo overflow");
}
if ((error_events & ISP_LL_EVENT_BUF_FULL)) {
//This error does not affect the operation of the ISP
ESP_EARLY_LOGD(TAG, "buffer full");
}
if ((error_events & ISP_LL_EVENT_HVNUM_SETTING_ERR) || (error_events & ISP_LL_EVENT_MIPI_HNUM_UNMATCH)) {
ESP_EARLY_LOGE(TAG, "hnum / vnum setting error");
}
if (error_events & ISP_LL_EVENT_GAMMA_XCOORD_ERR) {
ESP_EARLY_LOGE(TAG, "gamma xcoord error");
}
if (error_events & ISP_LL_EVENT_CROP_ERR) {
ESP_EARLY_LOGE(TAG, "crop error");
}
}
if (need_yield) {

View File

@@ -0,0 +1,292 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <assert.h>
#include <esp_types.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "esp_check.h"
#include "esp_cache.h"
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "driver/isp_dma.h"
#include "esp_private/dw_gdma.h"
#include "esp_private/isp_private.h"
#include "hal/color_hal.h"
#include "hal/isp_ll.h"
#include "hal/mipi_csi_brg_ll.h"
#define ISP_DMA_IN_BURST_LEN_DFT 8
#define ISP_DMA_OUT_BURST_LEN_DFT 16
struct esp_isp_dma_frame_ctx_t {
isp_proc_handle_t proc;
csi_brg_dev_t *csi_brg_hw;
dw_gdma_channel_handle_t dma_in_chan;
dw_gdma_channel_handle_t dma_out_chan;
SemaphoreHandle_t in_done_sem;
SemaphoreHandle_t out_done_sem;
dw_gdma_block_transfer_config_t dma_out_trans;
dw_gdma_block_transfer_config_t dma_in_trans;
uint32_t input_frame_size_64bit;
uint32_t output_frame_size_64bit;
};
static const char *TAG = "ISP_DMA";
static bool IRAM_ATTR s_isp_dma_done_cb(dw_gdma_channel_handle_t chan, const dw_gdma_trans_done_event_data_t *event_data, void *user_data)
{
(void)chan;
(void)event_data;
BaseType_t high_task_woken = pdFALSE;
xSemaphoreGiveFromISR(*(SemaphoreHandle_t *)user_data, &high_task_woken);
return high_task_woken == pdTRUE;
}
static dw_gdma_burst_items_t s_isp_dma_burst_len_to_items(uint32_t burst_len)
{
switch (burst_len) {
case 1: return DW_GDMA_BURST_ITEMS_1;
case 4: return DW_GDMA_BURST_ITEMS_4;
case 8: return DW_GDMA_BURST_ITEMS_8;
default: return DW_GDMA_BURST_ITEMS_8;
}
}
static esp_err_t s_isp_dma_sync_cacheable_buffer(void *buffer, size_t size, int flags, const char *buffer_name)
{
size_t cache_line_size = esp_cache_get_line_size_by_addr(buffer);
if (cache_line_size == 0) {
return ESP_OK;
}
ESP_RETURN_ON_ERROR(esp_cache_msync(buffer, size, flags), TAG, "sync %s buffer cache failed", buffer_name);
return ESP_OK;
}
static void s_isp_dma_frame_ctx_destroy(struct esp_isp_dma_frame_ctx_t *ctx)
{
if (!ctx) {
return;
}
if (ctx->csi_brg_hw) {
mipi_csi_brg_ll_enable(ctx->csi_brg_hw, false);
}
if (ctx->dma_in_chan) {
dw_gdma_channel_enable_ctrl(ctx->dma_in_chan, false);
dw_gdma_del_channel(ctx->dma_in_chan);
}
if (ctx->dma_out_chan) {
dw_gdma_channel_enable_ctrl(ctx->dma_out_chan, false);
dw_gdma_del_channel(ctx->dma_out_chan);
}
if (ctx->in_done_sem) {
vSemaphoreDelete(ctx->in_done_sem);
}
if (ctx->out_done_sem) {
vSemaphoreDelete(ctx->out_done_sem);
}
free(ctx);
}
esp_err_t isp_dma_configure_input(isp_proc_handle_t proc)
{
ESP_RETURN_ON_FALSE(proc, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
ESP_RETURN_ON_FALSE(proc->input_data_source == ISP_INPUT_DATA_SOURCE_DWGDMA, ESP_ERR_INVALID_ARG, TAG, "processor input source is not dwgdma");
bool valid_format = isp_ll_dma_set_data_type(proc->hal.hw, proc->in_color_format);
ESP_RETURN_ON_FALSE(valid_format, ESP_ERR_INVALID_ARG, TAG, "dma input only supports RAW8/RAW10/RAW12");
uint32_t in_bits_per_pixel = color_hal_pixel_format_fourcc_get_bit_depth(proc->in_color_format);
uint64_t frame_bits = (uint64_t)proc->h_res * proc->v_res * in_bits_per_pixel;
ESP_RETURN_ON_FALSE((frame_bits % 64) == 0, ESP_ERR_INVALID_ARG, TAG, "frame bits should be 64-bit aligned");
uint64_t frame_size_in_64bit = frame_bits / 64;
ESP_RETURN_ON_FALSE(frame_size_in_64bit <= ((1UL << 22) - 1), ESP_ERR_INVALID_ARG, TAG, "frame size exceeds hardware limit");
/*
* The input burst length is fixed at ISP_DMA_IN_BURST_LEN_DFT: it must be representable as a
* DW-GDMA MSIZE (1/4/8) and not exceed the ISP input async FIFO capacity (8 x 64-bit). It also
* has to equal the input GDMA dst MSIZE (derived from proc->dma_in_burst_len in
* isp_dma_new_frame_ctx), otherwise the async FIFO overflows.
*/
isp_ll_dma_set_frame_size(proc->hal.hw, (uint32_t)frame_size_in_64bit);
isp_ll_dma_set_burst_len(proc->hal.hw, ISP_DMA_IN_BURST_LEN_DFT);
isp_ll_dma_apply_config(proc->hal.hw);
proc->dma_in_burst_len = ISP_DMA_IN_BURST_LEN_DFT;
return ESP_OK;
}
esp_err_t isp_dma_new_frame_ctx(isp_proc_handle_t proc)
{
esp_err_t ret = ESP_FAIL;
ESP_RETURN_ON_FALSE(proc, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
ESP_RETURN_ON_FALSE(proc->input_data_source == ISP_INPUT_DATA_SOURCE_DWGDMA, ESP_ERR_INVALID_STATE, TAG, "processor input source is not dwgdma");
ESP_RETURN_ON_FALSE(!proc->dma_frame_ctx, ESP_ERR_INVALID_STATE, TAG, "dma frame context already exists");
uint32_t output_burst_len = proc->dma_out_burst_len ? proc->dma_out_burst_len : ISP_DMA_OUT_BURST_LEN_DFT;
ESP_RETURN_ON_FALSE(output_burst_len > 0 && output_burst_len <= 16, ESP_ERR_INVALID_ARG, TAG, "output burst len out of range");
uint32_t in_bits_per_pixel = color_hal_pixel_format_fourcc_get_bit_depth(proc->in_color_format);
uint32_t out_bits_per_pixel = color_hal_pixel_format_fourcc_get_bit_depth(proc->out_color_format);
uint64_t input_frame_bits = (uint64_t)proc->h_res * proc->v_res * in_bits_per_pixel;
uint64_t output_frame_bits = (uint64_t)proc->h_res * proc->v_res * out_bits_per_pixel;
ESP_RETURN_ON_FALSE((input_frame_bits % 64) == 0, ESP_ERR_INVALID_ARG, TAG, "input frame bits should be 64-bit aligned");
ESP_RETURN_ON_FALSE((output_frame_bits % 64) == 0, ESP_ERR_INVALID_ARG, TAG, "output frame bits should be 64-bit aligned");
struct esp_isp_dma_frame_ctx_t *ctx = heap_caps_calloc(1, sizeof(struct esp_isp_dma_frame_ctx_t), ISP_MEM_ALLOC_CAPS);
ESP_GOTO_ON_FALSE(ctx, ESP_ERR_NO_MEM, err, TAG, "no mem for dma frame context");
ctx->proc = proc;
ctx->input_frame_size_64bit = input_frame_bits / 64;
ctx->output_frame_size_64bit = output_frame_bits / 64;
csi_brg_dev_t *csi_brg_hw = MIPI_CSI_BRG_LL_GET_HW(proc->csi_brg_id);
ctx->csi_brg_hw = csi_brg_hw;
mipi_csi_brg_ll_set_intput_data_h_pixel_num(csi_brg_hw, proc->h_res);
mipi_csi_brg_ll_set_intput_data_v_row_num(csi_brg_hw, proc->v_res);
mipi_csi_brg_ll_set_burst_len(csi_brg_hw, 512);
mipi_csi_brg_ll_enable(csi_brg_hw, true);
ctx->out_done_sem = xSemaphoreCreateBinary();
ESP_GOTO_ON_FALSE(ctx->out_done_sem, ESP_ERR_NO_MEM, err, TAG, "no mem for output semaphore");
ctx->in_done_sem = xSemaphoreCreateBinary();
ESP_GOTO_ON_FALSE(ctx->in_done_sem, ESP_ERR_NO_MEM, err, TAG, "no mem for input semaphore");
dw_gdma_channel_alloc_config_t dma_out_alloc = {
.src = {
.block_transfer_type = DW_GDMA_BLOCK_TRANSFER_CONTIGUOUS,
.role = DW_GDMA_ROLE_PERIPH_CSI,
.handshake_type = DW_GDMA_HANDSHAKE_HW,
.num_outstanding_requests = 5,
.status_fetch_addr = MIPI_CSI_BRG_MEM_BASE,
},
.dst = {
.block_transfer_type = DW_GDMA_BLOCK_TRANSFER_CONTIGUOUS,
.role = DW_GDMA_ROLE_MEM,
.handshake_type = DW_GDMA_HANDSHAKE_HW,
.num_outstanding_requests = 5,
},
.flow_controller = DW_GDMA_FLOW_CTRL_SRC,
.chan_priority = 1,
};
ESP_GOTO_ON_ERROR(dw_gdma_new_channel(&dma_out_alloc, &ctx->dma_out_chan), err, TAG, "create output dma channel failed");
dw_gdma_channel_alloc_config_t dma_in_alloc = {
.src = {
.block_transfer_type = DW_GDMA_BLOCK_TRANSFER_CONTIGUOUS,
.role = DW_GDMA_ROLE_MEM,
.handshake_type = DW_GDMA_HANDSHAKE_SW,
.num_outstanding_requests = 2,
},
.dst = {
.block_transfer_type = DW_GDMA_BLOCK_TRANSFER_CONTIGUOUS,
.role = DW_GDMA_ROLE_PERIPH_ISP,
.handshake_type = DW_GDMA_HANDSHAKE_HW,
.num_outstanding_requests = 1,
},
.flow_controller = DW_GDMA_FLOW_CTRL_DST,
.chan_priority = 1,
};
ESP_GOTO_ON_ERROR(dw_gdma_new_channel(&dma_in_alloc, &ctx->dma_in_chan), err, TAG, "create input dma channel failed");
ctx->dma_out_trans = (dw_gdma_block_transfer_config_t) {
.src = {
.addr = MIPI_CSI_BRG_MEM_BASE,
.width = DW_GDMA_TRANS_WIDTH_64,
.burst_mode = DW_GDMA_BURST_MODE_FIXED,
.burst_items = DW_GDMA_BURST_ITEMS_512,
.burst_len = output_burst_len,
},
.dst = {
.width = DW_GDMA_TRANS_WIDTH_64,
.burst_mode = DW_GDMA_BURST_MODE_INCREMENT,
.burst_items = DW_GDMA_BURST_ITEMS_512,
.burst_len = output_burst_len,
},
.size = ctx->output_frame_size_64bit,
};
ctx->dma_in_trans = (dw_gdma_block_transfer_config_t) {
.src = {
.width = DW_GDMA_TRANS_WIDTH_64,
.burst_mode = DW_GDMA_BURST_MODE_INCREMENT,
.burst_items = DW_GDMA_BURST_ITEMS_32,
},
.dst = {
.addr = MIPI_CSI_BRG_MEM_BASE,
.width = DW_GDMA_TRANS_WIDTH_64,
.burst_mode = DW_GDMA_BURST_MODE_FIXED,
.burst_items = s_isp_dma_burst_len_to_items(proc->dma_in_burst_len),
},
.size = ctx->input_frame_size_64bit,
};
dw_gdma_event_callbacks_t dma_cbs = {
.on_full_trans_done = s_isp_dma_done_cb,
};
ESP_GOTO_ON_ERROR(dw_gdma_channel_register_event_callbacks(ctx->dma_out_chan, &dma_cbs, &ctx->out_done_sem), err, TAG, "register output dma callback failed");
ESP_GOTO_ON_ERROR(dw_gdma_channel_register_event_callbacks(ctx->dma_in_chan, &dma_cbs, &ctx->in_done_sem), err, TAG, "register input dma callback failed");
proc->dma_frame_ctx = ctx;
return ESP_OK;
err:
s_isp_dma_frame_ctx_destroy(ctx);
return ret;
}
void isp_dma_del_frame_ctx(isp_proc_handle_t proc)
{
if (!proc || !proc->dma_frame_ctx) {
return;
}
s_isp_dma_frame_ctx_destroy(proc->dma_frame_ctx);
proc->dma_frame_ctx = NULL;
}
esp_err_t esp_isp_dma_process_frame(isp_proc_handle_t proc, void *output_buffer, const void *input_buffer, uint32_t timeout_ms)
{
ESP_RETURN_ON_FALSE(proc && proc->dma_frame_ctx && output_buffer && input_buffer, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
ESP_RETURN_ON_FALSE((((uintptr_t)output_buffer) % 8) == 0, ESP_ERR_INVALID_ARG, TAG, "output buffer not 8-byte aligned");
ESP_RETURN_ON_FALSE((((uintptr_t)input_buffer) % 8) == 0, ESP_ERR_INVALID_ARG, TAG, "input buffer not 8-byte aligned");
esp_isp_dma_frame_ctx_t *ctx = proc->dma_frame_ctx;
size_t input_frame_size = ctx->input_frame_size_64bit * sizeof(uint64_t);
size_t output_frame_size = ctx->output_frame_size_64bit * sizeof(uint64_t);
ESP_RETURN_ON_ERROR(s_isp_dma_sync_cacheable_buffer((void *)input_buffer, input_frame_size,
ESP_CACHE_MSYNC_FLAG_DIR_C2M | ESP_CACHE_MSYNC_FLAG_UNALIGNED,
"input"),
TAG, "sync input buffer cache failed");
/*
* Discard dirty CPU cache lines before DMA writes the frame. Otherwise a
* later cache write-back could overwrite data produced by the ISP.
*/
ESP_RETURN_ON_ERROR(s_isp_dma_sync_cacheable_buffer(output_buffer, output_frame_size,
ESP_CACHE_MSYNC_FLAG_DIR_M2C, "output"),
TAG, "sync output buffer cache failed");
ctx->dma_out_trans.dst.addr = (uint32_t)output_buffer;
ctx->dma_in_trans.src.addr = (uint32_t)input_buffer;
ESP_RETURN_ON_ERROR(dw_gdma_channel_config_transfer(ctx->dma_out_chan, &ctx->dma_out_trans), TAG, "configure output transfer failed");
ESP_RETURN_ON_ERROR(dw_gdma_channel_config_transfer(ctx->dma_in_chan, &ctx->dma_in_trans), TAG, "configure input transfer failed");
dw_gdma_channel_enable_ctrl(ctx->dma_out_chan, true);
dw_gdma_channel_enable_ctrl(ctx->dma_in_chan, true);
isp_ll_dma_trigger_frame(ctx->proc->hal.hw);
if (xSemaphoreTake(ctx->in_done_sem, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) {
ESP_LOGE(TAG, "wait input done timeout");
return ESP_ERR_TIMEOUT;
}
if (xSemaphoreTake(ctx->out_done_sem, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) {
ESP_LOGE(TAG, "wait output done timeout");
return ESP_ERR_TIMEOUT;
}
return ESP_OK;
}

View File

@@ -28,7 +28,7 @@ extern "C" {
#define ISP_LL_PERIPH_NUMS 1U
#define ISP_LL_HSIZE_MAX 1920
#define ISP_LL_VSIZE_MAX 1080
#define ISP_LL_VSIZE_MAX 1280
/*---------------------------------------------------------------
Clock
@@ -535,6 +535,82 @@ static inline void isp_ll_enable_line_end_packet_exist(isp_dev_t *hw, bool en)
hw->frame_cfg.hsync_end_exist = en;
}
/**
* @brief Set DMA input data type
*
* @param[in] hw Hardware instance address
* @param[in] format color format, see `isp_color_t`
*
* @return true for valid format, false for invalid format
*/
static inline bool isp_ll_dma_set_data_type(isp_dev_t *hw, isp_color_t format)
{
bool valid = false;
switch (format) {
case ISP_COLOR_RAW8:
hw->dma_cntl.dma_data_type = 0x2A;
valid = true;
break;
case ISP_COLOR_RAW10:
hw->dma_cntl.dma_data_type = 0x2B;
valid = true;
break;
case ISP_COLOR_RAW12:
hw->dma_cntl.dma_data_type = 0x2C;
valid = true;
break;
default:
break;
}
return valid;
}
/**
* @brief Set DMA input burst length in units of 64-bit
*
* @param[in] hw Hardware instance address
* @param[in] burst_len Number of 64-bit beats in one DMA burst
*/
static inline void isp_ll_dma_set_burst_len(isp_dev_t *hw, uint32_t burst_len)
{
hw->dma_cntl.dma_burst_len = burst_len;
}
/**
* @brief Set DMA input total frame size in units of 64-bit
*
* @param[in] hw Hardware instance address
* @param[in] num64b Number of 64-bit words in one frame
*/
static inline void isp_ll_dma_set_frame_size(isp_dev_t *hw, uint32_t num64b)
{
hw->dma_raw_data.dma_raw_num_total = num64b;
hw->dma_raw_data.dma_raw_num_total_set = 1;
}
/**
* @brief Apply DMA input registers
*
* @param[in] hw Hardware instance address
*/
static inline void isp_ll_dma_apply_config(isp_dev_t *hw)
{
hw->dma_cntl.dma_update_reg = 1;
while (hw->dma_cntl.dma_update_reg);
}
/**
* @brief Trigger one DMA input frame transfer
*
* @param[in] hw Hardware instance address
*/
static inline void isp_ll_dma_trigger_frame(isp_dev_t *hw)
{
hw->dma_cntl.dma_en = 1;
}
/**
* @brief Get if demosaic is enabled
*

View File

@@ -415,6 +415,8 @@ static inline void dw_gdma_ll_channel_set_dst_master_port(dw_gdma_dev_t *dev, ui
{
if (mem_addr == MIPI_DSI_BRG_MEM_BASE) {
dev->ch[channel].ctl0.dms = DW_GDMA_LL_MASTER_PORT_MIPI_DSI;
} else if (mem_addr == MIPI_CSI_BRG_MEM_BASE) {
dev->ch[channel].ctl0.dms = DW_GDMA_LL_MASTER_PORT_MIPI_CSI;
} else {
dev->ch[channel].ctl0.dms = DW_GDMA_LL_MASTER_PORT_MEMORY;
}

View File

@@ -16,6 +16,7 @@ INPUT += \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_ccm.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_color.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_core.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_dma.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_crop.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_demosaic.h \
$(PROJECT_PATH)/components/esp_driver_isp/include/driver/isp_gamma.h \

View File

@@ -996,6 +996,10 @@ To embed a file into a project, rather than a component, you can call the functi
Place this line after the ``project()`` line in your project CMakeLists.txt file. Replace ``myproject.elf`` with your project name. The final argument can be ``TEXT`` to embed a null-terminated string, or ``BINARY`` to embed the content as-is.
Use the optional ``ALIGN`` argument to align the embedded data's start symbol to a positive power of two. For example, to align binary data to 16 bytes::
target_add_binary_data(myproject.elf "main/data.bin" BINARY ALIGN 16)
For an example of using this technique, see the "main" component of the file_serving example :example_file:`protocols/http_server/file_serving/main/CMakeLists.txt` - two files are loaded at build time and linked into the firmware.
.. highlight:: cmake

View File

@@ -67,6 +67,7 @@ The ISP driver offers following services:
- :ref:`isp-resource-allocation` - covers how to allocate ISP resources with properly set of configurations. It also covers how to recycle the resources when they finished working.
- :ref:`isp-enable-disable` - covers how to enable and disable an ISP processor.
- :ref:`isp-dma-input` - covers how to feed image frames stored in memory into the ISP through DW-GDMA.
- :ref:`isp-af-statistics` - covers how to get AF statistics one-shot or continuously.
- :ref:`isp-awb-statistics` - covers how to get AWB white patches statistics one-shot or continuously.
- :ref:`isp-ae-statistics` - covers how to get AE statistics one-shot or continuously.
@@ -240,6 +241,15 @@ Before doing ISP pipeline, you need to enable the ISP processor first, by callin
Calling :cpp:func:`esp_isp_disable` does the opposite, that is, put the driver back to the **init** state.
.. _isp-dma-input:
ISP DMA Input
~~~~~~~~~~~~~
Besides image streams from camera controllers, the ISP can also read image frames from system memory through DW-GDMA. To use DMA input, set :cpp:member:`esp_isp_processor_cfg_t::input_data_source` in :cpp:type:`esp_isp_processor_cfg_t` to :cpp:enumerator:`ISP_INPUT_DATA_SOURCE_DWGDMA`, and configure the input format, output format, and resolution according to the image frame.
DMA input is useful for feeding software-generated data, offline RAW images, or other test images in memory into the ISP. It can be used to validate an ISP pipeline without a camera sensor, reproduce issues with a specific input image. Call :cpp:func:`esp_isp_dma_process_frame` to send one input buffer to the ISP and write the processed image into an output buffer. The input and output buffers must be accessible by DMA. The driver synchronizes cacheable buffers automatically. Input buffers use an unaligned cache write-back; cacheable output buffer addresses and their derived frame sizes must be aligned to the cache line size.
ISP AF Controller
~~~~~~~~~~~~~~~~~
@@ -958,6 +968,7 @@ Application Examples
--------------------
* :example:`peripherals/isp/multi_pipelines` demonstrates how to use the ISP pipelines to process the image signals from camera sensors and display the video on LCD screen via DSI peripheral.
* :example:`peripherals/isp/dma_input` demonstrates how to feed a RAW8 BGGR image in memory into the ISP through DW-GDMA. ``pytest_isp_dma_input.py`` saves the processed RGB888 frames as PPM images and compares them pixel by pixel with the checked-in golden image.
* `esp_video/examples <https://github.com/espressif/esp-video-components/tree/master/esp_video/examples>`_ provides some examples of enabling ISP control algorithms.
API Reference
@@ -978,5 +989,6 @@ API Reference
.. include-build-file:: inc/isp_color.inc
.. include-build-file:: inc/isp_crop.inc
.. include-build-file:: inc/isp_core.inc
.. include-build-file:: inc/isp_dma.inc
.. include-build-file:: inc/components/esp_driver_isp/include/driver/isp_types.inc
.. include-build-file:: inc/components/esp_hal_cam/include/hal/isp_types.inc

View File

@@ -996,6 +996,10 @@ CMake 文件可以使用 ``IDF_TARGET`` 变量来获取当前的硬件目标。
并将这行代码放在项目 CMakeLists.txt 的 ``project()`` 命令之后,修改 ``myproject.elf`` 为你自己的项目名。如果最后一个参数是 ``TEXT``,那么构建系统会嵌入以 null 结尾的字符串,如果最后一个参数被设置为 ``BINARY``,则将文件内容按照原样嵌入。
可选的 ``ALIGN`` 参数用于将嵌入数据的起始符号对齐到指定的正整数 2 的幂。例如,将二进制数据按 16 字节对齐::
target_add_binary_data(myproject.elf "main/data.bin" BINARY ALIGN 16)
有关使用此技术的示例,请查看 file_serving 示例 :example_file:`protocols/http_server/file_serving/main/CMakeLists.txt` 中的 main 组件,两个文件会在编译时加载并链接到固件中。
.. highlight:: cmake

View File

@@ -67,6 +67,7 @@ ISP 驱动程序提供以下服务:
- :ref:`isp-resource-allocation` - 涵盖如何通过正确的配置来分配 ISP 资源,以及完成工作后如何回收资源。
- :ref:`isp-enable-disable` - 涵盖如何启用和禁用 ISP 处理器。
- :ref:`isp-dma-input` - 涵盖如何通过 DW-GDMA 将存储在内存中的图像帧送入 ISP。
- :ref:`isp-af-statistics` - 涵盖如何单次或连续获取 AF 统计信息。
- :ref:`isp-awb-statistics` - 涵盖如何单次或连续获取 AWB 白块统计信息。
- :ref:`isp-ae-statistics` - 涵盖如何单次或连续获取 AE 统计信息。
@@ -240,6 +241,15 @@ ISP
调用 :cpp:func:`esp_isp_disable` 函数会执行相反的操作,即将驱动程序恢复到 **init** 状态。
.. _isp-dma-input:
ISP DMA 输入
~~~~~~~~~~~~
除来自摄像头控制器的数据流外ISP 还可以通过 DW-GDMA 从系统存储中读取图像帧作为输入。使用 DMA 输入时,应在 :cpp:type:`esp_isp_processor_cfg_t` 中将 :cpp:member:`esp_isp_processor_cfg_t::input_data_source` 配置为 :cpp:enumerator:`ISP_INPUT_DATA_SOURCE_DWGDMA`,并根据输入图像格式设置输入、输出格式及分辨率。
DMA 输入适用于将软件生成的数据、离线保存的 RAW 图像或其他内存中的测试图像送入 ISP 进行处理。它可用于无摄像头传感器参与时验证 ISP 流水线、复现特定输入图像的问题。调用 :cpp:func:`esp_isp_dma_process_frame` 可以将一帧输入缓冲区送入 ISP并将处理后的图像写入输出缓冲区。输入和输出缓冲区需要满足 DMA 访问要求。驱动会自动同步带 cache 的缓冲区:输入使用允许未对齐的 cache writeback带 cache 的输出地址及其派生帧大小必须按 cache line 大小对齐。
ISP AF 控制器
~~~~~~~~~~~~~
@@ -958,6 +968,7 @@ Kconfig 选项 :ref:`CONFIG_ISP_CTRL_FUNC_IN_IRAM` 支持:
--------
* :example:`peripherals/isp/multi_pipelines` 演示了如何使用 ISP 流水线处理来自摄像头传感器的图像信号,并通过 DSI 外设在 LCD 屏幕上显示视频。
* :example:`peripherals/isp/dma_input` 演示了如何通过 DW-GDMA 将内存中的 RAW8 BGGR 图像送入 ISP。``pytest_isp_dma_input.py`` 会将处理后的 RGB888 帧保存为 PPM 图片,并与示例中提交的 golden 图片进行逐像素比较。
* `esp_video/examples <https://github.com/espressif/esp-video-components/tree/master/esp_video/examples>`_ 中包含自动启用 ISP 控制算法的一些示例。
API 参考
@@ -978,5 +989,6 @@ API 参考
.. include-build-file:: inc/isp_color.inc
.. include-build-file:: inc/isp_crop.inc
.. include-build-file:: inc/isp_core.inc
.. include-build-file:: inc/isp_dma.inc
.. include-build-file:: inc/components/esp_driver_isp/include/driver/isp_types.inc
.. include-build-file:: inc/components/esp_hal_cam/include/hal/isp_types.inc

View File

@@ -34,7 +34,7 @@ Below is short explanation of remaining files in the project folder.
└── README.md This is the file you are currently reading
```
For more information on structure and contents of ESP-IDF projects, please refer to Section [Build System v2](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/build-system-v2.html) of the ESP-IDF Programming Guide.
For more information on structure and contents of ESP-IDF projects, please refer to Section [Build System v2](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/build-system-v2/index.html) of the ESP-IDF Programming Guide.
## Troubleshooting

View File

@@ -283,6 +283,14 @@ examples/peripherals/i3c/i3c_lsm6dscx:
- esp_hal_i3c
- soc
examples/peripherals/isp/dma_input:
disable:
- if: SOC_ISP_SUPPORTED != 1
depends_components:
- esp_driver_dma
- esp_driver_isp
- soc
examples/peripherals/isp/multi_pipelines:
disable:
- if: SOC_MIPI_CSI_SUPPORTED != 1

View File

@@ -52,6 +52,16 @@ The accompanying pytest script captures the `IMAGE_META` and `IMAGE_BASE64` outp
It also compares the generated result with `golden_result.ppm` by hashing the decoded RGB pixel content. This turns the example into a regression test as well as a visual demo: the image must both render correctly for a human and match the stored golden output for CI.
### Viewing The Result Locally
Build and flash the example for your target, then run the pytest script from this example's directory:
```bash
pytest pytest_async_color_convert.py --target esp32p4 --port PORT
```
Replace `esp32p4` with another supported target and `PORT` with the board's serial device. Pytest prints the artifact directory after the test completes; open `async_color_convert_result.ppm` from that directory with a PPM-compatible image viewer.
## Replacing The Embedded UYVY Asset
The example embeds `main/assets/sample_96x64_uyvy.yuv`.

View File

@@ -0,0 +1,8 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(isp_dma_input)

View File

@@ -0,0 +1,44 @@
| Supported Targets | ESP32-P4 |
| ----------------- | -------- |
# ISP DMA Input Visual Test Example
## Overview
This example embeds a 240 x 280 RAW8 Bayer image of a real scene in flash, feeds it directly into the ISP through DW-GDMA, and prints the RGB888 output as base64. The ISP driver synchronizes the DMA buffers' cache automatically. The pytest script decodes the output into a PPM image and compares it with the checked-in golden image.
The data flow is:
1. The embedded 16-byte-aligned BGGR RAW8 image is transferred from mapped flash into the ISP via `DW-GDMA → ISP DMA input`.
2. The ISP processes the data (demosaic, color adjustment) and outputs RGB888 (BGR24 byte layout).
3. The RGB888 frame is base64-encoded and printed with machine-parseable markers.
4. pytest decodes the payload, swaps BGR→RGB, saves one PPM file per frame, and compares it with the golden image.
## Hardware Required
- An ESP32-P4 devkit with PSRAM (this example allocates the ISP output buffer from PSRAM).
## How to Use
Run the test locally. It builds and flashes the example, captures the serial output, and saves one PPM artifact per frame:
```
cd examples/peripherals/isp/dma_input
pytest pytest_isp_dma_input.py --target esp32p4 --port PORT
```
The pytest log directory (`dut.logdir`) contains `isp_dma_input_frame00.ppm`, the decoded RGB888 ISP output from the current hardware run. The repository includes [golden/golden.ppm](golden/golden.ppm), the checked-in reference image. The test compares the decoded RGB888 pixels with this reference image, making image-quality regressions visible in review and detectable in CI.
## Example Output
The example processes one embedded 240 x 280 BGGR RAW8 frame.
```text
Feeding 1 frames through ISP DMA input...
IMAGE_META frame=0 width=240 height=280 format=BGR24 encoding=base64
IMAGE_BASE64_BEGIN
IMAGE_BASE64 ...
IMAGE_BASE64_END
Frame 0 done
ISP DMA visual demo done.
```

Binary file not shown.

View File

@@ -0,0 +1,7 @@
idf_component_register(SRCS "isp_dma_example_main.c"
PRIV_REQUIRES esp_driver_isp esp_mm esp_psram mbedtls
INCLUDE_DIRS ".")
target_add_binary_data(${COMPONENT_LIB}
"${CMAKE_CURRENT_LIST_DIR}/assets/sensor_240x280_bggr.raw"
BINARY RENAME_TO "sensor_raw" ALIGN 16)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,122 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mbedtls/base64.h"
#include "esp_check.h"
#include "esp_heap_caps.h"
#include "driver/isp_dma.h"
#include "driver/isp_core.h"
#include "driver/isp_color.h"
#define EXAMPLE_WIDTH 240
#define EXAMPLE_HEIGHT 280
#define EXAMPLE_BASE64_CHUNK_LEN 384
#define EXAMPLE_DMA_ALIGN 64
#define EXAMPLE_FRAME_COUNT 1
/* CMake embeds this RAW asset in mapped flash with 16-byte alignment. */
extern const uint8_t sensor_raw_start[] asm("_binary_sensor_raw_start");
extern const uint8_t sensor_raw_end[] asm("_binary_sensor_raw_end");
static void example_configure_neutral_color(isp_proc_handle_t isp_proc)
{
esp_isp_color_config_t color_cfg = {
.color_contrast = { .integer = 1, .decimal = 0 },
.color_saturation = { .integer = 1, .decimal = 0 },
.color_hue = 0,
.color_brightness = 0,
};
ESP_ERROR_CHECK(esp_isp_color_configure(isp_proc, &color_cfg));
ESP_ERROR_CHECK(esp_isp_color_enable(isp_proc));
}
static void example_print_base64_payload(const unsigned char *encoded, size_t encoded_len)
{
printf("IMAGE_BASE64_BEGIN\n");
size_t chunk_count = 0;
for (size_t offset = 0; offset < encoded_len; offset += EXAMPLE_BASE64_CHUNK_LEN) {
size_t chunk_len = encoded_len - offset;
if (chunk_len > EXAMPLE_BASE64_CHUNK_LEN) {
chunk_len = EXAMPLE_BASE64_CHUNK_LEN;
}
printf("IMAGE_BASE64 %.*s\n", (int)chunk_len, (const char *)&encoded[offset]);
if ((++chunk_count % 16) == 0) {
/* Let the test host drain the UART without delaying every chunk. */
vTaskDelay(1);
}
}
printf("IMAGE_BASE64_END\n");
}
void app_main(void)
{
const uint32_t h_res = EXAMPLE_WIDTH;
const uint32_t v_res = EXAMPLE_HEIGHT;
const size_t in_size = (size_t)h_res * v_res; // RAW8: 1 byte/pixel
const size_t out_size = (size_t)h_res * v_res * 3; // RGB888: 3 bytes/pixel
isp_proc_handle_t isp_proc = NULL;
esp_isp_processor_cfg_t isp_cfg = {
.clk_hz = 240 * 1000 * 1000,
.input_data_source = ISP_INPUT_DATA_SOURCE_DWGDMA,
.input_data_color_type = ISP_COLOR_RAW8,
.output_data_color_type = ISP_COLOR_RGB888,
.bayer_order = COLOR_RAW_ELEMENT_ORDER_BGGR,
.has_line_start_packet = false,
.has_line_end_packet = false,
.h_res = h_res,
.v_res = v_res,
.dma_burst_size = 8,
};
ESP_ERROR_CHECK(esp_isp_new_processor(&isp_cfg, &isp_proc));
ESP_ERROR_CHECK(esp_isp_enable(isp_proc));
example_configure_neutral_color(isp_proc);
/* ISP writes this RGB frame through DMA, so PSRAM must be DMA-capable. */
uint8_t *isp_out_buf = heap_caps_aligned_calloc(EXAMPLE_DMA_ALIGN, 1, out_size,
MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
assert(isp_out_buf);
size_t embedded_raw_size = sensor_raw_end - sensor_raw_start;
assert(embedded_raw_size == in_size);
size_t encoded_len = 0;
int ret = mbedtls_base64_encode(NULL, 0, &encoded_len, isp_out_buf, out_size);
ESP_ERROR_CHECK((ret == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) ? ESP_OK : ESP_FAIL);
unsigned char *encoded = calloc(encoded_len + 1, 1);
assert(encoded);
printf("Feeding %d frames through ISP DMA input...\n", EXAMPLE_FRAME_COUNT);
for (int frame = 0; frame < EXAMPLE_FRAME_COUNT; frame++) {
/*
* The RAW image is immutable mapped flash. Its aligned address can be
* read by DMA directly, so no PSRAM copy is needed.
*/
ESP_ERROR_CHECK(esp_isp_dma_process_frame(isp_proc, isp_out_buf, sensor_raw_start, 1000));
size_t out_len = 0;
ESP_ERROR_CHECK(mbedtls_base64_encode(encoded, encoded_len + 1, &out_len, isp_out_buf, out_size) == 0 ? ESP_OK : ESP_FAIL);
printf("IMAGE_META frame=%d width=%u height=%u format=BGR24 encoding=base64\n",
frame, (unsigned)h_res, (unsigned)v_res);
example_print_base64_payload(encoded, out_len);
printf("Frame %d done\n", frame);
}
printf("ISP DMA visual demo done.\n");
ESP_ERROR_CHECK(esp_isp_color_disable(isp_proc));
ESP_ERROR_CHECK(esp_isp_disable(isp_proc));
ESP_ERROR_CHECK(esp_isp_del_processor(isp_proc));
free(encoded);
free(isp_out_buf);
}

View File

@@ -0,0 +1,181 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import base64
import logging
import re
from dataclasses import dataclass
from pathlib import Path
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_idf.utils import soc_filtered_targets
IMAGE_META_PATTERN = (
r'IMAGE_META frame=(?P<frame>\d+) width=(?P<width>\d+) height=(?P<height>\d+) '
r'format=(?P<format>\w+) encoding=(?P<encoding>\w+)'
)
IMAGE_META_RE = re.compile(IMAGE_META_PATTERN)
IMAGE_CHUNK_PATTERN = (
r'IMAGE_BASE64 (?P<payload>[A-Za-z0-9+/=]+?)'
r'(?=\r?\n|IMAGE_BASE64|IMAGE_BASE64_END|Frame )'
)
IMAGE_OUTPUT_TEMPLATE = 'isp_dma_input_frame{frame:02d}.ppm'
REFERENCE_IMAGE_PATH = Path(__file__).parent / 'golden' / 'golden.ppm'
EXPECTED_PIXEL_FORMAT = 'BGR24'
EXPECTED_ENCODING = 'base64'
RGB888_BYTES_PER_PIXEL = 3
PPM_MAGIC = b'P6'
PPM_MAX_VALUE = b'255'
@dataclass(frozen=True)
class ImageMetadata:
frame: int
width: int
height: int
pixel_format: str
encoding: str
@dataclass(frozen=True)
class RgbImage:
width: int
height: int
pixels_rgb888: bytes
def __post_init__(self) -> None:
expected_size = self.width * self.height * RGB888_BYTES_PER_PIXEL
if len(self.pixels_rgb888) != expected_size:
raise ValueError(f'Expected {expected_size} RGB bytes, got {len(self.pixels_rgb888)}')
def parse_image_metadata(meta_line: str) -> ImageMetadata:
match = IMAGE_META_RE.fullmatch(meta_line)
if not match:
raise ValueError(f'Invalid image metadata line: {meta_line}')
return ImageMetadata(
frame=int(match.group('frame')),
width=int(match.group('width')),
height=int(match.group('height')),
pixel_format=match.group('format'),
encoding=match.group('encoding'),
)
def collect_base64_payload(dut: Dut) -> list[str]:
payload_lines: list[str] = []
while True:
match = dut.expect(rf'IMAGE_BASE64_END|{IMAGE_CHUNK_PATTERN}', timeout=60)
if match.group(0).decode('utf-8') == 'IMAGE_BASE64_END':
return payload_lines
payload_lines.append(match.group('payload').decode('utf-8'))
def _bgr24_to_rgb888(raw_bytes: bytes) -> bytes:
rgb_bytes = bytearray(len(raw_bytes))
for offset in range(0, len(raw_bytes), 3):
blue, green, red = raw_bytes[offset : offset + 3]
rgb_bytes[offset : offset + 3] = (red, green, blue)
return bytes(rgb_bytes)
def _encode_ppm(image: RgbImage) -> bytes:
header = b'%s\n%d %d\n%s\n' % (PPM_MAGIC, image.width, image.height, PPM_MAX_VALUE)
return header + image.pixels_rgb888
def decode_bgr24_base64_image(metadata: ImageMetadata, payload_lines: list[str]) -> RgbImage:
if metadata.pixel_format != EXPECTED_PIXEL_FORMAT:
raise ValueError(f'Unsupported pixel format: {metadata.pixel_format}')
if metadata.encoding != EXPECTED_ENCODING:
raise ValueError(f'Unsupported payload encoding: {metadata.encoding}')
raw_bytes = base64.b64decode(''.join(payload_lines), validate=True)
expected_size = metadata.width * metadata.height * RGB888_BYTES_PER_PIXEL
if len(raw_bytes) != expected_size:
raise ValueError(f'Expected {expected_size} decoded bytes, got {len(raw_bytes)}')
return RgbImage(width=metadata.width, height=metadata.height, pixels_rgb888=_bgr24_to_rgb888(raw_bytes))
def save_ppm_artifact(image: RgbImage, output_path: Path) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
try:
output_path.write_bytes(_encode_ppm(image))
except OSError:
logging.exception('Failed to save ISP DMA artifact to %s', output_path)
return
logging.info('Saved ISP DMA artifact to %s', output_path)
def load_ppm_image(image_path: Path) -> RgbImage:
ppm_data = image_path.read_bytes()
try:
magic, dimensions, max_value, pixels_rgb888 = ppm_data.split(b'\n', 3)
width, height = (int(value) for value in dimensions.split())
except ValueError as error:
raise ValueError(f'Invalid PPM image: {image_path}') from error
if magic != PPM_MAGIC or max_value != PPM_MAX_VALUE:
raise ValueError(f'Unsupported PPM image: {image_path}')
return RgbImage(width=width, height=height, pixels_rgb888=pixels_rgb888)
def assert_image_matches_reference(result_image: RgbImage, reference_image: RgbImage) -> None:
assert (result_image.width, result_image.height) == (reference_image.width, reference_image.height), (
f'ISP output dimensions do not match reference image: '
f'{result_image.width}x{result_image.height} != {reference_image.width}x{reference_image.height}'
)
if result_image.pixels_rgb888 == reference_image.pixels_rgb888:
return
mismatch_offset = next(
offset
for offset, (actual, expected) in enumerate(zip(result_image.pixels_rgb888, reference_image.pixels_rgb888))
if actual != expected
)
pixel_index, channel = divmod(mismatch_offset, RGB888_BYTES_PER_PIXEL)
raise AssertionError(
f'ISP output does not match reference image at pixel {pixel_index}, channel {channel}: '
f'{result_image.pixels_rgb888[mismatch_offset]} != {reference_image.pixels_rgb888[mismatch_offset]}'
)
@pytest.mark.generic
@idf_parametrize('target', soc_filtered_targets('SOC_ISP_SUPPORTED == 1'), indirect=['target'])
def test_isp_dma_input_example(dut: Dut) -> None:
reference_image = load_ppm_image(REFERENCE_IMAGE_PATH)
frame_count_match = dut.expect(r'Feeding (?P<frame_count>\d+) frames through ISP DMA input...')
expected_frame_count = int(frame_count_match.group('frame_count').decode('utf-8'))
logging.info('Expecting %d ISP DMA frame(s)', expected_frame_count)
for expected_frame in range(expected_frame_count):
metadata = parse_image_metadata(dut.expect(IMAGE_META_PATTERN).group(0).decode('utf-8'))
assert metadata.frame == expected_frame, f'Out-of-order frame: expected {expected_frame}, got {metadata.frame}'
logging.info(
'Received frame metadata: frame=%d size=%dx%d format=%s',
metadata.frame,
metadata.width,
metadata.height,
metadata.pixel_format,
)
dut.expect_exact('IMAGE_BASE64_BEGIN')
logging.info('Receiving base64 image payload for frame %d', metadata.frame)
payload_lines = collect_base64_payload(dut)
logging.info('Received %d base64 chunk(s) for frame %d', len(payload_lines), metadata.frame)
result_image = decode_bgr24_base64_image(metadata, payload_lines)
logging.info('Decoded frame %d to RGB888 image', metadata.frame)
output_path = Path(dut.logdir) / IMAGE_OUTPUT_TEMPLATE.format(frame=metadata.frame)
save_ppm_artifact(result_image, output_path)
assert_image_matches_reference(result_image, reference_image)
dut.expect_exact(f'Frame {expected_frame} done')
dut.expect_exact('ISP DMA visual demo done.')

View File

@@ -0,0 +1 @@
CONFIG_SPIRAM=y

View File

@@ -60,8 +60,7 @@ The test writes the `PPM` file and compares it with `golden_output.ppm`. This ma
To run the pytest helper locally on hardware, build the example for your target first, then invoke the test script with the target and serial port:
```bash
idf.py set-target esp32p4 build
pytest --target esp32p4 --port PORT pytest_jpeg_decode.py
pytest pytest_jpeg_decode.py --target esp32p4 --port PORT
```
Replace `esp32p4` with another supported target such as `esp32s31` when needed.

View File

@@ -55,8 +55,7 @@ It also compares the generated JPEG with `golden_output.jpeg`. This turns the ex
To run the pytest helper locally on hardware, build the example for your target first, then invoke the test script with the target and serial port:
```bash
idf.py set-target esp32p4 build
pytest --target esp32p4 --port PORT pytest_jpeg_encode.py
pytest pytest_jpeg_encode.py --target esp32p4 --port PORT
```
Replace `esp32p4` with another supported target such as `esp32s31` when needed.

View File

@@ -74,6 +74,9 @@ append_line(".data")
append_line("#if !defined (__APPLE__) && !defined (__linux__)")
append_line(".section .rodata.embedded")
append_line("#endif")
if(DEFINED DATA_ALIGNMENT)
append_line(".balign ${DATA_ALIGNMENT}")
endif()
make_and_append_identifier("${varname}")
make_and_append_identifier("_binary_${varname}_start" "for objcopy compatibility")
append("${data}")

View File

@@ -103,9 +103,10 @@ endfunction()
# target_add_binary_data adds binary data into the built target,
# by converting it to a generated source file which is then compiled
# to a binary object as part of the build
# to a binary object as part of the build. ALIGN optionally sets the
# alignment of the embedded data's start symbol.
function(target_add_binary_data target embed_file embed_type)
cmake_parse_arguments(_ "" "RENAME_TO" "DEPENDS" ${ARGN})
cmake_parse_arguments(_ "" "RENAME_TO;ALIGN" "DEPENDS" ${ARGN})
idf_build_get_property(build_dir BUILD_DIR)
idf_build_get_property(idf_path IDF_PATH)
@@ -119,11 +120,24 @@ function(target_add_binary_data target embed_file embed_type)
set(rename_to_arg -D "VARIABLE_BASENAME=${__RENAME_TO}")
endif()
set(align_arg)
if(DEFINED __ALIGN)
if(NOT __ALIGN MATCHES "^[1-9][0-9]*$")
message(FATAL_ERROR "ALIGN must be a positive integer")
endif()
math(EXPR alignment_mask "${__ALIGN} & (${__ALIGN} - 1)")
if(NOT alignment_mask EQUAL 0)
message(FATAL_ERROR "ALIGN must be a power of two")
endif()
set(align_arg -D "DATA_ALIGNMENT=${__ALIGN}")
endif()
add_custom_command(OUTPUT "${embed_srcfile}"
COMMAND "${CMAKE_COMMAND}"
-D "DATA_FILE=${embed_file}"
-D "SOURCE_FILE=${embed_srcfile}"
${rename_to_arg}
${align_arg}
-D "FILE_TYPE=${embed_type}"
-P "${idf_path}/tools/cmake/scripts/data_file_embed_asm.cmake"
MAIN_DEPENDENCY "${embed_file}"

View File

@@ -833,7 +833,8 @@ endfunction()
#[[
target_add_binary_data(<target> <embed_file> <embed_type>
[RENAME_TO <symbol>])
[RENAME_TO <symbol>]
[ALIGN <alignment>]
[DEPENDS <dependency>...])
*target[in]*
@@ -853,6 +854,11 @@ endfunction()
Use the given symbol name for the embedded data. If no symbol name is
provided, the embed_file file name will be used instead.
*ALIGN[in,opt]*
Align the embedded data's start symbol to the given positive power
of two.
*DEPENDS[in,opt]*
List of additional dependencies for the generated file containing
@@ -863,7 +869,7 @@ endfunction()
build process.
#]]
function(target_add_binary_data target embed_file embed_type)
cmake_parse_arguments(_ "" "RENAME_TO" "DEPENDS" ${ARGN})
cmake_parse_arguments(_ "" "RENAME_TO;ALIGN" "DEPENDS" ${ARGN})
idf_build_get_property(build_dir BUILD_DIR)
# In cmakev1, the executable target was named "${project}.elf".
@@ -888,11 +894,24 @@ function(target_add_binary_data target embed_file embed_type)
set(rename_to_arg -D "VARIABLE_BASENAME=${__RENAME_TO}")
endif()
set(align_arg)
if(DEFINED __ALIGN)
if(NOT __ALIGN MATCHES "^[1-9][0-9]*$")
message(FATAL_ERROR "ALIGN must be a positive integer")
endif()
math(EXPR alignment_mask "${__ALIGN} & (${__ALIGN} - 1)")
if(NOT alignment_mask EQUAL 0)
message(FATAL_ERROR "ALIGN must be a power of two")
endif()
set(align_arg -D "DATA_ALIGNMENT=${__ALIGN}")
endif()
add_custom_command(OUTPUT "${embed_srcfile}"
COMMAND "${CMAKE_COMMAND}"
-D "DATA_FILE=${embed_file}"
-D "SOURCE_FILE=${embed_srcfile}"
${rename_to_arg}
${align_arg}
-D "FILE_TYPE=${embed_type}"
-P "${idf_path}/tools/cmake/scripts/data_file_embed_asm.cmake"
MAIN_DEPENDENCY "${embed_file}"