diff --git a/components/esp_driver_dma/CMakeLists.txt b/components/esp_driver_dma/CMakeLists.txt index ec28f99c5c0..6e9b57e330a 100644 --- a/components/esp_driver_dma/CMakeLists.txt +++ b/components/esp_driver_dma/CMakeLists.txt @@ -36,7 +36,7 @@ if(CONFIG_SOC_DW_GDMA_SUPPORTED) endif() if(CONFIG_SOC_DMA2D_SUPPORTED) - list(APPEND srcs "src/dma2d.c") + list(APPEND srcs "src/dma2d.c" "src/esp_async_color_convert.c" "src/async_color_convert_dma2d.c") endif() idf_component_register(SRCS ${srcs} diff --git a/components/esp_driver_dma/include/esp_async_color_convert.h b/components/esp_driver_dma/include/esp_async_color_convert.h new file mode 100644 index 00000000000..18399c2f15c --- /dev/null +++ b/components/esp_driver_dma/include/esp_async_color_convert.h @@ -0,0 +1,164 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include "esp_err.h" +#include "hal/color_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle of async color conversion driver instance + */ +typedef struct async_color_convert_context_t *async_color_convert_handle_t; + +/** + * @brief Async color conversion event data + */ +typedef struct { +} async_color_convert_event_data_t; + +/** + * @brief Async color conversion callback type + * + * @note This callback runs in ISR context. + * + * @param[in] conv_hdl Driver handle that produced this event + * @param[in] edata Event data for the completed request + * @param[in] cb_args User context passed to :cpp:func:`esp_async_color_convert` + * + * @return + * - true: a higher-priority task was woken and a yield is requested + * - false: no yield request + */ +typedef bool (*async_color_convert_isr_cb_t)(async_color_convert_handle_t conv_hdl, + async_color_convert_event_data_t *edata, + void *cb_args); + +/** + * @brief Async color conversion driver configuration + */ +typedef struct { + uint32_t backlog; /*!< Number of in-flight/pending requests. 0 means driver default. */ + size_t dma_burst_size; /*!< DMA burst length in bytes. 0 means driver default. */ + uint32_t intr_priority; /*!< Interrupt priority. 0 means default low/medium priority. */ +} async_color_convert_config_t; + +/** + * @brief Install async color conversion driver with the DMA2D backend + * + * This API allocates internal resources and creates a conversion context. + * + * @param[in] config Driver configuration + * @param[out] ret_hdl Returned driver handle + * + * @return + * - ESP_OK: Driver installed successfully + * - ESP_ERR_INVALID_ARG: Invalid argument + * - ESP_ERR_NO_MEM: Out of memory + * - ESP_ERR_NOT_FOUND: Required DMA2D resource is unavailable + * - others: Error from lower-level DMA2D driver + */ +esp_err_t esp_async_color_convert_install_dma2d(const async_color_convert_config_t *config, + async_color_convert_handle_t *ret_hdl); + +/** + * @brief Uninstall async color conversion driver + * + * @param[in] conv_hdl Driver handle returned by :cpp:func:`esp_async_color_convert_install_dma2d` + * + * @return + * - ESP_OK: Driver uninstalled successfully + * - ESP_ERR_INVALID_ARG: Invalid argument + * - ESP_ERR_INVALID_STATE: There are pending requests in the queue + */ +esp_err_t esp_async_color_convert_uninstall(async_color_convert_handle_t conv_hdl); + +/** + * @brief Async color conversion request + * + * Coordinates and size are in pixels. + * + * The source and destination windows are: + * - source: [src_x, src_x + copy_width) x [src_y, src_y + copy_height) + * - destination: [dst_x, dst_x + copy_width) x [dst_y, dst_y + copy_height) + * + * Both windows must be fully inside their corresponding image bounds. + * + * Conversion rule is inferred from source and destination formats: + * - If source and destination are the same format, it performs 2D copy only. + */ +typedef struct { + const void *src_buffer; /*!< Source picture base address */ + uint32_t src_stride; /*!< Source picture row stride in pixels */ + uint32_t src_height; /*!< Source picture height in pixels */ + uint32_t src_x; /*!< Source window x offset in pixels */ + uint32_t src_y; /*!< Source window y offset in pixels */ + + void *dst_buffer; /*!< Destination picture base address */ + uint32_t dst_stride; /*!< Destination picture row stride in pixels */ + uint32_t dst_height; /*!< Destination picture height in pixels */ + uint32_t dst_x; /*!< Destination window x offset in pixels */ + uint32_t dst_y; /*!< Destination window y offset in pixels */ + + uint32_t copy_width; /*!< Conversion window width in pixels */ + uint32_t copy_height; /*!< Conversion window height in pixels */ + + esp_color_fourcc_t src_color_format; /*!< Source pixel format */ + esp_color_fourcc_t dst_color_format; /*!< Destination pixel format */ + color_conv_std_rgb_yuv_t color_conv_std; /*!< RGB/YUV conversion standard for RGB888<->UYVY422 */ +} async_color_convert_request_t; + +/** + * @brief Submit an asynchronous 2D color conversion request + * + * The request is enqueued and completed later in DMA2D interrupt context. + * The callback can be NULL if no completion notification is needed. + * + * @param[in] conv_hdl Driver handle returned by :cpp:func:`esp_async_color_convert_install_dma2d` + * @param[in] request Color conversion request + * @param[in] cb_isr ISR callback invoked on conversion completion, can be NULL + * @param[in] cb_args User context passed to @p cb_isr + * + * @return + * - ESP_OK: Request accepted + * - ESP_ERR_INVALID_ARG: Invalid argument or invalid request fields + * - ESP_ERR_INVALID_STATE: No free internal transaction slot (queue full) + * - others: Error from lower-level DMA2D driver + */ +esp_err_t esp_async_color_convert(async_color_convert_handle_t conv_hdl, + const async_color_convert_request_t *request, + async_color_convert_isr_cb_t cb_isr, + void *cb_args); + +/** + * @brief Blocking 2D color conversion API built on async request path + * + * @note This API must not be called from ISR context. + * + * @param[in] conv_hdl Driver handle returned by :cpp:func:`esp_async_color_convert_install_dma2d` + * @param[in] request Color conversion request + * @param[in] timeout_ms Timeout in milliseconds. Currently only ``-1`` is supported, which waits forever. + * + * @return + * - ESP_OK: Conversion completed successfully + * - ESP_ERR_INVALID_ARG: Invalid argument, unsupported timeout, or invalid request fields + * - ESP_ERR_INVALID_STATE: Called from ISR context, or queue unavailable + * - others: Error from lower-level DMA2D driver + */ +esp_err_t esp_color_convert_blocking(async_color_convert_handle_t conv_hdl, + const async_color_convert_request_t *request, + int32_t timeout_ms); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_driver_dma/src/async_color_convert_dma2d.c b/components/esp_driver_dma/src/async_color_convert_dma2d.c new file mode 100644 index 00000000000..976a962401e --- /dev/null +++ b/components/esp_driver_dma/src/async_color_convert_dma2d.c @@ -0,0 +1,507 @@ +/* + * 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 "esp_check.h" +#include "esp_cache.h" +#include "esp_private/esp_cache_private.h" +#include "esp_heap_caps.h" +#include "esp_memory_utils.h" +#include "esp_async_color_convert_priv.h" +#include "soc/dma2d_channel.h" +#include "hal/dma2d_types.h" +#include "hal/dma2d_ll.h" +#include "hal/color_hal.h" + +ESP_LOG_ATTR_TAG(TAG, "async_color_dma2d"); + +typedef struct async_color_convert_dma2d_context_t async_color_convert_dma2d_context_t; +typedef struct async_color_convert_transaction async_color_convert_transaction_t; + +struct async_color_convert_transaction { + // Keep descriptors in dedicated cache-line-sized allocations so cache sync only + // touches descriptor state, not the surrounding transaction metadata. + dma2d_descriptor_t *tx_desc; // TX descriptor used by DMA2D source channel + dma2d_descriptor_t *rx_desc; // RX descriptor used by DMA2D destination channel + dma2d_trans_t *dma2d_trans_placeholder; // Opaque DMA2D transaction object storage + dma2d_trans_config_t dma2d_trans_config; // Per-request DMA2D transaction configuration + + async_color_convert_request_t request; // Cached user request used to build DMA2D transaction + dma2d_csc_config_t tx_csc; // Cached DMA2D TX CSC configuration resolved in task context + dma2d_csc_config_t rx_csc; // Cached DMA2D RX CSC configuration resolved in task context + async_color_convert_isr_cb_t cb_isr; // User ISR callback for this request + void *cb_args; // User callback argument + async_color_convert_dma2d_context_t *ctx; // Back pointer to parent context + + STAILQ_ENTRY(async_color_convert_transaction) queue_entry; // Node in idle transaction queue +}; + +struct async_color_convert_dma2d_context_t { + async_color_convert_context_t parent; // Base interface used by common API wrappers + dma2d_pool_handle_t pool; // DMA2D pool handle for enqueue/dequeue scheduling + size_t dma_burst_size; // DMA burst size applied on job picked + size_t desc_alloc_size; // Cache-line-sized DMA2D descriptor allocation size + portMUX_TYPE spinlock; // Protects idle_queue + uint32_t num_trans_objs; // Total number of transaction objects in trans_pool + _Atomic uint32_t idle_num; // Number of currently available transaction objects + _Atomic bool deleting; // Whether uninstall is in progress + async_color_convert_transaction_t *trans_pool; // Pre-allocated transaction object pool + STAILQ_HEAD(, async_color_convert_transaction) idle_queue; // Queue of available transaction objects +}; + +static esp_err_t async_color_convert_dma2d_del(async_color_convert_context_t *ctx); +static esp_err_t async_color_convert_dma2d_convert(async_color_convert_context_t *ctx, + const async_color_convert_request_t *request, + async_color_convert_isr_cb_t cb_isr, + void *cb_args); + +static bool is_rgb24_or_bgr24_fourcc(esp_color_fourcc_t fourcc) +{ + return fourcc == ESP_COLOR_FOURCC_BGR24 || fourcc == ESP_COLOR_FOURCC_RGB24; +} + +static dma2d_csc_config_t default_tx_csc_config(void) +{ + return (dma2d_csc_config_t) { + .tx_csc_option = DMA2D_CSC_TX_NONE, + .pre_scramble = DMA2D_SCRAMBLE_ORDER_BYTE2_1_0, + .post_scramble = DMA2D_SCRAMBLE_ORDER_BYTE2_1_0, + }; +} + +static dma2d_csc_config_t default_rx_csc_config(void) +{ + return (dma2d_csc_config_t) { + .rx_csc_option = DMA2D_CSC_RX_NONE, + .pre_scramble = DMA2D_SCRAMBLE_ORDER_BYTE2_1_0, + .post_scramble = DMA2D_SCRAMBLE_ORDER_BYTE2_1_0, + }; +} + +static bool resolve_dma2d_csc_configs(const async_color_convert_request_t *request, + dma2d_csc_config_t *out_tx_csc, + dma2d_csc_config_t *out_rx_csc) +{ + esp_color_fourcc_t src_fourcc = request->src_color_format; + esp_color_fourcc_t dst_fourcc = request->dst_color_format; + bool src_is_rgb24_or_bgr24 = is_rgb24_or_bgr24_fourcc(src_fourcc); + bool dst_is_rgb24_or_bgr24 = is_rgb24_or_bgr24_fourcc(dst_fourcc); + + *out_tx_csc = default_tx_csc_config(); + *out_rx_csc = default_rx_csc_config(); + + if (src_fourcc == dst_fourcc) { + return true; + } + + if (src_is_rgb24_or_bgr24 && dst_is_rgb24_or_bgr24) { + out_tx_csc->tx_csc_option = DMA2D_CSC_TX_SCRAMBLE; // RGB<->BGR conversion is just a scramble operation + out_tx_csc->pre_scramble = DMA2D_SCRAMBLE_ORDER_BYTE0_1_2; + return true; + } + + if (src_fourcc == ESP_COLOR_FOURCC_RGB16 && dst_fourcc == ESP_COLOR_FOURCC_BGR24) { + out_tx_csc->tx_csc_option = DMA2D_CSC_TX_RGB565_TO_RGB888; + return true; + } + + if (src_is_rgb24_or_bgr24 && dst_fourcc == ESP_COLOR_FOURCC_RGB16) { + out_tx_csc->tx_csc_option = DMA2D_CSC_TX_RGB888_TO_RGB565; + if (src_fourcc == ESP_COLOR_FOURCC_RGB24) { + out_tx_csc->pre_scramble = DMA2D_SCRAMBLE_ORDER_BYTE0_1_2; + } + return true; + } + + if (src_is_rgb24_or_bgr24 && dst_fourcc == ESP_COLOR_FOURCC_UYVY) { + if (request->color_conv_std == COLOR_CONV_STD_RGB_YUV_BT601) { + out_tx_csc->tx_csc_option = DMA2D_CSC_TX_RGB888_TO_YUV422_601; + } else if (request->color_conv_std == COLOR_CONV_STD_RGB_YUV_BT709) { + out_tx_csc->tx_csc_option = DMA2D_CSC_TX_RGB888_TO_YUV422_709; + } else { + return false; + } + if (src_fourcc == ESP_COLOR_FOURCC_RGB24) { + out_tx_csc->pre_scramble = DMA2D_SCRAMBLE_ORDER_BYTE0_1_2; + } + return true; + } + + if (src_fourcc == ESP_COLOR_FOURCC_UYVY && dst_fourcc == ESP_COLOR_FOURCC_BGR24) { + if (request->color_conv_std == COLOR_CONV_STD_RGB_YUV_BT601) { + out_tx_csc->tx_csc_option = DMA2D_CSC_TX_YUV422_TO_RGB888_601; + } else if (request->color_conv_std == COLOR_CONV_STD_RGB_YUV_BT709) { + out_tx_csc->tx_csc_option = DMA2D_CSC_TX_YUV422_TO_RGB888_709; + } else { + return false; + } + return true; + } + + return false; +} + +static inline bool needs_tx_csc(const dma2d_csc_config_t *tx_csc) +{ + return tx_csc->tx_csc_option != DMA2D_CSC_TX_NONE; +} + +static inline bool needs_rx_csc(const dma2d_csc_config_t *rx_csc) +{ + return rx_csc->rx_csc_option != DMA2D_CSC_RX_NONE; +} + +static esp_err_t sync_if_cacheable(void *addr, size_t size, int flags) +{ + return esp_cache_get_line_size_by_addr(addr) > 0 ? esp_cache_msync(addr, size, flags) : ESP_OK; +} + +static size_t get_picture_size_bytes(uint32_t stride, uint32_t height, uint32_t bit_depth) +{ + return (((size_t)stride * height * bit_depth) + 7) / 8; +} + +static esp_err_t validate_request(const async_color_convert_request_t *request) +{ + ESP_RETURN_ON_FALSE(request->src_color_format != 0 && request->dst_color_format != 0, + ESP_ERR_INVALID_ARG, TAG, "invalid color format"); + ESP_RETURN_ON_FALSE(request->src_buffer && request->dst_buffer, ESP_ERR_INVALID_ARG, TAG, "invalid buffer"); + ESP_RETURN_ON_FALSE(request->copy_width > 0 && request->copy_height > 0, ESP_ERR_INVALID_ARG, TAG, "invalid copy window"); + + uint64_t src_x_end = (uint64_t)request->src_x + request->copy_width; + uint64_t src_y_end = (uint64_t)request->src_y + request->copy_height; + uint64_t dst_x_end = (uint64_t)request->dst_x + request->copy_width; + uint64_t dst_y_end = (uint64_t)request->dst_y + request->copy_height; + + ESP_RETURN_ON_FALSE(src_x_end <= request->src_stride, ESP_ERR_INVALID_ARG, TAG, "source window out of width"); + ESP_RETURN_ON_FALSE(src_y_end <= request->src_height, ESP_ERR_INVALID_ARG, TAG, "source window out of height"); + ESP_RETURN_ON_FALSE(dst_x_end <= request->dst_stride, ESP_ERR_INVALID_ARG, TAG, "destination window out of width"); + ESP_RETURN_ON_FALSE(dst_y_end <= request->dst_height, ESP_ERR_INVALID_ARG, TAG, "destination window out of height"); + + ESP_RETURN_ON_FALSE(request->src_stride <= DMA2D_LL_DESC_2D_FIELD_MAX && + request->src_height <= DMA2D_LL_DESC_2D_FIELD_MAX && + request->dst_stride <= DMA2D_LL_DESC_2D_FIELD_MAX && + request->dst_height <= DMA2D_LL_DESC_2D_FIELD_MAX, + ESP_ERR_INVALID_ARG, TAG, "dimension exceeds DMA2D descriptor field limit"); + + return ESP_OK; +} + +static async_color_convert_transaction_t *try_acquire_trans(async_color_convert_dma2d_context_t *ctx) +{ + async_color_convert_transaction_t *trans = NULL; + portENTER_CRITICAL(&ctx->spinlock); + if (!atomic_load(&ctx->deleting)) { + trans = STAILQ_FIRST(&ctx->idle_queue); + if (trans) { + STAILQ_REMOVE_HEAD(&ctx->idle_queue, queue_entry); + atomic_fetch_sub(&ctx->idle_num, 1); + } + } + portEXIT_CRITICAL(&ctx->spinlock); + return trans; +} + +static void recycle_trans(async_color_convert_dma2d_context_t *ctx, + async_color_convert_transaction_t *trans) +{ + portENTER_CRITICAL_SAFE(&ctx->spinlock); + STAILQ_INSERT_TAIL(&ctx->idle_queue, trans, queue_entry); + atomic_fetch_add(&ctx->idle_num, 1); + portEXIT_CRITICAL_SAFE(&ctx->spinlock); +} + +static bool async_color_convert_done_cb(dma2d_channel_handle_t dma2d_chan, + dma2d_event_data_t *event_data, + void *user_data) +{ + bool need_yield = false; + async_color_convert_transaction_t *trans = (async_color_convert_transaction_t *)user_data; + async_color_convert_dma2d_context_t *ctx = trans->ctx; + (void)dma2d_chan; + (void)event_data; + + if (trans->cb_isr) { + async_color_convert_event_data_t edata = {}; + need_yield = trans->cb_isr(&ctx->parent, &edata, trans->cb_args); + } + trans->cb_isr = NULL; + trans->cb_args = NULL; + + recycle_trans(ctx, trans); + return need_yield; +} + +static bool async_color_convert_on_job_picked(uint32_t channel_num, + const dma2d_trans_channel_info_t *dma2d_chans, + void *user_config) +{ + async_color_convert_transaction_t *trans = (async_color_convert_transaction_t *)user_config; + + dma2d_channel_handle_t tx_chan = NULL; + dma2d_channel_handle_t rx_chan = NULL; + + for (uint32_t i = 0; i < channel_num; i++) { + if (dma2d_chans[i].dir == DMA2D_CHANNEL_DIRECTION_TX) { + tx_chan = dma2d_chans[i].chan; + } else { + rx_chan = dma2d_chans[i].chan; + } + } + + dma2d_trigger_t trig_periph = { + .periph = DMA2D_TRIG_PERIPH_M2M, + .periph_sel_id = SOC_DMA2D_TRIG_PERIPH_M2M_TX, + }; + dma2d_connect(tx_chan, &trig_periph); + trig_periph.periph_sel_id = SOC_DMA2D_TRIG_PERIPH_M2M_RX; + dma2d_connect(rx_chan, &trig_periph); + + async_color_convert_dma2d_context_t *ctx = trans->ctx; + dma2d_transfer_ability_t transfer_ability = { + .desc_burst_en = true, + .data_burst_length = ctx->dma_burst_size, + .access_ext_mem = true, + .mb_size = DMA2D_MACRO_BLOCK_SIZE_NONE, + }; + dma2d_set_transfer_ability(tx_chan, &transfer_ability); + dma2d_set_transfer_ability(rx_chan, &transfer_ability); + + dma2d_configure_color_space_conversion(tx_chan, &trans->tx_csc); + dma2d_configure_color_space_conversion(rx_chan, &trans->rx_csc); + + dma2d_rx_event_callbacks_t cbs = { + .on_recv_eof = async_color_convert_done_cb, + }; + dma2d_register_rx_event_callbacks(rx_chan, &cbs, trans); + + dma2d_set_desc_addr(rx_chan, (intptr_t)trans->rx_desc); + dma2d_set_desc_addr(tx_chan, (intptr_t)trans->tx_desc); + + dma2d_start(rx_chan); + dma2d_start(tx_chan); + return false; +} + +static void setup_desc(dma2d_descriptor_t *desc, + void *buffer, + uint32_t pic_w, + uint32_t pic_h, + uint32_t win_w, + uint32_t win_h, + uint32_t x, + uint32_t y, + uint32_t pbyte) +{ + memset(desc, 0, sizeof(*desc)); + desc->owner = DMA2D_DESCRIPTOR_BUFFER_OWNER_DMA; + desc->suc_eof = 1; + desc->dma2d_en = 1; + desc->ha_length = pic_w; + desc->va_size = pic_h; + desc->hb_length = win_w; + desc->vb_size = win_h; + desc->x = x; + desc->y = y; + desc->pbyte = pbyte; + desc->mode = DMA2D_DESCRIPTOR_BLOCK_RW_MODE_SINGLE; + desc->buffer = buffer; + desc->next = NULL; +} + +static esp_err_t async_color_convert_dma2d_convert(async_color_convert_context_t *ctx, + const async_color_convert_request_t *request, + async_color_convert_isr_cb_t cb_isr, + void *cb_args) +{ + esp_err_t ret = ESP_OK; + ESP_RETURN_ON_FALSE(ctx && request, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_ERROR(validate_request(request), TAG, "invalid request"); + + async_color_convert_dma2d_context_t *color_ctx = __containerof(ctx, async_color_convert_dma2d_context_t, parent); + async_color_convert_transaction_t *trans = try_acquire_trans(color_ctx); + ESP_RETURN_ON_FALSE(trans, ESP_ERR_INVALID_STATE, TAG, "no free transaction in pool"); + + trans->request = *request; // copy request to transaction object + trans->cb_isr = cb_isr; + trans->cb_args = cb_args; + + esp_color_fourcc_t src_fourcc = request->src_color_format; + esp_color_fourcc_t dst_fourcc = request->dst_color_format; + trans->tx_csc = default_tx_csc_config(); + trans->rx_csc = default_rx_csc_config(); + ESP_GOTO_ON_FALSE(resolve_dma2d_csc_configs(request, &trans->tx_csc, &trans->rx_csc), + ESP_ERR_INVALID_ARG, recycle_and_out, TAG, "unsupported color conversion mode"); + + trans->dma2d_trans_config.channel_flags = DMA2D_CHANNEL_FUNCTION_FLAG_SIBLING; + if (needs_tx_csc(&trans->tx_csc)) { + trans->dma2d_trans_config.channel_flags |= DMA2D_CHANNEL_FUNCTION_FLAG_TX_CSC; + } + if (needs_rx_csc(&trans->rx_csc)) { + trans->dma2d_trans_config.channel_flags |= DMA2D_CHANNEL_FUNCTION_FLAG_RX_CSC; + } + setup_desc(trans->tx_desc, + (void *)request->src_buffer, + request->src_stride, + request->src_height, + request->copy_width, + request->copy_height, + request->src_x, + request->src_y, + dma2d_desc_pixel_format_to_pbyte_value(src_fourcc)); + + setup_desc(trans->rx_desc, + request->dst_buffer, + request->dst_stride, + request->dst_height, + request->copy_width, + request->copy_height, + request->dst_x, + request->dst_y, + dma2d_desc_pixel_format_to_pbyte_value(dst_fourcc)); + + uint32_t src_bpp = color_hal_pixel_format_fourcc_get_bit_depth(src_fourcc); + uint32_t dst_bpp = color_hal_pixel_format_fourcc_get_bit_depth(dst_fourcc); + + size_t src_total_size = get_picture_size_bytes(request->src_stride, request->src_height, src_bpp); + size_t dst_total_size = get_picture_size_bytes(request->dst_stride, request->dst_height, dst_bpp); + + ESP_GOTO_ON_ERROR(sync_if_cacheable((void *)request->src_buffer, src_total_size, + ESP_CACHE_MSYNC_FLAG_DIR_C2M | ESP_CACHE_MSYNC_FLAG_UNALIGNED), + recycle_and_out, TAG, "source cache sync failed"); + + ESP_GOTO_ON_ERROR(sync_if_cacheable(request->dst_buffer, dst_total_size, + ESP_CACHE_MSYNC_FLAG_DIR_C2M | ESP_CACHE_MSYNC_FLAG_INVALIDATE | ESP_CACHE_MSYNC_FLAG_UNALIGNED), + recycle_and_out, TAG, "destination cache sync failed"); + + ESP_GOTO_ON_ERROR(sync_if_cacheable(trans->tx_desc, color_ctx->desc_alloc_size, + ESP_CACHE_MSYNC_FLAG_DIR_C2M | ESP_CACHE_MSYNC_FLAG_INVALIDATE), + recycle_and_out, TAG, "tx descriptor cache sync failed"); + + ESP_GOTO_ON_ERROR(sync_if_cacheable(trans->rx_desc, color_ctx->desc_alloc_size, + ESP_CACHE_MSYNC_FLAG_DIR_C2M | ESP_CACHE_MSYNC_FLAG_INVALIDATE), + recycle_and_out, TAG, "rx descriptor cache sync failed"); + + ESP_GOTO_ON_ERROR(dma2d_enqueue(color_ctx->pool, + &trans->dma2d_trans_config, + trans->dma2d_trans_placeholder), + recycle_and_out, TAG, "enqueue dma2d transaction failed"); + + return ESP_OK; + +recycle_and_out: + recycle_trans(color_ctx, trans); + return ret; +} + +static esp_err_t async_color_convert_dma2d_destroy(async_color_convert_dma2d_context_t *ctx) +{ + if (ctx->pool) { + dma2d_release_pool(ctx->pool); + } + if (ctx->trans_pool) { + for (uint32_t i = 0; i < ctx->num_trans_objs; i++) { + free(ctx->trans_pool[i].tx_desc); + free(ctx->trans_pool[i].rx_desc); + free(ctx->trans_pool[i].dma2d_trans_placeholder); + } + free(ctx->trans_pool); + } + free(ctx); + return ESP_OK; +} + +static esp_err_t async_color_convert_dma2d_del(async_color_convert_context_t *ctx) +{ + async_color_convert_dma2d_context_t *color_ctx = __containerof(ctx, async_color_convert_dma2d_context_t, parent); + bool can_destroy = false; + portENTER_CRITICAL(&color_ctx->spinlock); + atomic_store(&color_ctx->deleting, true); + can_destroy = (atomic_load(&color_ctx->idle_num) == color_ctx->num_trans_objs); + if (!can_destroy) { + atomic_store(&color_ctx->deleting, false); + } + portEXIT_CRITICAL(&color_ctx->spinlock); + ESP_RETURN_ON_FALSE(can_destroy, + ESP_ERR_INVALID_STATE, TAG, "pending transactions exist"); + return async_color_convert_dma2d_destroy(color_ctx); +} + +esp_err_t esp_async_color_convert_install_dma2d(const async_color_convert_config_t *config, + async_color_convert_handle_t *ret_hdl) +{ + esp_err_t ret = ESP_OK; + ESP_RETURN_ON_FALSE(config && ret_hdl, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + uint32_t trans_queue_len = config->backlog ? config->backlog : DEFAULT_COLOR_CONVERT_BACKLOG; + + async_color_convert_dma2d_context_t *ctx = heap_caps_calloc(1, sizeof(async_color_convert_dma2d_context_t), + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + ESP_RETURN_ON_FALSE(ctx, ESP_ERR_NO_MEM, TAG, "no mem for color convert context"); + + ctx->trans_pool = heap_caps_calloc(trans_queue_len, sizeof(async_color_convert_transaction_t), + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + ESP_GOTO_ON_FALSE(ctx->trans_pool, ESP_ERR_NO_MEM, err, TAG, "no mem for transaction pool"); + + esp_cache_get_alignment(MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA, &ctx->desc_alloc_size); + if (ctx->desc_alloc_size < sizeof(dma2d_descriptor_t)) { + ctx->desc_alloc_size = sizeof(dma2d_descriptor_t); + } + + ctx->num_trans_objs = trans_queue_len; + ctx->dma_burst_size = config->dma_burst_size ? config->dma_burst_size : 32; + portMUX_INITIALIZE(&ctx->spinlock); + STAILQ_INIT(&ctx->idle_queue); + atomic_init(&ctx->idle_num, trans_queue_len); + atomic_init(&ctx->deleting, false); + + dma2d_pool_config_t pool_cfg = { + .pool_id = 0, + .intr_priority = config->intr_priority, + }; + ESP_GOTO_ON_ERROR(dma2d_acquire_pool(&pool_cfg, &ctx->pool), err, TAG, "acquire dma2d pool failed"); + + for (uint32_t i = 0; i < trans_queue_len; i++) { + async_color_convert_transaction_t *trans = &ctx->trans_pool[i]; + trans->ctx = ctx; + // one DMA descriptor is enough for one color-conversion job since + // the driver will configure the descriptors in single-block mode and won't split the block into multiple tiles + trans->tx_desc = heap_caps_aligned_calloc(DMA2D_LL_DESC_ALIGNMENT, 1, ctx->desc_alloc_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + ESP_GOTO_ON_FALSE(trans->tx_desc, ESP_ERR_NO_MEM, err, TAG, "no memory for tx descriptor"); + trans->rx_desc = heap_caps_aligned_calloc(DMA2D_LL_DESC_ALIGNMENT, 1, ctx->desc_alloc_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + ESP_GOTO_ON_FALSE(trans->rx_desc, ESP_ERR_NO_MEM, err, TAG, "no memory for rx descriptor"); + trans->dma2d_trans_placeholder = heap_caps_calloc(1, dma2d_get_trans_elm_size(), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + ESP_GOTO_ON_FALSE(trans->dma2d_trans_placeholder, ESP_ERR_NO_MEM, err, TAG, "no memory for dma2d transaction placeholder"); + // dma2d_enqueue requires a long-lived transaction config, so we prepare it here with the common part configured. + // The per-request specific part will be filled in on job picked. + trans->dma2d_trans_config = (dma2d_trans_config_t) { + .tx_channel_num = 1, + .rx_channel_num = 1, + .channel_flags = DMA2D_CHANNEL_FUNCTION_FLAG_SIBLING, + .specified_tx_channel_mask = 0, + .specified_rx_channel_mask = 0, + .on_job_picked = async_color_convert_on_job_picked, + .user_config = trans, + }; + STAILQ_INSERT_TAIL(&ctx->idle_queue, trans, queue_entry); + } + + ctx->parent.convert = async_color_convert_dma2d_convert; + ctx->parent.del = async_color_convert_dma2d_del; + + *ret_hdl = &ctx->parent; + return ESP_OK; + +err: + async_color_convert_dma2d_destroy(ctx); + return ret; +} diff --git a/components/esp_driver_dma/src/esp_async_color_convert.c b/components/esp_driver_dma/src/esp_async_color_convert.c new file mode 100644 index 00000000000..383e5fb5ad6 --- /dev/null +++ b/components/esp_driver_dma/src/esp_async_color_convert.c @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" +#include "esp_check.h" +#include "esp_async_color_convert.h" +#include "esp_async_color_convert_priv.h" + +ESP_LOG_ATTR_TAG(TAG, "async_color_conv"); + +esp_err_t esp_async_color_convert_uninstall(async_color_convert_handle_t conv_hdl) +{ + ESP_RETURN_ON_FALSE(conv_hdl, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return conv_hdl->del(conv_hdl); +} + +esp_err_t esp_async_color_convert(async_color_convert_handle_t conv_hdl, + const async_color_convert_request_t *request, + async_color_convert_isr_cb_t cb_isr, + void *cb_args) +{ + ESP_RETURN_ON_FALSE(conv_hdl && request, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return conv_hdl->convert(conv_hdl, request, cb_isr, cb_args); +} + +typedef struct { + SemaphoreHandle_t done_sem; + StaticSemaphore_t done_sem_buffer; +} color_convert_blocking_context_t; + +static bool color_convert_blocking_cb(async_color_convert_handle_t conv_hdl, + async_color_convert_event_data_t *edata, + void *cb_args) +{ + BaseType_t high_task_woken = pdFALSE; + color_convert_blocking_context_t *ctx = (color_convert_blocking_context_t *)cb_args; + (void)conv_hdl; + (void)edata; + xSemaphoreGiveFromISR(ctx->done_sem, &high_task_woken); + return (high_task_woken == pdTRUE); +} + +esp_err_t esp_color_convert_blocking(async_color_convert_handle_t conv_hdl, + const async_color_convert_request_t *request, + int32_t timeout_ms) +{ + ESP_RETURN_ON_FALSE(conv_hdl && request, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(!xPortInIsrContext(), ESP_ERR_INVALID_STATE, TAG, "called from ISR context is not allowed"); + ESP_RETURN_ON_FALSE(timeout_ms == -1, ESP_ERR_INVALID_ARG, TAG, "only timeout -1 is supported"); + + color_convert_blocking_context_t ctx = {}; + ctx.done_sem = xSemaphoreCreateBinaryStatic(&ctx.done_sem_buffer); + + ESP_RETURN_ON_ERROR(esp_async_color_convert(conv_hdl, request, color_convert_blocking_cb, &ctx), TAG, "fail to start async color conversion"); + // Wait for the conversion to complete + xSemaphoreTake(ctx.done_sem, portMAX_DELAY); + return ESP_OK; +} diff --git a/components/esp_driver_dma/src/esp_async_color_convert_priv.h b/components/esp_driver_dma/src/esp_async_color_convert_priv.h new file mode 100644 index 00000000000..9fccdf930a1 --- /dev/null +++ b/components/esp_driver_dma/src/esp_async_color_convert_priv.h @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_private/dma2d.h" +#include "esp_async_color_convert.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define DEFAULT_COLOR_CONVERT_BACKLOG 8 + +typedef struct async_color_convert_context_t async_color_convert_context_t; + +struct async_color_convert_context_t { + esp_err_t (*convert)(async_color_convert_context_t *ctx, + const async_color_convert_request_t *request, + async_color_convert_isr_cb_t cb_isr, + void *cb_args); + esp_err_t (*del)(async_color_convert_context_t *ctx); +}; + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_driver_dma/test_apps/dma2d/main/CMakeLists.txt b/components/esp_driver_dma/test_apps/dma2d/main/CMakeLists.txt index bd6d24b9d8f..45b05834453 100644 --- a/components/esp_driver_dma/test_apps/dma2d/main/CMakeLists.txt +++ b/components/esp_driver_dma/test_apps/dma2d/main/CMakeLists.txt @@ -1,5 +1,6 @@ set(srcs "test_app_main.c" "test_dma2d.c" + "test_async_color_convert.c" "dma2d_test_utils.c") # In order for the cases defined by `TEST_CASE` to be linked into the final elf, diff --git a/components/esp_driver_dma/test_apps/dma2d/main/test_async_color_convert.c b/components/esp_driver_dma/test_apps/dma2d/main/test_async_color_convert.c new file mode 100644 index 00000000000..01dd14ae605 --- /dev/null +++ b/components/esp_driver_dma/test_apps/dma2d/main/test_async_color_convert.c @@ -0,0 +1,561 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "unity.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "soc/soc_caps.h" +#include "esp_heap_caps.h" +#include "hal/color_types.h" +#include "hal/color_hal.h" +#include "esp_async_color_convert.h" + +typedef struct { + SemaphoreHandle_t sem; + int cb_called; +} async_color_convert_user_ctx_t; + +static void fill_pattern(uint8_t *buf, size_t len, uint8_t seed) +{ + for (size_t i = 0; i < len; i++) { + buf[i] = (uint8_t)(seed + i * 13); + } +} + +static bool test_async_color_convert_cb(async_color_convert_handle_t conv_hdl, + async_color_convert_event_data_t *edata, + void *cb_args) +{ + (void)conv_hdl; + (void)edata; + async_color_convert_user_ctx_t *user_ctx = (async_color_convert_user_ctx_t *)cb_args; + user_ctx->cb_called++; + BaseType_t high_task_wakeup = pdFALSE; + xSemaphoreGiveFromISR(user_ctx->sem, &high_task_wakeup); + return (high_task_wakeup == pdTRUE); +} + +TEST_CASE("async color convert basic callback", "[async_color_convert]") +{ + const uint32_t width = 32; + const uint32_t height = 24; + const uint32_t pixel_num = width * height; + + uint16_t *src565 = heap_caps_aligned_calloc(64, pixel_num, sizeof(uint16_t), + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *dst_bgr24 = heap_caps_aligned_calloc(64, pixel_num, 3, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + TEST_ASSERT_NOT_NULL(src565); + TEST_ASSERT_NOT_NULL(dst_bgr24); + + for (uint32_t i = 0; i < pixel_num; i++) { + src565[i] = (uint16_t)((i * 13) ^ 0x5AA5); + } + + async_color_convert_config_t config = { + .backlog = 2, + .intr_priority = 0, + .dma_burst_size = 16, + }; + async_color_convert_handle_t conv_hdl = NULL; + TEST_ESP_OK(esp_async_color_convert_install_dma2d(&config, &conv_hdl)); + + async_color_convert_request_t req = { + .src_buffer = src565, + .src_stride = width, + .src_height = height, + .src_x = 0, + .src_y = 0, + .dst_buffer = dst_bgr24, + .dst_stride = width, + .dst_height = height, + .dst_x = 0, + .dst_y = 0, + .copy_width = width, + .copy_height = height, + .src_color_format = ESP_COLOR_FOURCC_RGB16, + .dst_color_format = ESP_COLOR_FOURCC_BGR24, + }; + + async_color_convert_user_ctx_t user_ctx = { + .sem = xSemaphoreCreateBinary(), + .cb_called = 0, + }; + TEST_ASSERT_NOT_NULL(user_ctx.sem); + + TEST_ESP_OK(esp_async_color_convert(conv_hdl, &req, test_async_color_convert_cb, &user_ctx)); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(user_ctx.sem, pdMS_TO_TICKS(200))); + TEST_ASSERT_EQUAL(1, user_ctx.cb_called); + + vSemaphoreDelete(user_ctx.sem); + TEST_ESP_OK(esp_async_color_convert_uninstall(conv_hdl)); + + free(src565); + free(dst_bgr24); +} + +TEST_CASE("async color convert roundtrip: RGB16<->BGR24", "[async_color_convert]") +{ + const uint32_t width = 32; + const uint32_t height = 20; + const uint32_t pixel_num = width * height; + + uint16_t *src565 = heap_caps_aligned_calloc(64, pixel_num, sizeof(uint16_t), + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *mid_bgr24 = heap_caps_aligned_calloc(64, pixel_num, 3, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint16_t *dst565 = heap_caps_aligned_calloc(64, pixel_num, sizeof(uint16_t), + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + TEST_ASSERT_NOT_NULL(src565); + TEST_ASSERT_NOT_NULL(mid_bgr24); + TEST_ASSERT_NOT_NULL(dst565); + + for (uint32_t i = 0; i < pixel_num; i++) { + src565[i] = (uint16_t)((i * 37) ^ 0xA55A); + } + + async_color_convert_config_t config = { + .backlog = 4, + .intr_priority = 0, + .dma_burst_size = 32, + }; + async_color_convert_handle_t conv_hdl = NULL; + TEST_ESP_OK(esp_async_color_convert_install_dma2d(&config, &conv_hdl)); + + async_color_convert_request_t req_565_to_bgr24 = { + .src_buffer = src565, + .src_stride = width, + .src_height = height, + .src_x = 0, + .src_y = 0, + .dst_buffer = mid_bgr24, + .dst_stride = width, + .dst_height = height, + .dst_x = 0, + .dst_y = 0, + .copy_width = width, + .copy_height = height, + .src_color_format = ESP_COLOR_FOURCC_RGB16, + .dst_color_format = ESP_COLOR_FOURCC_BGR24, + }; + + async_color_convert_request_t req_bgr24_to_565 = { + .src_buffer = mid_bgr24, + .src_stride = width, + .src_height = height, + .src_x = 0, + .src_y = 0, + .dst_buffer = dst565, + .dst_stride = width, + .dst_height = height, + .dst_x = 0, + .dst_y = 0, + .copy_width = width, + .copy_height = height, + .src_color_format = ESP_COLOR_FOURCC_BGR24, + .dst_color_format = ESP_COLOR_FOURCC_RGB16, + }; + + TEST_ESP_OK(esp_color_convert_blocking(conv_hdl, &req_565_to_bgr24, -1)); + TEST_ESP_OK(esp_color_convert_blocking(conv_hdl, &req_bgr24_to_565, -1)); + + // The final dst565 should be the same as the original src565 after round-trip conversion + TEST_ASSERT_EQUAL_MEMORY(src565, dst565, pixel_num * sizeof(uint16_t)); + + TEST_ESP_OK(esp_async_color_convert_uninstall(conv_hdl)); + + free(src565); + free(mid_bgr24); + free(dst565); +} + +TEST_CASE("async color convert bypasses color convert for 2D copy", "[async_color_convert]") +{ + const uint32_t src_stride = 48; + const uint32_t src_height = 28; + const uint32_t dst_stride = 64; + const uint32_t dst_height = 30; + const uint32_t copy_width = 32; + const uint32_t copy_height = 18; + const uint32_t src_x = 5; + const uint32_t src_y = 4; + const uint32_t dst_x = 7; + const uint32_t dst_y = 6; + const esp_color_fourcc_t fourcc = ESP_COLOR_FOURCC_RGB16; + const size_t bytes_per_pixel = color_hal_pixel_format_fourcc_get_bit_depth(fourcc) / 8; + const size_t src_size = (size_t)src_stride * src_height * bytes_per_pixel; + const size_t dst_size = (size_t)dst_stride * dst_height * bytes_per_pixel; + const size_t row_size = (size_t)copy_width * bytes_per_pixel; + + uint8_t *src = heap_caps_aligned_calloc(64, 1, src_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *dst = heap_caps_aligned_calloc(64, 1, dst_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *expected = heap_caps_aligned_calloc(64, 1, dst_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + TEST_ASSERT_NOT_NULL(src); + TEST_ASSERT_NOT_NULL(dst); + TEST_ASSERT_NOT_NULL(expected); + + fill_pattern(src, src_size, 0x3C); + memset(dst, 0xA5, dst_size); + memset(expected, 0xA5, dst_size); + + for (uint32_t row = 0; row < copy_height; row++) { + const size_t src_offset = ((size_t)(src_y + row) * src_stride + src_x) * bytes_per_pixel; + const size_t dst_offset = ((size_t)(dst_y + row) * dst_stride + dst_x) * bytes_per_pixel; + memcpy(expected + dst_offset, src + src_offset, row_size); + } + + async_color_convert_config_t config = { + .backlog = 2, + .intr_priority = 0, + .dma_burst_size = 16, + }; + async_color_convert_handle_t conv_hdl = NULL; + TEST_ESP_OK(esp_async_color_convert_install_dma2d(&config, &conv_hdl)); + + async_color_convert_request_t req = { + .src_buffer = src, + .src_stride = src_stride, + .src_height = src_height, + .src_x = src_x, + .src_y = src_y, + .dst_buffer = dst, + .dst_stride = dst_stride, + .dst_height = dst_height, + .dst_x = dst_x, + .dst_y = dst_y, + .copy_width = copy_width, + .copy_height = copy_height, + .src_color_format = fourcc, + .dst_color_format = fourcc, + }; + + TEST_ESP_OK(esp_color_convert_blocking(conv_hdl, &req, -1)); + TEST_ASSERT_EQUAL_MEMORY(expected, dst, dst_size); + + TEST_ESP_OK(esp_async_color_convert_uninstall(conv_hdl)); + + free(src); + free(dst); + free(expected); +} + +static uint8_t clamp_to_u8(int value) +{ + if (value < 0) { + return 0; + } + if (value > 255) { + return 255; + } + return (uint8_t)value; +} + +static void uyvy_to_bgr24_reference_pixel(uint8_t y, uint8_t u, uint8_t v, + color_conv_std_rgb_yuv_t color_conv_std, + uint8_t *out_bgr) +{ + static const int bt601[3][4] = { + { 298, 0, 409, -56906 }, + { 298, -100, -208, 34707 }, + { 298, 516, 0, -70836 }, + }; + static const int bt709[3][4] = { + { 298, 0, 459, -63367 }, + { 298, -55, -136, 19681 }, + { 298, 541, 0, -73918 }, + }; + + const int (*coeff)[4] = (color_conv_std == COLOR_CONV_STD_RGB_YUV_BT709) ? bt709 : bt601; + int r = (coeff[0][0] * y + coeff[0][1] * u + coeff[0][2] * v + coeff[0][3] + 128) >> 8; + int g = (coeff[1][0] * y + coeff[1][1] * u + coeff[1][2] * v + coeff[1][3] + 128) >> 8; + int b = (coeff[2][0] * y + coeff[2][1] * u + coeff[2][2] * v + coeff[2][3] + 128) >> 8; + out_bgr[0] = clamp_to_u8(b); + out_bgr[1] = clamp_to_u8(g); + out_bgr[2] = clamp_to_u8(r); +} + +static void uyvy_to_bgr24_reference_image(const uint8_t *src_uyvy, uint8_t *dst_bgr24, + uint32_t src_stride, uint32_t dst_stride, + uint32_t copy_width, uint32_t copy_height, + color_conv_std_rgb_yuv_t color_conv_std) +{ + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, copy_width % 2, "UYVY width must be even"); + + for (uint32_t y = 0; y < copy_height; y++) { + for (uint32_t x = 0; x < copy_width; x += 2) { + size_t src_idx = ((size_t)y * src_stride + x) * 2; + size_t dst_idx0 = ((size_t)y * dst_stride + x) * 3; + size_t dst_idx1 = ((size_t)y * dst_stride + x + 1) * 3; + uint8_t u = src_uyvy[src_idx + 0]; + uint8_t y0 = src_uyvy[src_idx + 1]; + uint8_t v = src_uyvy[src_idx + 2]; + uint8_t y1 = src_uyvy[src_idx + 3]; + + uyvy_to_bgr24_reference_pixel(y0, u, v, color_conv_std, &dst_bgr24[dst_idx0]); + uyvy_to_bgr24_reference_pixel(y1, u, v, color_conv_std, &dst_bgr24[dst_idx1]); + } + } +} + +TEST_CASE("async color convert swaps RGB24 and BGR24 byte order", "[async_color_convert]") +{ + const uint32_t width = 4; + const uint32_t height = 2; + const size_t pixel_count = width * height; + const size_t buf_size = pixel_count * 3; + static const uint8_t src_rgb24[] = { + 0x10, 0x20, 0x30, 0x7F, 0x80, 0x81, 0xAA, 0x55, 0xFE, 0x01, 0xC0, 0x99, + 0xDE, 0xAD, 0xBE, 0x00, 0x11, 0x22, 0x44, 0x88, 0xCC, 0xF0, 0x0D, 0x42, + }; + static const uint8_t src_bgr24[] = { + 0x30, 0x20, 0x10, 0x81, 0x80, 0x7F, 0xFE, 0x55, 0xAA, 0x99, 0xC0, 0x01, + 0xBE, 0xAD, 0xDE, 0x22, 0x11, 0x00, 0xCC, 0x88, 0x44, 0x42, 0x0D, 0xF0, + }; + + TEST_ASSERT_EQUAL(sizeof(src_rgb24), buf_size); + TEST_ASSERT_EQUAL(sizeof(src_bgr24), buf_size); + + uint8_t *rgb24 = heap_caps_aligned_calloc(64, 1, buf_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *bgr24 = heap_caps_aligned_calloc(64, 1, buf_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *dst_bgr24 = heap_caps_aligned_calloc(64, 1, buf_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *dst_rgb24 = heap_caps_aligned_calloc(64, 1, buf_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + TEST_ASSERT_NOT_NULL(rgb24); + TEST_ASSERT_NOT_NULL(bgr24); + TEST_ASSERT_NOT_NULL(dst_bgr24); + TEST_ASSERT_NOT_NULL(dst_rgb24); + memcpy(rgb24, src_rgb24, buf_size); + memcpy(bgr24, src_bgr24, buf_size); + memset(dst_bgr24, 0xA5, buf_size); + memset(dst_rgb24, 0x5A, buf_size); + + async_color_convert_config_t config = { + .backlog = 1, + .intr_priority = 0, + .dma_burst_size = 16, + }; + async_color_convert_handle_t conv_hdl = NULL; + TEST_ESP_OK(esp_async_color_convert_install_dma2d(&config, &conv_hdl)); + + async_color_convert_request_t req_rgb_to_bgr = { + .src_buffer = rgb24, + .src_stride = width, + .src_height = height, + .src_x = 0, + .src_y = 0, + .dst_buffer = dst_bgr24, + .dst_stride = width, + .dst_height = height, + .dst_x = 0, + .dst_y = 0, + .copy_width = width, + .copy_height = height, + .src_color_format = ESP_COLOR_FOURCC_RGB24, + .dst_color_format = ESP_COLOR_FOURCC_BGR24, + }; + async_color_convert_request_t req_bgr_to_rgb = { + .src_buffer = bgr24, + .src_stride = width, + .src_height = height, + .src_x = 0, + .src_y = 0, + .dst_buffer = dst_rgb24, + .dst_stride = width, + .dst_height = height, + .dst_x = 0, + .dst_y = 0, + .copy_width = width, + .copy_height = height, + .src_color_format = ESP_COLOR_FOURCC_BGR24, + .dst_color_format = ESP_COLOR_FOURCC_RGB24, + }; + + TEST_ESP_OK(esp_color_convert_blocking(conv_hdl, &req_rgb_to_bgr, -1)); + TEST_ASSERT_EQUAL_MEMORY(src_bgr24, dst_bgr24, buf_size); + + TEST_ESP_OK(esp_color_convert_blocking(conv_hdl, &req_bgr_to_rgb, -1)); + TEST_ASSERT_EQUAL_MEMORY(src_rgb24, dst_rgb24, buf_size); + + TEST_ESP_OK(esp_async_color_convert_uninstall(conv_hdl)); + free(rgb24); + free(bgr24); + free(dst_bgr24); + free(dst_rgb24); +} + +// Verifies the scramble route and BGR24/RGB24->UYVY conversion compose correctly. +TEST_CASE("async color convert RGB24 and BGR24 inputs produce identical UYVY output", "[async_color_convert]") +{ + const uint32_t width = 4; + const uint32_t height = 2; + const size_t pixel_count = width * height; + const size_t rgb_size = pixel_count * 3; + const size_t uyvy_size = pixel_count * 2; + static const uint8_t src_rgb24[] = { + 0x10, 0x20, 0x30, 0x7F, 0x80, 0x81, 0xAA, 0x55, 0xFE, 0x01, 0xC0, 0x99, + 0xDE, 0xAD, 0xBE, 0x00, 0x11, 0x22, 0x44, 0x88, 0xCC, 0xF0, 0x0D, 0x42, + }; + static const uint8_t src_bgr24[] = { + 0x30, 0x20, 0x10, 0x81, 0x80, 0x7F, 0xFE, 0x55, 0xAA, 0x99, 0xC0, 0x01, + 0xBE, 0xAD, 0xDE, 0x22, 0x11, 0x00, 0xCC, 0x88, 0x44, 0x42, 0x0D, 0xF0, + }; + const color_conv_std_rgb_yuv_t conv_std = COLOR_CONV_STD_RGB_YUV_BT601; + + TEST_ASSERT_EQUAL(sizeof(src_rgb24), rgb_size); + TEST_ASSERT_EQUAL(sizeof(src_bgr24), rgb_size); + + uint8_t *rgb24 = heap_caps_aligned_calloc(64, 1, rgb_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *bgr24 = heap_caps_aligned_calloc(64, 1, rgb_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *dst_from_rgb24 = heap_caps_aligned_calloc(64, 1, uyvy_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *dst_from_bgr24 = heap_caps_aligned_calloc(64, 1, uyvy_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + TEST_ASSERT_NOT_NULL(rgb24); + TEST_ASSERT_NOT_NULL(bgr24); + TEST_ASSERT_NOT_NULL(dst_from_rgb24); + TEST_ASSERT_NOT_NULL(dst_from_bgr24); + memcpy(rgb24, src_rgb24, rgb_size); + memcpy(bgr24, src_bgr24, rgb_size); + + async_color_convert_config_t config = { + .backlog = 2, + .intr_priority = 0, + .dma_burst_size = 16, + }; + async_color_convert_handle_t conv_hdl = NULL; + TEST_ESP_OK(esp_async_color_convert_install_dma2d(&config, &conv_hdl)); + + memset(dst_from_rgb24, 0xA5, uyvy_size); + memset(dst_from_bgr24, 0x5A, uyvy_size); + + async_color_convert_request_t req_rgb24_to_uyvy = { + .src_buffer = rgb24, + .src_stride = width, + .src_height = height, + .src_x = 0, + .src_y = 0, + .dst_buffer = dst_from_rgb24, + .dst_stride = width, + .dst_height = height, + .dst_x = 0, + .dst_y = 0, + .copy_width = width, + .copy_height = height, + .src_color_format = ESP_COLOR_FOURCC_RGB24, + .dst_color_format = ESP_COLOR_FOURCC_UYVY, + .color_conv_std = conv_std, + }; + async_color_convert_request_t req_bgr24_to_uyvy = { + .src_buffer = bgr24, + .src_stride = width, + .src_height = height, + .src_x = 0, + .src_y = 0, + .dst_buffer = dst_from_bgr24, + .dst_stride = width, + .dst_height = height, + .dst_x = 0, + .dst_y = 0, + .copy_width = width, + .copy_height = height, + .src_color_format = ESP_COLOR_FOURCC_BGR24, + .dst_color_format = ESP_COLOR_FOURCC_UYVY, + .color_conv_std = conv_std, + }; + + TEST_ESP_OK(esp_color_convert_blocking(conv_hdl, &req_rgb24_to_uyvy, -1)); + TEST_ESP_OK(esp_color_convert_blocking(conv_hdl, &req_bgr24_to_uyvy, -1)); + TEST_ASSERT_EQUAL_MEMORY(dst_from_bgr24, dst_from_rgb24, uyvy_size); + + TEST_ESP_OK(esp_async_color_convert_uninstall(conv_hdl)); + free(rgb24); + free(bgr24); + free(dst_from_rgb24); + free(dst_from_bgr24); +} + +TEST_CASE("async color convert UYVY->BGR24 matches reference", "[async_color_convert]") +{ + const uint32_t src_stride = 32; + const uint32_t dst_stride = 64; + const uint32_t height = 2; + const uint32_t copy_width = 6; + const uint32_t copy_height = 2; + const size_t src_size = src_stride * height * 2; + const size_t dst_size = dst_stride * height * 3; + static const uint8_t sample_uyvy[] = { + 128, 16, 128, 235, 90, 81, 240, 145, 240, 200, 16, 54, + 54, 32, 200, 210, 180, 100, 90, 180, 16, 235, 240, 16, + }; + TEST_ASSERT_EQUAL(sizeof(sample_uyvy), copy_width * copy_height * 2); + + uint8_t *src = heap_caps_aligned_calloc(64, 1, src_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *dst = heap_caps_aligned_calloc(64, 1, dst_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + uint8_t *expected = heap_caps_aligned_calloc(64, 1, dst_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + TEST_ASSERT_NOT_NULL(src); + TEST_ASSERT_NOT_NULL(dst); + TEST_ASSERT_NOT_NULL(expected); + for (uint32_t row = 0; row < copy_height; row++) { + memcpy(src + row * src_stride * 2, sample_uyvy + row * copy_width * 2, copy_width * 2); + } + + async_color_convert_config_t config = { + .backlog = 2, + .intr_priority = 0, + .dma_burst_size = 16, + }; + async_color_convert_handle_t conv_hdl = NULL; + TEST_ESP_OK(esp_async_color_convert_install_dma2d(&config, &conv_hdl)); + + const color_conv_std_rgb_yuv_t conv_stds[] = { + COLOR_CONV_STD_RGB_YUV_BT601, + COLOR_CONV_STD_RGB_YUV_BT709, + }; + for (size_t i = 0; i < sizeof(conv_stds) / sizeof(conv_stds[0]); i++) { + memset(dst, 0xA5, dst_size); + memset(expected, 0xA5, dst_size); + uyvy_to_bgr24_reference_image(src, expected, src_stride, dst_stride, copy_width, copy_height, conv_stds[i]); + + async_color_convert_request_t req = { + .src_buffer = src, + .src_stride = src_stride, + .src_height = height, + .src_x = 0, + .src_y = 0, + .dst_buffer = dst, + .dst_stride = dst_stride, + .dst_height = height, + .dst_x = 0, + .dst_y = 0, + .copy_width = copy_width, + .copy_height = copy_height, + .src_color_format = ESP_COLOR_FOURCC_UYVY, + .dst_color_format = ESP_COLOR_FOURCC_BGR24, + .color_conv_std = conv_stds[i], + }; + + TEST_ESP_OK(esp_color_convert_blocking(conv_hdl, &req, -1)); + TEST_ASSERT_EQUAL_MEMORY(expected, dst, dst_size); + } + + TEST_ESP_OK(esp_async_color_convert_uninstall(conv_hdl)); + free(src); + free(dst); + free(expected); +} diff --git a/components/esp_hal_dma/esp32p4/include/hal/dma2d_ll.h b/components/esp_hal_dma/esp32p4/include/hal/dma2d_ll.h index a524173ff42..4eb70c39136 100644 --- a/components/esp_hal_dma/esp32p4/include/hal/dma2d_ll.h +++ b/components/esp_hal_dma/esp32p4/include/hal/dma2d_ll.h @@ -88,6 +88,7 @@ #define DMA2D_LL_CHANNEL_PERIPH_SEL_BIT_WIDTH (3) #define DMA2D_LL_DESC_ALIGNMENT 8 // Descriptor must be aligned to 8 bytes +#define DMA2D_LL_DESC_2D_FIELD_MAX 0x3FFFU // 2D descriptor width/height/coordinate fields are 14-bit #ifdef __cplusplus extern "C" { @@ -1053,7 +1054,7 @@ static inline void dma2d_ll_tx_configure_color_space_conv(dma2d_dev_t *dev, uint input_sel = 7; break; case DMA2D_CSC_TX_SCRAMBLE: - input_sel = 2; // Or 3 + input_sel = 3; // Other 3-byte/pixel input path proc_en = false; output_sel = 2; break; diff --git a/components/esp_hal_dma/esp32s31/include/hal/dma2d_ll.h b/components/esp_hal_dma/esp32s31/include/hal/dma2d_ll.h index 04acb948bb2..9e8921d447f 100644 --- a/components/esp_hal_dma/esp32s31/include/hal/dma2d_ll.h +++ b/components/esp_hal_dma/esp32s31/include/hal/dma2d_ll.h @@ -78,6 +78,7 @@ #define DMA2D_LL_CHANNEL_PERIPH_SEL_BIT_WIDTH (3) #define DMA2D_LL_DESC_ALIGNMENT 8 // Descriptor must be aligned to 8 bytes +#define DMA2D_LL_DESC_2D_FIELD_MAX 0x3FFFU // 2D descriptor width/height/coordinate fields are 14-bit #ifdef __cplusplus extern "C" { @@ -1030,7 +1031,7 @@ static inline void dma2d_ll_tx_configure_color_space_conv(dma2d_dev_t *dev, uint input_sel = 7; break; case DMA2D_CSC_TX_SCRAMBLE: - input_sel = 2; // Or 3 + input_sel = 3; // Other 3-byte/pixel input path proc_en = false; output_sel = 2; break; diff --git a/components/esp_lcd/CMakeLists.txt b/components/esp_lcd/CMakeLists.txt index 795856ff2fc..f5044ee4a5e 100644 --- a/components/esp_lcd/CMakeLists.txt +++ b/components/esp_lcd/CMakeLists.txt @@ -13,10 +13,6 @@ set(includes "include" "interface") set(priv_requires "esp_mm" "esp_psram" "esp_pm" "esp_driver_i2s" "esp_driver_dma") set(public_requires "esp_driver_gpio" "esp_driver_i2c" "esp_driver_spi" "esp_driver_parlio" "esp_hal_lcd") -if(CONFIG_SOC_DMA2D_SUPPORTED) - list(APPEND srcs "src/esp_async_fbcpy.c") -endif() - if(CONFIG_SOC_I2C_SUPPORTED) list(APPEND srcs "i2c/esp_lcd_panel_io_i2c.c") endif() diff --git a/components/esp_lcd/dsi/esp_lcd_panel_dpi.c b/components/esp_lcd/dsi/esp_lcd_panel_dpi.c index 246f1509ad2..14ec95ec3b3 100644 --- a/components/esp_lcd/dsi/esp_lcd_panel_dpi.c +++ b/components/esp_lcd/dsi/esp_lcd_panel_dpi.c @@ -6,11 +6,11 @@ #include #include "esp_lcd_panel_interface.h" #include "esp_lcd_mipi_dsi.h" +#include "esp_async_color_convert.h" #include "esp_intr_alloc.h" #include "esp_clk_tree.h" #include "esp_cache.h" #include "mipi_dsi_priv.h" -#include "esp_async_fbcpy.h" #include "esp_memory_utils.h" #include "esp_private/dw_gdma.h" #include "hal/color_hal.h" @@ -45,8 +45,7 @@ struct esp_lcd_dpi_panel_t { esp_lcd_panel_draw_bitmap_hook_t draw_bitmap_hook; // Draw bitmap hook function void* hook_ctx; // Hook context bool (*on_hook_end)(esp_lcd_panel_handle_t panel); // Callback to be invoked when the draw bitmap hook completes its operation - esp_async_fbcpy_handle_t fbcpy_handle; // Use DMA2D to do frame buffer copy (only when using DMA2D draw bitmap hook) - SemaphoreHandle_t draw_sem; // A semaphore used to synchronize the draw operations when DMA2D is used + async_color_convert_handle_t fbcpy_handle; // Async color convert handle used for same-format DMA2D frame buffer copy #if CONFIG_PM_ENABLE esp_pm_lock_handle_t pm_lock; // Power management lock @@ -66,24 +65,18 @@ static bool dpi_panel_draw_bitmap_hook_end(esp_lcd_panel_t *panel) return false; } -static bool async_fbcpy_done_cb(esp_async_fbcpy_handle_t mcp, esp_async_fbcpy_event_data_t *event, void *cb_args) +static bool async_fbcpy_done_cb(async_color_convert_handle_t conv_hdl, async_color_convert_event_data_t *event, void *cb_args) { bool need_yield = false; esp_lcd_dpi_panel_t *dpi_panel = (esp_lcd_dpi_panel_t *)cb_args; - - // release the draw semaphore first - BaseType_t task_woken = pdFALSE; - xSemaphoreGiveFromISR(dpi_panel->draw_sem, &task_woken); - if (task_woken == pdTRUE) { - need_yield = true; - } + (void)conv_hdl; + (void)event; if (dpi_panel->on_hook_end) { if (dpi_panel->on_hook_end(&dpi_panel->base)) { need_yield = true; } } - return need_yield; } @@ -489,28 +482,31 @@ static esp_err_t dpi_panel_draw_bitmap_dma2d_hook(esp_lcd_panel_t *panel, const { ESP_LOGV(TAG, "copy draw buffer by DMA2D"); esp_lcd_dpi_panel_t *dpi_panel = __containerof(panel, esp_lcd_dpi_panel_t, base); - // ensure the previous draw operation is finished - ESP_RETURN_ON_FALSE(xSemaphoreTake(dpi_panel->draw_sem, 0) == pdTRUE, ESP_ERR_INVALID_STATE, - TAG, "previous draw operation is not finished"); + (void)hook_ctx; - esp_async_fbcpy_trans_desc_t fbcpy_trans_config = { + async_color_convert_request_t fbcpy_trans_config = { .src_buffer = hook_data->src_data, .dst_buffer = hook_data->dst_data, - .src_buffer_size_x = hook_data->src_x_size, - .src_buffer_size_y = hook_data->src_y_size, - .dst_buffer_size_x = hook_data->dst_x_size, - .dst_buffer_size_y = hook_data->dst_y_size, - .src_offset_x = hook_data->src_x_start, - .src_offset_y = hook_data->src_y_start, - .dst_offset_x = hook_data->dst_x_start, - .dst_offset_y = hook_data->dst_y_start, - .copy_size_x = hook_data->src_x_end - hook_data->src_x_start, - .copy_size_y = hook_data->src_y_end - hook_data->src_y_start, - .pixel_format_fourcc_id = dpi_panel->in_color_format, + .src_stride = hook_data->src_x_size, + .src_height = hook_data->src_y_size, + .dst_stride = hook_data->dst_x_size, + .dst_height = hook_data->dst_y_size, + .src_x = hook_data->src_x_start, + .src_y = hook_data->src_y_start, + .dst_x = hook_data->dst_x_start, + .dst_y = hook_data->dst_y_start, + .copy_width = hook_data->src_x_end - hook_data->src_x_start, + .copy_height = hook_data->src_y_end - hook_data->src_y_start, + // For this DMA2D hook we only do window copy from draw buffer to frame buffer. + // Source and destination color formats are intentionally set to the same value to disable CSC. + .src_color_format = dpi_panel->in_color_format, + .dst_color_format = dpi_panel->in_color_format, }; - // save the on_hook_end callback, and invoke it when the async memcpy is done + // The async color convert backend owns source/destination cache sync for the + // DMA2D copy path, so the LCD driver should not perform extra cache sync here. + // Save the completion callback and invoke it when the async frame buffer copy finishes. dpi_panel->on_hook_end = hook_data->on_hook_end; - ESP_RETURN_ON_ERROR(esp_async_fbcpy(dpi_panel->fbcpy_handle, &fbcpy_trans_config, async_fbcpy_done_cb, dpi_panel), TAG, "async memcpy failed"); + ESP_RETURN_ON_ERROR(esp_async_color_convert(dpi_panel->fbcpy_handle, &fbcpy_trans_config, async_fbcpy_done_cb, dpi_panel), TAG, "async frame buffer copy failed"); return ESP_OK; } @@ -535,13 +531,13 @@ esp_err_t esp_lcd_dpi_panel_enable_dma2d(esp_lcd_panel_handle_t panel) // Check if built-in DMA2D draw bitmap hook is registered ESP_RETURN_ON_FALSE(!dpi_panel->fbcpy_handle, ESP_ERR_INVALID_STATE, TAG, "draw bitmap DMA2D hook is already registered"); - // Initialize DMA2D resources - esp_async_fbcpy_config_t fbcpy_config = {}; - ESP_RETURN_ON_ERROR(esp_async_fbcpy_install(&fbcpy_config, &dpi_panel->fbcpy_handle), TAG, "install async memcpy 2d failed"); + // Initialize the async color convert backend used by the built-in DMA2D copy hook. + // Use its default backlog to queue multiple frame buffer copy requests. + async_color_convert_config_t fbcpy_config = { + .dma_burst_size = 128, // for better performance + }; + ESP_RETURN_ON_ERROR(esp_async_color_convert_install_dma2d(&fbcpy_config, &dpi_panel->fbcpy_handle), TAG, "install async frame buffer copy backend failed"); - dpi_panel->draw_sem = xSemaphoreCreateBinaryWithCaps(DSI_MEM_ALLOC_CAPS); - ESP_GOTO_ON_FALSE(dpi_panel->draw_sem, ESP_ERR_NO_MEM, err, TAG, "no memory for draw semaphore"); - xSemaphoreGive(dpi_panel->draw_sem); // Register the DMA2D draw bitmap hook esp_lcd_panel_hooks_t hooks = { .draw_bitmap_hook = dpi_panel_draw_bitmap_dma2d_hook, @@ -552,13 +548,10 @@ esp_err_t esp_lcd_dpi_panel_enable_dma2d(esp_lcd_panel_handle_t panel) err: if (dpi_panel->fbcpy_handle) { - esp_async_fbcpy_uninstall(dpi_panel->fbcpy_handle); + esp_async_color_convert_uninstall(dpi_panel->fbcpy_handle); dpi_panel->fbcpy_handle = NULL; } - if (dpi_panel->draw_sem) { - vSemaphoreDeleteWithCaps(dpi_panel->draw_sem); - dpi_panel->draw_sem = NULL; - } + dpi_panel->on_hook_end = NULL; return ret; } @@ -575,13 +568,10 @@ esp_err_t esp_lcd_dpi_panel_disable_dma2d(esp_lcd_panel_handle_t panel) }; ESP_RETURN_ON_ERROR(esp_lcd_dpi_panel_register_hooks(panel, &hooks, NULL), TAG, "unregister DMA2D draw bitmap hook failed"); if (dpi_panel->fbcpy_handle) { - ESP_RETURN_ON_ERROR(esp_async_fbcpy_uninstall(dpi_panel->fbcpy_handle), TAG, "uninstall DMA2D failed"); + ESP_RETURN_ON_ERROR(esp_async_color_convert_uninstall(dpi_panel->fbcpy_handle), TAG, "uninstall DMA2D failed"); dpi_panel->fbcpy_handle = NULL; } - if (dpi_panel->draw_sem) { - vSemaphoreDeleteWithCaps(dpi_panel->draw_sem); - dpi_panel->draw_sem = NULL; - } + dpi_panel->on_hook_end = NULL; return ESP_OK; } @@ -647,7 +637,9 @@ static esp_err_t dpi_panel_draw_bitmap_2d(esp_lcd_panel_t *panel, int x_start, i } } else if (dpi_panel->draw_bitmap_hook) { // copy using draw bitmap hook ESP_LOGV(TAG, "copy draw buffer by draw bitmap hook"); - // Note, whether the previous draw operation is finished should be ensured by the hook + // Note, whether the previous draw operation is finished should be ensured by the hook. + // For the built-in DMA2D hook, cache maintenance of the source and destination + // buffers is handled inside the async color convert driver. esp_lcd_draw_bitmap_hook_data_t hook_data = { .dst_data = frame_buffer, diff --git a/components/esp_lcd/priv_include/esp_async_fbcpy.h b/components/esp_lcd/priv_include/esp_async_fbcpy.h deleted file mode 100644 index 481b3aed2b7..00000000000 --- a/components/esp_lcd/priv_include/esp_async_fbcpy.h +++ /dev/null @@ -1,90 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include "esp_err.h" -#include "hal/color_types.h" - -/** - * @brief Async FrameBuffer copy context - */ -typedef struct esp_async_fbcpy_context_t *esp_async_fbcpy_handle_t; - -/** - * @brief Async FrameBuffer copy configuration - */ -typedef struct { -} esp_async_fbcpy_config_t; - -/** - * @brief Install Async FrameBuffer copy driver - * - * @param config Async FrameBuffer copy configuration - * @param mcp Returned Async FrameBuffer copy handle - * @return - * - ESP_OK: Install Async FrameBuffer copy driver successfully - * - ESP_ERR_INVALID_ARG: Install Async FrameBuffer copy driver failed because of invalid argument - * - ESP_ERR_NO_MEM: Install Async FrameBuffer copy driver failed because of out of memory - * - ESP_FAIL: Install Async FrameBuffer copy driver failed because of other error - */ -esp_err_t esp_async_fbcpy_install(const esp_async_fbcpy_config_t *config, esp_async_fbcpy_handle_t *mcp); - -/** - * @brief Uninstall Async FrameBuffer copy driver - * - * @param mcp Async FrameBuffer copy handle - * @return - * - ESP_OK: Uninstall Async FrameBuffer copy driver successfully - * - ESP_ERR_INVALID_ARG: Uninstall Async FrameBuffer copy driver failed because of invalid argument - * - ESP_FAIL: Uninstall Async FrameBuffer copy driver failed because of other error - */ -esp_err_t esp_async_fbcpy_uninstall(esp_async_fbcpy_handle_t mcp); - -/** - * @brief Async FrameBuffer copy transaction descriptor - */ -typedef struct { - const void *src_buffer; /*!< Source buffer */ - void *dst_buffer; /*!< Destination buffer */ - size_t src_buffer_size_x; /*!< Source buffer size in x direction, size count in the number of pixels */ - size_t src_buffer_size_y; /*!< Source buffer size in y direction, size count in the number of pixels */ - size_t dst_buffer_size_x; /*!< Destination buffer size in x direction, size count in the number of pixels */ - size_t dst_buffer_size_y; /*!< Destination buffer size in y direction, size count in the number of pixels */ - size_t src_offset_x; /*!< Copy action will start from this offset in source buffer in the x direction, offset count in the number of pixels */ - size_t src_offset_y; /*!< Copy action will start from this offset in source buffer in the y direction, offset count in the number of pixels */ - size_t dst_offset_x; /*!< Copy action will start from this offset in destination buffer in the x direction, offset count in the number of pixels */ - size_t dst_offset_y; /*!< Copy action will start from this offset in destination buffer in the y direction, offset count in the number of pixels */ - size_t copy_size_x; /*!< Copy size in the x direction, size count in the number of pixels */ - size_t copy_size_y; /*!< Copy size in the y direction, size count in the number of pixels */ - esp_color_fourcc_t pixel_format_fourcc_id; /*!< Pixel format unique ID */ -} esp_async_fbcpy_trans_desc_t; - -/** - * @brief Async FrameBuffer copy event data - */ -typedef struct { -} esp_async_fbcpy_event_data_t; - -/** - * @brief Async FrameBuffer copy event callback prototype - */ -typedef bool (*esp_async_fbcpy_event_callback_t)(esp_async_fbcpy_handle_t mcp, esp_async_fbcpy_event_data_t *event_data, void *cb_args); - -/** - * @brief Start Async FrameBuffer copy transaction - * - * @param mcp Async FrameBuffer copy handle - * @param transaction Async FrameBuffer copy transaction descriptor - * @param memcpy_done_cb Callback function that will be invoked when Async FrameBuffer copy transaction finishes - * @param cb_args User data - * @return - * - ESP_OK: Start Async FrameBuffer copy transaction successfully - * - ESP_ERR_INVALID_ARG: Start Async FrameBuffer copy transaction failed because of invalid argument - * - ESP_FAIL: Start Async FrameBuffer copy transaction failed because of other error - */ -esp_err_t esp_async_fbcpy(esp_async_fbcpy_handle_t mcp, esp_async_fbcpy_trans_desc_t* transaction, - esp_async_fbcpy_event_callback_t memcpy_done_cb, void *cb_args); diff --git a/components/esp_lcd/src/esp_async_fbcpy.c b/components/esp_lcd/src/esp_async_fbcpy.c deleted file mode 100644 index 448e0d996de..00000000000 --- a/components/esp_lcd/src/esp_async_fbcpy.c +++ /dev/null @@ -1,219 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_check.h" -#include "esp_cache.h" -#include "esp_heap_caps.h" -#include "soc/dma2d_channel.h" -#include "hal/cache_hal.h" -#include "hal/cache_ll.h" -#include "hal/dma2d_ll.h" -#include "esp_private/dma2d.h" -#include "esp_async_fbcpy.h" - -#define ALIGN_UP(num, align) (((num) + ((align) - 1)) & ~((align) - 1)) - -static const char *TAG = "async_fbcpy"; - -typedef struct esp_async_fbcpy_context_t { - dma2d_pool_handle_t client; // DMA2D client - dma2d_descriptor_t* tx_desc; // DMA2D TX descriptor - dma2d_descriptor_t* rx_desc; // DMA2D RX descriptor - dma2d_trans_t* trans_desc; // DMA2D transaction descriptor - size_t dma_desc_size; // DMA2D descriptor size - esp_async_fbcpy_event_callback_t memcpy_done_cb; // memory copy done callback - void *cb_args; // callback arguments -} esp_async_fbcpy_context_t; - -static esp_err_t async_fbcpy_del_context(esp_async_fbcpy_context_t* ctx) -{ - if (ctx->tx_desc) { - free(ctx->tx_desc); - } - if (ctx->rx_desc) { - free(ctx->rx_desc); - } - if (ctx->trans_desc) { - free(ctx->trans_desc); - } - if (ctx->client) { - dma2d_release_pool(ctx->client); - } - free(ctx); - return ESP_OK; -} - -esp_err_t esp_async_fbcpy_install(const esp_async_fbcpy_config_t *config, esp_async_fbcpy_handle_t *mcp) -{ - esp_err_t ret = ESP_OK; - esp_async_fbcpy_context_t *ctx = NULL; - dma2d_trans_t* trans_desc = NULL; - dma2d_descriptor_t* dma_tx_desc = NULL; - dma2d_descriptor_t* dma_rx_desc = NULL; - dma2d_pool_handle_t dma2d_client = NULL; - - ESP_RETURN_ON_FALSE(config && mcp, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); - // allocate context memory - ctx = heap_caps_calloc(1, sizeof(esp_async_fbcpy_context_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - ESP_GOTO_ON_FALSE(ctx, ESP_ERR_NO_MEM, err, TAG, "no mem for esp_async_fbcpy_context_t"); - // according to the dma2d design, the transaction descriptor is also saved by the user - trans_desc = heap_caps_calloc(1, dma2d_get_trans_elm_size(), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - ESP_GOTO_ON_FALSE(trans_desc, ESP_ERR_NO_MEM, err, TAG, "no mem for trans_desc"); - ctx->trans_desc = trans_desc; - // allocate memory for DMA descriptor, the descriptor must be allocated from the internal memory, and alignment to the cache line size - uint32_t data_cache_line_size = cache_hal_get_cache_line_size(CACHE_LL_LEVEL_INT_MEM, CACHE_TYPE_DATA); - size_t alignment = MAX(DMA2D_LL_DESC_ALIGNMENT, data_cache_line_size); - size_t dma_desc_mem_size = ALIGN_UP(sizeof(dma2d_descriptor_align8_t), alignment); - dma_tx_desc = heap_caps_aligned_calloc(alignment, 1, dma_desc_mem_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - dma_rx_desc = heap_caps_aligned_calloc(alignment, 1, dma_desc_mem_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - ESP_GOTO_ON_FALSE(dma_tx_desc && dma_rx_desc, ESP_ERR_NO_MEM, err, TAG, "no memory for DMA2D descriptors"); - ctx->tx_desc = dma_tx_desc; - ctx->rx_desc = dma_rx_desc; - ctx->dma_desc_size = dma_desc_mem_size; - - // initialize DMA2D client - dma2d_pool_config_t dma2d_client_config = {}; // all follow default configurations - ESP_GOTO_ON_ERROR(dma2d_acquire_pool(&dma2d_client_config, &dma2d_client), err, TAG, "create DMA2D client failed"); - ctx->client = dma2d_client; - - *mcp = ctx; - return ESP_OK; - -err: - if (ctx) { - async_fbcpy_del_context(ctx); - } - return ret; -} - -esp_err_t esp_async_fbcpy_uninstall(esp_async_fbcpy_handle_t mcp) -{ - ESP_RETURN_ON_FALSE(mcp, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); - return async_fbcpy_del_context(mcp); -} - -static void async_memcpy_setup_dma2d_descriptor(esp_async_fbcpy_context_t* mcp_ctx, esp_async_fbcpy_trans_desc_t* transaction) -{ - dma2d_descriptor_t* tx_desc = mcp_ctx->tx_desc; - dma2d_descriptor_t* rx_desc = mcp_ctx->rx_desc; - size_t dma_desc_size = mcp_ctx->dma_desc_size; - uint8_t dma2d_pbyte = dma2d_desc_pixel_format_to_pbyte_value(transaction->pixel_format_fourcc_id); - - tx_desc->buffer = (void*)transaction->src_buffer; - tx_desc->next = NULL; - tx_desc->dma2d_en = 1; - tx_desc->suc_eof = 1; - tx_desc->ha_length = transaction->src_buffer_size_x; - tx_desc->va_size = transaction->src_buffer_size_y; - tx_desc->hb_length = transaction->copy_size_x; - tx_desc->vb_size = transaction->copy_size_y; - tx_desc->x = transaction->src_offset_x; - tx_desc->y = transaction->src_offset_y; - tx_desc->pbyte = dma2d_pbyte; - tx_desc->mode = DMA2D_DESCRIPTOR_BLOCK_RW_MODE_SINGLE; - tx_desc->owner = DMA2D_DESCRIPTOR_BUFFER_OWNER_DMA; - - rx_desc->buffer = transaction->dst_buffer; - rx_desc->next = NULL; - rx_desc->dma2d_en = 1; - rx_desc->suc_eof = 1; - rx_desc->ha_length = transaction->dst_buffer_size_x; - rx_desc->va_size = transaction->dst_buffer_size_y; - rx_desc->hb_length = transaction->copy_size_x; - rx_desc->vb_size = transaction->copy_size_y; - rx_desc->x = transaction->dst_offset_x; - rx_desc->y = transaction->dst_offset_y; - rx_desc->pbyte = dma2d_pbyte; - rx_desc->mode = DMA2D_DESCRIPTOR_BLOCK_RW_MODE_SINGLE; - rx_desc->owner = DMA2D_DESCRIPTOR_BUFFER_OWNER_DMA; - - esp_cache_msync(tx_desc, dma_desc_size, ESP_CACHE_MSYNC_FLAG_DIR_C2M | ESP_CACHE_MSYNC_FLAG_INVALIDATE); - esp_cache_msync(rx_desc, dma_desc_size, ESP_CACHE_MSYNC_FLAG_DIR_C2M | ESP_CACHE_MSYNC_FLAG_INVALIDATE); -} - -static bool dma2d_memcpy_done_cb(dma2d_channel_handle_t dma2d_chan, dma2d_event_data_t *event_data, void *user_data) -{ - bool need_yield = false; - esp_async_fbcpy_context_t* mcp = (esp_async_fbcpy_context_t*)user_data; - - if (mcp->memcpy_done_cb) { - need_yield = mcp->memcpy_done_cb(mcp, NULL, mcp->cb_args); - } - - return need_yield; -} - -static bool dma2d_job_picked_cb(uint32_t num_chans, const dma2d_trans_channel_info_t *dma2d_chans, void *user_data) -{ - esp_async_fbcpy_context_t* mcp = (esp_async_fbcpy_context_t*)user_data; - dma2d_channel_handle_t tx_chan = NULL; - dma2d_channel_handle_t rx_chan = NULL; - for (uint32_t i = 0; i < num_chans; i++) { - if (dma2d_chans[i].dir == DMA2D_CHANNEL_DIRECTION_TX) { - tx_chan = dma2d_chans[i].chan; - } - if (dma2d_chans[i].dir == DMA2D_CHANNEL_DIRECTION_RX) { - rx_chan = dma2d_chans[i].chan; - } - } - dma2d_trigger_t trig_periph = { - .periph = DMA2D_TRIG_PERIPH_M2M, - .periph_sel_id = SOC_DMA2D_TRIG_PERIPH_M2M_TX, - }; - dma2d_connect(tx_chan, &trig_periph); - trig_periph.periph_sel_id = SOC_DMA2D_TRIG_PERIPH_M2M_RX; - dma2d_connect(rx_chan, &trig_periph); - - dma2d_rx_event_callbacks_t dma_cbs = { - .on_recv_eof = dma2d_memcpy_done_cb, - }; - dma2d_register_rx_event_callbacks(rx_chan, &dma_cbs, mcp); - - // 2D-DMA channel data burst length is set to the maximum burst length by default, which meets the encryption alignment restriction - // so even if flash encryption is enabled, it can work properly - - dma2d_set_desc_addr(tx_chan, (intptr_t)(mcp->tx_desc)); - dma2d_set_desc_addr(rx_chan, (intptr_t)(mcp->rx_desc)); - - dma2d_start(tx_chan); - dma2d_start(rx_chan); - - return false; -} - -esp_err_t esp_async_fbcpy(esp_async_fbcpy_handle_t mcp, esp_async_fbcpy_trans_desc_t* transaction, esp_async_fbcpy_event_callback_t memcpy_done_cb, void *cb_args) -{ - ESP_RETURN_ON_FALSE(mcp && transaction, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); - ESP_RETURN_ON_FALSE(transaction->copy_size_x > 0 && transaction->copy_size_y > 0, ESP_ERR_INVALID_ARG, TAG, "invalid copy size"); - mcp->memcpy_done_cb = memcpy_done_cb; - mcp->cb_args = cb_args; - - // Write back the user's draw buffer only when it is behind cache, so that the DMA can see the correct data. - // Note, the user src buffer may not be contiguous, write back from the head to the tail anyways. - size_t bits_per_pixel = color_hal_pixel_format_fourcc_get_bit_depth(transaction->pixel_format_fourcc_id); - size_t copy_head = (transaction->src_offset_x + transaction->src_offset_y * transaction->src_buffer_size_x) * bits_per_pixel / 8; - size_t copy_size = (transaction->copy_size_x + (transaction->copy_size_y - 1) * transaction->src_buffer_size_x) * bits_per_pixel / 8; - void *copy_start = (void *)((const uint8_t *)transaction->src_buffer + copy_head); - size_t cache_line_size = esp_cache_get_line_size_by_addr(copy_start); - if (cache_line_size > 0) { - ESP_RETURN_ON_ERROR(esp_cache_msync(copy_start, copy_size, ESP_CACHE_MSYNC_FLAG_DIR_C2M | ESP_CACHE_MSYNC_FLAG_UNALIGNED), TAG, "writeback draw buffer failed"); - } - - // mount the data to the DMA descriptor - async_memcpy_setup_dma2d_descriptor(mcp, transaction); - - // submit the DMA2D request - static dma2d_trans_config_t dma2d_trans_conf = { - .tx_channel_num = 1, - .rx_channel_num = 1, - .channel_flags = DMA2D_CHANNEL_FUNCTION_FLAG_SIBLING, - .on_job_picked = dma2d_job_picked_cb, - }; - dma2d_trans_conf.user_config = mcp; - ESP_RETURN_ON_ERROR(dma2d_enqueue(mcp->client, &dma2d_trans_conf, mcp->trans_desc), TAG, "DMA2D enqueue failed"); - return ESP_OK; -} diff --git a/docs/conf_common.py b/docs/conf_common.py index 50b9e0d8ff8..e7e236ea5ef 100644 --- a/docs/conf_common.py +++ b/docs/conf_common.py @@ -396,6 +396,7 @@ conditional_include_dict = { 'SOC_HMAC_SUPPORTED': ['api-reference/peripherals/hmac.rst'], 'SOC_GDMA_SUPPORT_CRC': ['api-reference/peripherals/async_crc.rst'], 'SOC_ASYNC_MEMCPY_SUPPORTED': ['api-reference/peripherals/async_memcpy.rst'], + 'SOC_DMA2D_SUPPORTED': ['api-reference/peripherals/async_color_convert.rst'], 'SOC_KEY_MANAGER_SUPPORTED': ['api-reference/peripherals/key_manager.rst'], 'CONFIG_IDF_TARGET_ARCH_XTENSA': XTENSA_DOCS, 'CONFIG_IDF_TARGET_ARCH_RISCV': RISCV_DOCS, diff --git a/docs/doxygen/Doxyfile b/docs/doxygen/Doxyfile index c3b6427519b..4376c85f718 100644 --- a/docs/doxygen/Doxyfile +++ b/docs/doxygen/Doxyfile @@ -126,6 +126,7 @@ INPUT = \ $(PROJECT_PATH)/components/esp_driver_dac/include/driver/dac_oneshot.h \ $(PROJECT_PATH)/components/esp_driver_dac/include/driver/dac_types.h \ $(PROJECT_PATH)/components/esp_driver_dma/include/esp_async_crc.h \ + $(PROJECT_PATH)/components/esp_driver_dma/include/esp_async_color_convert.h \ $(PROJECT_PATH)/components/esp_driver_dma/include/esp_async_memcpy.h \ $(PROJECT_PATH)/components/esp_driver_gpio/include/driver/dedic_gpio.h \ $(PROJECT_PATH)/components/esp_driver_gpio/include/driver/gpio.h \ diff --git a/docs/en/api-reference/peripherals/async_color_convert.rst b/docs/en/api-reference/peripherals/async_color_convert.rst new file mode 100644 index 00000000000..7980144afa6 --- /dev/null +++ b/docs/en/api-reference/peripherals/async_color_convert.rst @@ -0,0 +1,317 @@ +============================= +Asynchronous Color Conversion +============================= + +:link_to_translation:`zh_CN:[中文]` + +This document introduces the Async Color Convert driver in ESP-IDF. The table of contents is as follows: + +.. contents:: + :local: + :depth: 2 + +Overview +======== + +{IDF_TARGET_NAME} provides a DMA2D engine that can offload 2D copy and color conversion work from the CPU. + +This driver is useful when your application needs to: + +- convert an image from one pixel format to another +- copy only a window of a larger image +- queue multiple conversions without doing the work on the CPU +- move between RGB and UYVY formats while selecting the RGB/YUV conversion standard + +The Async Color Convert driver wraps DMA2D request preparation, queueing, and completion handling into a small API that supports both: + +- asynchronous submission with an ISR callback +- a simpler blocking API built on top of the same request path + +Quick Start +=========== + +If you are new to this driver, start with the simplest workflow: + +1. Install the driver +2. Prepare one :cpp:type:`async_color_convert_request_t` +3. Submit the conversion through either the blocking or non-blocking API +4. Consume the converted output buffer after the conversion completes +5. Either submit another request or uninstall the driver when finished + +The typical usage flow is: + +.. mermaid:: + + flowchart TD + install["Install driver
esp_async_color_convert_install_dma2d"] --> request["Prepare request
async_color_convert_request_t"] + request --> blocking["Blocking path
esp_color_convert_blocking"] + request --> nonBlocking["Non-blocking path
esp_async_color_convert"] + nonBlocking --> callback["Wait for callback or task notification"] + blocking --> result["Use converted buffer"] + callback --> result + result --> request + result --> uninstall["Optional cleanup
esp_async_color_convert_uninstall"] + +Scenario 1: Start with One Blocking Conversion +============================================== + +The easiest way to learn the API is to convert one image and wait until the conversion is complete. + +The following flow mirrors the :example:`peripherals/dma/async_color_convert` example. It converts one embedded UYVY422 image into BGR24 and then lets the application consume the converted output: + +.. code:: c + + async_color_convert_handle_t conv_hdl = NULL; // Driver handle returned by the install API + async_color_convert_config_t config = { + .backlog = 1, // One in-flight request is enough for this simple blocking example + .dma_burst_size = 16, // Start with the default burst size used by the example + }; + // Create one Async Color Convert driver instance backed by DMA2D. + ESP_ERROR_CHECK(esp_async_color_convert_install_dma2d(&config, &conv_hdl)); + + async_color_convert_request_t req = { + .src_buffer = sample_96x64_uyvy_yuv_start, // Source image can be in flash or RAM, as long as DMA can access it + .src_stride = 96, // Source image row stride, in pixels + .src_height = 64, // Source image height, in pixels + .src_x = 0, // Start from the left edge of the source image + .src_y = 0, + .dst_buffer = dst_bgr, // Destination buffer in DMA-capable RAM + .dst_stride = 96, // Destination image row stride, in pixels + .dst_height = 64, // Destination image height, in pixels + .dst_x = 0, // Write the converted output from the top-left corner + .dst_y = 0, + .copy_width = 96, // Convert the full image width, in pixels + .copy_height = 64, // Convert the full image height, in pixels + .src_color_format = ESP_COLOR_FOURCC_UYVY, // Source pixels are UYVY422 + .dst_color_format = ESP_COLOR_FOURCC_BGR24, // Destination pixels are BGR24 (used as RGB888 in this driver) + .color_conv_std = COLOR_CONV_STD_RGB_YUV_BT601, // RGB/YUV standard used for this conversion pair + }; + + // Wait until DMA2D finishes the conversion. -1 means wait forever. + ESP_ERROR_CHECK(esp_color_convert_blocking(conv_hdl, &req, -1)); + + // Release the driver after all conversions are done. + ESP_ERROR_CHECK(esp_async_color_convert_uninstall(conv_hdl)); + +This flow introduces the most important ideas: + +- :cpp:func:`esp_async_color_convert_install_dma2d` creates the driver instance +- :cpp:type:`async_color_convert_request_t` describes the source image, destination image, and conversion window +- :cpp:func:`esp_color_convert_blocking` waits until the hardware finishes the conversion +- :cpp:func:`esp_async_color_convert_uninstall` releases the driver resources + +For the blocking API, ``timeout_ms = -1`` means wait forever. Other timeout values are currently unsupported and return ``ESP_ERR_INVALID_ARG``. + +Understanding ``async_color_convert_request_t`` +----------------------------------------------- + +Most application issues come from building the request incorrectly, so it is worth understanding the structure carefully. + +.. important:: + + In :cpp:type:`async_color_convert_request_t`, all geometry fields are measured in **pixels**, not bytes. This includes ``src_stride``, ``src_height``, ``src_x``, ``src_y``, ``dst_stride``, ``dst_height``, ``dst_x``, ``dst_y``, ``copy_width``, and ``copy_height``. + + ``src_stride`` and ``dst_stride`` are row strides, not conversion widths. They describe how many pixels each full image row spans in memory, so they can be larger than ``copy_width`` when converting a window inside a larger image. + +The structure describes two things at the same time: + +- the full source and destination images in memory +- the rectangular window that should be converted + +The key fields are: + +- :cpp:member:`async_color_convert_request_t::src_buffer` + Base address of the source image +- :cpp:member:`async_color_convert_request_t::src_stride` + Source image row stride in pixels +- :cpp:member:`async_color_convert_request_t::src_height` + Source image height in pixels +- :cpp:member:`async_color_convert_request_t::src_x` and :cpp:member:`async_color_convert_request_t::src_y` + Top-left corner of the source window +- :cpp:member:`async_color_convert_request_t::dst_buffer` + Base address of the destination image +- :cpp:member:`async_color_convert_request_t::dst_stride` + Destination image row stride in pixels +- :cpp:member:`async_color_convert_request_t::dst_height` + Destination image height in pixels +- :cpp:member:`async_color_convert_request_t::dst_x` and :cpp:member:`async_color_convert_request_t::dst_y` + Top-left corner of where the converted window should be written +- :cpp:member:`async_color_convert_request_t::copy_width` and :cpp:member:`async_color_convert_request_t::copy_height` + Size of the rectangle to convert +- :cpp:member:`async_color_convert_request_t::src_color_format` and :cpp:member:`async_color_convert_request_t::dst_color_format` + Source and destination pixel formats +- :cpp:member:`async_color_convert_request_t::color_conv_std` + RGB/YUV conversion standard, used for RGB <-> YUV conversions + +Both the source window and destination window must stay within the bounds of their corresponding images. + +Supported Conversions +--------------------- + +The following format pairs are currently supported by this driver: + +.. list-table:: + :header-rows: 1 + + * - Source format + - Destination format + - Conversion standard + * - ``ESP_COLOR_FOURCC_RGB16`` + - ``ESP_COLOR_FOURCC_RGB16`` + - N/A + * - ``ESP_COLOR_FOURCC_BGR24`` + - ``ESP_COLOR_FOURCC_BGR24`` + - N/A + * - ``ESP_COLOR_FOURCC_RGB24`` + - ``ESP_COLOR_FOURCC_RGB24`` + - N/A + * - ``ESP_COLOR_FOURCC_UYVY`` + - ``ESP_COLOR_FOURCC_UYVY`` + - N/A + * - ``ESP_COLOR_FOURCC_BGR24`` + - ``ESP_COLOR_FOURCC_RGB24`` + - N/A + * - ``ESP_COLOR_FOURCC_RGB24`` + - ``ESP_COLOR_FOURCC_BGR24`` + - N/A + * - ``ESP_COLOR_FOURCC_RGB16`` + - ``ESP_COLOR_FOURCC_BGR24`` + - N/A + * - ``ESP_COLOR_FOURCC_BGR24`` + - ``ESP_COLOR_FOURCC_RGB16`` + - N/A + * - ``ESP_COLOR_FOURCC_RGB24`` + - ``ESP_COLOR_FOURCC_RGB16`` + - N/A + * - ``ESP_COLOR_FOURCC_BGR24`` + - ``ESP_COLOR_FOURCC_UYVY`` + - BT.601 + * - ``ESP_COLOR_FOURCC_BGR24`` + - ``ESP_COLOR_FOURCC_UYVY`` + - BT.709 + * - ``ESP_COLOR_FOURCC_RGB24`` + - ``ESP_COLOR_FOURCC_UYVY`` + - BT.601 + * - ``ESP_COLOR_FOURCC_RGB24`` + - ``ESP_COLOR_FOURCC_UYVY`` + - BT.709 + * - ``ESP_COLOR_FOURCC_UYVY`` + - ``ESP_COLOR_FOURCC_BGR24`` + - BT.601 + * - ``ESP_COLOR_FOURCC_UYVY`` + - ``ESP_COLOR_FOURCC_BGR24`` + - BT.709 + +.. note:: + + Always set :cpp:member:`async_color_convert_request_t::src_color_format` and + :cpp:member:`async_color_convert_request_t::dst_color_format`. + Set :cpp:member:`async_color_convert_request_t::color_conv_std` when converting between RGB and YUV. + +Scenario 2: Use the Asynchronous API with a Callback +==================================================== + +Once the blocking flow is clear, the next step is to queue a request and let the driver notify you from interrupt context when it is finished. + +.. code:: c + + static bool color_conv_done_cb(async_color_convert_handle_t conv_hdl, + async_color_convert_event_data_t *edata, + void *cb_args) + { + BaseType_t high_task_wakeup = pdFALSE; // Required by FreeRTOS when an ISR wakes a task + SemaphoreHandle_t sem = (SemaphoreHandle_t)cb_args; // User context passed at submit time + // Notify a waiting task that the conversion has finished. + xSemaphoreGiveFromISR(sem, &high_task_wakeup); + // Return true when the unblocked task should run immediately after the ISR. + return high_task_wakeup == pdTRUE; + } + + async_color_convert_request_t req = { + .src_buffer = src_buf, // Source image base address + .src_stride = src_width, // Source image row stride, in pixels + .src_height = src_height, + .src_x = 0, + .src_y = 0, + .dst_buffer = dst_buf, // Destination image base address + .dst_stride = dst_width, // Destination image row stride, in pixels + .dst_height = dst_height, + .dst_x = 0, + .dst_y = 0, + .copy_width = copy_width, + .copy_height = copy_height, + .src_color_format = ESP_COLOR_FOURCC_RGB16, + .dst_color_format = ESP_COLOR_FOURCC_BGR24, + }; + + // Queue one asynchronous request. The callback runs later in ISR context. + ESP_ERROR_CHECK(esp_async_color_convert(conv_hdl, &req, color_conv_done_cb, sem)); + // Wait in task context until the callback gives the semaphore. + xSemaphoreTake(sem, portMAX_DELAY); + +The callback runs in ISR context, so keep it short and only use ISR-safe APIs such as ``xSemaphoreGiveFromISR`` or ``xQueueSendFromISR``. + +Operational Notes +================= + +Driver Configuration +-------------------- + +The driver configuration fields are: + +- :cpp:member:`async_color_convert_config_t::backlog` + Maximum number of in-flight or pending requests. ``0`` uses a driver default. +- :cpp:member:`async_color_convert_config_t::dma_burst_size` + DMA burst size in bytes. ``0`` uses a driver default. +- :cpp:member:`async_color_convert_config_t::intr_priority` + DMA2D interrupt priority. ``0`` uses the default low/medium priority. + +DMA Burst Size +-------------- + +The ``dma_burst_size`` affects DMA transfer efficiency: + +- Larger burst sizes may improve throughput +- Larger burst sizes can also increase bus occupancy, so they are not always best for every workload +- Common starting values are 16, 32, and 64 bytes + +The best value depends on the chip's DMA controller capabilities and how much memory bandwidth is shared with other active components in the system. + +Thread Safety and ISR Rules +--------------------------- + +- The driver is thread-safe. Requests from different tasks are serialized through the internal queue. +- :cpp:func:`esp_async_color_convert` can be called from tasks to enqueue requests. +- The callback type :cpp:type:`async_color_convert_isr_cb_t` runs in ISR context. +- Do not call blocking APIs from the callback. +- :cpp:func:`esp_color_convert_blocking` must not be called from ISR context. + +Uninstalling the Driver +----------------------- + +When the driver is no longer needed: + +.. code:: c + + // Uninstall only after all queued conversions have completed. + ESP_ERROR_CHECK(esp_async_color_convert_uninstall(conv_hdl)); + +If requests are still pending, :cpp:func:`esp_async_color_convert_uninstall` returns :c:macro:`ESP_ERR_INVALID_STATE`. + +Application Example +=================== + +- :example:`peripherals/dma/async_color_convert` shows a beginner-friendly blocking conversion flow: + + - an embedded ``.yuv`` image is read directly from mapped flash + - DMA2D converts the image from UYVY422 to BGR24 + - the converted output is base64-encoded and printed to the console + - pytest reconstructs the image as a PNG artifact and compares it against a golden reference image + +API Reference +============= + +Async Color Convert Driver Functions +------------------------------------ + +.. include-build-file:: inc/esp_async_color_convert.inc diff --git a/docs/en/api-reference/peripherals/async_crc.rst b/docs/en/api-reference/peripherals/async_crc.rst index fe2b4c21c43..b22fc3a9db7 100644 --- a/docs/en/api-reference/peripherals/async_crc.rst +++ b/docs/en/api-reference/peripherals/async_crc.rst @@ -1,5 +1,5 @@ ============================ -Asynchronous CRC (Async CRC) +Asynchronous CRC Calculation ============================ :link_to_translation:`zh_CN:[中文]` diff --git a/docs/en/api-reference/peripherals/index.rst b/docs/en/api-reference/peripherals/index.rst index 3dcd8684769..f6459453860 100644 --- a/docs/en/api-reference/peripherals/index.rst +++ b/docs/en/api-reference/peripherals/index.rst @@ -9,6 +9,7 @@ Peripherals API :SOC_ADC_SUPPORTED: adc/index :SOC_ANA_CMPR_SUPPORTED: ana_cmpr :SOC_GDMA_SUPPORT_CRC: async_crc + :SOC_DMA2D_SUPPORTED: async_color_convert :SOC_ASYNC_MEMCPY_SUPPORTED: async_memcpy :SOC_BITSCRAMBLER_SUPPORTED: bitscrambler :SOC_MIPI_CSI_SUPPORTED: camera_driver diff --git a/docs/zh_CN/api-reference/peripherals/async_color_convert.rst b/docs/zh_CN/api-reference/peripherals/async_color_convert.rst new file mode 100644 index 00000000000..32a62dd9aee --- /dev/null +++ b/docs/zh_CN/api-reference/peripherals/async_color_convert.rst @@ -0,0 +1,317 @@ +================ +异步色彩格�转� +================ + +:link_to_translation:`en:[English]` + +本文介� ESP-IDF 中的异步色彩转�驱动。目录如下: + +.. contents:: + :local: + :depth: 2 + +概述 +==== + +{IDF_TARGET_NAME} �供 DMA2D 引擎,�以把 2D 拷�和色彩转�工作从 CPU �载到硬件执行。 + +这个驱动适�用于: + +- 将图�从一��素格�转�为�一��素格� +- �转�大图中的一个矩形窗� +- 将多个转�请求排队,而�是让 CPU 自己��素�� +- 在 RGB 和 UYVY 格�之间转�,并选择 RGB/YUV 转�标准 + +异步色彩转�驱动对 DMA2D 的请求准备�队列管�和完�通知�了�装,�时�供两�使用方�: + +- 带 ISR 回调通知的异步�交接� +- 基于�一路径实现�对新手更�好的阻塞接� + +快速开始 +======== + +如果你是第一次使用这个驱动,建议从最简�的�程开始: + +1. 安装驱动 +2. 准备一个 :cpp:type:`async_color_convert_request_t` +3. 通过阻塞或�阻塞 API �起转� +4. 在转�完��使用输出 buffer +5. 继续�交新请求,或在结�时�载驱动 + +典型使用�程如下: + +.. mermaid:: + + flowchart TD + install["安装驱动
esp_async_color_convert_install_dma2d"] --> request["准备请求
async_color_convert_request_t"] + request --> blocking["阻塞路径
esp_color_convert_blocking"] + request --> nonBlocking["�阻塞路径
esp_async_color_convert"] + nonBlocking --> callback["等待回调或任务通知"] + blocking --> result["使用转�结果 buffer"] + callback --> result + result --> request + result --> uninstall["�选清�
esp_async_color_convert_uninstall"] + +场景 1:先从一次阻塞转æ�¢å¼€å§‹ +============================ + +ç�†è§£è¿™ä¸ªé©±åŠ¨çš„æœ€ç®€å�•æ–¹å¼�,就是先完æˆ�一次转æ�¢ï¼Œå¹¶åœ¨å‡½æ•°è¿”回时直接拿到结果。 + +下é�¢çš„æµ�程与 :example:`peripherals/dma/async_color_convert` 示例一致。它把一个嵌入在 flash 中的 UYVY422 图åƒ�转æ�¢ä¸º BGR24,然å�Žç”±åº”用继续处ç�†è½¬æ�¢å�Žçš„输出: + +.. code:: c + + async_color_convert_handle_t conv_hdl = NULL; // 安装驱动å�Žè¿”回的å�¥æŸ„,å�Žç»­ API 都è¦�用到它 + async_color_convert_config_t config = { + .backlog = 1, // 这个阻塞示例一次å�ªå¤„ç�†ä¸€ä¸ªè¯·æ±‚,因此 1 就够了 + .dma_burst_size = 16, // 先使用示例里的默认 burst 大å°�å�³å�¯ + }; + // 创建一个基于 DMA2D å�Žç«¯çš„异步色彩转æ�¢é©±åŠ¨å®žä¾‹ã€‚ + ESP_ERROR_CHECK(esp_async_color_convert_install_dma2d(&config, &conv_hdl)); + + async_color_convert_request_t req = { + .src_buffer = sample_96x64_uyvy_yuv_start, // æº�图åƒ�å�¯ä»¥åœ¨ flash 或者 RAM 中,å�ªè¦� DMA å�¯è®¿é—®å�³å�¯ + .src_stride = 96, // æº�图åƒ�的行跨度,å�•ä½�是åƒ�ç´  + .src_height = 64, // æº�图åƒ�高度,å�•ä½�是åƒ�ç´  + .src_x = 0, // 从æº�图åƒ�左上角开始å�–窗å�£ + .src_y = 0, + .dst_buffer = dst_bgr, // 目标 buffer ä½�于 DMA å�¯è®¿é—®çš„ RAM 中 + .dst_stride = 96, // 目标图åƒ�的行跨度,å�•ä½�是åƒ�ç´  + .dst_height = 64, // 目标图åƒ�高度,å�•ä½�是åƒ�ç´  + .dst_x = 0, // 从目标图åƒ�左上角开始写入结果 + .dst_y = 0, + .copy_width = 96, // 转æ�¢æ•´å¼ å›¾çš„宽度,å�•ä½�是åƒ�ç´  + .copy_height = 64, // 转æ�¢æ•´å¼ å›¾çš„高度,å�•ä½�是åƒ�ç´  + .src_color_format = ESP_COLOR_FOURCC_UYVY, // æº�åƒ�ç´ æ ¼å¼�为 UYVY422 + .dst_color_format = ESP_COLOR_FOURCC_BGR24, // 目标åƒ�ç´ æ ¼å¼�为 BGR24(本驱动里用它表示 RGB888) + .color_conv_std = COLOR_CONV_STD_RGB_YUV_BT601, // 该 RGB/YUV 转æ�¢ä½¿ç”¨çš„æ ‡å‡† + }; + + // 阻塞等待 DMA2D 完æˆ�转æ�¢ã€‚-1 表示一直等到完æˆ�为止。 + ESP_ERROR_CHECK(esp_color_convert_blocking(conv_hdl, &req, -1)); + + // 所有转æ�¢ç»“æ�Ÿå�Žï¼Œé‡Šæ”¾é©±åŠ¨èµ„æº�。 + ESP_ERROR_CHECK(esp_async_color_convert_uninstall(conv_hdl)); + +这个æµ�程里最é‡�è¦�的概念有: + +- :cpp:func:`esp_async_color_convert_install_dma2d` 创建驱动实例 +- :cpp:type:`async_color_convert_request_t` æ��è¿°æº�图åƒ�ã€�目标图åƒ�以å�Šè¦�转æ�¢çš„窗å�£ +- :cpp:func:`esp_color_convert_blocking` 会一直等待,直到硬件完æˆ�转æ�¢ +- :cpp:func:`esp_async_color_convert_uninstall` 释放驱动资æº� + +对于阻塞 API,``timeout_ms = -1`` 表示永久等待。其他 timeout 值目å‰�ä¸�支æŒ�,会返回 ``ESP_ERR_INVALID_ARG``。 + +ç�†è§£ ``async_color_convert_request_t`` +-------------------------------------- + +这个驱动最容易出错的地方,通常ä¸�是安装驱动,而是请求å�‚数填写ä¸�正确,因此ç�†è§£ :cpp:type:`async_color_convert_request_t` 很é‡�è¦�。 + +.. important:: + + 在 :cpp:type:`async_color_convert_request_t` 中,所有几何相关字段的å�•ä½�都是 **åƒ�ç´ **,ä¸�是字节。包括 ``src_stride``ã€�``src_height``ã€�``src_x``ã€�``src_y``ã€�``dst_stride``ã€�``dst_height``ã€�``dst_x``ã€�``dst_y``ã€�``copy_width`` å’Œ ``copy_height``。 + + ``src_stride`` å’Œ ``dst_stride`` 表示的是æ¯�一整行在内存中跨越多少åƒ�素,也就是行跨度,ä¸�是本次转æ�¢çª—å�£çš„宽度。当你å�ªè½¬æ�¢å¤§å›¾ä¸­çš„一个窗å�£æ—¶ï¼Œå®ƒä»¬å�¯ä»¥å¤§äºŽ ``copy_width``。 + +这个结构体å�Œæ—¶æ��述了两件事: + +- æº�图åƒ�和目标图åƒ�在内存中的完整布局 +- 本次实际è¦�转æ�¢çš„矩形窗å�£ + +关键字段å�«ä¹‰å¦‚下: + +- :cpp:member:`async_color_convert_request_t::src_buffer` + æº�图åƒ�基地å�€ +- :cpp:member:`async_color_convert_request_t::src_stride` + æº�图åƒ�的行跨度,å�•ä½�为åƒ�ç´  +- :cpp:member:`async_color_convert_request_t::src_height` + æº�图åƒ�高度,å�•ä½�为åƒ�ç´  +- :cpp:member:`async_color_convert_request_t::src_x` å’Œ :cpp:member:`async_color_convert_request_t::src_y` + æº�窗å�£å·¦ä¸Šè§’å��æ ‡ +- :cpp:member:`async_color_convert_request_t::dst_buffer` + 目标图åƒ�基地å�€ +- :cpp:member:`async_color_convert_request_t::dst_stride` + 目标图åƒ�的行跨度,å�•ä½�为åƒ�ç´  +- :cpp:member:`async_color_convert_request_t::dst_height` + 目标图åƒ�高度,å�•ä½�为åƒ�ç´  +- :cpp:member:`async_color_convert_request_t::dst_x` å’Œ :cpp:member:`async_color_convert_request_t::dst_y` + 转æ�¢ç»“果写入目标图åƒ�时的左上角å��æ ‡ +- :cpp:member:`async_color_convert_request_t::copy_width` å’Œ :cpp:member:`async_color_convert_request_t::copy_height` + 本次è¦�转æ�¢çš„矩形窗å�£å°ºå¯¸ +- :cpp:member:`async_color_convert_request_t::src_color_format` å’Œ :cpp:member:`async_color_convert_request_t::dst_color_format` + æº�和目标åƒ�ç´ æ ¼å¼� +- :cpp:member:`async_color_convert_request_t::color_conv_std` + RGB/YUV 转æ�¢æ ‡å‡†ï¼Œç”¨äºŽ RGB å’Œ YUV 之间的转æ�¢ + +æº�窗å�£å’Œç›®æ ‡çª—å�£éƒ½å¿…须完整è�½åœ¨å�„自图åƒ�的边界之内。 + +支æŒ�的转æ�¢æ ¼å¼� +-------------- + +本驱动当å‰�支æŒ�以下格å¼�组å�ˆï¼š + +.. list-table:: + :header-rows: 1 + + * - æº�æ ¼å¼� + - 目标格å¼� + - 转æ�¢æ ‡å‡† + * - ``ESP_COLOR_FOURCC_RGB16`` + - ``ESP_COLOR_FOURCC_RGB16`` + - ä¸�适用 + * - ``ESP_COLOR_FOURCC_BGR24`` + - ``ESP_COLOR_FOURCC_BGR24`` + - ä¸�适用 + * - ``ESP_COLOR_FOURCC_RGB24`` + - ``ESP_COLOR_FOURCC_RGB24`` + - ä¸�适用 + * - ``ESP_COLOR_FOURCC_UYVY`` + - ``ESP_COLOR_FOURCC_UYVY`` + - ä¸�适用 + * - ``ESP_COLOR_FOURCC_BGR24`` + - ``ESP_COLOR_FOURCC_RGB24`` + - ä¸�适用 + * - ``ESP_COLOR_FOURCC_RGB24`` + - ``ESP_COLOR_FOURCC_BGR24`` + - ä¸�适用 + * - ``ESP_COLOR_FOURCC_RGB16`` + - ``ESP_COLOR_FOURCC_BGR24`` + - ä¸�适用 + * - ``ESP_COLOR_FOURCC_BGR24`` + - ``ESP_COLOR_FOURCC_RGB16`` + - ä¸�适用 + * - ``ESP_COLOR_FOURCC_RGB24`` + - ``ESP_COLOR_FOURCC_RGB16`` + - ä¸�适用 + * - ``ESP_COLOR_FOURCC_BGR24`` + - ``ESP_COLOR_FOURCC_UYVY`` + - BT.601 + * - ``ESP_COLOR_FOURCC_BGR24`` + - ``ESP_COLOR_FOURCC_UYVY`` + - BT.709 + * - ``ESP_COLOR_FOURCC_RGB24`` + - ``ESP_COLOR_FOURCC_UYVY`` + - BT.601 + * - ``ESP_COLOR_FOURCC_RGB24`` + - ``ESP_COLOR_FOURCC_UYVY`` + - BT.709 + * - ``ESP_COLOR_FOURCC_UYVY`` + - ``ESP_COLOR_FOURCC_BGR24`` + - BT.601 + * - ``ESP_COLOR_FOURCC_UYVY`` + - ``ESP_COLOR_FOURCC_BGR24`` + - BT.709 + +.. note:: + + 所有请求都需è¦�设置 :cpp:member:`async_color_convert_request_t::src_color_format` å’Œ + :cpp:member:`async_color_convert_request_t::dst_color_format`。 + 当在 RGB å’Œ YUV 之间转æ�¢æ—¶ï¼Œè¿˜éœ€è¦�设置 :cpp:member:`async_color_convert_request_t::color_conv_std`。 + +场景 2:使用异步接å�£å’Œå›žè°ƒå‡½æ•° +============================== + +ç�†è§£äº†é˜»å¡žæµ�程之å�Žï¼Œä¸‹ä¸€æ­¥å°±æ˜¯æŠŠè¯·æ±‚排入队列,并在硬件完æˆ�å�Žç”±ä¸­æ–­ä¸Šä¸‹æ–‡ä¸­çš„回调通知你。 + +.. code:: c + + static bool color_conv_done_cb(async_color_convert_handle_t conv_hdl, + async_color_convert_event_data_t *edata, + void *cb_args) + { + BaseType_t high_task_wakeup = pdFALSE; // FreeRTOS 在 ISR 中唤醒任务时需è¦�这个å�˜é‡� + SemaphoreHandle_t sem = (SemaphoreHandle_t)cb_args; // æ��交请求时传进æ�¥çš„用户上下文 + // 用 ISR-safe 的方å¼�通知等待中的任务:这次转æ�¢å·²ç»�完æˆ�。 + xSemaphoreGiveFromISR(sem, &high_task_wakeup); + // 如果刚æ‰�唤醒了更高优先级任务,就请求在 ISR 退出å�Žç«‹åˆ»åˆ‡æ�¢è¿‡åŽ»ã€‚ + return high_task_wakeup == pdTRUE; + } + + async_color_convert_request_t req = { + .src_buffer = src_buf, // æº�图åƒ�基地å�€ + .src_stride = src_width, // æº�图åƒ�的行跨度,å�•ä½�是åƒ�ç´  + .src_height = src_height, + .src_x = 0, + .src_y = 0, + .dst_buffer = dst_buf, // 目标图åƒ�基地å�€ + .dst_stride = dst_width, // 目标图åƒ�的行跨度,å�•ä½�是åƒ�ç´  + .dst_height = dst_height, + .dst_x = 0, + .dst_y = 0, + .copy_width = copy_width, + .copy_height = copy_height, + .src_color_format = ESP_COLOR_FOURCC_RGB16, + .dst_color_format = ESP_COLOR_FOURCC_BGR24, + }; + + // æ��交一个异步请求。函数返回时,硬件å�¯èƒ½è¿˜åœ¨æ‰§è¡Œè½¬æ�¢ã€‚ + ESP_ERROR_CHECK(esp_async_color_convert(conv_hdl, &req, color_conv_done_cb, sem)); + // 在任务上下文中等待回调释放信å�·é‡�。 + xSemaphoreTake(sem, portMAX_DELAY); + +回调è¿�行在 ISR 上下文中,因此应尽é‡�ä¿�æŒ�简短,并且å�ªè°ƒç”¨ ISR-safe API,例如 ``xSemaphoreGiveFromISR`` 或 ``xQueueSendFromISR``。 + +è¿�行注æ„�事项 +============ + +驱动é…�ç½® +-------- + +驱动é…�置字段如下: + +- :cpp:member:`async_color_convert_config_t::backlog` + 最大待处ç�†è¯·æ±‚数。``0`` 表示使用驱动默认值。 +- :cpp:member:`async_color_convert_config_t::dma_burst_size` + DMA burst 大å°�,å�•ä½�为字节。``0`` 表示使用驱动默认值。 +- :cpp:member:`async_color_convert_config_t::intr_priority` + DMA2D 中断优先级。``0`` 表示使用默认低/中优先级。 + +DMA çª�å�‘大å°� +------------ + +``dma_burst_size`` 会影å“� DMA 传输效率: + +- 较大的çª�å�‘大å°�å�¯èƒ½æ��高å�žå��é‡� +- 较大的çª�å�‘大å°�也å�¯èƒ½å¢žåŠ æ€»çº¿å� ç”¨ï¼Œå› æ­¤å¹¶ä¸�一定适å�ˆæ‰€æœ‰å·¥ä½œè´Ÿè½½ +- 常è§�的起始å�–值有 16ã€�32 å’Œ 64 字节 + +最佳å�–值å�–决于芯片的 DMA 控制器能力,以å�Šç³»ç»Ÿä¸­å…¶ä»–活跃组件对内存带宽的共享情况。 + +线程安全与 ISR 规则 +------------------- + +- 驱动是线程安全的。ä¸�å�Œä»»åŠ¡æ��交的请求会通过内部队列串行化。 +- :cpp:func:`esp_async_color_convert` å�¯ä»¥åœ¨ä»»åŠ¡ä¸Šä¸‹æ–‡ä¸­è°ƒç”¨ï¼Œç”¨äºŽæŽ’é˜Ÿè¯·æ±‚ã€‚ +- 回调类型 :cpp:type:`async_color_convert_isr_cb_t` è¿�行在 ISR 上下文中。 +- ä¸�è¦�在回调里调用阻塞 API。 +- :cpp:func:`esp_color_convert_blocking` ä¸�能在 ISR 上下文中调用。 + +å�¸è½½é©±åЍ +-------- + +当驱动ä¸�å†�需è¦�时: + +.. code:: c + + // å�ªæœ‰åœ¨æ‰€æœ‰æŽ’队请求都完æˆ�å�Žï¼Œæ‰�能安全å�¸è½½é©±åŠ¨ã€‚ + ESP_ERROR_CHECK(esp_async_color_convert_uninstall(conv_hdl)); + +如果ä»�有请求未完æˆ�,:cpp:func:`esp_async_color_convert_uninstall` 会返回 :c:macro:`ESP_ERR_INVALID_STATE`。 + +应用示例 +======== + +- :example:`peripherals/dma/async_color_convert` 展示了一个é�¢å�‘åˆ�学者的阻塞转æ�¢æµ�程: + + - 从映射到 flash 的嵌入å¼� ``.yuv`` 图åƒ�直接读å�–输入 + - 使用 DMA2D 将图åƒ�从 UYVY422 转æ�¢ä¸º BGR24 + - 将转æ�¢ç»“æžœå�š base64 ç¼–ç �å�Žè¾“出到控制å�° + - ç”± pytest é‡�建为 PNG 工件,并与 golden å�‚考图进行比对 + +API å�‚考 +======== + +异步颜色转æ�¢é©±åŠ¨ç¨‹åº�函数 +------------------------ + +.. include-build-file:: inc/esp_async_color_convert.inc diff --git a/docs/zh_CN/api-reference/peripherals/async_crc.rst b/docs/zh_CN/api-reference/peripherals/async_crc.rst index b257d027d9c..79f42f44c60 100644 --- a/docs/zh_CN/api-reference/peripherals/async_crc.rst +++ b/docs/zh_CN/api-reference/peripherals/async_crc.rst @@ -1,6 +1,6 @@ -==================== -异步 CRC (Async CRC) -==================== +============= +异步 CRC 计算 +============= :link_to_translation:`en:[English]` diff --git a/docs/zh_CN/api-reference/peripherals/index.rst b/docs/zh_CN/api-reference/peripherals/index.rst index ba506840eed..e0de40dd7c1 100644 --- a/docs/zh_CN/api-reference/peripherals/index.rst +++ b/docs/zh_CN/api-reference/peripherals/index.rst @@ -9,6 +9,7 @@ :SOC_ADC_SUPPORTED: adc/index :SOC_ANA_CMPR_SUPPORTED: ana_cmpr :SOC_GDMA_SUPPORT_CRC: async_crc + :SOC_DMA2D_SUPPORTED: async_color_convert :SOC_ASYNC_MEMCPY_SUPPORTED: async_memcpy :SOC_BITSCRAMBLER_SUPPORTED: bitscrambler :SOC_MIPI_CSI_SUPPORTED: camera_driver diff --git a/examples/peripherals/.build-test-rules.yml b/examples/peripherals/.build-test-rules.yml index ffcc63b3969..d96380deb48 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -100,6 +100,12 @@ examples/peripherals/dac/dac_cosine_wave: - esp_driver_dac - soc +examples/peripherals/dma/async_color_convert: + disable: + - if: SOC_DMA2D_SUPPORTED != 1 + depends_components: + - esp_driver_dma + examples/peripherals/dma/async_crc: disable: - if: SOC_GDMA_SUPPORT_CRC != 1 diff --git a/examples/peripherals/dma/async_color_convert/CMakeLists.txt b/examples/peripherals/dma/async_color_convert/CMakeLists.txt new file mode 100644 index 00000000000..3192fb91ee2 --- /dev/null +++ b/examples/peripherals/dma/async_color_convert/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(async_color_convert_example) diff --git a/examples/peripherals/dma/async_color_convert/README.md b/examples/peripherals/dma/async_color_convert/README.md new file mode 100644 index 00000000000..77efb5f5dc2 --- /dev/null +++ b/examples/peripherals/dma/async_color_convert/README.md @@ -0,0 +1,69 @@ +| Supported Targets | ESP32-P4 | ESP32-S31 | +| ----------------- | -------- | --------- | + +# Async Color Convert Example + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +## Overview + +This example demonstrates how to use the Async Color Convert driver (`esp_async_color_convert.h`) with the DMA2D backend. + +The example performs: + +- Loading an embedded UYVY422 raw image from flash +- Letting DMA2D read the source image directly from mapped flash +- Performing a blocking UYVY422 -> RGB888 conversion with the Async Color Convert driver +- Base64-encoding the converted BGR24 image and printing it with machine-parseable markers +- Letting pytest decode the payload, save a PPM artifact, and compare it against a golden reference image + +## Hardware Required + +Any board with a supported ESP target that mentioned in the above table can be used. + +## Build and Flash + +Run `idf.py -p PORT flash monitor` to build and flash the project. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +```text +Loading embedded UYVY image from flash... +Embedded image size: 12288 bytes +Converting UYVY422 -> RGB888... +Converted image size: 18432 bytes +IMAGE_META width=96 height=64 format=BGR24 encoding=base64 +IMAGE_BASE64_BEGIN +IMAGE_BASE64 ... +IMAGE_BASE64 ... +IMAGE_BASE64_END +Async color convert visual demo done. +``` + +## Visual Result In Pytest + +The accompanying pytest script captures the `IMAGE_META` and `IMAGE_BASE64` output, reconstructs the converted image, and saves it as: + +- `dut.logdir/async_color_convert_result.ppm` + +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. + +## Replacing The Embedded UYVY Asset + +The example embeds `main/assets/sample_96x64_uyvy.yuv`. + +You can regenerate a compatible asset from any PNG with `ffmpeg`. One simple workflow is: + +```bash +ffmpeg -y -i input.png -vf scale=96:64 -pix_fmt uyvy422 -f rawvideo sample_96x64_uyvy.yuv +``` + +After replacing the `.yuv` file, rebuild and flash the example. The firmware will emit the converted image as base64, and pytest will save the resulting PPM artifact automatically. If you intend the new image to become the expected output, update `golden_result.ppm` as well so the regression check stays in sync. + +## Troubleshooting + +(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.) diff --git a/examples/peripherals/dma/async_color_convert/golden_result.ppm b/examples/peripherals/dma/async_color_convert/golden_result.ppm new file mode 100644 index 00000000000..0f987ed1191 Binary files /dev/null and b/examples/peripherals/dma/async_color_convert/golden_result.ppm differ diff --git a/examples/peripherals/dma/async_color_convert/main/CMakeLists.txt b/examples/peripherals/dma/async_color_convert/main/CMakeLists.txt new file mode 100644 index 00000000000..85414639ef9 --- /dev/null +++ b/examples/peripherals/dma/async_color_convert/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "async_color_convert_example_main.c" + PRIV_REQUIRES esp_driver_dma mbedtls + INCLUDE_DIRS "." + EMBED_FILES "assets/sample_96x64_uyvy.yuv") diff --git a/examples/peripherals/dma/async_color_convert/main/assets/sample_96x64_uyvy.yuv b/examples/peripherals/dma/async_color_convert/main/assets/sample_96x64_uyvy.yuv new file mode 100644 index 00000000000..d551abaead1 --- /dev/null +++ b/examples/peripherals/dma/async_color_convert/main/assets/sample_96x64_uyvy.yuv @@ -0,0 +1 @@ +n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��n��rœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœrœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœwœœ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œ{œ!œœ$œœ$œ€€€€€€€€€€€€€€€€€€€€€€€€€€€Rœœ$œ€€€€€€€€€€€€€€€€€€€€€€€€€€€Rœœ$œ€€€€€€€€€€€€€€€€€€€€€€€€€€€Rœœ$œœ$œ„œ'œ„œ'œ€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë‚Sœ„œ'œm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ‚Sœ„œ'œ[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘‚Sœ„œ'œ„œ'œˆœ)œˆœ)œ€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë„Uœˆœ)œm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ„Uœˆœ)œ[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘„Uœˆœ)œˆœ)œŒ›,›Œ›,›€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë†V›Œ›,›m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ†V›Œ›,›[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘†V›Œ›,›Œ›,›�›.›�›.›€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ëˆW›�›.›m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQˆW›�›.›[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘ˆW›�›.›�›.›•›1›•›1›€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ëŠY›•›1›m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQŠY›•›1›[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘ŠY›•›1›•›1›™›4›™›4›€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ëŒZ›™›4›m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQŒZ›™›4›[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘ŒZ›™›4›™›4›�›6›�›6›€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë�[›�›6›m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ�[›�›6›[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘�[›�›6›�›6›¢›9›¢›9›€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë‘\›¢›9›m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ‘\›¢›9›[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘‘\›¢›9›¢›9›¦š<š¦š<š€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë“^š¦š<šm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ“^š¦š<š[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘“^š¦š<š¦š<šªš>šªš>š€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë•_šªš>šm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ•_šªš>š[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘•_šªš>šªš>š¯šAš¯šAš€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë—`š¯šAšm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ—`š¯šAš[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘—`š¯šAš¯šAšw�M�w�M�€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë|g�w�M�m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ|g�w�M�[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘|g�w�M�w�M�|�P�|�P�€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë~h�|�P�m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ~h�|�P�[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘~h�|�P�|�P�€ŒRŒ€ŒRŒ€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€iŒ€ŒRŒm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ€iŒ€ŒRŒ[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘€iŒ€ŒRŒ€ŒRŒ„ŒUŒ„ŒUŒ€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë‚jŒ„ŒUŒm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ‚jŒ„ŒUŒ[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘‚jŒ„ŒUŒ„ŒUŒˆŒXŒˆŒXŒ€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë„lŒˆŒXŒm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ„lŒˆŒXŒ[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘„lŒˆŒXŒˆŒXŒ�Œ[Œ�Œ[Œ€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë†mŒ�Œ[Œm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ†mŒ�Œ[Œ[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘†mŒ�Œ[Œ�Œ[Œ‘‹^‹‘‹^‹€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë‰o‹‘‹^‹m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ‰o‹‘‹^‹[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘‰o‹‘‹^‹‘‹^‹–‹`‹–‹`‹€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë‹p‹–‹`‹m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ‹p‹–‹`‹[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘‹p‹–‹`‹–‹`‹š‹c‹š‹c‹€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë�q‹š‹c‹m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ�q‹š‹c‹[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘�q‹š‹c‹š‹c‹ž‹f‹ž‹f‹€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë�s‹ž‹f‹m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ�s‹ž‹f‹[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘�s‹ž‹f‹ž‹f‹¢‹h‹¢‹h‹€€€€€€€€€€€€€€€€€€€€€€€€€€‘t‹¢‹h‹€€€€€€€€€€€€€€€€€€€€€€€€€€‘t‹¢‹h‹€€€€€€€€€€€€€€€€€€€€€€€€€€‘t‹¢‹h‹¢‹h‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹¯ŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsŠ€ë€ë€ë€ë€ë€ë€ë€ëšëyгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuŠœŠ{ë€ë€ëÍeBeÍeBeÍeBeÍeBe§eaë€ë€ë¸ŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuŠ�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}€}�ë§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe€ë€ë�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}€}��}‚}�}‚}�}‚}�}‚}�}‚}…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|‚|‚ë§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe€ë€ë…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|€€…|„|…|„|…|„|…|„|…|„|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe„ëƒ|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|„|ƒO»‘‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|‡|…ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|‡|…O‘»»£»‡…|�|‰|�|‰|�|‰|�|‰|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe‰ë†|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|O‘»»£»»£»‰†|‘|Œ|‘|Œ|‘|Œ|‘|Œ|–|�|–|�|–|�|–|�|–|�|–|�|‹|‡ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|‹|‡»£»»£»»£»‹‡|–|�|–|�|–|�|–|�|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe�ë‰|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|O‘»»£»»£»»£»�‰|š|‘|š|‘|š|‘|š|‘|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe�ëŠ|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|�|Š»£»»£»»£»»£»�Š|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|£{—{£{—{£{—{£{—{£{—{‘{‹ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{O‘»»£»»£»»£»»£»O»‘£{—{£{—{£{—{£{—{§{™{§{™{§{™{§{™{§{™{”{�ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{”{�»£»»£»»£»»£»»£»O»‘§{™{§{™{§{™{§{™{«{œ{«{œ{«{œ{«{œ{«{œ{§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe–ëŽ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{–{ŽO‘»»£»»£»»£»»£»»£»O»‘«{œ{«{œ{«{œ{«{œ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe˜ë�{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{O‘»»£»»£»»£»»£»»£»»£»O»‘°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{´z¢z´z¢z´z¢z´z¢z´z¢z§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBešë‘z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢zšz‘»£»»£»»£»»£»»£»»£»»£»O»‘´z¢z´z¢z´z¢z´z¢z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeœë’z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥zO‘»»£»»£»»£»»£»»£»»£»»£»»£»œ’z¹z¥z¹z¥z¹z¥z½z§z½z§z½z§z½z§z½z§z§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBežë”z½z§z½z§z½z§z½z§z½z§z½z§z½z§z½z§z½z§z½z§zžz”»£»»£»»£»»£»»£»»£»»£»»£»»£»ž”z½z§z½z§z½z§zÁzªzÁzªzÁzªzÁzªzÁzªz§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe¡ë•zÁzªzÁzªzÁzªzÁzªzÁzªzÁzªzÁzªzÁzªzÁzªzÁzªzO‘»»£»»£»»£»»£»»£»»£»»£»»£»»£»¡•zÁzªzÁzªzÁzªzŠm¶mŠm¶mŠm¶mŠm¶mŠm¶m§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe…ë›mŠm¶mŠm¶mŠm¶mŠm¶mŠm¶mŠm¶mŠm¶mŠm¶mŠm¶m€€»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»…›mŠm¶mŠm¶mŠm¶mŽl¹lŽl¹lŽl¹lŽl¹lŽl¹l‡lœëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaëŽl¹lŽl¹lŽl¹lŽl¹lŽl¹lŽl¹lŽl¹lŽl¹lŽl¹l‡lœ»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»‡œlŽl¹lŽl¹lŽl¹l’l»l’l»l’l»l’l»l’l»l‰lžëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë’l»l’l»l’l»l’l»l’l»l’l»l’l»l’l»l’l»lO‘»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»‰žl’l»l’l»l’l»l—l¾l—l¾l—l¾l—l¾l—l¾l—l¾l§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe‹ëŸl—l¾l—l¾l—l¾l—l¾l—l¾l—l¾l—l¾l—l¾l‹lŸ»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»O»‘—l¾l—l¾l—l¾l›lÁl›lÁl›lÁl›lÁl›lÁl›lÁl§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe�ë l›lÁl›lÁl›lÁl›lÁl›lÁl›lÁl›lÁl›lÁlO‘»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»O»‘›lÁl›lÁl›lÁlŸlÃlŸlÃlŸlÃlŸlÃlŸlÃlŸlÃl�l¢ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaëŸlÃlŸlÃlŸlÃlŸlÃlŸlÃlŸlÃlŸlÃlŸlÃl�l¢»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»O»‘ŸlÃlŸlÃlŸlÃl£lÆl£lÆl£lÆl£lÆl£lÆl£lÆl£lÆl§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe’ë£l£lÆl£lÆl£lÆl£lÆl£lÆl£lÆl£lÆl£lÆlO‘»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»O»‘£lÆl£lÆl£lÆl¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk”k¤ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk€€»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»O»‘¨kÈk¨kÈk¨kÈk¬kËk¬kËk¬kËk¬kËk¬kËk¬kËk¬kËk¬kËk§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe–ë¦k¬kËk¬kËk¬kËk¬kËk¬kËk¬kËk¬kËk–k¦»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»–¦k¬kËk¬kËk°kÎk°kÎk°kÎk°kÎk°kÎk°kÎk°kÎk°kÎk˜k§ë§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe€ë€ë°kÎk°kÎk°kÎk°kÎk°kÎk°kÎk°kÎk°kÎkO‘»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»˜§k°kÎk°kÎkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkšk¨ë§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe€ë€ëµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkšk¨€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€š¨kµkÐkµkÐk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk�k©ë€ë€ëÍeBeÍeBeÍeBeÍeBe§eaë€ë€ë¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk€ë€ë€ë€ë€ë€ë€ë€ëŸë«k½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞj \ No newline at end of file diff --git a/examples/peripherals/dma/async_color_convert/main/async_color_convert_example_main.c b/examples/peripherals/dma/async_color_convert/main/async_color_convert_example_main.c new file mode 100644 index 00000000000..93a50b6f661 --- /dev/null +++ b/examples/peripherals/dma/async_color_convert/main/async_color_convert_example_main.c @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "mbedtls/base64.h" +#include "esp_async_color_convert.h" +#include "esp_check.h" +#include "esp_heap_caps.h" + +#define EXAMPLE_WIDTH 96 +#define EXAMPLE_HEIGHT 64 +#define EXAMPLE_BASE64_CHUNK_LEN 96 + +/* These linker symbols are generated automatically for the file added by + * EMBED_FILES in CMakeLists.txt. They let the example treat the embedded + * raw .yuv asset as a byte array stored in flash. */ +extern const uint8_t sample_96x64_uyvy_yuv_start[] asm("_binary_sample_96x64_uyvy_yuv_start"); +extern const uint8_t sample_96x64_uyvy_yuv_end[] asm("_binary_sample_96x64_uyvy_yuv_end"); + +static void print_base64_payload(const unsigned char *encoded, size_t encoded_len) +{ + /* The payload is split into short lines so the UART log stays easy to + * parse from pytest and less likely to be damaged by very long lines. */ + printf("IMAGE_BASE64_BEGIN\n"); + 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]); + } + printf("IMAGE_BASE64_END\n"); +} + +void app_main(void) +{ + /* UYVY422 stores 2 bytes per pixel on average, while BGR/RGB888 uses + * 3 bytes per pixel. The example keeps the image size small so the + * buffers and UART payload stay beginner-friendly. */ + const size_t pixel_num = EXAMPLE_WIDTH * EXAMPLE_HEIGHT; + const size_t yuv422_size = pixel_num * 2; + const size_t rgb888_size = pixel_num * 3; + const size_t embedded_size = sample_96x64_uyvy_yuv_end - sample_96x64_uyvy_yuv_start; + + printf("Loading embedded UYVY image from flash...\n"); + printf("Embedded image size: %zu bytes\n", embedded_size); + assert(embedded_size == yuv422_size); + + /* The destination buffer still needs DMA-capable internal RAM because + * DMA2D writes the converted pixels into this memory region. */ + uint8_t *dst_bgr = heap_caps_aligned_calloc(64, 1, rgb888_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + assert(dst_bgr); + + async_color_convert_config_t config = { + .backlog = 1, // because we use the blocking API, so only need 1 in-flight request at most + .dma_burst_size = 16, + }; + async_color_convert_handle_t conv_hdl = NULL; + + /* Install the async color convert driver with the DMA2D backend. + * The returned handle is used by later conversion requests. */ + ESP_ERROR_CHECK(esp_async_color_convert_install_dma2d(&config, &conv_hdl)); + + /* This request describes one full-frame conversion: + * - source buffer: UYVY422 image + * - destination buffer: BGR24 image + * - stride/height: layout of each image in memory + * - copy_width/copy_height: region to convert */ + async_color_convert_request_t req_yuv_to_bgr = { + /* DMA2D can read the source image directly from mapped flash, so the + * example does not need an extra CPU copy into internal RAM first. */ + .src_buffer = sample_96x64_uyvy_yuv_start, + .src_stride = EXAMPLE_WIDTH, + .src_height = EXAMPLE_HEIGHT, + .src_x = 0, + .src_y = 0, + .dst_buffer = dst_bgr, + .dst_stride = EXAMPLE_WIDTH, + .dst_height = EXAMPLE_HEIGHT, + .dst_x = 0, + .dst_y = 0, + .copy_width = EXAMPLE_WIDTH, + .copy_height = EXAMPLE_HEIGHT, + .src_color_format = ESP_COLOR_FOURCC_UYVY, + .dst_color_format = ESP_COLOR_FOURCC_BGR24, + .color_conv_std = COLOR_CONV_STD_RGB_YUV_BT601, + }; + + printf("Converting UYVY422 -> RGB888...\n"); + /* This example uses the blocking API for simplicity: the call returns only + * after the hardware conversion is finished and dst_bgr contains the result. */ + ESP_ERROR_CHECK(esp_color_convert_blocking(conv_hdl, &req_yuv_to_bgr, -1)); + printf("Converted image size: %zu bytes\n", rgb888_size); + + /* Base64 turns the binary BGR image into printable ASCII so it can be + * safely transported through the serial console and reconstructed by pytest. */ + size_t encoded_len = 0; + int ret = mbedtls_base64_encode(NULL, 0, &encoded_len, dst_bgr, rgb888_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); + ESP_ERROR_CHECK(mbedtls_base64_encode(encoded, encoded_len + 1, &encoded_len, dst_bgr, rgb888_size) == 0 ? ESP_OK : ESP_FAIL); + + /* IMAGE_META plus the chunked IMAGE_BASE64 lines form a tiny text protocol + * that the pytest script understands and converts back into a PPM file. */ + printf("IMAGE_META width=%u height=%u format=BGR24 encoding=base64\n", EXAMPLE_WIDTH, EXAMPLE_HEIGHT); + print_base64_payload(encoded, encoded_len); + printf("Async color convert visual demo done.\n"); + + ESP_ERROR_CHECK(esp_async_color_convert_uninstall(conv_hdl)); + free(encoded); + free(dst_bgr); +} diff --git a/examples/peripherals/dma/async_color_convert/pytest_async_color_convert.py b/examples/peripherals/dma/async_color_convert/pytest_async_color_convert.py new file mode 100644 index 00000000000..eb0d0fb0eb6 --- /dev/null +++ b/examples/peripherals/dma/async_color_convert/pytest_async_color_convert.py @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import base64 +import hashlib +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 width=(\d+) height=(\d+) format=(\w+) encoding=(\w+)' +IMAGE_META_RE = re.compile(rf'^{IMAGE_META_PATTERN}$') +IMAGE_CHUNK_RE = re.compile(r'^IMAGE_BASE64 ([A-Za-z0-9+/=]+)$') +IMAGE_OUTPUT_NAME = 'async_color_convert_result.ppm' +GOLDEN_IMAGE_NAME = 'golden_result.ppm' +EXPECTED_PIXEL_FORMAT = 'BGR24' +EXPECTED_ENCODING = 'base64' +PPM_MAGIC = b'P6' +PPM_MAX_VALUE = b'255' + + +@dataclass(frozen=True) +class ImageMetadata: + 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 * 3 + 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.match(meta_line) + if not match: + raise ValueError(f'Invalid image metadata line: {meta_line}') + + return ImageMetadata( + width=int(match.group(1)), + height=int(match.group(2)), + pixel_format=match.group(3), + encoding=match.group(4), + ) + + +def collect_base64_payload(dut: Dut) -> list[str]: + payload_lines: list[str] = [] + while True: + match = dut.expect(r'(IMAGE_BASE64_END|IMAGE_BASE64 [A-Za-z0-9+/=]+\r?\n)') + line = match.group(1).decode('utf-8').strip() + if line == 'IMAGE_BASE64_END': + return payload_lines + + chunk_match = IMAGE_CHUNK_RE.match(line) + assert chunk_match is not None + payload_lines.append(chunk_match.group(1)) + + +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 _load_ppm(path: Path) -> RgbImage: + ppm_bytes = path.read_bytes() + header_match = re.match(rb'^P6\s+(\d+)\s+(\d+)\s+(\d+)\s', ppm_bytes) + if not header_match: + raise ValueError('Invalid PPM header') + + width = int(header_match.group(1)) + height = int(header_match.group(2)) + max_value = header_match.group(3) + if width <= 0 or height <= 0: + raise ValueError('Unsupported PPM dimensions') + if max_value != PPM_MAX_VALUE: + raise ValueError(f'Unsupported PPM max value: {max_value.decode("ascii", errors="replace")}') + + pixel_data = ppm_bytes[header_match.end() :] + expected_size = width * height * 3 + if len(pixel_data) != expected_size: + raise ValueError(f'Expected {expected_size} PPM pixel bytes, got {len(pixel_data)}') + + return RgbImage(width=width, height=height, pixels_rgb888=pixel_data) + + +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 * 3 + 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 async color convert artifact to %s', output_path) + return + + logging.info('Saved async color convert artifact to %s', output_path) + + +def rgb_pixel_digest(image: RgbImage) -> str: + digest = hashlib.sha256() + digest.update(image.width.to_bytes(4, 'big')) + digest.update(image.height.to_bytes(4, 'big')) + digest.update(image.pixels_rgb888) + return digest.hexdigest() + + +def assert_image_matches_golden(result_image: RgbImage, golden_path: Path) -> None: + assert golden_path.is_file(), f'Golden image not found: {golden_path}' + golden_image = _load_ppm(golden_path) + + assert rgb_pixel_digest(result_image) == rgb_pixel_digest(golden_image), ( + f'Generated image does not match golden file: {golden_path.name}' + ) + + +@pytest.mark.generic +@idf_parametrize('target', soc_filtered_targets('SOC_DMA2D_SUPPORTED == 1'), indirect=['target']) +def test_async_color_convert_example(dut: Dut) -> None: + dut.expect_exact('Loading embedded UYVY image from flash...') + dut.expect(r'Embedded image size: \d+ bytes') + dut.expect_exact('Converting UYVY422 -> RGB888...') + dut.expect(r'Converted image size: \d+ bytes') + + metadata = parse_image_metadata(dut.expect(IMAGE_META_PATTERN).group(0).decode('utf-8')) + + dut.expect_exact('IMAGE_BASE64_BEGIN') + payload_lines = collect_base64_payload(dut) + + result_image = decode_bgr24_base64_image(metadata, payload_lines) + output_path = Path(dut.logdir) / IMAGE_OUTPUT_NAME + save_ppm_artifact(result_image, output_path) + assert_image_matches_golden(result_image, Path(__file__).with_name(GOLDEN_IMAGE_NAME)) + + dut.expect_exact('Async color convert visual demo done.')