diff --git a/components/esp_driver_isp/CMakeLists.txt b/components/esp_driver_isp/CMakeLists.txt index e70375f3291..fe69095f571 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 "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" 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..adaeb52bd6a --- /dev/null +++ b/components/esp_driver_isp/include/driver/isp_dma.h @@ -0,0 +1,40 @@ +/* + * 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 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 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 076ef6ab894..140f9b9f261 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 */ @@ -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 diff --git a/components/esp_driver_isp/src/isp_core.c b/components/esp_driver_isp/src/isp_core.c index c97423cc29c..13d0ee28cdf 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 "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) { 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..83cecbf2743 --- /dev/null +++ b/components/esp_driver_isp/src/isp_dma.c @@ -0,0 +1,292 @@ +/* + * 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_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; +} diff --git a/components/esp_hal_cam/esp32p4/include/hal/isp_ll.h b/components/esp_hal_cam/esp32p4/include/hal/isp_ll.h index 5749c95d4b7..d6599b607b3 100644 --- a/components/esp_hal_cam/esp32p4/include/hal/isp_ll.h +++ b/components/esp_hal_cam/esp32p4/include/hal/isp_ll.h @@ -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 * diff --git a/components/esp_hal_dma/esp32p4/include/hal/dw_gdma_ll.h b/components/esp_hal_dma/esp32p4/include/hal/dw_gdma_ll.h index 0295fee38cc..fe183849be0 100644 --- a/components/esp_hal_dma/esp32p4/include/hal/dw_gdma_ll.h +++ b/components/esp_hal_dma/esp32p4/include/hal/dw_gdma_ll.h @@ -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; } diff --git a/docs/doxygen/Doxyfile_esp32p4 b/docs/doxygen/Doxyfile_esp32p4 index 8ae3c77644e..f207c7181fa 100644 --- a/docs/doxygen/Doxyfile_esp32p4 +++ b/docs/doxygen/Doxyfile_esp32p4 @@ -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 \ diff --git a/docs/en/api-guides/build-system.rst b/docs/en/api-guides/build-system.rst index ac57d66a3e9..3f10f099fc2 100644 --- a/docs/en/api-guides/build-system.rst +++ b/docs/en/api-guides/build-system.rst @@ -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 diff --git a/docs/en/api-reference/peripherals/isp.rst b/docs/en/api-reference/peripherals/isp.rst index f53ce1d440d..3ef72afeb37 100644 --- a/docs/en/api-reference/peripherals/isp.rst +++ b/docs/en/api-reference/peripherals/isp.rst @@ -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 `_ 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 diff --git a/docs/zh_CN/api-guides/build-system.rst b/docs/zh_CN/api-guides/build-system.rst index b3f72737849..9e4c577ca67 100644 --- a/docs/zh_CN/api-guides/build-system.rst +++ b/docs/zh_CN/api-guides/build-system.rst @@ -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 diff --git a/docs/zh_CN/api-reference/peripherals/isp.rst b/docs/zh_CN/api-reference/peripherals/isp.rst index 25edb05f509..d355f117ac7 100644 --- a/docs/zh_CN/api-reference/peripherals/isp.rst +++ b/docs/zh_CN/api-reference/peripherals/isp.rst @@ -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 `_ 中包含自动启用 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 diff --git a/examples/build_system/cmakev2/get-started/hello_world/README.md b/examples/build_system/cmakev2/get-started/hello_world/README.md index 63693a0cbf2..16366d1fb98 100644 --- a/examples/build_system/cmakev2/get-started/hello_world/README.md +++ b/examples/build_system/cmakev2/get-started/hello_world/README.md @@ -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 diff --git a/examples/peripherals/.build-test-rules.yml b/examples/peripherals/.build-test-rules.yml index 3abb02941c3..a05e258ee6b 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -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 diff --git a/examples/peripherals/dma/async_color_convert/README.md b/examples/peripherals/dma/async_color_convert/README.md index 77efb5f5dc2..1f73d026889 100644 --- a/examples/peripherals/dma/async_color_convert/README.md +++ b/examples/peripherals/dma/async_color_convert/README.md @@ -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`. 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..149af62f193 --- /dev/null +++ b/examples/peripherals/isp/dma_input/README.md @@ -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. +``` diff --git a/examples/peripherals/isp/dma_input/golden/golden.ppm b/examples/peripherals/isp/dma_input/golden/golden.ppm new file mode 100644 index 00000000000..ba31afa2173 Binary files /dev/null and b/examples/peripherals/isp/dma_input/golden/golden.ppm differ 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..4bebe72a62c --- /dev/null +++ b/examples/peripherals/isp/dma_input/main/CMakeLists.txt @@ -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) diff --git a/examples/peripherals/isp/dma_input/main/assets/sensor_240x280_bggr.raw b/examples/peripherals/isp/dma_input/main/assets/sensor_240x280_bggr.raw new file mode 100644 index 00000000000..d6415e3b273 --- /dev/null +++ b/examples/peripherals/isp/dma_input/main/assets/sensor_240x280_bggr.raw @@ -0,0 +1,470 @@ +0/>LyȾĹ̿Ͻ̾ȿͽ»n]=>'&98=;;9774944;:77965635<8467;<59<;97=697=>/;@9=8687;866966<869814873485154<997;78:.0.U +,  @#j= * +,'oþȿĿȼǽŽ˾ɿопӽ˿ʹźſĹr\;3"9xIE41-+&&! 021086679499%7::5;7;445;9::;7;95=<9'847:7589;3876894<9:A9=<;<;8789888=7;75754477;98395<586976><<993448685/M $/5 X8 / % "ÿ̿˻лſ˾ҿվؼξɿʾźiU1,(cģmV@9-.)%$/11476856794589977:?988@<6:999;768:46?94855589=679;7:98<98:>::;;;85;-7067;<59797;36440778;6>56995:<297547540}I *OA'}G +0)!ǽϾ̿ͻȼƺͽǹʻֺ̺̺μѾٽѼ׼ѽ;ϽӼվ˿ŻʾǼӿɿƾǾŹƼZEAط{TD4/))%"4.5672443805;9=:87:947<4643:;466866518:398=75=:*8583989;;:;4;6<7998>:<::772:34587=674213737086<99<8:99;;>;85;&qFN:0!a4 ) +' +ý»˼ŻȾȿǻοι̺̼ɺʿʾ˾ȾμͼϺϽվӾԿοҾϾѻѽλлɽӽѽҿ;ӿнп˻ѽʾ˷Żɼʽǻ˺̿ȾͿȼºƭn[=6)'#/55434:794039973865685>;<4762456872:74<613;58+8877989778565638:0239774>5/467:32749255238427676;875.;6;=9>?998313LQI9)D+¼žþúĿȽȻ¿»ո˾ƾżȼʿɿ˽̺̻̺ͼ;˻οѿҾսٿտѿпҾнӼн׻Ͻɼн˹ͻͿʿ̻־ѾӺϽּҾмӸ̼ľǷķ»ɼǹŹǻź˻ŻúƻȺŽĿͿrJ>/51457335288:636976479:84:9488845=96:5743325/666966574695?9:::878192776;68184723968975<68:52:76794599874::68536861.NE:3UǺȶŹƻ»¸ƹƳżǷŷķŻĵijŲĺƹ˵ɸͺʷȺ̻ʿѽνԼк׾ϽϾ׿ԿѾպڽԻֻӵӻԼҽҿѷϻμ̸βй̸Ի˻˸ӿȾҼɸȵѽ̸̿˹̼ѻο̻źɷʶ˷ǸɼŻƹɾ̷ʱ˺ȿ˽ȿƻǾǿ˴e52630847426641265444:547583686283674186465;4;3267.574128158;:295533666:2544888474481/485239;666:8563417:<;48757881/=F?4$·ŷüijŻƷĶüôŲĸijµƴɴͻʶõ¹ŹɾҹζιҼʼԻ͹Ͽ̺׺ι˾ؾϾεѽǿƻοԸκ̻ϵϴͺϺʶʺҺѹϵʹȴѸʺµƺȹȹǾʿͽǹֻƷʽ̯Ǹ¸Ķ÷·ɷŹƺ̿ĸɿʸ˽ŸûƸɼļȿʾ.3513/142431764/616034052-75754246.56<51186365651232>756149506496521558470,03436234963546564666:9832661578/4576;7%341B?3ů˴ȴǸijĵŷѴͶȺ̹͸ǵͺθʺйп˹Ȼ̶Һ̻ҼҺɺʼ˻Ʒκ͹ƾͺηʹȷɵƵȳʱȴмͶȶƻŲö̵ζǺȶȸ͹Ҹ̻͹˱ȸȵƴɲóķ÷õŷĹȽŶüúȽ¼ĸĸȴ/34523052/2421344483501451:40626/47464867925664:;8310/2772333229275622665/;714461523065624847336668443333357437;3/24.(0=ñIJòòȲƱʷͷ̴Ǵɴ̲µι˹˴̶кҷշ̸ĺͷظϽʹ˱жͳͶʹŻʹȶͰҴͺʸϹɰDzɯͱijǴŷǵůʴȸ˱ǺijĶƴƵIJIJį±ɱ´ƹ¼üûŷŷú0/.520547252/1,/.1.0...21.80341154.551555042853231113433841:3346774334446826054-69266304233065143.46685755461954+330..'&¯ìįįïȲɴƵ̳ɵĶ̮ȹ˷öǶеȸ˶ɷʴǸʶɲ̯ǵȷdzdzȷdzɷɵȱƱɰƱƲijǭ¯ɱŷƴîõįİıŷɵųûƲ¶ĴøƵŷ54/2.64-5310+4013/0.202013/21120/12424013075/+872'57:67642:62396104463431578374/2133/3/403/.115143373176349332032002-,-(îƩIJDzǵȰȳĴɶƶ̰ɰòʵͷαʱ°ȴŶ˳ʷͳ˴ϱеưįγʸĴ²̱dzǮĭĬìŭʱŭ°ï²IJȰȰĶĬı°ð±įƶõ1+0-/0/.,//3014++2//5+.26/11.11.0111/0163464/45632050253485745/4358401516.31431/6//.3155341./224406216824522/2/./+/0*+(*¬®³ǫ°űƳűɮ̫ȱȳòȲɰƭȴʹŰɰʲ˵±Ů²ͳªɫƯįŰĬïŰ۬®«éîê­í©2,+.-1-.*1,2.,0,0.200.1..22/4//459/221043423051217411014513155357/22833-03423.50303-221/.33./312326213.12321322/.,-*&)*'ê³IJƮ̯ɯĥȲʲūð˲ʮİ̯ͰƬѲòųέįįƫDZDZǬİǬëīçèªì«.,,,-/+,,,,/9012..0-1//3/.-0/2.11220.61202/-1.0./213216.42/111/65652083122//4-210+,/.00..141/,4-20524-.22///0-1,--(''($(­ìêïȩ˰ƲƯŬʮ˰˪®ʴ¬ŪŪħƪ©ì©,-,-0./,./0+/,-+,++//0+-,/0/.03/1,.)-+,013/.32,/025...20-41/4630:*6-,010/+010*-//(/-,0/.-1//0,0-+-002+,,,/--,+%,(.(%+$%$ëǧ¬­ŬʧĩɪĬīéĦŮìíì§ŭĨȪī(,+-,+-+-./)-)--,-*//-./..1-,0-+13131*.23:-3-/421//00/5.2/225/338-0017153..100230-*/--0/.310/+(.01&,0.)+,.1/-+*).%((**"§¨çíī¯èήɬ̩ǬǬȧ¤ĬƧƤ(/,**(((),/(+*'+'),,,.02.*++-2,+.20++1/.1-002///0111.*.20/6/2212324/221++0-,0.0/,/,21/3-/.2.1*+,,3.*.''+,+(/+**($(+""|"ƨ¤éŨŧêƨǩƫĩ¦¬èǡ¤¦ħzz,')*,-.)+(--).-+****)*)./.,+2-/+40-,,+,...01/./310+1/414040+333/41.20.-,1.*12.,,-.+,1,3,*/(+2.,-*-++-/-.+$($-'*&%"# "z xŨȦŬèçèħéëå¤åĨħĤ¬}zxt(-)+&+)&/()*0*(+*.+,)/)/6+(-1...302/01211,,4-02,/50/1/211/000.1/-11120/--.+.1)--00/./--+-+/3,,),-(,&)(*)*+"#'+%%"&$} yzvu¦í£èçéĠä|y{wywruo"%#%("&)(-++)++(,2*,*--.0/),*)3--,,,,0/20).2-023,/3/4-11.3(/130-,+.1111/0/,-7...,-,,-.**)+)'*+*++,)+('&(&'$#% &#&z'~v y"xtn ãà§ɨ¤§zzzy{s{sxnsm"%&)'%$)/'&&(*--+(+*&.+1)0,,1---,/)..2/-1,+.11+0.*211..+2.+-20010///1.-/...*/0-0.-/2/*-+,,,+2&-&.)#$%)"(%()'##!x$|%y$yqrpppǩŦĩŢæ~y}zw}yxpxrznwnrmsj'&(%''+%)*%*%')').)++)*-+-+,-/.-2./--0,//.$/,++*.203-02.+-2423-0/15++0,1--*+-1/*,41,0&,-+)$*-'%(*&%%'%&*$&)(#$~x#} ss#o!pnolãŸååß¡ää¤ģ}|{y~wr~myrur{kuelkkg"&&#&#(%'*%*',%*%$+)-**,),-,,.-,+-,---0-/..-04+1--.1,211,00./6./,1)/22-0-,/,,.-**/02-&(--(/))(***)*((&&#&&#z z!w#uu!pqlkggeǧéè¦ã|}~yvt~synsoskplkhjikfld!!$')((%*'*$%&)/')/*++)(),++,-/*.-*).//+/0/3/12/2+--.-1.2,300020*1-,-1-*.--++.+0--()*-+))))/&&%'+*$*#&($${#y{y!t!t!pooiefh)d_}ģ¥}{vyvv~r|sznumulrjuneegde_h]"}#'|&$%$)##$)&)*.'$&)'(*),-+0,(*+-.,.+--.,+1/.+,+10,-.-/.-//+,04,01005,/.2+.5('./,-*/-))''%$*$('$(%#%%#$"{"zu!txrq"p l!ogk#ncb\}ŧäĠĞ~y{zwq~uvnsktjtktkslofobjfjce_fWz$} ~#(~ }!"*)%%%'(+&#','%'+*)&)+.-.*3,3/,,2+-7--,20,.5.,+0/420/.//223-*.*/15+--.+***,+,(()(*,')((#$&&$"##|%z)w&y!v rrn!kklhfefb`_zĤĠ}z}{tror~pyprksmqimeldobhbk_fZe`v$z|}#)!&$"%$'%+()&'"-&(*)*).*--./-3*/.,0(/-0//)./1001/0+-.0-1..0-/-//001/11,,*./)),--++-)++&&&)''%$~$""~%y y'ww"p p!pnmiji``bb_Yww|~x|¦¡¢âäŸ}zxyxzxq}rzqymxjtitfldkfieked`cbg[bY|z${"z"{#z }&!{$"$%($&*&"&('&&,)(,/+)(.0.,12*2++0204010,-1/0/24-/,.+.0--,0/+,)0//-&*(*,#*+-)(%,*'((&"$*$~""y!z%u${"wu!n&q n!ogmgedcb]]_Z~wvyy|}~¤ţġĢâ~}|yyzwrq|q~oyotjrhqgnelejcl`eag_`[bYc]qv#y!|"}y#}#% %#"'%&&)%(')/)*,',+(,/0,(,-/.13)/)-0110../21/120.0./1.'120--0...,/,*()/+-*&(*&)-*&,)$&$')|$y#~%y!u x!ps mnllhecb]_^^[\tsv|zzx}~£âĤåƤĞx~yr{wq~x~t{sxlvnqiwhqflkh^l_hbc`h`e\a\aYwrr$x x!x#z"{"}&!"! $)'('''())(%(+*+&)'(/0)-++--0400/.*1..04,.-//100./.-3.-,0-*,--(*),0,).*%')(%'#"#&'z)~!~%{'x%xvrq!ponmihida`_`]^]Zn|suv{|z{y~}}ĜƟˤģ¡ģ~{{}yxxtq}rzo{mvkslujrjncjbk_k^dbc_eZa[\]^Wp"p s#w ty u#y$y#{$x%|'}&#%&&$''(-')1*)&*)+)-))(*.//./-+,-./,0++0111,+.--1-5..20+.+),,.*/,+)'(*,'($#&'(!!~%{$&{"|"wr#ts"qu!rkg#h"gd"dcde`\XXZX{nyrvxrvwyy}¡¢¢ããá¥}|{tvr~r}r|onzhojvhrhm`majcjk]aa]i]`Z_Xc[]Ol!tqtu yt&v$z#z$"}#$%#%('))+))+,+&%.'.-.)&',302-34*12,0.2./1,3,.1/,0--0110/1*.*+,.+))/,($(!(*%$)),"%'"!w!}"z#z"k"z omoltm fia_]cb\]X\V~o{n~qxtvt{wy~~£Ɵ¡¡àť˦ǥǢĥ~~zxvut|o|nl}kvi{irdobn`eghcmbbf[X`[]\dX^Rmllutu v$x#y!v$x }#"}"$%#"*#%(,'+'-.++0/1(/-,+-+1,//232.--1/00.+//3/,/100,-0-,.2-+/2,-*+)''*('"$)%$'%}%#z!~"{ s!v!p%r!o!p"nkjfhbdfc_[\WYWXvk|mo~t~qqrwuwz{~â ģĥ¢äâĤĤ}|xxsswqzn}ro~nwiqfrftcoefem_gbc[`\cZ^XaW^UZWilnn s nq!w&x"x#y!{$x!&}'"&# (',('%+))(),--*-,.+/0,-'.2-++100223.+0/100-.1/-./+(1+**)(%+,(((+'(('(''$&'"!|#z#z&uvso$l#s lmf!dfg_aa`_XZWcSzezp~mzpqtpuvzyz~}ȡģ ţĢĤáŤĢãĞ|zy|vtv}r}m|kumtisisirbthsam_l]nbhZf[a[bX^U\^ZXiio!mr!oq$u&r!u$v!{#~"}%~%~('&"&%'%)&-)**,)+,,02,-4.+0(*(-/2..34-,/1/32100/,,,--0--...*1,'-))&*,%$($('$&}$~"|"w"v'v!tp"sp"qmkk!fifd___][[ZWTVzhskwk{l{mpsu|quv}|}} Ġ£ãƣǢŦǢá£ģà~|xuvq{q~q{l{rwmuhvgqaphp_l_f[k`i`eXf\]T]V_WZS\Ugten l!mp o$q"y#u"x#z"y#~%',$$%(%(+*%)$*/1(-&,--++*.+(+,,+23-0,1-,+1./0.+*//-./1--,.-,*&%-*)))*)()%(%&}~#}{$x#rrs$qt"l!r!hlggdf`_`[XZWUTVVrgshxmyj~m|q{rs~xuwyxy›¡ßǢ¡ĨŤçţàè£ĝ}|ywvvs~p~izl{kykxjrbmemco\lak`g]oYfY`YcV\Z]UWUXVfhdkn!p#qrq$u q#s!x#x# {${*-&'()*+'*-(++2-.)-*2/,/-5/.21--,00.2154203423,.22)*+/+-(*)*((+*),.+&)$#'$${(|'zu%u"s!s p!r"h%kl jhief]c_Zd]XV_XTU|dthsfwnyl}l|owsuzyxy}||œĢŢȥߊ¦ȡãƦĨŦ墢¡~y{tvtvnnzm}n~lxewethjbpdpcoee`l]j[cZdZ]Y\U]VYR\Qejh knq!m"or q#v$z#u#w{$#'|$")&%!.$('+&*/+,(--,-,/.,./-,+0**,-,.1--,014.1//10,-0-1/)/%&)-+/&(&&&'&$!|(y"{{t$u"v!s!q&sno"ihhg fc`_g^\X\XUURQyjukxglzp}l}o}ovqquvzyşŞƢ¦¤¦ĠȤ¡Ɵŝģ¡ğ~}}|zxtttuqxo{ozktkteqjvhnemeagn`h\dYcZa[bZ_UWZZS]RWSffkjkpn"nqr q u!x!v'x#}#{&&(&'($'(),&)***,+.*0+,--///+/6311031.2../220/./0+/-002,(+0&,',%.&*&%('$~%|&#~"|%s w"u t p"v l"o!k hmeggfgb_][][WdSTPxgrfrewiwl|mzi}msrtxvwwxz}~áǦ̫§é¡ĦȤƜŢĠ}}}zztruunp|lxivgwgrjqdtcoci^jam^f^f[_]^Y^R\W]SZRYSdgc!ilhmmqv#qt"zu v#u{"}(y"$$&$(#$)')'+*,..*-*(*0,.7/21//6-2-2+/1.211-6/,-/.---(-*-(+-)*)%(&(&&''~$|"y!w&~$w u#r tq qlh ki!fcdaadb`][WXSUPRrfohrb}n|i|mjpvlrwxvv}~w~£ȥȧşĥƢ¤¦¢~}~{{~uxqrqolxkwkwmuhvgjepdp`jblbi\f`d_]V^X]TVVYQ\TYPfbfhkp!lm#nr m!v&u sw x){$}&%'|$%(+,(%&(++.*+-'/-)02,-%./3/3721,.10002422.45-30-,.0.),*.**,('%')(&("~"z'~*|#yw!t!rn"l!j"jkeg ewdad_a][[YSgTRPldqevgwixj|km~mmprp~tsuwxx|}|~~àá¤âǞȤƢʞŸǤȟ¡~{}|yxurr}qn|kzhykvgvfvgnbqbkcf^c]lZfYe[bZ]X_TYYhTaOZM_edhjj#i klq&uu t]VNLIHKDBFACGFFCLKOR U_"^%d!k%s&w(x,)2545502-0430071.00/(2-,-./+**+0)---','()('%''~&"~(~"w#x"v$s"s$rv qmnig jhdba^\[^WXUVUQNPkci^sgzgzhxe~nwlnprtJ0+!(#&!% %!%&"" % % %!$ )!-#-&,%0'2-0+5.7*;-:.<3;1?9B7F?IAWHZRZUi[sfzgrz|~~|uutuqqzmyq{pzkzixhvfscq_jdh_j]d]b\h\dY`X_U_[pQ\QZQTNc`bghj#lljio o o      ! " ! " +$ " '& %" +' & ( ' +'+ * *.+1 0146<CGL!T#b#d!i*r/r+x-,3/,/+.++*&%*/#%$%&%~%$$|$z zx!q sr"m#mm njkgcdbaaaZ^YYUUSPPLshqgofscyh{jyjwj~lml|p}L  #&!$(!" ("(!,(,$+&0%,#*",%*%.$2$/'.',&,(1(1'1'/'0$0$2%1%,'-&/*0+9*5+405.81:3D7H;L@NGWKaTi\kdtkxlytu}~~~~z{}wxtqpn|j|lzgwesgtbscjZpamcl_h[eWdYbUZfZTXU[Q\RbPUIbadckhkjk!m"ls!r     + +  +!    + ! %" # " $'& &' ' ) ) * ' +*$ $ & & & - &$ ) ( * +$ &) ( %( % " " +! +&' & ) ,-69@HN#V!_"h*q(v+{&{#|$z"{&z tp suq kngg d f_`a_``YYZWVRUOLRi`mcr`tfvcwi|l~o}k|kmp}T !!'##&$)#!($-$% ,$*"-$0&0#-&+'(',&+%.&1'-%*$)&-"**+&,()(.'.)-&/%,$0$.$/'+*-#0)/#("($.#+$*"& )"'("&*$!#"+%-,65@AHLUT^_njvpwpqqm}m|mvmygqfqcubtascqem^i]h^bXWhVY`Z_SZRZR\N[OSed`ehfjfhl k"m%mq'         + +#   " +! +! " " " # #$ && +# # & $$ ) +% $$ +( ' +% % *# +%#! ' % # $ ' ! " % %"! +      $,7D Sq"n qkjlg"bdhcbb_^WYVXTYRNTNo`m^ldqetg|iwhskzlkk~mb*!"#""&&(!'&) % %!%( (#+$,#2%+%.$+$*%&"+%-'1!+*%-)+%,&+#.',(*$)$)",%,!+%*$/%)"%"(!*!'$*"##""# $7tqo|q~lxkwjthrcpfxejdk_n^k_e\gZ_ZaX_U]T_XWRXNVRSNb^*gbddhgml#o!jn.#   +!  + # ! + $ $$ % ( +" "$ $ +$  +!%& # $% % %$ #! "# # $!" # ! +          f s!uj!i higdfda\]]WVXVURVRPMnbobgbsetgxgyj{ixkpgl~d4*  :&=&='8"1#4$:$8%5!5#4)$!&!% )!%$)$&&#)"#$&")$&!(!*%+ &"("(#(!'"# (")!&")%)!)!'!& %!'$$"!!   5moprxkxkwfudvencqdm`kco_mYfWaYbVbV]UWRYQZT[SQQUJ_b!afcb jfhmn l$h: +  $VSQNJIEE EE G" $ % "! " !  " + ! + +! + "  +!       lonmijdfdc_ccXYYSVURVRLNIp\idtbtcrcu`zhvgyk~iklwdC0&8$<=#<$8#9#>(9%8!# ;urunnqnskpaD%':A<><@87:6=7:44303,/1&%$!#!& ! "$ "#,)(( (%$     Ktl~mxm|iyhviufogmalag_i\lWh]e\_ZcY^W^QfW`QULURSMVJ`cc`eceh hl!okjE +  " $#%$ $ &$&% iostwqsuxvx.E#K$IIGLIJ JF F$  // +0 * +0 +,))) +& +% + 8--*($%    h"i!llg fik)haa[_\\Z#`VSVQPPJNpbqaparbrbzdzf{kyl~l~nxn|hQ  5!5!6#9#:$7#;!:&<&:""2rnsrnvtyu}lK(0IJKKIJEMJJLKIGGLLKJJIC'#-7:;5998:89768-8365414+&DeUmMgOfJcKcGYDS>V?Q:A A4E1800(*$ !(#Xupjmujyfqvlewdpbp_p\k]x`k\aWc[eW^Y_U\PXRXRVISLPJ\_bdacfjjl"mneQ +   "  ""$# %%"& bonooputz{x2E$L!M J J"H I!NK#M K# 5 7 ; +79 +6 ;97 2 9(&[$V(U(Z'W$X+^)Q'T!U!I @)|$x!ne f]VF< 4  0lookgg ke da`_`\%bWXVRORQOOMr^mhkbscrgpexfvdzn{ikzggY$  2 6"7#:#<#5";$;&>$B%')pomtrvvxvwlN")EHJKQKJJIKGJJMLPJMJOJA# +89;;?:=8;::69;677677:) .JqUzW{WtVyU{SmTnXjUpRZ#;[_X~V|WT{UvNwKjIO >b|lo{kzhwgxguds`penck^l`jZcZcYfWcU[UXTYPWQWLYJXMQIU_!_`agifgn#lkj W +   ! +"$# + "' +& + Xoq stuwsqyv5BK"KJIH!PK!P$SK"! 6 +7: 8 9: 7 77 7 4-$T*W%W%W$U'U$U T#Q$P"D + Y'('''&'}%x)z'{ + +F#pml$hgkbcaa^^]W]YWSSU JMNJJo\r^o`pgsfugxfshwmwh}k}lzjh, 4"7$8"9$9 @"B%9%;&9&/{nsqptquvxusZ'GKHJJJMNKJJKLKDNMPLRJ=# -86787777<897779<67746 4Tt[[yUsUvSwRoTtRsToUQ$IWYZq\~S~T}R|T|OF  Vetnwozg}iyirhpdmci`q\k_k\eYbXaWbWbVfTmQ^PYMXPTLPMSHX[acec#cfg h giib  "#$ +# %"##  +Spnttqrs|wu8BK$K"KJ#M L!J#M J!M  +6 7 +7 7 7 6 : : 86 0 7&]$W#S#V#U$U,c$T#N"R!7q+(+)'~''~$}(}!s  ] jiikjg f``a^^XX ZXWT!USPSILImXm^ncjan`sgxfxdunzgg|l~iu:  +!3 83%15!9$3"="9&.hkpqprrsvuvud#EEDHJHJKHNIHJJNLKLLPI=" -77688;868978979685971BWvYxWwVvVuVkUlVqToSuN;.VZ^WVXSSQzR|J/ +0df}jzkvj{hweyfmasbnan^l`l]k[bWaX`W^S]TgT]SUQVLVLUFOJYZb^bdgbjji!lg!b "%! !" "# ! Eqrrvotstx"y@ +BFI$IJNL LJ"M#K 69 < : 6 9 <4 65 . <'T#V,X%T$W"T#R%O&S$T* *'*%''%$z'x"g b$hkke"lgdc`^_[[XXVWTSS`NIHKf]lat]rrkeqdvbtezdugyiwovluF '7#8$7"7%9$<&=$=&5%7Rmqpsrmuyvy{m!AI@FHHKLGMIKKHMKLNJMH9 -6;;8:95287688:967830,LY~VyV}WzYyRWsPmTmUnH.K\\ZXY~URU}SxPvB  Ksg~mnzjyh{eqrtex_t^k\i_fVhZbW`^bWbQcQ_S`SYNWNWPSMQM\_\dbhcdjifh*qg   $ "! '$!($4"trrqnr!twxzH ="H!H"JL!L!H!KMO!K  9 : :7 5 74 7 4 4 +G'X$T'[#T$R#Q#Q'R#V#M"+()*(&$}&{$u#T  !h$ljkhgcb^a_^^XWS[SR#RQMPKKJi[o]iZk_nbueudxdvjyjuj{lxhvW   !/4$8$5!6$5%7 9$>$9=eknssrsurtxr>FGGEIKIEKFHIKJKJIJIC9!.7534<897398693469571*SW~T{Zx_wWoWrQpSpUqQiC"\[`ZU[~OS}TzPyOr, +\uh|jhxhxgyhseo_q`i_j]ieh[gVdU^U^U]U^NiO]RVOTHOJPIRG]_^^cbb efgg"l ih" !  "!!#$ )qmqppsou tzN<E I!KH!K LM M%MI6 +3 7 7 5 +8 2 +3 2 2 +$M*S&U&[#[!U$U QN"QM"')+&'('z)|$|&w 5 + :hkgf chfdl_f`[^ZYSTPQMOKIJKiYk\j[giharewc~iugsfsgwhyeua)   1 9 55#6#7$7#7":"42_mmqmptrutx?IHGGIEJGKLFIIFIKKJIF80748984779=:864979436' _XtWqTrZyStSsSwPvPqQd= #uTZZWSU|Q|R~P}Pe + Biyh~hzkviyfserbrbnbn[lbiYe\h[dW]VbSYPWNZRYOSOUKTKPGOI[XXa!^`ffefhgf"g0 #"% +"#%  +! nnmptsrwvyS :IIH"G!J#LH J#L G4 7 +9 2 +8 8 +8 +6 8 +2#N(Y&T$W,X%O(T&P%T$T%S1%(%%((({#y$|%s"  Pgikhgfb]b]]bY^WUXSPQNJLHGEj]l\o[iap^ofxdvdsftg{fhykx`9 2 54 :"5!3#5"6"4#;#Qknmorstpuu9H?GEKFKGHIGHFFIIMNLE3.9166:6756557;6825761,c[xUpRrYqSpSsSpQnPmSd%-zZ\UYU}W~S{QzTwLS + "[hvg|hkzjx`rdrcrco`n`jYmYbZaXfT_SZS\S^QSOTLTLSNTGkDMC[ \Z_^^_dagfml!h#@    " $ !#% %ykmplppxrz[ 8FF"G!IF F IFHH7 4 7 +93 3 235 5 Q(Z(U!S&Z"R$STP&Q&E b&*'&|({)x$z%t%v$u \k gkgfecbca]^SXUWVQORMLKENDaZeYi\afoarat_scparhtdwlykubP + ..!765 4"9 4":$= Iinooqopzux4EDDHGFKEH?DBIGFIJHGF1!-66738487678444558441!5nZwWz`rQtPlQlSpPkNuKT  Rmfwiyj{gqewes`q]v^o_l`iZfd^SaVcZ`P]PXQZTYMTKUIRHUI^GQFY[X\^eb i`fe d!ihZ +  + ! [ egk inlpqpg +,H AEHEIF!EFE 77 5 +22 3 2 +3 4 5#Q'WTS&S"O&P$P)P!P - o&+$%"}+$|%v"u$V &fm lfffb^_]]j\WYYTUQNOKJJJLFd[jWj]n^gYmas^iaqaucxdzdlvdi4  +  ! 7+S1b9e:p>uJKRVV+CFGHHF3/45:6756654233344032( EjYyTwTsTuRpTsMqQlMqNV 8VT[TWRWzNwPvNs2 6bd{j}hvjvesdvbrcp`iZoacZgWdXeVbU\S]R[NWLXKZNYMRJRJXDMGSX\]\^_dbffdki]    +  +     + +    +   $%* +/064:  1 3. 1 0 3 0 . 1 . +R%V%W%S"W(P&M%N$R%K!w+%y"}'*%}({#y(v#7  ;ighh!hbab__[[XWXXW PSNOMOJGIDfVc_dXj]j]nao\p`odvexbzixh~itE 2-)#!     +       !%!&& ' /A7L=S=[?]ER?[BYGaE\<% +KNzSxUvV|V{QxWvNoNvLf  Mb{exh}fzguex`u_kZkdh^i\iZdn\WdU`VYZXSVP_OWLYQTKOJSGQGLDZX[U[[^b`eehhc`  1A <A;=85 7 +7( +  +                + +   + .:@IOOU"Y'[&^ + Thefgeda]ba^bY[V%UTSNRMJNHGGG_[gXiYjZgZk]kcmcqarctexk~a}jsT MQOPUSUUSTX '% !#"!! " +#+f(^ WPE7741(     +    +         + + .\dri|izg{`sdq`qal]gZj]h^`YjWcWfW\X[V^QZOZMVPSJOHQMPINFLEf WW[]X^]baacffd%  0P R W V _ T X +W +ZO ++'+$(,+%+* B>89<< 9 3 6 2 +  .$ "     +     + bi!iead_b__`^XYURT_TPLLKKHHFF\TgWeVh\e\l]m_jaoapcsatgxexc}Z. GNRKTURWZUW" &'*%)('"'&'&$))+*-*) 1FABC@BAEA@# .($"#&'!%"  @ustrniic`d^]Z_YSTQOI% &7e8[4S,J&C#:#4 5(% + @lhxgpvqcwew^qbs`q_h]icfWbV`UcX_S`R_RZP\OWITHRMSGOFOGOCF=S[TYY_\`b`b c!fec4 % Q ORR SQ Z S ^ +S +)'()'%(*+& + A@@ ?D FBAC >    +   +]z <v r m c b[X U QD + ad!edbdac_]["`TSNVRQRQNJNIEDHDaVdZd\gZhXg^l^m`r`nctdocrdzdz[> + +,HNPRRQTW[[( #!"%)''"*#*(*&())%(* 3F?BAAE=CFB" *.-(%,/+,&dy5h|||vvy}rpo "Tkongtgtbsfuftap]uah^fYfWgUhT_R^S\RZOZRSKULVKVFOFKHMENDIERUVXXY__^_eboheF  UP V P S T U TU +X $)&*()))++@@@BE=?@DC +       + no + `|x xyxs s p o P  *"cef"j!ce^b[i_XZ^TUSONKJIKGHED@hYiSfRfZgZgZkam]o\sbqavdnhtcraQ  ?RPNSRQVTT7 %!*#'*&!)&&&)$'$)$+)  +);BAAB=?CCC.+//-.-)1$ "pg }|wwyxssoji ;_pexeyexkxcrbm_o^k^o\n\jYaXqQ`S_UaS`MrWbLXJYKQLKKMIMHRBJEUWTYVZ[Y]bc'da`dW  R Q M +N Q W U T ZU$$%%&*+*,* C@>C<@BD>=        +   y\n~z w w t s pt n 1  Bfga eda]^^YXVWWWTNSPKJIJEHDB?aTaUeUfZiXj^i\f_qbq`rdrbubu`ybb- + .OMPPRUWc]B"$&%('&$($%'+'*%)'*(!'1>?CDC ?=          G }{xwxv vs {m + U`ecfdc\]]Y^\UVRUMaJKJKGCHGBAfQgT_TeYeVgec]f^m]per_vbxjs`qcl; |POQSTRTUXO3 &&'&%'%($)((')''&(,# "7B9BA@?CAB*&)+),+-' C{; O}yvuxsrluJ* 1abvhygxfpalaq\j[dYoZiZf[c[bU_X`U[N^MRPcP^LRHRKUHLGPEODGAHASRUSV[`]__^%dec`_  @ MP +Q VQ U X T U  !(')&&' (*+E?@ ?A@?C C <   +        +,x{ +| w q r o r n l  \dfddc_aXYX\ZUXT^NNPDKGIIDEAA]OdQeYcYjXgXiXe\n]o^nkndshv_vesK + aPONRRRVTVTJ + "&$#%&$'#("%*'(&)%(! 9A?E>?@D>A.)))-/+.,Ql$ 'd|w{rsrpom. +Hl`zfrcsarcsdp^q_mZgZdWd[dXhVbR_P\Ne[oMXIWHPEQHNFMHPEKDG@JBOSUWVYZ[\\__u b!b a +5N P OM O Q +X T +R , &%%'!'%*(('I=?ABB;@<7  + +      *|| xut +u u ps ]  aad!cb `^l]^Y\UXQRRSLIIGDHECCD=\Oj[nPgVdXbVd[i\n[q[n^ubl`pbtbuX+  HONPOOSTWQSe +%!%'&'&"+#'$(%)$'%& 7C@?=@@@@@ +&*-*-&+( aW +>pzywxtrrmp + +Yeescubseobn_l_o_lXgZd]hYeUdU`S_P[OVLWORKPHUJTHODOAIDK>GBE>SNMSYZY[[^` `^!dad5 + &OL NO MV +R N U ; ")'()&')(C@@AA@AA?;   +    ~ + C |uus v pl lG  +:_c g^d^]\]XZYVVMONQNFIKFGBAC?AYP[O\PcYdVeXhXe\lXl\pZuaubt_v`uZ? .ILLMOOSRTQ| !#$)$'#''+$'%)%()'$$ 0=C>@@ 9@ > A?@3 +    +    +w +Ux z r t s +r +p +nm -  + ^e_ae]a`][\X\UPRPQUNJMJIECBA?C\MaSaXaUcVbUdZg[m]k]mcn\u`obvbzaT + + 8LKKLNPTSW +$#'!##'#%#$&'%'!+'' .A?=>>AA:@ (**,.,-(&*|{r) |{swvrunnk[> + )\`pexeu^n`o\oZpYj\j\dZeVuP_V\Q`N[RXMUPXOUITJOJQCIHOCE@JAF?I@MRSTUTZ\\^e!_ca^bU  #' +,)/ +5)< >:##!#"$%' F<@6? ><C?2  +  +  + 3 d +iw s +t u q pn +m f Y ige_e]`[_Y VWRQTQNKMJFFGGD>A?>[ObQ`P^RbUfYdZf_mYm`m]m^p`naqar^_*     +! !! b0570:79;; (++*+*')# 9W3{srtprpogD! +Accneub{bsbr_jbi[iYk\kWbWbYbSaO`R^PZMRKTJULXJPHLJODJC?FC@IRN[NZP^R]UgV\WfWhTk[k_t\n_t]o`s`d; +    + +   +  +          + +       + *+/.0256897;;B@?( *fJLQQUVSW]! "Ojbxdoctam^n_g_k[jXcWfV`TYP_PcTcO[MWNWOUISH\HQBLGPCIDG?D?D<@>JLPORWSZYZ]^[]`]_    +    +  7 77 33 1. / -    !   +   +   +  +   +  " +2Zc`d^b]\[YZTQSPNNOMOJFHCCE@?><VOWO\Q`QcShN^Q_eaWkUe_d]lWoak_kt_I  +   7+G/D)C&F)H+D-G)E1G `'$$#" ~{P "t|njc[zVsRoQ[ !<5-'##    + +  + + 8VeZsgs`s`r_n`p\mZhYcYdWbPeWfQ\PbTcK\QZJVKTFPFOGSBMEI@L@C@D=G9LONOOVOWXXW[ZZ^^`$ +      ? FE H D FMKI2 + +%''( +& %+"   + ,810 -+ ,')& +!   + + +   C'dcb]^][XYVWTUTLNFMKFKJGFAD@?>>YL[PaRdK`QqP_ViQhViVdXi\lZl^s^q^mX, +     2%D%B*I+F*B,M1I*E0I o+)%'%'''%v 9ƾ9UC;8776668) ,87411-..& P_h`kct]o]o]l[m[nUjWfZ\ScS_P^RULWNQHYLZHUJRIQKOJKDLCJBJ=E?E?C;OLNTOQRUWYUWX]^]a4  +     +A @ F JH KHMM 7 ) (% ( * +&"'&   + 6?;99=>:69 + 9>:789576 +T`d`\]^][XWaVQTNOMNOHJFEFC;>? :@ ]M^MXNeM\R`Q_WcTeRdVcYmXiXh\l^kjbYB + +      .+A(G-I/J)H,J.G,I*I'n*&&'(%+$$s @̾(j@7:::::78 59=!999":$5!7,   5\]hanck]r^p]n[jWg\iXgW[X_R\T\O[QZMYMSKWGVHODMDKBHFE>K@F;H?A=F9JKKMVPURRVY\V[]_]E    +  @DG J FH KH I6( #&&$ & $ +'(  +;755775571 )::8747344  \]a`^XZZXZTSVOLLJOLDFGBG C@A?<;= [HWKTMaO\QaSaVbWdUgZbZeYk]m[n^k^pZV +     #)E'C.H.G+H/F,K,J+J, g'%(%)(%$'jEѹy=95843566~::;8354"53 Kd^l]m]oan_jZkZl]hXgVfUXU_T]N[ShOVMZG_ESFPIOEMBLEOBF@I?CD;A?D;>6IINNNLLRSVVYX\[`]W      ; AC D E E CFG ; $ +% %%($ % % $e B69895744  4<;893466#  +@]]^^Z]ZUXTVQOPMNLNIGFH@C@==@ ;:7 WHYIYM^L^P\NYOaV`SfTeXa^n]iYe[m]n\gD  +     (C-D,C,D-D*G*D.H*F& W'$$%$'& 'dK϶z(876857453N % 956875630 N[m\gZkak^nYkXcTbUfW_b^T]S^P`N_N^KXIZMNJRESAR?MAJ@I>GBF:8736325 655747321 QY[[[\[]UXSUNQPLPHNHIGCBA<A?==6= \NYHWKZJZKZPWR_PfWjQeYaUcXfTkVm[nZjO!     -A,A.C-G)G/D'C+G/F* +T($#&"$%'&] Tϳa-79:85535/6, 7; 969825-  ,VXlYo[o^n^cchXgWb[YZcO_P[P[PUMYJ_HUJWLVDOEJCMDFBG@I>E:E;I=A9?<:9HIGQMPOLNMU\UZSYVZ)      ,?ADE >>G F? !((#''$ +# +;=9;283446954574532 UY_[[\ZZTRQTRNLLKMKEBEC?C?>:<= :9QFWNRKVKYNXN\RsNcMePeUeScVfWcYi[jZiT5  +  +  '<*B(C)C*E1?)D(C-F) J)"#&'&!%%T WְG 487325332'!2 73877414! + A]YeXh]k[hVkWfZaUaTbT^OaRZPZMWJXKSIQHRJRGNHPCJBGBICF@D;D6?8=6IHHRMPNPNPSTWWWZYX:     + +     %> @ ;BCD B E?  $!# " +$!$#)<26762543 6945610.1  !YZY[XYWUQURTNOQNLJJCDBCCA@>< ;969\ISIRLTLZKUJYPaP]S^QcS_W_UiUkWjWgZgYH  +       */$0(8&7%9'6%:)=+! + :*$!#"(%$$T WΥ2 + 763561456 +4 6!6"023510 + -RcZnWn[nWcYhWeYhQ^S`P\Q[O\OVPUOTNVIVONFODSCl:OBHCK?E?E;E:E:@9<6?8GJILJLMPNUNSQV[YY[H      +  +        +lw "%%!:47664334 $76252122%  9V_Z ZYTVVTSOQMQOLGDGJJFC==A>8?863LGPFSHRKTNYJXO[OZO`OaT_R_UcVkWiViXjYS% *KEL?B757440./.--+ +!    + +  +   )/57#:,C1N2Y0Q"] f$m%k)w(},(k0-10/12,) + +EV_^g[iYf[eXeSg_jW\R_R[M[O_LWJXJ[JUAYKQEMGS@M@A=B>G@D;D=A8??:98 7PBTDRDUHWIUJXL[N]L_UaR`QhVcTdYfZbWhWc5 PI 0qQ@\fVjRkVdU_T^O^P\L4 ('0#+ ( )&!   +   +   +   + + +  'WSgVjZfUlXlXeSdSaRbO_QbQ]N[JZJYJWGMFRERFMCMCNAJ@K?F8H6CF>@>F8?;>:=5=7=7:4HDAKJGLIPPQRRSTUSZV"  768;@@;>:h$+,--..*-@c%g df ggi"e> +75 4797:75  +          +UXYZYRRUMNPNOJHKFEEAEB>><<>995 5 47 SDMDSDVJZJWLYJZN\O^N\P\S~OhU`VgQfYkVbO0 ybjK`ud{gyfwdsfsa|f}_E&98?7A9B8B9H9D;=5; !  + + +  +  $K^[iWnZeVjUdRcP[S[M[YNL^IZJVGTJWJPIQFJGIALAPAJ>D@?5?4:794G>EEDJHKOKOOSORSaUR8 + + i39:8>9:;=,$+..)--;cgcid `!d d!=:9576: 753   + +  + +     4TW[XWRQQPPNWJJIJFEEC@=>@<>89<5640 KCODUFSFTKUJVJULWH[L^LYLcP_R_RbS_UbRiSE ^řQh@Zwvdcu`{dq`sioezaC(>5?5B4@5B8@4>7B5; !   + +  ;SbVhXjXcTbV[X[SaP]QcP`NdHSGVKRJQILDLCLCLBJBE?@>H>F=<6E8@;>5>7?5=670FDCHDJHJLILMOMRSXPRH + +94<673=:C; +.,-..,,.8cg#`]dk+d`4666986981  +         +     GSVVUWXRMPRJKJKGFEGCB>?;=<6:9975 72QAOEPEQIQHRJRIWHXLVNWP\OsL`SdR`S^SfSfRT% + +>˯02D6;9D3D3B5>5>72 !    + +  + 'NT_TbReXbO^S`Q_P_T^Q[MZLZHTJSGQHMEKDKBLEGAJ=J=B=F=>=?9@:?8<7<7<6<4<4ADAEIBHEGJLMOLVQRUOM   '5983:8>< +y,)-),.,* +3!adb_]bcf1725 94 35 6% +   +   + +  + +  + + PRTSUSRLPMMMQEIEH?ADA>=><797 887 5 4 1 IGIAGANGTDPKTHOKWGVNUMVI^O`NaPgLcTbT^OY4 $hεH-t 6Us^ndwcw`s`sbp\q[7 -?4C3B7A5>5>3>7@3-  + + + + + + >\RfXbXbU`V_PXR[O]NWO\KXLhGYFOGMFNFNENBJAI@HAI=I:A6@8E5A8C8@5>5:3G@?DHCKIHFILQNKP!KTSQ 337769=;l&(&+)-))"$,\__Z`aY`. 65 3 635, 2#    + +       + +OSTRQPXPOKMFKJECEGDAC@=<88;87 3 8 44.S?S@JBLEPDQFRFRJRHQLXJTLTMXN_NdOiS^R_S\C A߲ܮ\{q&2QgXj[mZn[jYj]obqT,*;3=1<2<0>0=2>280(  +   + + + + 'J]RaQ_QbXxO^O^N]LXRUK]JSFNIQFOJQCMAMAKF;D:D5E7@8A9?6;5<3=/;5828.>J>DFBCHJGLNLOPKMRPM!  51625379 +`%+(')&(!!$)UXT][]WY)340/2. 01!         + +   + .SPOSPLQJIJLFDAHDDD>A><<:3 7=9635 1, 2 J?S@J@QBRGREPESBQIVJTLXKWMYOYJ\L_McN_Q^H*  $b_iz|\Tjuw{y}m- *JaXdVeThSiYi\iXgM.$:08/91;29/80639-#  + + + + +   + + =Q\OaO^PZN`OWP[J\LYDTJTIPHTFPHPDODODN?ICF@I@D>A:@9G:>:>7<795949281705.;?BCCDEGILHIKMIMJLNN0    +' #$(+2. *18;;BBE2978:BDD'+ *,.+0.    + +   +     AUOONQKOJGFIHJDDD@?><=::9 : 85566 2 4 3 0 J>NBJBM@NDN@KFQEPFQHSKVIUJXIYNeSiM^M`R]I?  *'%      "! +!$%  + +   + + + +$JKVM[Q]OYLZNYP\IZHXGUJSFRGRGPENAH?JCI=F?8A5>8@6=8:2;282:54/21;<A>=ECGIHGJJNGKKRRM>   ! &                   +       +   +      +    + + +     + JOONPHNNKMJGIDFEC??>@?:;8397522 2/ 1 -C9FAL=H=MDJBRAPCSHVIQKQFSLVJZJVM\NZL_MZKI$ +  +!,%$        +     $!$"    +          +  + +  6SNYNYK\P]NVNYNVJRGUJPFOAOCMGR=MDF?E>FAF=F;H;A7=;B8>6=6;5:27182509-3.7.><=BBBDCMFHIGJKKLNLMJ       +             +      + +    +   KMNMPSPGKIJDFAB<@@=<9 :<787 +595 221 / /- D;G;J@M@MBKAMCKCRDSFOGPITHSHUJYI\M[O^N_MW5$!"!!$ %! !!       + +   +      ! + + + + CXN^JYO]NXLZMWGXGRDNCUJPCSDL@N?QAO@F9=>@C@CADGDJFJHKLMNGB:><;>?:> ;;: 9:7754 44 3 +. )( ) & +& & +             + +      +   4KKNLJJIHIBHDC@?BB@=8=8888755 4 510 //)C=B:D=I>H?MBRDPDLANDSITFRHTHUJTLYLYHTO\JUIVENGRBPCTDTGRCPHVGUHUDSGOJRHWGZFXKTLUMTKVHWJTEUGREUFQFXHNFJ@IBF=A3GLJMKIJFJGDEEEDAAA@:><; 97 58 5703 10 /+ - , H8C;B>F=L@IANDPANCNCSENEOHTJUIWL[IWLZMVM]N]MXJYJTKTJ^LdOfKYI[JXKZLWMXJ[J[OXKYO`P^O[J\P\NfQcNeNaMaR_M]KcNbL[N[MWQaHaIZOTK]HYM]JSN[H\ITJWKYHTHQ=K>E:D?>86.7-0(1'+"%&#+BQHYG]JYNVKVJSHTKUIQGQBQHJHOBIAMCMBL?I;C=F>C;A6?9A6<5A6;8;4:0:55150713.5+5-:=@<@@GCEF?CHGKHGIHMNMQSPNKOOQNORRPRQRSRQQRO!VSPTOUOOQPSNOOMMQOLLOLMNLLFGJHGDE@@@@@@JFENJJFLFCGFFA@D?@@<= ;9789444 2 0 42 * 0, , DTALHPHQJXIXHTFWGUL`IXI\L]R[L\PdXyNaObMbScTpVeP`QaSaTaS`OaWeQgVdQbRgVpQfShRfT`QfRfSaWeQaSsOeRpPfNcSeUhScQ_QeR]P_P]U_Q`L^M]O`NWO\IXJWGTGSISGSERKOGRDTIVIUGTJZIVJSEWHSBQFRARCJDI=KDLBJ>H=H=G;C8=4=49684726.7/6-3/1,-.0'?<<>??=>DDFADHIDHGEVKIVSRQOOQTRYPRQUTVPROUUTTTTURTVQUPXSQRTRSTUQSRKTPPOTOQRPMNILKNGHHHIEFDBE@ADC@<A?@;9:68:56 6341/0 . 0. ,,G;F9ETAKAI@F@K=J@J>I;E7C=B8@5?6>6>3,6:37082833.4/7.3+0*0)698;=<>>?@E@CHEHDIIFS%550201*#%&'&"~wpogVUQTSTQRORRQOQNKSPPPORPPPQPUSNQKPMKMKHLIMGGHHFGDEED?F?C?=>;@; :: 7575535 1 2 2 0 + + - +* , +@8D:G:C=CF@IAKD9?:@6@5=7;8=5;6719180502.6,1*3).*,-**:77;:=??@BABDDECEGFV08,10311148657882262,/,.1)((%%*|ul`k`\\ZUSVSRTRSPPKLNLLLJKEEFHGDAADCA@?@:><=9: 5677 454/. 2 + .,- , +*'A:C4H>E>E;JAG;I>K?J?MBLCLFNBPGOEQFUIU\ܧ۰ޮީקעАË|q~`q[pZlTmRiSYHVGvGZGQGSBO@PBNDMEKEKBO?H>H?J?G?J=G>F;D;A6C8@8A8>9?8=5:581;091501-2*/*/..*/).%:58< 9?<==?BCC@ECCFk03/-153.63533:87-0-/2+0-.+-,-''/('%%'#)(')%,),.,.*)77575,/1&VZQIFHAGBHFDA?A?:>??87/7866 33 3 0 2 ,/ . + +-+) ) =9?:C8B9D;B=F@D>JC8@;D:A5B7>6@6@293;4;39381;05.5+/,.*1-/).%-&9768:=@<<?;BADFBC{60-030042/2213/3.+0//-,.1,*,-1+,,+)('')-/-,-1.-.(**(1851,110/30y3GFDDCF@A@?@==8;;8:6 88 666 2 33. 0 + .-. ) ,))@5C;@8?9C7K:A9C=JBI>O?KBODNCMDOEjε~ݙeIRCRFOCK@I@K=K@KAJ?DALAF=E:D:B9B8D5?9B9<7@2=38374:3B;DI 8+/5./.01.422...3/&)*.-%'+.(-23$)124-...,***+*)'(1*.,(336/.1131/1+qFEB?>B@@C@@?:=;964772242320/ +/ , ) + ++ & '@4>9<4A6E;>7C>D;KKEQIu󿻉JTDMAK@KBLDK?K=H?I=KBF=E=D9C:C;@8A6>7@9<7:4=69575645/1/5+000+-+**+*,((%.-47 79;4:<9<?AC@J#4.,..-).5/-2-..032*&-+)%/1,/0(0./112/-+/+,)/-1+%,)(+-),.2324224+.,.yEC<??>>=A<=@7<89 ;54416 3 +3 11 ++ - +0 / * ) (% , ;6=7A6A;>6?8B9F=C>F>E=HCNANPcRAN=RBL@L>F;I=E@G>C?E<@=A:@6?;>8@6@3<584;2<59.4421606-500)/,.+2'0)*'(-+*4 675 7;< <9?9A@X+1.+2...-/-/611/54.*%(&+*,*,,(.,).,,,+*+)+-++%),&-'+.,..+*622-3371+,2%WAD@=@>><=;<;7;978 6151 3 0 0 +- 1 +/ (* +( ) & ') ) +=6>6<8?3A8=;@:C:BOAJ@JAF@D?F,))(,.)-*)())*'&,*+.11)./3...3,+FBCB>:<?7:;97 53 6352014 - 4-2 , ++ +) & '&() 7217<6@7>7A7>:@;E9FXo¿rBI?IAH>GD:D;E8B9A8E:<6@5:6;1=5>1;084528/5.4.1+2,/.2)+(-%+%(%)++'(*433 357 9 :; 8C3)&('*,,4+,+*,3/,/-'"' ',,,)&+++(''.*,0+%)'+)**&(*)'('')-+&'/,613242/(,,-(C?> ?9<;=:9962 9 541/ 0 0 . 4- ) ) , , +(*& $ + . ( A4>883<4A6?:=7B;C:FBlչkX;J=K@J=E:E8E:@;B9C8@6@3>6<6;3<0:393404/5040212,3*.*,&.(,','#!+&,-'(1 22 38; 577D.)&#)%*+.++,**/-,,2(*!+,(%'&(#+')%!%%).-)'#*1+',(''*+.(*&))'#'43+10-0+*.4-3![>==979<=76;68 61+3 2 . - 3 +0) +( . % $'% , &(7/:/61<7A6D3@8A9@:yحǪOL8=5;1:38191;1742240/03-2(-,0(,$+'('+$(%*(,).(,+1122 +74 57E%+$'$)))+%*)&'.,*+-.,%$,(&)'''&()+&.&**$,(+.$-(+-(&-*,)(-&(,)))*,0*14104,/+-.+B ?<=2; :6976 7 141222/. - (' .)) ( & ! & %, '<4<2>1:3>8?4>8;R~sAD9E9@7;295;6=4:2;14052704,3/2+2*/+0'/&-&+&+$'&.(0+/)2)6 3344 78X0&"&'$( !((%%)',)*+''("'($'(('&'&'#)+&,)())&#+*++$&)%+,))"'%%'')(-132./-.+)*-..9:9:93 : 35117 +1. //1 , 1 ) / '( & & $ #% & ' ( + +<492825195=4FV۬߇c>E9G8@8=6=7@7?5=3;6:1>1;3:070506/3-0-1,/&+'-),%)&*'&")%*%('*)+)2 421 35Y%&&""#$ (,(%+%%+*$&$$'&$& )&(*%$&&$"$+)+(%)0,*'&&+)&'#&*(#"%&&#/())/)/,..+*(/-00)| 9858887456011 +01. .+, )) ) ( +) (" ) +) ' '( +>-64:3:4;5MhޙhS;L8A8>8<:;7B7<5:3817251705/6/4-3.1*6)3)0&,()#)*+&(%'%,&+'+).(-1/38j&$" $'%" %'%&&.)%#%,#$!"!##%%()"&'"%!"($!(#"''*'"%&&**%++)($$%))$$(&/)1--1-.-++%+* \:7 62958 225 03 /2 / ' +* ) ) ( +$) +'% & +% (% ( +92919185QuޙǰIE9D9?4@6>4<5;57283:2621.3,4,4.0-0)/)/'.*+()%+$)#(%&')*+)&**%/009v)'!%' "'+('%&&,''%*$&"'(&"&)%%'# %$"+&,!)&%$*")**%%%&'+&,'$'%''#(!(%&')+-+0-)-(-')(H 8466444241.1 + , , +(% &) %# " ( &)( * +) +306.7:f|åBF9?2@6>3=7@78292<08.6/6-3,0*0,/&-')&***$+%%!+()(-&0*/(,',). 09& ##!'%! &!$'&*-+%("!$$'!$%##$$/''()'))*)$*%#'++#%!#%''&!$%)%(%%&,$$((-+.-),(-'&*/+ =35 346 121 /.0/ + +, '( +('$ & +" ' +% ) & ' % & 7-8;nԚߓf:@4>3 3 12, , / * , ++ ) ) '% $ "! +& +'& & %$ % " +& ڛږףِӕܘv5:/4,3-5.1.0-/,1)1+0',+(%)%%" #''%(%+&*$'$&')"%#($ #!"!! $%!$(&!$#""!!#' !%!(!#!)'%%"'")('%##!!#$#$%%&$&/!$#(**+)+(&((*&)-(%!%$#%#20. , 00( ) *) ( ' ' # ! # +' +' ! '% % ' +%"ڕܙڔtX081803,0.2,2)0)/)1(0)' '%"$$)#$'*('#+&%"$%*#&$%% ! ""!!!$&!&%& !! $"!!$&$ !$%( $''($%$ #'"&!#'%%'# &$#'*$-& %$",& %$)(((**#$! "k-. - - , , +' ( $ & +# % +"#$$&$%' +$ & $ $ԒٖגқܧޙZE-3-6,3+/*.+,&.+.%-')&-56.&$%'%%*'$"'%%!"$*#*&'% # $ #!&#$%""($ !"'#&#!#$% # !'(  &$ #"$%$""$ ! %,&#$&$" "$%#$$#*.)'$!#"$#!!N/- , , ' ) & (+ 3 5 +2 (% % " # $  % ( "Ғ՜ٚךߞ졙A9)5+1.-(,-+*0-7*::@8?6975*%('&(%'"$$&"*('!&&!%% $%%"$!$##"! ! # ""!(!"! "#$(!"!'#!"%%&!!%! &&&( "!$! #'""& !'#$$"$$#"%#$6 , +, +0 -18778;8 7&# & +%% #! +%% ҕՕԒۖޝ֙؞q36040<7?>06A:;;<6:0.%&$%&'"* &"#"$$$%#%! " $%&!!$&"$$#  %!  !! !#!&#"  !" "!"#(##"$ %$#"$&')"%%'($$& ;<=;=<7;9974 3 $ +" +&! $$ +# !Ӓ͎ӏܓٝޖ}XAKA:>?=E;B;CC=@9@;A9@<97=6;7<5867-$"###*$&#$%$""$" "!!#!% # !$"$& $#! $!$! #"!"%!!"$# # *&"#$#)%" !&%$R@==;;:;9 566 7* % ! !!$ȊɋֈӒΒߝڛߗЉ܌ڢޘRLBF?A=?:A<<:>9?9;6;796=45)(#)%"$###%$"'! $!# !$ !#!! #" $ !$ #&!##%#%!$"& $'$!#"K?: =9 897: 76 3 0 +$$ "&%'! +ȇćÊNJ͒ԕߙߚܔיޜܑݎDE:F>D9C9A9B:<689:59264721%%"%$'#$!$"""!  !!   %  "" !! # " ##%"$#" !$*""C> : 9 9: 7 8 6 +53 32 +" "!"! +ȇĉ̏ˌ͈ʋˍюҐ՗ޓɈ؍ތ܍ܕܓۖ|fAK=F:A:<4859:93316/50) "%#!#$#&   """   "! %#  !( ""   "$$#"'"#" # s<> <=;677 3 3 2 . .  $" +ƐŠʌɍώҐ˔Б֑ޕؒۖޚڐӍޔڏޑܔ׏Ζg[;D681910350313-/..($! ~~z$! !!!  ! !! !o6 9864424 2 1 , ,##xx}ƊɅʑNJŁÄɅǁ҇ЃՁ̂ɉҋӍڑ؋ъχ҇؉ڌڍލۍݍّߑޗَэܑՑՑӒΒьĄȇDžbQ::9>7<64376513/704/1)2))" su~|~!!#  ! U 585 355 22 2- - ( lz}yǍ·‚ǀdžɄȅǃĄґЋ׌Ӏʇ͆̈ғێ؇׊ސݐލ׈Ԑىؒ݊ۉ݆ۏݑߒى܍Պߓ؋Ӎߋۋؓ݋ܙܖ؍ΌĊ̎·֒ѐՑːŊƆŅŃÇȈHE7;8;6559636534-0-0-)*+( _}~{}} }}!   Z =397 3 40 - /,- ,%r}}~~}|y~ʈʃƁÃňΊΊӄ˃{΁ȂӃ΂ʌ֌ڍʁÈωҋԖ܎׈ی߆߈ߑ؃͊ԅێܐߍܔ݋Ҏߎސߎ݉؊ߍՕږݚ֗ђψzy‰ωЍ̉Ɉʄƈńƃud=9:;4<2324/4-/00/-.,(-*($ +~upy|yyx~~#!!#!|&l95731 +1 2 -1 1+ +' ۉwsnvv~{uw{ʇƂDŽ~Æȇˁ|ֈˁɅφȇЈÉ҈΁Ӈӈ֐،ӎۈߏԍ݇܆Հ҇҅Ԅ؇ݎݎ݊ߏ܉ܓۑًދڏޑՑޙܕ֑ڐۏۊ؈ߐߊӉޏׇٕ̓֗ӖӔээΈƊȃ€DžÈʈ…]I6;4416.00/112-,---*'(&$#{wym_rm}}usx~!}{ }O +7 -51110 ) ) +% ( $ ircWs\w{}vux{v|y͋Ʉ̀̀ƀłƒɃˁυ҇҅яɋψ܋چĉЁՃЅ҅̀ͅ؈քԈيۊޅ߆؈׎׌׎Ԑވݐڈ׃҆ӅȄ֐באԏΈъԊԆ̃Ӌԇч́̋ωВχό͌̇ɐхdžɆxÁƒ{|}w~J?36381202/0.-*-*-'($*%$!_o[cdy}w}rwtrs}yz{{}{}~{|y}|zxy}r? 6 12 , - . (* %& %|`bcj}{~{puvqow{z}}zz|{zyŁ}~{Žҍ΍ӂӉև̃ҋ։΁ʀĀ̈́ՅЃˉ҂ЇЈԐՊ։؃ʃ׋ڏՏممӋ҈ϋՋҏԌҎΎχΌҐЍӆɇψʇńÇ͈ˀ͊ъȊÇĈÆÈƁ{~~wzvy{oa3?22/.01////-,((%&&##!SZYis|yyyutvsprw{{xxx}vy~~!{!zyzxy{xf 5 0, 1 /*( ( '%!{\|`jm~y|xrtstsq|y|~xzy{}|zɃʆȍэІчԊDŽͅΆ|ʅ؆ԃ̂†̍˃΅ƁӁDžԋ҆ԍՊ׊σ˄Ԉʀ̃Ӈσ΋ԇŁԏЅ̓ϊτŅʀǀ~DŽ̊ɃɇςȈ|Ʉ~̇ŃłɉĊņƉ‰~{{{~|wxwr^M430110/+-,)(%.)%%$"$cjiruzvrrsqtspt|uxx~~{}}{zzy}{}x}} z}y}y|v~{||vusqU0 .. +* - *)"# kkjuvvtrorprtpv{uzyy{|zuq}zxÂ~~wxLJʃЁ{̅̆ȇʁÂĀȆʆфΉ҆ы׌ЉՌш̓Ѐʆ͂̃φӋڏԈȀ~χÀʀĄDžDžƆ΄ÇÄyĉ}~yw}zrxxxysqtnyH<-2/0*+),+)%*!$"##kiimvsuqrnp|xprsu{x|vy~{ru~w||xwy|}y~}z~}|y~~ |}{}}zzu|zxvxvrunqm? +. 1-%& &! ijhnstzpqqsuqtpspxvpw{xuw|zz~Ā~}}ȀЇԉlj҅΃z~΅ˀˌӐֈυǃƄă͂΀ˈՅˇ}}ĂuÅΈ΃ʁ΂}|ɁĀ{}z}{|zxz~vttwuwjnqeY71,+-))-'(&(!"$ %ggcirsuoemlovkosroupswtur{x{vq|y~~z||}~|~  ~}}x}}~}|||||yz}yyx{}xxvttztpulm[0 .'' '## "bbhhjpldeoqmnqqqmortuwyuwk}qnww}{}~z}ƀ|}xyw~z}}~ȇ҄ςˁȀǀʆzˇ|~ƃ|zx~~}zŃz|zx}z{yxrxvpzowlxtnlpimP<-,%,*&$&!#%!! "^hjlkjk hg `sohmhnqproqp{|tpxz}z"pzzzxzzxvrwt|wzwtzvzwzxy~{z~xsz~|w}}~{z"x{zyqxlxopmosngiehbA , +'*! #  \ No newline at end of file diff --git a/examples/peripherals/isp/dma_input/main/isp_dma_example_main.c b/examples/peripherals/isp/dma_input/main/isp_dma_example_main.c new file mode 100644 index 00000000000..11e2e8c72bf --- /dev/null +++ b/examples/peripherals/isp/dma_input/main/isp_dma_example_main.c @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#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); +} 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..e5603445560 --- /dev/null +++ b/examples/peripherals/isp/dma_input/pytest_isp_dma_input.py @@ -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\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_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\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.') 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 diff --git a/examples/peripherals/jpeg/jpeg_decode/README.md b/examples/peripherals/jpeg/jpeg_decode/README.md index 24cb0ca5947..df3acb7520d 100644 --- a/examples/peripherals/jpeg/jpeg_decode/README.md +++ b/examples/peripherals/jpeg/jpeg_decode/README.md @@ -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. diff --git a/examples/peripherals/jpeg/jpeg_encode/README.md b/examples/peripherals/jpeg/jpeg_encode/README.md index 2c32e2d705a..bd06bef71b5 100644 --- a/examples/peripherals/jpeg/jpeg_encode/README.md +++ b/examples/peripherals/jpeg/jpeg_encode/README.md @@ -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. diff --git a/tools/cmake/scripts/data_file_embed_asm.cmake b/tools/cmake/scripts/data_file_embed_asm.cmake index 5d19e7a1ba9..b8612976894 100644 --- a/tools/cmake/scripts/data_file_embed_asm.cmake +++ b/tools/cmake/scripts/data_file_embed_asm.cmake @@ -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}") diff --git a/tools/cmake/utilities.cmake b/tools/cmake/utilities.cmake index 1ce16f2b911..301720ce14b 100644 --- a/tools/cmake/utilities.cmake +++ b/tools/cmake/utilities.cmake @@ -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}" diff --git a/tools/cmakev2/utilities.cmake b/tools/cmakev2/utilities.cmake index a1c5c4e0851..ce2700bf390 100644 --- a/tools/cmakev2/utilities.cmake +++ b/tools/cmakev2/utilities.cmake @@ -833,7 +833,8 @@ endfunction() #[[ target_add_binary_data( - [RENAME_TO ]) + [RENAME_TO ] + [ALIGN ] [DEPENDS ...]) *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}"