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..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 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..a22bed0863a --- /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, 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; + 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 71b2c33c1a3..52eab1c24fe 100644 --- a/docs/doxygen/Doxyfile_esp32p4 +++ b/docs/doxygen/Doxyfile_esp32p4 @@ -18,6 +18,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-reference/peripherals/isp.rst b/docs/en/api-reference/peripherals/isp.rst index c35c4b33a06..d9f6336f365 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; if cacheable memory is used, perform the required cache synchronization before and after the DMA transfer. + ISP AF Controller ~~~~~~~~~~~~~~~~~ @@ -957,6 +967,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 @@ -977,5 +988,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-reference/peripherals/isp.rst b/docs/zh_CN/api-reference/peripherals/isp.rst index 1790af45b80..96ec09a8e8e 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 的内存,请在 DMA 传输前后执行必要的 cache 同步。 + ISP AF 控制器 ~~~~~~~~~~~~~ @@ -956,6 +966,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 参考 @@ -976,5 +987,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/peripherals/.build-test-rules.yml b/examples/peripherals/.build-test-rules.yml index be3f29e3801..c7577b399d3 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -228,6 +228,14 @@ examples/peripherals/i3c/i3c_i2c_basic: - *common_components - esp_driver_i3c +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..b554725300c --- /dev/null +++ b/examples/peripherals/isp/dma_input/README.md @@ -0,0 +1,49 @@ +| 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, copies it into a DMA-capable PSRAM input buffer, feeds it into the ISP through DW-GDMA, and prints the RGB888 output as base64. 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 BGGR RAW8 image is copied from flash into the ISP DMA input buffer. +2. The image 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, 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 DMA input/output buffers 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. +``` + +## 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/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..8d7c2262783 --- /dev/null +++ b/examples/peripherals/isp/dma_input/main/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register(SRCS "isp_dma_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") 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_main.c b/examples/peripherals/isp/dma_input/main/isp_dma_main.c new file mode 100644 index 00000000000..4460e7502ac --- /dev/null +++ b/examples/peripherals/isp/dma_input/main/isp_dma_main.c @@ -0,0 +1,128 @@ +/* + * 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 240 +#define EXAMPLE_HEIGHT 280 +#define EXAMPLE_BASE64_CHUNK_LEN 384 +#define EXAMPLE_BASE64_DELAY_MS 10 +#define EXAMPLE_DMA_ALIGN 64 +#define EXAMPLE_FRAME_COUNT 1 + +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 *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(EXAMPLE_BASE64_DELAY_MS)); + } + 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 embedded_raw_size = sensor_raw_end - sensor_raw_start; + assert(embedded_raw_size == in_size); + memcpy(isp_in_buf, sensor_raw_start, embedded_raw_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++) { + 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..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