diff --git a/components/esp_driver_isp/CMakeLists.txt b/components/esp_driver_isp/CMakeLists.txt index 476fb925a22..c0236118d20 100644 --- a/components/esp_driver_isp/CMakeLists.txt +++ b/components/esp_driver_isp/CMakeLists.txt @@ -6,10 +6,16 @@ set(public_include "include") set(priv_requires "esp_driver_gpio") -set(requires) + +if(${target} STREQUAL "linux") + set(requires "") +else() + set(requires "esp_hw_support" "esp_mm") +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" diff --git a/components/esp_driver_isp/include/driver/isp_core.h b/components/esp_driver_isp/include/driver/isp_core.h index bcc2c4de5cd..6869fac72cb 100644 --- a/components/esp_driver_isp/include/driver/isp_core.h +++ b/components/esp_driver_isp/include/driver/isp_core.h @@ -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 diff --git a/components/esp_driver_isp/include/driver/isp_dma.h b/components/esp_driver_isp/include/driver/isp_dma.h new file mode 100644 index 00000000000..7bb2cc8585d --- /dev/null +++ b/components/esp_driver_isp/include/driver/isp_dma.h @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#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 Input buffer content should be ready before calling this function. If the buffers are in + * cacheable memory, the caller should synchronize them around DMA access. Buffer sizes are + * derived from the ISP processor resolution and pixel formats. Both buffers must be 8-byte + * aligned. This function blocks until both input and output DMA channels finish. + * + * @param[in] proc Processor handle + * @param[in] output_buffer Destination buffer for ISP output (8-byte aligned) + * @param[in] input_buffer Source input buffer for RAW frame data (8-byte aligned) + * @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 diff --git a/components/esp_driver_isp/include/esp_private/isp_private.h b/components/esp_driver_isp/include/esp_private/isp_private.h index 3bc3dda446c..38b9268bc8e 100644 --- a/components/esp_driver_isp/include/esp_private/isp_private.h +++ b/components/esp_driver_isp/include/esp_private/isp_private.h @@ -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 */ @@ -60,6 +60,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; @@ -72,10 +74,14 @@ typedef struct isp_processor_t { portMUX_TYPE spinlock; color_space_pixel_format_t in_color_format; color_space_pixel_format_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[SOC_ISP_AF_CTLR_NUMS]; isp_awb_ctlr_t awb_ctlr; @@ -133,6 +139,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 diff --git a/components/esp_driver_isp/src/isp_core.c b/components/esp_driver_isp/src/isp_core.c index 03203d3999a..6047c84dc9e 100644 --- a/components/esp_driver_isp/src/isp_core.c +++ b/components/esp_driver_isp/src/isp_core.c @@ -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 "soc/isp_periph.h" #include "soc/soc_caps.h" @@ -79,7 +80,19 @@ 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_get_bit_depth((color_space_pixel_format_t) { + .color_type_id = 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"); } @@ -176,7 +189,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.color_space == COLOR_SPACE_RGB && proc_config->input_data_source == ISP_INPUT_DATA_SOURCE_DVP) { + if ((out_color_format.color_type_id == ISP_COLOR_RGB888 || out_color_format.color_type_id == 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) { @@ -187,16 +202,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); } @@ -210,6 +232,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 @@ -341,20 +365,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) { diff --git a/components/esp_driver_isp/src/isp_dma.c b/components/esp_driver_isp/src/isp_dma.c new file mode 100644 index 00000000000..4492a32779d --- /dev/null +++ b/components/esp_driver_isp/src/isp_dma.c @@ -0,0 +1,265 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "sdkconfig.h" +#include "esp_log.h" +#include "esp_check.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 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, (isp_color_t)proc->in_color_format.color_type_id); + 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_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_get_bit_depth(proc->in_color_format); + uint32_t out_bits_per_pixel = color_hal_pixel_format_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; + 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; +} diff --git a/components/hal/esp32p4/include/hal/dw_gdma_ll.h b/components/hal/esp32p4/include/hal/dw_gdma_ll.h index 30f0c35e5b3..6572bbdac02 100644 --- a/components/hal/esp32p4/include/hal/dw_gdma_ll.h +++ b/components/hal/esp32p4/include/hal/dw_gdma_ll.h @@ -409,6 +409,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; } diff --git a/components/hal/esp32p4/include/hal/isp_ll.h b/components/hal/esp32p4/include/hal/isp_ll.h index fce2736d0f6..45d2577bc48 100644 --- a/components/hal/esp32p4/include/hal/isp_ll.h +++ b/components/hal/esp32p4/include/hal/isp_ll.h @@ -27,7 +27,7 @@ extern "C" { #define ISP_LL_GET_HW(num) (((num) == 0) ? (&ISP) : NULL) #define ISP_LL_HSIZE_MAX 1920 -#define ISP_LL_VSIZE_MAX 1080 +#define ISP_LL_VSIZE_MAX 1280 /*--------------------------------------------------------------- Clock @@ -529,6 +529,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 * diff --git a/docs/en/api-reference/peripherals/isp.rst b/docs/en/api-reference/peripherals/isp.rst index ed849a9b984..5b16b38bf2b 100644 --- a/docs/en/api-reference/peripherals/isp.rst +++ b/docs/en/api-reference/peripherals/isp.rst @@ -240,6 +240,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, or generate inspectable output images in pytest. 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; if cacheable memory is used, perform the required cache synchronization before and after the DMA transfer. + ISP AF Controller ~~~~~~~~~~~~~~~~~ @@ -847,7 +856,6 @@ Calling :cpp:func:`esp_isp_crop_disable` does the opposite, that is, put the dri - The crop region cannot exceed the boundaries of the original image - Adjust the display medium (such as LCD) size according to the cropped resolution to ensure complete display and avoid black borders or stretching. - .. _isp-callback: Register Event Callbacks @@ -957,6 +965,8 @@ 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 software-generated RAW8 Bayer data into the ISP through DW-GDMA and save the processed RGB888 frames as PPM images in pytest. +* `esp_video/examples `_ provides some examples of enabling ISP control algorithms. API Reference ------------- diff --git a/docs/zh_CN/api-reference/peripherals/isp.rst b/docs/zh_CN/api-reference/peripherals/isp.rst index a17b5b82bab..7b550ef227a 100644 --- a/docs/zh_CN/api-reference/peripherals/isp.rst +++ b/docs/zh_CN/api-reference/peripherals/isp.rst @@ -240,6 +240,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 流水线、复现特定输入图像的问题,或在 pytest 中生成可检查的输出图像。调用 :cpp:func:`esp_isp_dma_process_frame` 可以将一帧输入缓冲区送入 ISP,并将处理后的图像写入输出缓冲区。输入和输出缓冲区需要满足 DMA 访问要求;若使用带 cache 的内存,请在 DMA 传输前后执行必要的 cache 同步。 + ISP AF 控制器 ~~~~~~~~~~~~~ @@ -956,6 +965,8 @@ Kconfig 选项 :ref:`CONFIG_ISP_CTRL_FUNC_IN_IRAM` 支持: -------- * :example:`peripherals/isp/multi_pipelines` 演示了如何使用 ISP 流水线处理来自摄像头传感器的图像信号,并通过 DSI 外设在 LCD 屏幕上显示视频。 +* :example:`peripherals/isp/dma_input` 演示了如何通过 DW-GDMA 将软件生成的 RAW8 Bayer 数据送入 ISP,并在 pytest 中将处理后的 RGB888 帧保存为 PPM 图片。 +* `esp_video/examples `_ 中包含自动启用 ISP 控制算法的一些示例。 API 参考 -------- diff --git a/examples/peripherals/.build-test-rules.yml b/examples/peripherals/.build-test-rules.yml index 00aff054b4c..a5212fe6b9b 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -171,6 +171,14 @@ examples/peripherals/i2s/i2s_recorder: - esp_driver_spi - esp_driver_i2s +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 diff --git a/examples/peripherals/isp/dma_input/CMakeLists.txt b/examples/peripherals/isp/dma_input/CMakeLists.txt new file mode 100644 index 00000000000..813a03e8521 --- /dev/null +++ b/examples/peripherals/isp/dma_input/CMakeLists.txt @@ -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) diff --git a/examples/peripherals/isp/dma_input/README.md b/examples/peripherals/isp/dma_input/README.md new file mode 100644 index 00000000000..3298c0ab2c3 --- /dev/null +++ b/examples/peripherals/isp/dma_input/README.md @@ -0,0 +1,53 @@ +| Supported Targets | ESP32-P4 | +| ----------------- | -------- | + +# ISP DMA Input Visual Test Example + +## Overview + +This example generates a standard RAW8 Bayer color-bar pattern in software, writes it into a DMA-capable PSRAM input buffer, feeds it into the ISP through DW-GDMA, applies the ISP color adjustment module, and prints the RGB888 output as base64. The pytest script decodes the output into PPM images for inspection. + +The data flow is: + +1. A synthetic RAW8 Bayer **color-bar** pattern is generated in software into the ISP DMA input buffer. +2. The pattern is transferred into the ISP via `DW-GDMA → ISP DMA input`. +3. The ISP processes the data (demosaic, color adjustment) and outputs RGB888 (BGR24 byte layout). +4. The RGB888 frame is base64-encoded and printed with machine-parseable markers. +5. pytest decodes the payload, swaps BGR→RGB, and saves one PPM file per frame. + +## Hardware Required + +- An ESP32-P4 devkit with PSRAM (this example allocates the ISP DMA input/output buffers from PSRAM). + +If you replace the software-generated pattern with a RAW image embedded in flash, copy it into a DMA-capable buffer before feeding it to the ISP DMA input path. + +## 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 `reference.ppm` for the standard reference color-bar image and `isp_dma_input_frame00.ppm`, ... for the decoded ISP output frames. The pytest script checks that the decoded frame contains the expected standard color-bar structure and verifies that the ISP color brightness adjustment brightens the black bar. + +ISP feature-specific setup is kept under `main/isp_features//`, and pytest feature checks are kept under `pytest_features/.py`. The current color brightness check lives in `main/isp_features/color/` and `pytest_features/color_brightness.py`; new ISP feature checks can follow the same pattern while reusing the common DW-GDMA input/output and image parsing flow. + +## Example Output + +Each frame uses the same standard vertical color-bar input pattern with positive color brightness enabled to validate the ISP color adjustment module. + +```text +Feeding 1 frames through ISP DMA input... +IMAGE_META frame=0 width=128 height=96 format=BGR24 encoding=base64 color_brightness=64 +IMAGE_BASE64_BEGIN +IMAGE_BASE64 ... +IMAGE_BASE64_END +Frame 0 done +ISP DMA visual demo done. +``` + +## Reference + +- [ESP-IDF: Image Signal Processor](https://docs.espressif.com/projects/esp-idf/en/latest/esp32p4/api-reference/peripherals/isp.html) diff --git a/examples/peripherals/isp/dma_input/main/CMakeLists.txt b/examples/peripherals/isp/dma_input/main/CMakeLists.txt new file mode 100644 index 00000000000..af79c8d577c --- /dev/null +++ b/examples/peripherals/isp/dma_input/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "isp_dma_main.c" + PRIV_REQUIRES esp_driver_isp esp_mm esp_psram mbedtls + INCLUDE_DIRS ".") diff --git a/examples/peripherals/isp/dma_input/main/isp_dma_main.c b/examples/peripherals/isp/dma_input/main/isp_dma_main.c new file mode 100644 index 00000000000..111b3fd71cc --- /dev/null +++ b/examples/peripherals/isp/dma_input/main/isp_dma_main.c @@ -0,0 +1,153 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "mbedtls/base64.h" +#include "esp_check.h" +#include "esp_cache.h" +#include "esp_heap_caps.h" +#include "driver/isp_dma.h" +#include "driver/isp_core.h" +#include "driver/isp_color.h" + +#define EXAMPLE_WIDTH 128 +#define EXAMPLE_HEIGHT 96 +#define EXAMPLE_BASE64_CHUNK_LEN 384 +#define EXAMPLE_DMA_ALIGN 64 +#define EXAMPLE_FRAME_COUNT 2 + +static const uint8_t s_color_bars[8][3] = { + {255, 255, 255}, // white + {255, 255, 0}, // yellow + { 0, 255, 255}, // cyan + { 0, 255, 0}, // green + {255, 0, 255}, // magenta + {255, 0, 0}, // red + { 0, 0, 255}, // blue + { 0, 0, 0}, // black +}; + +static void s_generate_raw8_color_bars(uint8_t *raw, uint32_t w, uint32_t h) +{ + for (uint32_t y = 0; y < h; y++) { + bool even_row = ((y & 1) == 0); + for (uint32_t x = 0; x < w; x++) { + uint32_t bar = (x * 8) / w; + uint8_t r = s_color_bars[bar][0]; + uint8_t g = s_color_bars[bar][1]; + uint8_t b = s_color_bars[bar][2]; + + bool even_col = ((x & 1) == 0); + if (even_row) { + raw[y * w + x] = even_col ? b : g; + } else { + raw[y * w + x] = even_col ? g : r; + } + } + } +} + +static void *s_alloc_dma_buffer(size_t size) +{ + return heap_caps_aligned_calloc(EXAMPLE_DMA_ALIGN, 1, size, + MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); +} + +static void s_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 s_print_base64_payload(const unsigned char *encoded, size_t encoded_len) +{ + printf("IMAGE_BASE64_BEGIN\n"); + fflush(stdout); + 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]); + fflush(stdout); + vTaskDelay(pdMS_TO_TICKS(1)); + } + printf("IMAGE_BASE64_END\n"); + fflush(stdout); +} + +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)); + s_configure_neutral_color(isp_proc); + + uint8_t *isp_in_buf = s_alloc_dma_buffer(in_size); + uint8_t *isp_out_buf = s_alloc_dma_buffer(out_size); + assert(isp_in_buf && isp_out_buf); + + 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++) { + s_generate_raw8_color_bars(isp_in_buf, h_res, v_res); + + ESP_ERROR_CHECK(esp_cache_msync(isp_in_buf, in_size, ESP_CACHE_MSYNC_FLAG_DIR_C2M)); + ESP_ERROR_CHECK(esp_isp_dma_process_frame(isp_proc, isp_out_buf, isp_in_buf, 1000)); + ESP_ERROR_CHECK(esp_cache_msync(isp_out_buf, out_size, ESP_CACHE_MSYNC_FLAG_DIR_M2C)); + + 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); + s_print_base64_payload(encoded, out_len); + printf("Frame %d done\n", frame); + } + printf("ISP DMA visual demo done.\n"); + + free(encoded); + 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)); + heap_caps_free(isp_in_buf); + heap_caps_free(isp_out_buf); +} diff --git a/examples/peripherals/isp/dma_input/pytest_isp_dma_input.py b/examples/peripherals/isp/dma_input/pytest_isp_dma_input.py new file mode 100644 index 00000000000..393b040cf0a --- /dev/null +++ b/examples/peripherals/isp/dma_input/pytest_isp_dma_input.py @@ -0,0 +1,175 @@ +# 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\d+) width=(?P\d+) height=(?P\d+) ' + r'format=(?P\w+) encoding=(?P\w+)' +) +IMAGE_META_RE = re.compile(IMAGE_META_PATTERN) +IMAGE_CHUNK_PATTERN = ( + r'IMAGE_BASE64 (?P[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_NAME = 'reference.ppm' +EXPECTED_PIXEL_FORMAT = 'BGR24' +EXPECTED_ENCODING = 'base64' +STANDARD_COLOR_BARS_RGB888 = ( + (255, 255, 255), # white + (255, 255, 0), # yellow + (0, 255, 255), # cyan + (0, 255, 0), # green + (255, 0, 255), # magenta + (255, 0, 0), # red + (0, 0, 255), # blue + (0, 0, 0), # black +) +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 generate_standard_color_bar_image(width: int, height: int) -> RgbImage: + pixels = bytearray(width * height * RGB888_BYTES_PER_PIXEL) + for y in range(height): + for x in range(width): + bar_index = (x * len(STANDARD_COLOR_BARS_RGB888)) // width + offset = (y * width + x) * RGB888_BYTES_PER_PIXEL + pixels[offset : offset + RGB888_BYTES_PER_PIXEL] = STANDARD_COLOR_BARS_RGB888[bar_index] + + return RgbImage(width=width, height=height, pixels_rgb888=bytes(pixels)) + + +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 assert_image_is_meaningful(image: RgbImage) -> None: + distinct_pixels = {image.pixels_rgb888[i : i + 3] for i in range(0, len(image.pixels_rgb888), 3)} + assert len(distinct_pixels) > 1, 'ISP output is a single flat color; the pipeline likely produced no real data' + + +@pytest.mark.generic +@idf_parametrize('target', soc_filtered_targets('SOC_ISP_SUPPORTED == 1'), indirect=['target']) +def test_isp_dma_input_example(dut: Dut) -> None: + frame_count_match = dut.expect(r'Feeding (?P\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, + ) + if expected_frame == 0: + reference_image = generate_standard_color_bar_image(metadata.width, metadata.height) + save_ppm_artifact(reference_image, Path(dut.logdir) / REFERENCE_IMAGE_NAME) + + 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_is_meaningful(result_image) + dut.expect_exact(f'Frame {expected_frame} done') + + dut.expect_exact('ISP DMA visual demo done.') diff --git a/examples/peripherals/isp/dma_input/sdkconfig.defaults b/examples/peripherals/isp/dma_input/sdkconfig.defaults new file mode 100644 index 00000000000..cc641ea6033 --- /dev/null +++ b/examples/peripherals/isp/dma_input/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_SPIRAM=y