diff --git a/components/esp_driver_jpeg/include/driver/jpeg_decode.h b/components/esp_driver_jpeg/include/driver/jpeg_decode.h index 16ea92bf782..a658ddfbb79 100644 --- a/components/esp_driver_jpeg/include/driver/jpeg_decode.h +++ b/components/esp_driver_jpeg/include/driver/jpeg_decode.h @@ -97,7 +97,9 @@ esp_err_t jpeg_decoder_get_info(const uint8_t *bit_stream, uint32_t stream_size, * returned through the `out_size` pointer. * * @note 1.Please make sure that the content of `bit_stream` pointer cannot be modified until this function returns. - * 2.Please note that the output size of image is always the multiple of 16 depends on protocol of JPEG. + * 2.For JPEGs encoded with YUV420 or YUV422 sampling, the decoded output dimensions can be padded + * to 16-pixel boundaries by the JPEG block layout. Make sure `decode_outbuf` is large enough for + * that padded output size, not only for the visible width and height. * * @param[in] decoder_engine Handle of the JPEG decoder instance to use for processing. * @param[in] decode_cfg Config structure of decoder. diff --git a/components/esp_driver_jpeg/jpeg_common.c b/components/esp_driver_jpeg/jpeg_common.c index 7edc931dba9..43459200f4a 100644 --- a/components/esp_driver_jpeg/jpeg_common.c +++ b/components/esp_driver_jpeg/jpeg_common.c @@ -23,6 +23,7 @@ #include "esp_log.h" #include "esp_check.h" #include "hal/jpeg_periph.h" +#include "esp_psram.h" #if JPEG_USE_RETENTION_LINK #include "esp_private/sleep_retention.h" #endif @@ -291,3 +292,17 @@ esp_err_t jpeg_check_intr_priority(jpeg_codec_handle_t jpeg_codec, int intr_prio ESP_RETURN_ON_FALSE(!intr_priority_conflict, ESP_ERR_INVALID_STATE, TAG, "intr_priority conflict, already is %d but attempt to %d", jpeg_codec->intr_priority, intr_priority); return ret; } + +bool jpeg_check_dma2d_buffer(const void *buffer) +{ +#if CONFIG_SECURE_FLASH_ENC_ENABLED + // jpeg cannot handle encrypted data. + if (esp_ptr_external_ram(buffer) && !esp_psram_ptr_is_no_enc(buffer)) { + return false; + } + if (esp_ptr_in_drom(buffer)) { + return false; + } +#endif + return true; +} diff --git a/components/esp_driver_jpeg/jpeg_decode.c b/components/esp_driver_jpeg/jpeg_decode.c index 41a713927fa..5e59d07fb48 100644 --- a/components/esp_driver_jpeg/jpeg_decode.c +++ b/components/esp_driver_jpeg/jpeg_decode.c @@ -17,6 +17,7 @@ #include "hal/cache_ll.h" #include "hal/cache_hal.h" #include "hal/jpeg_defs.h" +#include "hal/hal_utils.h" #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "freertos/semphr.h" @@ -287,6 +288,10 @@ esp_err_t jpeg_decoder_process(jpeg_decoder_handle_t decoder_engine, const jpeg_ ESP_RETURN_ON_FALSE(_check_buffer_alignment(decode_outbuf, outbuf_size, outbuf_cache_line_size), ESP_ERR_INVALID_ARG, TAG, "jpeg decode decode_outbuf or out_buffer size is not aligned, please use jpeg_alloc_decoder_mem to malloc your buffer"); + // both the bitstream and output buffer are accessed by the 2D-DMA + ESP_RETURN_ON_FALSE(jpeg_check_dma2d_buffer(bit_stream) && jpeg_check_dma2d_buffer(decode_outbuf), ESP_ERR_INVALID_ARG, TAG, + "jpeg decode buffer is not 16-byte aligned or not in unencrypted PSRAM, please use jpeg_alloc_decoder_mem to malloc your buffer"); + esp_err_t ret = ESP_OK; #if CONFIG_PM_ENABLE @@ -428,15 +433,21 @@ void *jpeg_alloc_decoder_mem(size_t size, const jpeg_decode_memory_alloc_cfg_t * FOr input buffer(for decoder is PSRAM write to 2DDMA), no restriction for any align (both cache writeback and requirement from 2DDMA). */ size_t cache_align = 0; + size_t buffer_align = 0; esp_cache_get_alignment(MALLOC_CAP_SPIRAM, &cache_align); - if (mem_cfg->buffer_direction == JPEG_DEC_ALLOC_OUTPUT_BUFFER) { - size = JPEG_ALIGN_UP(size, cache_align); - *allocated_size = size; - return heap_caps_aligned_calloc(cache_align, 1, size, MALLOC_CAP_SPIRAM); - } else { - *allocated_size = size; - return heap_caps_calloc(1, size, MALLOC_CAP_SPIRAM); + buffer_align = MAX(cache_align, JPEG_DMA2D_BUFFER_ALIGN); + size = JPEG_ALIGN_UP(size, buffer_align); + *allocated_size = size; + // To simplify the logic, we always use the LCM of cache and 2D-DMA alignment to satisfy both requirements + void *buffer = heap_caps_aligned_calloc(buffer_align, 1, size, JPEG_SPIRAM_ALLOC_CAPS); + if (buffer == NULL) { +#if CONFIG_SPIRAM_ENC_EXEMPT + ESP_LOGE(TAG, "no mem for %zu bytes decode buffer in unencrypted PSRAM, please enlarge CONFIG_SPIRAM_ENC_EXEMPT_SIZE", size); +#else + ESP_LOGE(TAG, "no mem for %zu bytes decode buffer", size); +#endif } + return buffer; } /**************************************************************** diff --git a/components/esp_driver_jpeg/jpeg_encode.c b/components/esp_driver_jpeg/jpeg_encode.c index 1d2df2afaeb..e7b3758d7e5 100644 --- a/components/esp_driver_jpeg/jpeg_encode.c +++ b/components/esp_driver_jpeg/jpeg_encode.c @@ -19,6 +19,7 @@ #include "hal/jpeg_ll.h" #include "hal/cache_hal.h" #include "hal/cache_ll.h" +#include "hal/hal_utils.h" #include "esp_private/dma2d.h" #include "jpeg_private.h" #include "driver/jpeg_encode.h" @@ -175,6 +176,8 @@ esp_err_t jpeg_encoder_process(jpeg_encoder_handle_t encoder_engine, const jpeg_ ESP_RETURN_ON_FALSE(bit_stream, ESP_ERR_INVALID_ARG, TAG, "jpeg encode output buffer is null"); ESP_RETURN_ON_FALSE(out_size, ESP_ERR_INVALID_ARG, TAG, "jpeg encode picture out_size is null"); ESP_RETURN_ON_FALSE(((uintptr_t)bit_stream % cache_hal_get_cache_line_size(CACHE_LL_LEVEL_EXT_MEM, CACHE_TYPE_DATA)) == 0, ESP_ERR_INVALID_ARG, TAG, "jpeg encode bit stream is not aligned, please use jpeg_alloc_encoder_mem to malloc your buffer"); + // both the input picture and output bitstream are accessed by the 2D-DMA + ESP_RETURN_ON_FALSE(jpeg_check_dma2d_buffer(encode_inbuf) && jpeg_check_dma2d_buffer(bit_stream), ESP_ERR_INVALID_ARG, TAG, "jpeg encode buffer is not 16-byte aligned or not in unencrypted PSRAM, please use jpeg_alloc_encoder_mem to malloc your buffer"); esp_err_t ret = ESP_OK; @@ -394,15 +397,21 @@ void *jpeg_alloc_encoder_mem(size_t size, const jpeg_encode_memory_alloc_cfg_t * For input buffer(for decoder is PSRAM write to 2DDMA), no restriction for any align (both cache writeback and requirement from 2DDMA). */ size_t cache_align = 0; + size_t buffer_align = 0; esp_cache_get_alignment(MALLOC_CAP_SPIRAM, &cache_align); - if (mem_cfg->buffer_direction == JPEG_ENC_ALLOC_OUTPUT_BUFFER) { - size = JPEG_ALIGN_UP(size, cache_align); - *allocated_size = size; - return heap_caps_aligned_calloc(cache_align, 1, size, MALLOC_CAP_SPIRAM); - } else { - *allocated_size = size; - return heap_caps_calloc(1, size, MALLOC_CAP_SPIRAM); + buffer_align = MAX(cache_align, JPEG_DMA2D_BUFFER_ALIGN); + size = JPEG_ALIGN_UP(size, buffer_align); + *allocated_size = size; + // To simplify the logic, we always use the LCM of cache and 2D-DMA alignment to satisfy both requirements + void *buffer = heap_caps_aligned_calloc(buffer_align, 1, size, JPEG_SPIRAM_ALLOC_CAPS); + if (buffer == NULL) { +#if CONFIG_SPIRAM_ENC_EXEMPT + ESP_LOGE(TAG, "no mem for %zu bytes encode buffer in unencrypted PSRAM, please enlarge CONFIG_SPIRAM_ENC_EXEMPT_SIZE", size); +#else + ESP_LOGE(TAG, "no mem for %zu bytes encode buffer", size); +#endif } + return buffer; } /**************************************************************** diff --git a/components/esp_driver_jpeg/jpeg_private.h b/components/esp_driver_jpeg/jpeg_private.h index 3a038ce321b..d4051d4bf00 100644 --- a/components/esp_driver_jpeg/jpeg_private.h +++ b/components/esp_driver_jpeg/jpeg_private.h @@ -31,6 +31,17 @@ extern "C" { #define JPEG_INTR_ALLOC_FLAG (ESP_INTR_FLAG_SHARED) #define JPEG_ALIGN_UP(num, align) (((num) + ((align) - 1)) & ~((align) - 1)) +// Buffers fed to the 2D-DMA must be at least 16-byte aligned. +#define JPEG_DMA2D_BUFFER_ALIGN 16 + +// The JPEG codec cannot work with encrypted buffer, because it deals with macro block. When an +// unencrypted PSRAM region is reserved (CONFIG_SPIRAM_ENC_EXEMPT), codec buffers +// must come from it; otherwise use normal PSRAM. +#if CONFIG_SPIRAM_ENC_EXEMPT +#define JPEG_SPIRAM_ALLOC_CAPS (MALLOC_CAP_SPIRAM_NO_ENC) +#else +#define JPEG_SPIRAM_ALLOC_CAPS (MALLOC_CAP_SPIRAM) +#endif // Use retention link only when the target supports sleep retention and PM is enabled #define JPEG_USE_RETENTION_LINK (CONFIG_PM_ENABLE && CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP) @@ -251,6 +262,18 @@ esp_err_t jpeg_isr_deregister(jpeg_codec_handle_t jpeg_codec, jpeg_isr_handler_t */ esp_err_t jpeg_check_intr_priority(jpeg_codec_handle_t jpeg_codec, int intr_priority); +/** + * @brief Validate a user buffer that will be accessed by the 2D-DMA + * + * The buffer must be 16-byte aligned. When CONFIG_SPIRAM_ENC_EXEMPT is enabled, + * a PSRAM buffer must reside in the unencrypted carve-out, since the 2D-DMA + * cannot access encrypted PSRAM. Internal RAM buffers are always accepted. + * + * @param buffer Buffer pointer provided by the user + * @return true if the buffer can be used by the 2D-DMA, false otherwise + */ +bool jpeg_check_dma2d_buffer(const void *buffer); + /** * @brief Create sleep retention link * diff --git a/components/esp_driver_jpeg/test_apps/jpeg_test_apps/CMakeLists.txt b/components/esp_driver_jpeg/test_apps/jpeg_test_apps/CMakeLists.txt index 4dd2a1896dc..28a4b082fae 100644 --- a/components/esp_driver_jpeg/test_apps/jpeg_test_apps/CMakeLists.txt +++ b/components/esp_driver_jpeg/test_apps/jpeg_test_apps/CMakeLists.txt @@ -9,8 +9,8 @@ set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/test_apps/components") include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(jpeg_test) -target_add_binary_data(jpeg_test.elf "${IDF_PATH}/examples/peripherals/jpeg/jpeg_decode/resources/esp720.jpg" BINARY) -target_add_binary_data(jpeg_test.elf "${IDF_PATH}/examples/peripherals/jpeg/jpeg_decode/resources/esp1080.jpg" BINARY) +target_add_binary_data(jpeg_test.elf "resources/esp720.jpg" BINARY) +target_add_binary_data(jpeg_test.elf "resources/esp1080.jpg" BINARY) target_add_binary_data(jpeg_test.elf "resources/no_huff.jpg" BINARY) target_add_binary_data(jpeg_test.elf "resources/esp480.rgb" BINARY) diff --git a/examples/peripherals/jpeg/jpeg_decode/resources/esp1080.jpg b/components/esp_driver_jpeg/test_apps/jpeg_test_apps/resources/esp1080.jpg similarity index 100% rename from examples/peripherals/jpeg/jpeg_decode/resources/esp1080.jpg rename to components/esp_driver_jpeg/test_apps/jpeg_test_apps/resources/esp1080.jpg diff --git a/examples/peripherals/jpeg/jpeg_decode/resources/esp720.jpg b/components/esp_driver_jpeg/test_apps/jpeg_test_apps/resources/esp720.jpg similarity index 100% rename from examples/peripherals/jpeg/jpeg_decode/resources/esp720.jpg rename to components/esp_driver_jpeg/test_apps/jpeg_test_apps/resources/esp720.jpg diff --git a/components/esp_psram/system_layer/esp_psram.c b/components/esp_psram/system_layer/esp_psram.c index 573c589867c..089e74fca76 100644 --- a/components/esp_psram/system_layer/esp_psram.c +++ b/components/esp_psram/system_layer/esp_psram.c @@ -19,6 +19,7 @@ #include "freertos/FreeRTOS.h" #include "esp_heap_caps_init.h" #include "esp_psram.h" +#include "esp_macros.h" #include "esp_mmu_map.h" #include "hal/mmu_hal.h" #include "hal/mmu_ll.h" diff --git a/components/hal/esp32s31/include/hal/mmu_ll.h b/components/hal/esp32s31/include/hal/mmu_ll.h index 75b9bd55365..40a95f6ec6c 100644 --- a/components/hal/esp32s31/include/hal/mmu_ll.h +++ b/components/hal/esp32s31/include/hal/mmu_ll.h @@ -588,6 +588,24 @@ static inline uint32_t mmu_ll_entry_id_to_vaddr_base(uint32_t mmu_id, uint32_t e return mmu_ll_laddr_to_vaddr(laddr, type, (mmu_id == MMU_LL_FLASH_MMU_ID) ? MMU_TARGET_FLASH0 : MMU_TARGET_PSRAM0); } +/** + * Write a PSRAM MMU entry without the SENSITIVE bit, used only for the + * carved-out unencrypted region (see CONFIG_SPIRAM_ENC_EXEMPT). + * + * No anti-FI check: the SENSITIVE bit is intentionally clear, and an FI flip + * that sets it would force decryption of plaintext data (garbage, fails safe). + */ +__attribute__((always_inline)) static inline void mmu_ll_write_entry_no_enc(uint32_t mmu_id, uint32_t entry_id, uint32_t mmu_val) +{ + HAL_ASSERT(mmu_id == MMU_LL_PSRAM_MMU_ID); + + mmu_val |= SOC_MMU_PSRAM_VALID; + mmu_val |= SOC_MMU_ACCESS_PSRAM; + + REG_WRITE(SPI_MEM_S_MMU_ITEM_INDEX_REG, entry_id); + REG_WRITE(SPI_MEM_S_MMU_ITEM_CONTENT_REG, mmu_val); +} + #ifdef __cplusplus } #endif diff --git a/components/soc/esp32s31/include/soc/Kconfig.soc_caps.in b/components/soc/esp32s31/include/soc/Kconfig.soc_caps.in index 023070cf68a..27dfec13fc5 100644 --- a/components/soc/esp32s31/include/soc/Kconfig.soc_caps.in +++ b/components/soc/esp32s31/include/soc/Kconfig.soc_caps.in @@ -1271,6 +1271,10 @@ config SOC_FLASH_ENCRYPTED_XTS_AES_BLOCK_MAX int default 64 +config SOC_PSRAM_ENCRYPTION_PAGE_CONFIGURABLE + bool + default y + config SOC_RECOVERY_BOOTLOADER_SUPPORTED bool default y diff --git a/components/soc/esp32s31/include/soc/soc_caps.h b/components/soc/esp32s31/include/soc/soc_caps.h index f0549b97bc7..30b88f16ded 100644 --- a/components/soc/esp32s31/include/soc/soc_caps.h +++ b/components/soc/esp32s31/include/soc/soc_caps.h @@ -473,6 +473,9 @@ #define SOC_FLASH_ENCRYPTION_XTS_AES_SUPPORT_PSEUDO_ROUND 1 #define SOC_FLASH_ENCRYPTED_XTS_AES_BLOCK_MAX (64) +/*-------------------------- PSRAM Encryption CAPS----------------------------*/ +#define SOC_PSRAM_ENCRYPTION_PAGE_CONFIGURABLE 1 /* PSRAM encryption can be configured on a MMU page basis */ + /*------------------------Bootloader CAPS---------------------------------*/ /* Support Recovery Bootloader */ #define SOC_RECOVERY_BOOTLOADER_SUPPORTED (1) diff --git a/docs/en/api-reference/peripherals/jpeg.rst b/docs/en/api-reference/peripherals/jpeg.rst index b999d838148..48d938ae5b1 100644 --- a/docs/en/api-reference/peripherals/jpeg.rst +++ b/docs/en/api-reference/peripherals/jpeg.rst @@ -25,6 +25,7 @@ This document covers the following sections: - :ref:`jpeg-pixel-storage-layout` - covers color space order overview required in this JPEG decoder and encoder. - :ref:`jpeg-thread-safety` - lists which APIs are guaranteed to be thread safe by the driver. - :ref:`jpeg-power-management` - describes how JPEG driver would be affected by power consumption. +- :ref:`jpeg-flash-encryption` - describes how to use the JPEG codec correctly when flash/PSRAM encryption is enabled. - :ref:`jpeg-kconfig-options` - lists the supported Kconfig options that can bring different effects to the driver. .. _jpeg-resource-allocation: @@ -136,18 +137,13 @@ Overall, You can take following code as reference, the code is going to decode a .rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_BGR, }; - size_t tx_buffer_size; size_t rx_buffer_size; jpeg_decode_memory_alloc_cfg_t rx_mem_cfg = { .buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER, }; - jpeg_decode_memory_alloc_cfg_t tx_mem_cfg = { - .buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER, - }; - - uint8_t *bit_stream = (uint8_t*)jpeg_alloc_decoder_mem(jpeg_size, &tx_mem_cfg, &tx_buffer_size); + const uint8_t *bit_stream = embedded_jpeg_start; uint8_t *out_buf = (uint8_t*)jpeg_alloc_decoder_mem(1920 * 1088 * 3, &rx_mem_cfg, &rx_buffer_size); jpeg_decode_picture_info_t header_info; @@ -158,11 +154,11 @@ Overall, You can take following code as reference, the code is going to decode a There are some tips that can help you use this driver more accurately: -1. In above code, you should make sure the `bit_stream` and `out_buf` should be aligned by certain rules. We provide a helper function :cpp:func:`jpeg_alloc_decoder_mem` to help you malloc a buffer which is aligned in both size and address. +1. In above code, you should make sure the output buffer `out_buf` follows the driver's alignment requirements. We provide a helper function :cpp:func:`jpeg_alloc_decoder_mem` to help you allocate a buffer with aligned size and address. -2. The content of `bit_stream` buffer should not be changed until :cpp:func:`jpeg_decoder_process` returns. +2. The content of `bit_stream` should not be changed until :cpp:func:`jpeg_decoder_process` returns. This input buffer can come directly from flash-mapped embedded data or any other memory region that stays readable for the full call. -3. The width and height of output picture would be 16 bytes aligned if original picture is compressed by YUV420 or YUV422. For example, if the input picture is 1080*1920, the output picture will be 1088*1920. That is the restriction of jpeg protocol. Please provide sufficient output buffer memory. +3. If the source JPEG uses YUV420 or YUV422 sampling, the decoded output dimensions can be padded up to 16-pixel boundaries. For example, if the visible image size is 1080*1920, the decoder may require an output buffer sized for 1088*1920 pixels. This comes from the JPEG block layout, so please provide enough output buffer memory for the padded image, not only for the visible width and height. .. _jpeg-encoder-engine: @@ -573,6 +569,24 @@ When power management is enabled (i.e., :ref:`CONFIG_PM_ENABLE` is set), the sys Whenever the user is decoding or encoding via JPEG (i.e., calling :cpp:func:`jpeg_encoder_process` or :cpp:func:`jpeg_decoder_process`), the driver guarantees that the power management lock is acquired by setting it to :cpp:enumerator:`esp_pm_lock_type_t::ESP_PM_CPU_FREQ_MAX`. Once the encoding or decoding is finished, the driver releases the lock and the system can enter Light-sleep. +.. _jpeg-flash-encryption: + +Usage Under Encryption +^^^^^^^^^^^^^^^^^^^^^^ + +The JPEG codec moves data via the 2D-DMA, and the JPEG codec **cannot process encrypted data**. Therefore, when PSRAM encryption is enabled, the JPEG input/output buffers must reside in an unencrypted memory region, otherwise encoding/decoding fails. + +To support the encrypted scenario, the driver does the following: + +- When ``CONFIG_SPIRAM_ENC_EXEMPT`` is enabled, :cpp:func:`jpeg_alloc_decoder_mem` and :cpp:func:`jpeg_alloc_encoder_mem` allocate buffers from the unencrypted PSRAM region (``MALLOC_CAP_SPIRAM_NO_ENC``) automatically. +- The allocated buffers satisfy both the cache line alignment and the byte alignment required by the 2D-DMA. + +Please note the following when using it: + +1. It is recommended to always allocate buffers via :cpp:func:`jpeg_alloc_encoder_mem` / :cpp:func:`jpeg_alloc_decoder_mem` to ensure correct alignment and memory region. + +2. The size of the unencrypted region is determined by ``CONFIG_SPIRAM_ENC_EXEMPT_SIZE``. Since the JPEG buffer size depends on the image resolution and cannot be predicted automatically, configure it according to the largest image you actually process. If the region is insufficient, the allocation fails and an error log is printed, suggesting to enlarge ``CONFIG_SPIRAM_ENC_EXEMPT_SIZE``. Also note that this value must not be greater than or equal to the actual PSRAM size, otherwise the unencrypted region is disabled. + .. _jpeg-kconfig-options: Kconfig Options @@ -594,7 +608,7 @@ The JPEG driver usage of hardware resources and its dependency status are shown Application Examples -------------------- -- :example:`peripherals/jpeg/jpeg_decode` demonstrates how to use the JPEG hardware decoder to decode JPEG pictures of different sizes (1080p and 720p) into RGB format, showcasing the flexibility and speed of hardware decoding. +- :example:`peripherals/jpeg/jpeg_decode` demonstrates how to use the JPEG hardware decoder to parse one embedded JPEG, decode it into RGB888, stream the raw output as base64 over UART, and validate the result with pytest. - :example:`peripherals/jpeg/jpeg_encode` demonstrates how to use the JPEG hardware encoder to encode an embedded 720p raw picture, stream the JPEG as base64 over UART, and validate the result with pytest. diff --git a/docs/zh_CN/api-reference/peripherals/jpeg.rst b/docs/zh_CN/api-reference/peripherals/jpeg.rst index 40beeec7192..f53c1e35cd4 100644 --- a/docs/zh_CN/api-reference/peripherals/jpeg.rst +++ b/docs/zh_CN/api-reference/peripherals/jpeg.rst @@ -25,6 +25,7 @@ JPEG 常用于数字图像,尤其是数码摄影图像的有损压缩。压缩 - :ref:`jpeg-pixel-storage-layout`,涵盖了 JPEG 解码器和编码器所需的颜色空间顺序。 - :ref:`jpeg-thread-safety`,列出了驱动程序能保证线程安全的 API。 - :ref:`jpeg-power-management`,描述了影响 JPEG 驱动程序功耗的因素。 +- :ref:`jpeg-flash-encryption`,介绍了在 flash/PSRAM 加密场景下如何正确使用 JPEG 编解码器。 - :ref:`jpeg-kconfig-options`,列出了支持的 Kconfig 选项,可以为驱动程序带来不同的效果。 .. _jpeg-resource-allocation: @@ -136,18 +137,13 @@ JPEG 解码器引擎 .rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_BGR, }; - size_t tx_buffer_size; size_t rx_buffer_size; jpeg_decode_memory_alloc_cfg_t rx_mem_cfg = { .buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER, }; - jpeg_decode_memory_alloc_cfg_t tx_mem_cfg = { - .buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER, - }; - - uint8_t *bit_stream = (uint8_t*)jpeg_alloc_decoder_mem(jpeg_size, &tx_mem_cfg, &tx_buffer_size); + const uint8_t *bit_stream = embedded_jpeg_start; uint8_t *out_buf = (uint8_t*)jpeg_alloc_decoder_mem(1920 * 1088 * 3, &rx_mem_cfg, &rx_buffer_size); jpeg_decode_picture_info_t header_info; @@ -158,11 +154,11 @@ JPEG 解码器引擎 参考以下提示,可以更准确地使用该驱动程序: -1. 在上述代码中,应确保 `bit_stream` 和 `out_buf` 按照一定的规则对齐。可以通过 :cpp:func:`jpeg_alloc_decoder_mem` 函数来分配一个在大小和地址上都对齐的缓冲区。 +1. 在上述代码中,应确保输出缓冲区 `out_buf` 满足驱动的对齐要求。可以通过 :cpp:func:`jpeg_alloc_decoder_mem` 函数来分配一个在大小和地址上都对齐的缓冲区。 -2. 在 :cpp:func:`jpeg_decoder_process` 返回前, `bit_stream` 缓冲区的内容不应有更改。 +2. 在 :cpp:func:`jpeg_decoder_process` 返回前, `bit_stream` 指向的输入内容不应有更改。该输入缓冲区既可以直接来自映射到 flash 的嵌入式数据,也可以来自其他在整个调用期间保持可读的内存区域。 -3. 如果原始图片以 YUV420 或 YUV422 格式压缩,则输出图片的宽度和高度将会以 16 字节对齐。例如,如果输入图片大小为 1080*1920,则输出图片大小为 1088*1920。这是 jpeg 协议的限制,所以请准备足够的输出缓冲区内存。 +3. 如果源 JPEG 使用 YUV420 或 YUV422 采样方式,解码后的输出图像尺寸可能会被补齐到 16 像素边界。例如,当可见图像大小为 1080*1920 时,解码器可能需要按 1088*1920 像素来分配输出缓冲区。这来自 JPEG 的块布局限制,因此请按补齐后的图像尺寸而不是仅按可见宽高准备足够的输出缓冲区内存。 .. _jpeg-encoder-engine: @@ -573,6 +569,24 @@ YUV420 每当用户通过 JPEG 进行解码或编码(即调用 :cpp:func:`jpeg_encoder_process` 或 :cpp:func:`jpeg_decoder_process`)时,驱动程序会将电源管理设定为 :cpp:enumerator:`esp_pm_lock_type_t::ESP_PM_CPU_FREQ_MAX`,确保获取电源管理锁。一旦编码或解码完成,驱动程序将释放锁,则系统可以进入 Light-sleep 模式。 +.. _jpeg-flash-encryption: + +加密场景下的使用 +^^^^^^^^^^^^^^^^ + +JPEG 编解码器通过 2D-DMA 搬运数据,而 JPEG 编解码器 **无法处理已加密的数据**。因此在开启 PSRAM 加密时,需要让 JPEG 的输入/输出缓冲区位于非加密的内存区域,否则编解码会失败。 + +为支持加密场景,驱动程序做了如下处理: + +- 当启用 ``CONFIG_SPIRAM_ENC_EXEMPT`` 时, :cpp:func:`jpeg_alloc_decoder_mem` 和 :cpp:func:`jpeg_alloc_encoder_mem` 会自动从非加密 PSRAM 区域(``MALLOC_CAP_SPIRAM_NO_ENC``)分配缓冲区。 +- 分配的缓冲区会同时满足 cache 行对齐与 2D-DMA 的字节对齐要求。 + +使用时请注意: + +1. 建议始终通过 :cpp:func:`jpeg_alloc_encoder_mem` / :cpp:func:`jpeg_alloc_decoder_mem` 分配缓冲区,以保证对齐与内存区域正确。 + +2. 非加密区的大小由 ``CONFIG_SPIRAM_ENC_EXEMPT_SIZE`` 决定。由于 JPEG 缓冲区大小取决于图像分辨率,无法自动预测,需根据实际处理的最大图像自行配置。若该区域不足,分配会失败并打印错误日志,提示增大 ``CONFIG_SPIRAM_ENC_EXEMPT_SIZE``;同时注意该值不能大于等于实际 PSRAM 容量,否则非加密区会被禁用。 + .. _jpeg-kconfig-options: Kconfig 选项 @@ -594,7 +608,7 @@ Kconfig 选项 应用程序示例 ------------ -- :example:`peripherals/jpeg/jpeg_decode` 演示了如何使用 JPEG 硬件解码器将不同大小的 JPEG 图片(1080p 和 720p)解码为 RGB 格式,展示了硬件解码的速度和灵活性。 +- :example:`peripherals/jpeg/jpeg_decode` 演示了如何使用 JPEG 硬件解码器解析一张嵌入式 JPEG,将其解码为 RGB888,通过 UART 输出 base64 原始结果,并使用 pytest 做回归校验。 - :example:`peripherals/jpeg/jpeg_encode` 演示了如何使用 JPEG 硬件编码器对一张嵌入式 720p 原始图像进行编码,并通过 UART 输出 base64 JPEG,再用 pytest 做结果校验。 diff --git a/examples/peripherals/.build-test-rules.yml b/examples/peripherals/.build-test-rules.yml index 14c607dde44..41a59e33b9b 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -298,7 +298,7 @@ examples/peripherals/isp/multi_pipelines: examples/peripherals/jpeg/jpeg_decode: disable: - - if: SOC_JPEG_CODEC_SUPPORTED != 1 or SOC_SDMMC_HOST_SUPPORTED != 1 + - if: SOC_JPEG_DECODE_SUPPORTED != 1 depends_components: - esp_driver_dma - esp_hal_jpeg diff --git a/examples/peripherals/jpeg/jpeg_decode/CMakeLists.txt b/examples/peripherals/jpeg/jpeg_decode/CMakeLists.txt index 991a6f74668..9c93839d5b4 100644 --- a/examples/peripherals/jpeg/jpeg_decode/CMakeLists.txt +++ b/examples/peripherals/jpeg/jpeg_decode/CMakeLists.txt @@ -5,4 +5,4 @@ cmake_minimum_required(VERSION 3.22) include($ENV{IDF_PATH}/tools/cmake/project.cmake) # "Trim" the build. Include the minimal set of components, main, and anything it depends on. idf_build_set_property(MINIMAL_BUILD ON) -project(jpeg_decode) +project(jpeg_decode_example) diff --git a/examples/peripherals/jpeg/jpeg_decode/README.md b/examples/peripherals/jpeg/jpeg_decode/README.md index dd94d672b81..24cb0ca5947 100644 --- a/examples/peripherals/jpeg/jpeg_decode/README.md +++ b/examples/peripherals/jpeg/jpeg_decode/README.md @@ -5,21 +5,25 @@ ## Overview -This example demonstrates how to use the JPEG hardware decoder to decode a 1080p and a 720p picture: +This example demonstrates how to use the JPEG hardware decoder to decode one embedded JPEG image into `RGB888` raw bytes in default `BGR24` order. -If you have a bunch of big JPEG picture need to be decoded, such as `*.jpg` -> `*.rgb`, and this example uses hardware JPEG decoder to accelerate the decoding. +The example performs: -## How to use example +- Embedding `main/assets/image.jpg` into the application image +- Letting the JPEG decoder read the embedded JPEG bitstream directly from flash +- Parsing the JPEG header with `jpeg_decoder_get_info()` +- Decoding the image into an `RGB888` output buffer with default `BGR24` byte order +- Allocating the output buffer for padded dimensions when the JPEG block layout rounds width or height up to 16-pixel boundaries +- Base64-encoding the decoded raw pixels and printing them with machine-parseable UART markers +- Letting pytest rebuild `jpeg_decode_result.ppm` for inspection and compare it against `golden_output.ppm` -### Prerequisites Required +## Hardware Required -This example demonstrates the flexibility of decoding pictures by decoding two different sizes: one in 1080p and another in 720p. It showcases how you can easily modify the code to meet your specific requirements, such as only decoding 1080p photos. +Any board based on a supported target can be used. No SD card or external storage setup is required. -### Build and Flash +## Build and Flash -Before you start build and flash this example, please put the image `esp720.jpg` and `esp1080.jpg` in your sdcard. - -Enter `idf.py -p PORT flash monitor` to build, flash and monitor the project. +Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. (To exit the serial monitor, type ``Ctrl-]``.) @@ -27,32 +31,53 @@ See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/l ## Example Output -```bash -I (1116) jpeg.example: Initializing SD card -I (1116) gpio: GPIO[43]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 -I (1126) gpio: GPIO[44]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 -I (1136) gpio: GPIO[39]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 -I (1146) gpio: GPIO[40]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 -I (1156) gpio: GPIO[41]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 -I (1166) gpio: GPIO[42]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 -I (1416) gpio: GPIO[42]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 -Name: SD64G -Type: SDHC/SDXC -Speed: 40.00 MHz (limit: 40.00 MHz) -Size: 60906MB -CSD: ver=2, sector_size=512, capacity=124735488 read_bl_len=9 -SSR: bus_width=4 -I (1436) jpeg.example: jpg_file_1080:/sdcard/esp1080.jpg -I (1696) jpeg.example: jpg_file_1080:/sdcard/esp720.jpg -I (1796) jpeg.example: header parsed, width is 1920, height is 1080 -I (1846) jpeg.example: raw_file_1080:/sdcard/out.rgb -I (11836) jpeg.example: raw_file_720:/sdcard/out2.rgb -I (13336) jpeg.example: Card unmounted -I (13336) main_task: Returned from app_main() +```text +Loading embedded JPEG from flash... +Embedded JPEG size: 43700 bytes +JPEG header parsed: width=320 height=240 +Decoding JPEG -> RGB888... +Decoded RGB888 size: 245760 bytes +JPEG_DECODE_INFO width=320 height=240 padded_width=320 padded_height=256 format=RGB888 encoding=base64 size=245760 +JPEG_DECODE_BASE64_BEGIN +JPEG_DECODE_BASE64 ... +JPEG_DECODE_BASE64 ... +JPEG_DECODE_BASE64_END +JPEG decode demo done. ``` -Also, the helper script [open_raw_picture.py](./open_raw_picture.py) simplifies the visualization of the output on your computer. For this to work, go to `examples/peripheral/jpeg/jpeg_decode` and install the requirements by running `pip install -r requirements.txt`. +`padded_width` and `padded_height` report the actual decoded buffer dimensions. For JPEGs whose block layout pads the output to 16-pixel boundaries, these values can be larger than the visible `width` and `height`, so the output buffer must be sized for the padded image. + +## Pytest Regression Check + +The accompanying `pytest_jpeg_decode.py` script waits for the `JPEG_DECODE_INFO` and `JPEG_DECODE_BASE64` markers, reconstructs the decoded raw pixel output, crops away padded rows, and saves the visible image as: + +- `dut.logdir/jpeg_decode_result.ppm` + +The test writes the `PPM` file and compares it with `golden_output.ppm`. This makes the example both a functional regression test and a host-side artifact generator for inspection. + +## Running Pytest Locally And Viewing The Image + +To run the pytest helper locally on hardware, build the example for your target first, then invoke the test script with the target and serial port: + +```bash +idf.py set-target esp32p4 build +pytest --target esp32p4 --port PORT pytest_jpeg_decode.py +``` + +Replace `esp32p4` with another supported target such as `esp32s31` when needed. + +`pytest-embedded` stores per-test logs under `$IDF_PATH/pytest-embedded/`. The script writes the reconstructed image to `jpeg_decode_result.ppm` inside that test log directory, so after the test finishes you can open the generated `PPM` file locally with an image viewer that supports `PPM` to inspect the decoded output. + +## Replacing The Embedded JPEG Asset + +If you want to try another input image, replace: + +- `main/assets/image.jpg` + +Keep the replacement as a baseline JPEG with the same general scale if you want UART log volume and test runtime to stay small. + +After replacing the asset, rerun the example and update `golden_output.ppm` from the generated `dut.logdir/jpeg_decode_result.ppm` artifact if the new output should become the expected result. ## 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.) \ No newline at end of file +(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/jpeg/jpeg_decode/golden_output.ppm b/examples/peripherals/jpeg/jpeg_decode/golden_output.ppm new file mode 100644 index 00000000000..3face34b43d Binary files /dev/null and b/examples/peripherals/jpeg/jpeg_decode/golden_output.ppm differ diff --git a/examples/peripherals/jpeg/jpeg_decode/main/CMakeLists.txt b/examples/peripherals/jpeg/jpeg_decode/main/CMakeLists.txt index 96f77a9d2c8..1d366ad37fe 100644 --- a/examples/peripherals/jpeg/jpeg_decode/main/CMakeLists.txt +++ b/examples/peripherals/jpeg/jpeg_decode/main/CMakeLists.txt @@ -1,3 +1,5 @@ -idf_component_register(SRCS "jpeg_decode_main.c" - PRIV_REQUIRES fatfs esp_driver_jpeg esp_psram - INCLUDE_DIRS ".") +idf_component_register(SRCS "jpeg_decode_example_main.c" + PRIV_REQUIRES esp_driver_jpeg mbedtls + INCLUDE_DIRS ".") + +target_add_binary_data(${COMPONENT_LIB} "${CMAKE_CURRENT_LIST_DIR}/assets/image.jpg" BINARY RENAME_TO "example_jpeg") diff --git a/examples/peripherals/jpeg/jpeg_decode/main/Kconfig.projbuild b/examples/peripherals/jpeg/jpeg_decode/main/Kconfig.projbuild deleted file mode 100644 index f6154c38edd..00000000000 --- a/examples/peripherals/jpeg/jpeg_decode/main/Kconfig.projbuild +++ /dev/null @@ -1,18 +0,0 @@ -menu "JPEG Decode Example menu" - - config EXAMPLE_FORMAT_IF_MOUNT_FAILED - bool "Format the card if mount failed" - default n - help - If this config item is set, format_if_mount_failed will be set to true and the card will be formatted if - the mount has failed. - - config EXAMPLE_SDMMC_IO_POWER_INTERNAL_LDO - depends on SOC_SDMMC_IO_POWER_EXTERNAL - bool "SDMMC IO power supply comes from internal LDO (READ HELP!)" - default y - help - Please read the schematic first and check if the SDMMC VDD is connected to any internal LDO output. - If the SDMMC is powered by an external supplier, unselect me - -endmenu diff --git a/examples/peripherals/jpeg/jpeg_decode/main/assets/image.jpg b/examples/peripherals/jpeg/jpeg_decode/main/assets/image.jpg new file mode 100644 index 00000000000..cacad090507 Binary files /dev/null and b/examples/peripherals/jpeg/jpeg_decode/main/assets/image.jpg differ diff --git a/examples/peripherals/jpeg/jpeg_decode/main/jpeg_decode_example_main.c b/examples/peripherals/jpeg/jpeg_decode/main/jpeg_decode_example_main.c new file mode 100644 index 00000000000..d545841f9f3 --- /dev/null +++ b/examples/peripherals/jpeg/jpeg_decode/main/jpeg_decode_example_main.c @@ -0,0 +1,154 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include +#include +#include +#include +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/jpeg_decode.h" +#include "mbedtls/base64.h" + +#define EXAMPLE_BASE64_CHUNK_LEN 96 +#define EXAMPLE_BYTES_PER_PIXEL 3 + +/* These linker symbols are generated automatically for the file added by + * EMBED_FILES in CMakeLists.txt. They let the example treat the embedded + * JPEG asset as a byte array stored in flash. */ +extern const uint8_t example_jpeg_start[] asm("_binary_example_jpeg_start"); +extern const uint8_t example_jpeg_end[] asm("_binary_example_jpeg_end"); + +/* For JPEGs encoded with block-based chroma subsampling, the decoder output + * buffer dimensions can be padded up to 16-pixel boundaries. This helper + * rounds visible width or height up to that padded size. */ +static uint32_t align_up_to_16(uint32_t value) +{ + return (value + 15U) & ~15U; +} + +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("JPEG_DECODE_BASE64_BEGIN\n"); + size_t chunk_idx = 0; + for (size_t offset = 0; offset < encoded_len; offset += EXAMPLE_BASE64_CHUNK_LEN, ++chunk_idx) { + size_t chunk_len = encoded_len - offset; + if (chunk_len > EXAMPLE_BASE64_CHUNK_LEN) { + chunk_len = EXAMPLE_BASE64_CHUNK_LEN; + } + printf("JPEG_DECODE_BASE64 %.*s\n", (int)chunk_len, (const char *)&encoded[offset]); + /* Yield periodically to avoid watchdog triggers on long payloads. */ + if ((chunk_idx % 16U) == 15U) { + vTaskDelay(1); + } + } + printf("JPEG_DECODE_BASE64_END\n"); +} + +void app_main(void) +{ + const size_t embedded_size = example_jpeg_end - example_jpeg_start; + jpeg_decoder_handle_t jpeg_handle = NULL; + uint8_t *decoded_pixels = NULL; + uint8_t *input_buf = NULL; + unsigned char *encoded = NULL; + + printf("Loading embedded JPEG from flash...\n"); + printf("Embedded JPEG size: %zu bytes\n", embedded_size); + + /* Parse the JPEG header to learn the image dimensions. */ + jpeg_decode_picture_info_t header_info; + ESP_ERROR_CHECK(jpeg_decoder_get_info(example_jpeg_start, embedded_size, &header_info)); + printf("JPEG header parsed: width=%" PRIu32 " height=%" PRIu32 "\n", header_info.width, header_info.height); + + /* The hardware decoder can pad the output image dimensions up to + * 16-pixel boundaries, so the actual buffer may be larger than + * width * height * 3. Allocate for the padded dimensions to avoid + * out-of-bounds writes. */ + const uint32_t padded_width = align_up_to_16(header_info.width); + const uint32_t padded_height = align_up_to_16(header_info.height); + const uint32_t output_bytes = padded_width * padded_height * EXAMPLE_BYTES_PER_PIXEL; + + /* Ask the driver to allocate an output buffer for decoded pixels. + * JPEG_DEC_ALLOC_OUTPUT_BUFFER tells the driver this memory will hold + * the decode result (as opposed to an input bitstream buffer). */ + jpeg_decode_memory_alloc_cfg_t mem_cfg = { + .buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER, + }; + size_t decoded_buffer_size = 0; + decoded_pixels = (uint8_t *)jpeg_alloc_decoder_mem(output_bytes, &mem_cfg, &decoded_buffer_size); + assert(decoded_pixels != NULL); + + /* Create a decoder engine instance. The timeout_ms value is the maximum + * time the hardware is allowed to spend on a single decode call. */ + jpeg_decode_engine_cfg_t decode_eng_cfg = { + .timeout_ms = 80, + }; + ESP_ERROR_CHECK(jpeg_new_decoder_engine(&decode_eng_cfg, &jpeg_handle)); + + /* RGB888 outputs 3 bytes per pixel. BGR order matches the default byte + * layout expected by OpenCV and many display pipelines. */ + jpeg_decode_cfg_t decode_cfg = { + .output_format = JPEG_DECODE_OUT_FORMAT_RGB888, + .rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_BGR, + }; + + /* jpeg don't handle the encrypted data.*/ + const uint8_t *bit_stream = example_jpeg_start; +#if CONFIG_SECURE_FLASH_ENC_ENABLED + size_t input_buffer_size = 0; + jpeg_decode_memory_alloc_cfg_t in_mem_cfg = { + .buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER, + }; + input_buf = (uint8_t *)jpeg_alloc_decoder_mem(embedded_size, &in_mem_cfg, &input_buffer_size); + assert(input_buf != NULL); + memcpy(input_buf, example_jpeg_start, embedded_size); + bit_stream = input_buf; +#endif + + uint32_t decoded_size = 0; + printf("Decoding JPEG -> RGB888...\n"); + ESP_ERROR_CHECK(jpeg_decoder_process( + jpeg_handle, + &decode_cfg, + bit_stream, + embedded_size, + decoded_pixels, + decoded_buffer_size, + &decoded_size + )); + printf("Decoded RGB888 size: %" PRIu32 " bytes\n", decoded_size); + + /* Base64 turns the binary pixel data into printable ASCII so it can be + * safely transported through the serial console and reconstructed by + * pytest. The two-pass pattern (first call with NULL output to get the + * required buffer size, then the real encode) is standard mbedtls usage. */ + size_t encoded_len = 0; + int ret = mbedtls_base64_encode(NULL, 0, &encoded_len, decoded_pixels, decoded_size); + ESP_ERROR_CHECK((ret == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) ? ESP_OK : ESP_FAIL); + encoded = calloc(encoded_len + 1, 1); + assert(encoded != NULL); + ESP_ERROR_CHECK(mbedtls_base64_encode(encoded, encoded_len + 1, &encoded_len, decoded_pixels, decoded_size) == 0 ? ESP_OK : ESP_FAIL); + + /* JPEG_DECODE_INFO plus the chunked JPEG_DECODE_BASE64 lines form a tiny + * text protocol that the pytest script understands and converts back + * into a PPM golden file for comparison. */ + printf("JPEG_DECODE_INFO width=%" PRIu32 " height=%" PRIu32 + " padded_width=%" PRIu32 " padded_height=%" PRIu32 + " format=RGB888 encoding=base64 size=%" PRIu32 "\n", + header_info.width, header_info.height, padded_width, padded_height, decoded_size); + print_base64_payload(encoded, encoded_len); + printf("JPEG decode demo done.\n"); + + ESP_ERROR_CHECK(jpeg_del_decoder_engine(jpeg_handle)); + free(encoded); + free(decoded_pixels); + free(input_buf); +} diff --git a/examples/peripherals/jpeg/jpeg_decode/main/jpeg_decode_main.c b/examples/peripherals/jpeg/jpeg_decode/main/jpeg_decode_main.c deleted file mode 100644 index fff835472e0..00000000000 --- a/examples/peripherals/jpeg/jpeg_decode/main/jpeg_decode_main.c +++ /dev/null @@ -1,210 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Unlicense OR CC0-1.0 - */ -#include -#include -#include "esp_heap_caps.h" -#include "esp_vfs_fat.h" -#include "sdmmc_cmd.h" -#include "driver/sdmmc_host.h" -#include "esp_attr.h" -#include "driver/jpeg_decode.h" -#include "sd_pwr_ctrl_by_on_chip_ldo.h" - -static const char *TAG = "jpeg.example"; -static sdmmc_card_t *s_card; -#define MOUNT_POINT "/sdcard" - -const static char jpg_file_1080[] = "/sdcard/esp1080.jpg"; -const static char raw_file_1080[] = "/sdcard/out.rgb"; -const static char jpg_file_720[] = "/sdcard/esp720.jpg"; -const static char raw_file_720[] = "/sdcard/out2.rgb"; - -#if CONFIG_IDF_TARGET_ESP32S31 -#define TIMEOUT_MS 80 -#else -#define TIMEOUT_MS 40 -#endif - -static esp_err_t sdcard_init(void) -{ - esp_err_t ret = ESP_OK; - esp_vfs_fat_sdmmc_mount_config_t mount_config = { -#ifdef CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED - .format_if_mount_failed = true, -#else - .format_if_mount_failed = false, -#endif // EXAMPLE_FORMAT_IF_MOUNT_FAILED - .max_files = 5, - .allocation_unit_size = 16 * 1024 - }; - const char mount_point[] = MOUNT_POINT; - ESP_LOGI(TAG, "Initializing SD card"); - - sdmmc_host_t host = SDMMC_HOST_DEFAULT(); - host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; - -#if CONFIG_EXAMPLE_SDMMC_IO_POWER_INTERNAL_LDO - sd_pwr_ctrl_ldo_config_t ldo_config = { - .ldo_chan_id = 4, // `LDO_VO4` is used as the SDMMC IO power - }; - sd_pwr_ctrl_handle_t pwr_ctrl_handle = NULL; - - ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle); - if (ret != ESP_OK) { - ESP_LOGE(TAG, "Failed to new an on-chip ldo power control driver"); - return ret; - } - host.pwr_ctrl_handle = pwr_ctrl_handle; -#endif - - // This initializes the slot without card detect (CD) and write protect (WP) signals. - // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - slot_config.width = 4; - slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP; - - ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &s_card); - - if (ret != ESP_OK) { - if (ret == ESP_FAIL) { - ESP_LOGE(TAG, "Failed to mount filesystem. " - "If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option."); - } else { - ESP_LOGE(TAG, "Failed to initialize the card (%s). " - "Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret)); - } - return ret; - } - // Card has been initialized, print its properties - sdmmc_card_print_info(stdout, s_card); - return ret; -} - -static void sdcard_deinit(void) -{ - const char mount_point[] = MOUNT_POINT; - esp_vfs_fat_sdcard_unmount(mount_point, s_card); -#if SOC_SDMMC_IO_POWER_EXTERNAL - esp_err_t ret = sd_pwr_ctrl_del_on_chip_ldo(s_card->host.pwr_ctrl_handle); - if (ret != ESP_OK) { - ESP_LOGE(TAG, "Failed to delete on-chip ldo power control driver"); - return; - } -#endif -} - -void app_main(void) -{ - ESP_ERROR_CHECK(sdcard_init()); - - jpeg_decoder_handle_t jpgd_handle; - - jpeg_decode_engine_cfg_t decode_eng_cfg = { - .timeout_ms = TIMEOUT_MS, - }; - - ESP_ERROR_CHECK(jpeg_new_decoder_engine(&decode_eng_cfg, &jpgd_handle)); - - jpeg_decode_cfg_t decode_cfg_rgb = { - .output_format = JPEG_DECODE_OUT_FORMAT_RGB888, - .rgb_order = JPEG_DEC_RGB_ELEMENT_ORDER_BGR, - }; - - jpeg_decode_cfg_t decode_cfg_gray = { - .output_format = JPEG_DECODE_OUT_FORMAT_GRAY, - }; - - jpeg_decode_memory_alloc_cfg_t rx_mem_cfg = { - .buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER, - }; - - jpeg_decode_memory_alloc_cfg_t tx_mem_cfg = { - .buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER, - }; - - FILE *file_jpg_1080p = fopen(jpg_file_1080, "rb"); - ESP_LOGI(TAG, "jpg_file_1080:%s", jpg_file_1080); - if (file_jpg_1080p == NULL) { - ESP_LOGE(TAG, "fopen file_jpg_1080p error"); - return; - } - - fseek(file_jpg_1080p, 0, SEEK_END); - int jpeg_size_1080p = ftell(file_jpg_1080p); - fseek(file_jpg_1080p, 0, SEEK_SET); - size_t tx_buffer_size_1080p = 0; - uint8_t *tx_buf_1080p = (uint8_t*)jpeg_alloc_decoder_mem(jpeg_size_1080p, &tx_mem_cfg, &tx_buffer_size_1080p); - if (tx_buf_1080p == NULL) { - ESP_LOGE(TAG, "alloc 1080p tx buffer error"); - return; - } - fread(tx_buf_1080p, 1, jpeg_size_1080p, file_jpg_1080p); - fclose(file_jpg_1080p); - - FILE *file_jpg_720p = fopen(jpg_file_720, "rb"); - ESP_LOGI(TAG, "jpg_file_1080:%s", jpg_file_720); - if (file_jpg_720p == NULL) { - ESP_LOGE(TAG, "fopen file_jpg_720p error"); - return; - } - fseek(file_jpg_720p, 0, SEEK_END); - int jpeg_size_720p = ftell(file_jpg_720p); - fseek(file_jpg_720p, 0, SEEK_SET); - size_t tx_buffer_size_720p = 0; - uint8_t *tx_buf_720p = (uint8_t*)jpeg_alloc_decoder_mem(jpeg_size_720p, &tx_mem_cfg, &tx_buffer_size_720p); - if (tx_buf_720p == NULL) { - ESP_LOGE(TAG, "alloc 720p tx buffer error"); - return; - } - fread(tx_buf_720p, 1, jpeg_size_720p, file_jpg_720p); - fclose(file_jpg_720p); - - size_t rx_buffer_size_1080p = 0; - size_t rx_buffer_size_720p = 0; - uint8_t *rx_buf_1080p = (uint8_t*)jpeg_alloc_decoder_mem(1920 * 1088 * 3, &rx_mem_cfg, &rx_buffer_size_1080p); - uint8_t *rx_buf_720p = (uint8_t*)jpeg_alloc_decoder_mem(720 * 1280, &rx_mem_cfg, &rx_buffer_size_720p); - if (rx_buf_1080p == NULL) { - ESP_LOGE(TAG, "alloc 1080p rx buffer error"); - return; - } - if (rx_buf_720p == NULL) { - ESP_LOGE(TAG, "alloc 720p rx buffer error"); - return; - } - - // Get the jpg header information (This step is optional) - jpeg_decode_picture_info_t header_info; - ESP_ERROR_CHECK(jpeg_decoder_get_info(tx_buf_1080p, jpeg_size_1080p, &header_info)); - ESP_LOGI(TAG, "header parsed, width is %" PRId32 ", height is %" PRId32, header_info.width, header_info.height); - - uint32_t out_size_1080p = 0; - uint32_t out_size_720p = 0; - ESP_ERROR_CHECK(jpeg_decoder_process(jpgd_handle, &decode_cfg_rgb, tx_buf_1080p, jpeg_size_1080p, rx_buf_1080p, rx_buffer_size_1080p, &out_size_1080p)); - ESP_ERROR_CHECK(jpeg_decoder_process(jpgd_handle, &decode_cfg_gray, tx_buf_720p, jpeg_size_720p, rx_buf_720p, rx_buffer_size_720p, &out_size_720p)); - - // Write two pictures. - FILE *file_rgb_1080p = fopen(raw_file_1080, "wb"); - ESP_LOGI(TAG, "raw_file_1080:%s", raw_file_1080); - if (file_rgb_1080p == NULL) { - ESP_LOGE(TAG, "fopen file_rgb_1080p error"); - return; - } - fwrite(rx_buf_1080p, 1, out_size_1080p, file_rgb_1080p); - fclose(file_rgb_1080p); - - FILE *file_rgb_720p = fopen(raw_file_720, "wb"); - ESP_LOGI(TAG, "raw_file_720:%s", raw_file_720); - if (file_rgb_720p == NULL) { - ESP_LOGE(TAG, "fopen file_rgb_720p error"); - return; - } - fwrite(rx_buf_720p, 1, out_size_720p, file_rgb_720p); - fclose(file_rgb_720p); - - sdcard_deinit(); - ESP_LOGI(TAG, "Card unmounted"); - -} diff --git a/examples/peripherals/jpeg/jpeg_decode/open_raw_picture.py b/examples/peripherals/jpeg/jpeg_decode/open_raw_picture.py deleted file mode 100644 index afb243db1d8..00000000000 --- a/examples/peripherals/jpeg/jpeg_decode/open_raw_picture.py +++ /dev/null @@ -1,189 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD -# SPDX-License-Identifier: Unlicense OR CC0-1.0 -import argparse - -import cv2 as cv -import numpy as np -from numpy.typing import NDArray - - -def open_picture(path): # type: (str) -> list[int] - with open(path, 'rb') as f: - data = f.read() - f.close() - new_data = [int(x) for x in data] - return new_data - - -def picture_show_rgb888(data, h, w): # type: (list[int], int, int) -> None - data = np.array(data).reshape(h, w, 3).astype(np.uint8) - cv.imshow('data', data) - cv.waitKey() - - -def picture_show_rgb565(data, h, w): # type: (list[int], int, int) -> None - - new_data = [0] * ((len(data) // 2) * 3) - for i in range(len(data)): - if i % 2 != 0: - new_data[3 * (i - 1) // 2 + 2] = (data[i] & 0xf8) - new_data[3 * (i - 1) // 2 + 1] |= (data[i] & 0x7) << 5 - else: - new_data[3 * i // 2] = (data[i] & 0x1f) << 3 - new_data[3 * i // 2 + 1] |= (data[i] & 0xe0) >> 3 - - new_data = np.array(new_data).reshape(h, w, 3).astype(np.uint8) - cv.imshow('data', new_data) - cv.waitKey() - - -def picture_show_gray(data, h, w): # type: (list[int], int, int) -> None - new_data = np.array(data).reshape(h, w, 1).astype(np.uint8) - cv.imshow('data', new_data) - cv.waitKey() - - -def convert_YUV_to_RGB(Y, U, V): # type: (NDArray, NDArray, NDArray) -> tuple[NDArray, NDArray, NDArray] - B = np.clip(Y + 1.7790 * (U - 128), 0, 255).astype(np.uint8) - G = np.clip(Y - 0.3455 * (U - 128) - 0.7169 * (V - 128), 0, 255).astype(np.uint8) - R = np.clip(Y + 1.4075 * (V - 128), 0, 255).astype(np.uint8) - - return B, G, R - - -def picture_show_yuv420(data, h, w): # type: (list[int], int, int) -> None - new_u = [0] * (h * w) - new_v = [0] * (h * w) - new_y = [0] * (h * w) - - for i in range(int(h * w * 1.5)): - is_even_row = ((i // (w * 1.5)) % 2 == 0) - if is_even_row: - if (i % 3 == 0): - new_u[(i // 3) * 2] = data[i] - new_u[(i // 3) * 2 + 1] = data[i] - else: - if (i % 3 == 0): - new_u[(i // 3) * 2] = new_u[int((i - (w * 1.5)) // 3) * 2] - new_u[(i // 3) * 2 + 1] = new_u[int((i - (w * 1.5)) // 3) * 2 + 1] - - for i in range(int(h * w * 1.5)): - if (i // (w * 1.5)) % 2 != 0 and (i % 3 == 0): - idx = (i // 3) * 2 - new_v[idx] = data[i] - new_v[idx + 1] = data[i] - - for i in range(int(h * w * 1.5)): - if (i // (w * 1.5)) % 2 == 0 and (i % 3 == 0): - idx = (i // 3) * 2 - new_v[idx] = new_v[int((i + (w * 1.5)) // 3) * 2] - new_v[idx + 1] = new_v[int((i + (w * 1.5)) // 3) * 2 + 1] - - new_y = [data[i] for i in range(int(h * w * 1.5)) if i % 3 != 0] - - Y = np.array(new_y) - U = np.array(new_u) - V = np.array(new_v) - - B, G, R = convert_YUV_to_RGB(Y, U, V) - # Merge channels - new_data = np.stack((B, G, R), axis=-1) - new_data = np.array(new_data).reshape(h, w, 3).astype(np.uint8) - - # Display the image - cv.imshow('data', new_data) - cv.waitKey() - - -def picture_show_yuv422(data, h, w): # type: (list[int], int, int) -> None - # Reshape the input data to a 2D array - data_array = np.array(data).reshape(h, w * 2) - - # Separate Y, U, and V channels - Y = data_array[:, 1::2] - U = data_array[:, 0::4].repeat(2, axis=1) - V = data_array[:, 2::4].repeat(2, axis=1) - - # Convert YUV to RGB - B, G, R = convert_YUV_to_RGB(Y, U, V) - - # Merge channels - new_data = np.stack((B, G, R), axis=-1) - - # Display the image - cv.imshow('data', new_data) - cv.waitKey() - - -def picture_show_yuv444(data, h, w): # type: (list[int], int, int) -> None - # Reshape the input data to a 2D array - data_array = np.array(data).reshape(h, w * 3) - - # Separate Y, U, and V channels - Y = data_array[:, 2::3] - U = data_array[:, 1::3] - V = data_array[:, 0::3] - - # Convert YUV to RGB - B, G, R = convert_YUV_to_RGB(Y, U, V) - - # Merge channels - new_data = np.stack((B, G, R), axis=-1) - - # Display the image - cv.imshow('data', new_data) - cv.waitKey() - - -def main(): # type: () -> None - parser = argparse.ArgumentParser(description='which mode need to show') - - parser.add_argument( - '--pic_path', - type=str, - help='What is the path of your picture', - required=True) - - parser.add_argument( - '--pic_type', - type=str, - help='What type you want to show', - required=True, - choices=['rgb565', 'rgb888', 'gray', 'yuv422', 'yuv420', 'yuv444']) - - parser.add_argument( - '--height', - type=int, - help='the picture height', - default=480) - - parser.add_argument( - '--width', - type=int, - help='the picture width', - default=640) - - args = parser.parse_args() - - height = args.height - width = args.width - - data = open_picture(args.pic_path) - if (args.pic_type == 'rgb565'): - picture_show_rgb565(data, height, width) - elif (args.pic_type == 'rgb888'): - picture_show_rgb888(data, height, width) - elif (args.pic_type == 'gray'): - picture_show_gray(data, height, width) - elif (args.pic_type == 'yuv420'): - picture_show_yuv420(data, height, width) - elif (args.pic_type == 'yuv422'): - picture_show_yuv422(data, height, width) - elif (args.pic_type == 'yuv444'): - picture_show_yuv444(data, height, width) - else: - print('This type is not supported in this script!') - - -if __name__ == '__main__': - main() diff --git a/examples/peripherals/jpeg/jpeg_decode/pytest_jpeg_decode.py b/examples/peripherals/jpeg/jpeg_decode/pytest_jpeg_decode.py new file mode 100644 index 00000000000..8e5cd75774a --- /dev/null +++ b/examples/peripherals/jpeg/jpeg_decode/pytest_jpeg_decode.py @@ -0,0 +1,239 @@ +# 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 + +DECODE_OUTPUT_NAME = 'jpeg_decode_result.ppm' +GOLDEN_OUTPUT_NAME = 'golden_output.ppm' +GOLDEN_OUTPUT_PATH = Path(__file__).with_name(GOLDEN_OUTPUT_NAME) +EXPECTED_PIXEL_FORMAT = 'RGB888' +EXPECTED_ENCODING = 'base64' +RGB888_BYTES_PER_PIXEL = 3 +PPM_MAGIC = b'P6' +PPM_MAX_VALUE = b'255' +DECODE_INFO_PATTERN = ( + r'JPEG_DECODE_INFO width=(?P\d+) height=(?P\d+) ' + r'padded_width=(?P\d+) padded_height=(?P\d+) ' + r'format=(?P\w+) encoding=(?P\w+) size=(?P\d+)' +) +DECODE_INFO_RE = re.compile(DECODE_INFO_PATTERN) +DECODE_CHUNK_PATTERN = r'JPEG_DECODE_BASE64 (?P[A-Za-z0-9+/=]+)' +DECODE_CHUNK_RE = re.compile(DECODE_CHUNK_PATTERN) +PPM_HEADER_RE = re.compile(rb'^P6\s+(?P\d+)\s+(?P\d+)\s+(?P\d+)\s') + + +@dataclass(frozen=True, slots=True) +class DecodeMetadata: + width: int + height: int + padded_width: int + padded_height: int + pixel_format: str + encoding: str + size: int + + def __post_init__(self) -> None: + if self.width <= 0 or self.height <= 0: + raise ValueError(f'Invalid dimensions: {self.width}x{self.height}') + if self.padded_width < self.width or self.padded_height < self.height: + raise ValueError( + f'Padded size ({self.padded_width}x{self.padded_height}) ' + f'smaller than visible size ({self.width}x{self.height})' + ) + if self.pixel_format != EXPECTED_PIXEL_FORMAT: + raise ValueError(f'Unsupported pixel format: {self.pixel_format}') + if self.encoding != EXPECTED_ENCODING: + raise ValueError(f'Unsupported encoding: {self.encoding}') + + @property + def padded_image_size(self) -> int: + return self.padded_width * self.padded_height * RGB888_BYTES_PER_PIXEL + + +@dataclass(frozen=True, slots=True) +class RgbImage: + width: int + height: int + pixels_rgb888: bytes + + def __post_init__(self) -> None: + expected_size = self.width * self.height * RGB888_BYTES_PER_PIXEL + if len(self.pixels_rgb888) != expected_size: + raise ValueError(f'Expected {expected_size} RGB bytes, got {len(self.pixels_rgb888)}') + + +def parse_decode_metadata(meta_line: str) -> DecodeMetadata: + match = DECODE_INFO_RE.fullmatch(meta_line) + if not match: + raise ValueError(f'Invalid decode metadata line: {meta_line}') + + return DecodeMetadata( + width=int(match.group('width')), + height=int(match.group('height')), + padded_width=int(match.group('padded_width')), + padded_height=int(match.group('padded_height')), + pixel_format=match.group('format'), + encoding=match.group('encoding'), + size=int(match.group('size')), + ) + + +def collect_base64_payload(dut: Dut) -> list[str]: + payload_lines: list[str] = [] + while True: + # The example prints the decoded frame as multiple short UART lines + # instead of one giant base64 blob, so collect and join them here. + match = dut.expect(rf'(?PJPEG_DECODE_BASE64_END|{DECODE_CHUNK_PATTERN}\r?\n)', timeout=60) + line = match.group('line').decode('utf-8').strip() + if line == 'JPEG_DECODE_BASE64_END': + return payload_lines + + chunk_match = DECODE_CHUNK_RE.fullmatch(line) + assert chunk_match is not None + payload_lines.append(chunk_match.group('payload')) + + +def _crop_visible_bgr888(raw_bytes: bytes, metadata: DecodeMetadata) -> bytes: + # The hardware can write into a padded decode buffer whose width/height are + # rounded up to JPEG block boundaries. Pytest only wants the visible image, + # so keep the useful bytes from each row and discard the padded tail rows. + if len(raw_bytes) != metadata.padded_image_size: + raise ValueError(f'Expected {metadata.padded_image_size} padded BGR bytes, got {len(raw_bytes)}') + + visible_row_size = metadata.width * RGB888_BYTES_PER_PIXEL + padded_row_size = metadata.padded_width * RGB888_BYTES_PER_PIXEL + return b''.join( + raw_bytes[offset : offset + visible_row_size] + for offset in range(0, padded_row_size * metadata.height, padded_row_size) + ) + + +def _bgr888_to_rgb888(raw_bytes: bytes) -> bytes: + # The decoder's RGB888 mode uses BGR24 byte layout by default. Swap the + # first and third byte in each pixel so the PPM artifact becomes standard + # RGB order that common desktop image tools expect. + rgb_bytes = bytearray(raw_bytes) + rgb_bytes[0::3], rgb_bytes[2::3] = raw_bytes[2::3], raw_bytes[0::3] + return bytes(rgb_bytes) + + +def decode_base64_image(metadata: DecodeMetadata, payload_lines: list[str]) -> RgbImage: + # The DUT sends the raw decode buffer as base64 over UART because the test + # environment only observes text logs. Rebuild bytes on the host, crop away + # decoder padding, then normalize the pixel order for image comparison. + raw_bytes = base64.b64decode(''.join(payload_lines), validate=True) + if len(raw_bytes) != metadata.size: + raise ValueError(f'Expected {metadata.size} decoded bytes, got {len(raw_bytes)}') + + visible_bgr888 = _crop_visible_bgr888(raw_bytes, metadata) + return RgbImage( + width=metadata.width, + height=metadata.height, + pixels_rgb888=_bgr888_to_rgb888(visible_bgr888), + ) + + +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 = PPM_HEADER_RE.match(ppm_bytes) + if not header_match: + raise ValueError('Invalid PPM header') + + width = int(header_match.group('width')) + height = int(header_match.group('height')) + max_value = header_match.group('max_value') + 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() :] + return RgbImage(width=width, height=height, pixels_rgb888=pixel_data) + + +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 JPEG decode artifact to %s', output_path) + return + + logging.info('Saved JPEG decode artifact to %s', output_path) + + +def image_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 PPM not found: {golden_path}' + golden_image = _load_ppm(golden_path) + + assert image_digest(result_image) == image_digest(golden_image), ( + f'Generated image does not match golden file: {golden_path.name}' + ) + + +def run_jpeg_decode_example(dut: Dut) -> None: + dut.expect_exact('Loading embedded JPEG from flash...') + dut.expect(r'Embedded JPEG size: \d+ bytes') + dut.expect(r'JPEG header parsed: width=\d+ height=\d+') + dut.expect_exact('Decoding JPEG -> RGB888...') + dut.expect(r'Decoded RGB888 size: \d+ bytes') + + metadata_line = dut.expect(DECODE_INFO_PATTERN).group(0).decode('utf-8') + metadata = parse_decode_metadata(metadata_line) + + dut.expect_exact('JPEG_DECODE_BASE64_BEGIN') + # Collect the machine-readable payload before the example prints its final + # completion line so we keep the UART parsing strictly in output order. + payload_lines = collect_base64_payload(dut) + dut.expect_exact('JPEG decode demo done.') + + result_image = decode_base64_image(metadata, payload_lines) + output_path = Path(dut.logdir) / DECODE_OUTPUT_NAME + save_ppm_artifact(result_image, output_path) + assert_image_matches_golden(result_image, GOLDEN_OUTPUT_PATH) + + +@pytest.mark.generic +@idf_parametrize('target', soc_filtered_targets('SOC_JPEG_DECODE_SUPPORTED == 1'), indirect=['target']) +def test_jpeg_decode_example(dut: Dut) -> None: + run_jpeg_decode_example(dut) + + +@pytest.mark.flash_encryption +@pytest.mark.parametrize( + 'config', + [ + 'flash_enc', + ], + indirect=True, +) +@idf_parametrize( + 'target', + soc_filtered_targets('SOC_JPEG_DECODE_SUPPORTED == 1 and SOC_FLASH_ENC_SUPPORTED == 1'), + indirect=['target'], +) +def test_jpeg_decode_example_with_flash_encryption(dut: Dut) -> None: + run_jpeg_decode_example(dut) diff --git a/examples/peripherals/jpeg/jpeg_decode/requirements.txt b/examples/peripherals/jpeg/jpeg_decode/requirements.txt deleted file mode 100644 index b96544bd0b2..00000000000 --- a/examples/peripherals/jpeg/jpeg_decode/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -opencv-python -numpy diff --git a/examples/peripherals/jpeg/jpeg_decode/sdkconfig.ci.default b/examples/peripherals/jpeg/jpeg_decode/sdkconfig.ci.default new file mode 100644 index 00000000000..f70e1f1910d --- /dev/null +++ b/examples/peripherals/jpeg/jpeg_decode/sdkconfig.ci.default @@ -0,0 +1 @@ +# Default CI build, inherits sdkconfig.defaults diff --git a/examples/peripherals/jpeg/jpeg_decode/sdkconfig.ci.flash_enc b/examples/peripherals/jpeg/jpeg_decode/sdkconfig.ci.flash_enc new file mode 100644 index 00000000000..2412c23e67a --- /dev/null +++ b/examples/peripherals/jpeg/jpeg_decode/sdkconfig.ci.flash_enc @@ -0,0 +1,7 @@ +CONFIG_PARTITION_TABLE_OFFSET=0x9000 +CONFIG_SECURE_FLASH_ENC_ENABLED=y +CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y +CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC=y +CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y +CONFIG_SPIRAM_ENC_EXEMPT=y +CONFIG_SPIRAM_ENC_EXEMPT_SIZE=4096 diff --git a/examples/peripherals/jpeg/jpeg_decode/sdkconfig.defaults b/examples/peripherals/jpeg/jpeg_decode/sdkconfig.defaults index 7a366088b81..cc641ea6033 100644 --- a/examples/peripherals/jpeg/jpeg_decode/sdkconfig.defaults +++ b/examples/peripherals/jpeg/jpeg_decode/sdkconfig.defaults @@ -1,5 +1 @@ -# SPIRAM configurations - -CONFIG_IDF_EXPERIMENTAL_FEATURES=y CONFIG_SPIRAM=y -CONFIG_SPIRAM_SPEED_200M=y diff --git a/examples/peripherals/jpeg/jpeg_encode/README.md b/examples/peripherals/jpeg/jpeg_encode/README.md index ea6c2a8cac0..2c32e2d705a 100644 --- a/examples/peripherals/jpeg/jpeg_encode/README.md +++ b/examples/peripherals/jpeg/jpeg_encode/README.md @@ -50,6 +50,19 @@ The accompanying `pytest_jpeg_encode.py` script captures the `JPEG_META` and `JP It also compares the generated JPEG with `golden_output.jpeg`. This turns the example into both a functional regression test and a host-side artifact generator that makes the encoded result easy to inspect. +## Running Pytest Locally And Viewing The Image + +To run the pytest helper locally on hardware, build the example for your target first, then invoke the test script with the target and serial port: + +```bash +idf.py set-target esp32p4 build +pytest --target esp32p4 --port PORT pytest_jpeg_encode.py +``` + +Replace `esp32p4` with another supported target such as `esp32s31` when needed. + +`pytest-embedded` stores per-test logs under `$IDF_PATH/pytest-embedded/`. The script writes the reconstructed image to `jpeg_encode_result.jpeg` inside that test log directory, so after the test finishes you can open the generated JPEG locally with any image viewer to inspect the encoded output. + ## Replacing The Embedded RGB Asset If you want to regenerate a compatible raw frame from another input image, one simple workflow is: diff --git a/examples/peripherals/jpeg/jpeg_encode/main/jpeg_encode_example_main.c b/examples/peripherals/jpeg/jpeg_encode/main/jpeg_encode_example_main.c index 3dbadaae07d..b41b7699014 100644 --- a/examples/peripherals/jpeg/jpeg_encode/main/jpeg_encode_example_main.c +++ b/examples/peripherals/jpeg/jpeg_encode/main/jpeg_encode_example_main.c @@ -8,6 +8,7 @@ #include #include #include +#include #include "mbedtls/base64.h" #include "esp_check.h" #include "driver/jpeg_encode.h" @@ -44,6 +45,7 @@ void app_main(void) const size_t embedded_size = esp720p_rgb_end - esp720p_rgb_start; uint32_t jpeg_size = 0; jpeg_encoder_handle_t jpeg_handle = NULL; + uint8_t *rgb_buf = NULL; printf("Loading embedded BGR24 image from flash...\n"); printf("Embedded raw image size: %zu bytes\n", embedded_size); @@ -60,6 +62,18 @@ void app_main(void) .height = EXAMPLE_HEIGHT, }; + const uint8_t *rgb_src = esp720p_rgb_start; +#if CONFIG_SECURE_FLASH_ENC_ENABLED + size_t input_buffer_size = 0; + jpeg_encode_memory_alloc_cfg_t rx_mem_cfg = { + .buffer_direction = JPEG_ENC_ALLOC_INPUT_BUFFER, + }; + rgb_buf = (uint8_t *)jpeg_alloc_encoder_mem(EXAMPLE_RGB_FRAME_SIZE, &rx_mem_cfg, &input_buffer_size); + assert(rgb_buf != NULL); + memcpy(rgb_buf, esp720p_rgb_start, EXAMPLE_RGB_FRAME_SIZE); + rgb_src = rgb_buf; +#endif + size_t result_buffer_size = 0; /* The output JPEG is compressed, so the example does not need to reserve * a full raw-frame worth of space for the bitstream. This 10:1 estimate @@ -78,9 +92,8 @@ void app_main(void) }; ESP_ERROR_CHECK(jpeg_new_encoder_engine(&encode_eng_cfg, &jpeg_handle)); - printf("JPEG encoder will read the embedded raw buffer directly from flash.\n"); printf("Encoding BGR24(raw) -> JPEG...\n"); - ESP_ERROR_CHECK(jpeg_encoder_process(jpeg_handle, &enc_config, esp720p_rgb_start, EXAMPLE_RGB_FRAME_SIZE, + ESP_ERROR_CHECK(jpeg_encoder_process(jpeg_handle, &enc_config, rgb_src, EXAMPLE_RGB_FRAME_SIZE, jpeg_buf, result_buffer_size, &jpeg_size)); printf("Encoded JPEG size: %" PRIu32 " bytes\n", jpeg_size); @@ -103,4 +116,5 @@ void app_main(void) ESP_ERROR_CHECK(jpeg_del_encoder_engine(jpeg_handle)); free(encoded); free(jpeg_buf); + free(rgb_buf); } diff --git a/examples/peripherals/jpeg/jpeg_encode/pytest_jpeg_encode.py b/examples/peripherals/jpeg/jpeg_encode/pytest_jpeg_encode.py index 5278b90ca2e..e8c74f261e3 100644 --- a/examples/peripherals/jpeg/jpeg_encode/pytest_jpeg_encode.py +++ b/examples/peripherals/jpeg/jpeg_encode/pytest_jpeg_encode.py @@ -101,12 +101,9 @@ def assert_jpeg_matches_golden(result_bytes: bytes, golden_path: Path) -> None: ) -@pytest.mark.generic -@idf_parametrize('target', soc_filtered_targets('SOC_JPEG_ENCODE_SUPPORTED == 1'), indirect=['target']) -def test_jpeg_encode_example(dut: Dut) -> None: +def run_jpeg_encode_example(dut: Dut) -> None: dut.expect_exact('Loading embedded BGR24 image from flash...') dut.expect(r'Embedded raw image size: \d+ bytes') - dut.expect_exact('JPEG encoder will read the embedded raw buffer directly from flash.') dut.expect_exact('Encoding BGR24(raw) -> JPEG...') dut.expect(r'Encoded JPEG size: \d+ bytes') @@ -123,3 +120,26 @@ def test_jpeg_encode_example(dut: Dut) -> None: assert_jpeg_matches_golden(jpeg_bytes, GOLDEN_IMAGE_PATH) dut.expect_exact('JPEG encode demo done.') + + +@pytest.mark.generic +@idf_parametrize('target', soc_filtered_targets('SOC_JPEG_ENCODE_SUPPORTED == 1'), indirect=['target']) +def test_jpeg_encode_example(dut: Dut) -> None: + run_jpeg_encode_example(dut) + + +@pytest.mark.flash_encryption +@pytest.mark.parametrize( + 'config', + [ + 'flash_enc', + ], + indirect=True, +) +@idf_parametrize( + 'target', + soc_filtered_targets('SOC_JPEG_ENCODE_SUPPORTED == 1 and SOC_FLASH_ENC_SUPPORTED == 1'), + indirect=['target'], +) +def test_jpeg_encode_example_with_flash_encryption(dut: Dut) -> None: + run_jpeg_encode_example(dut) diff --git a/examples/peripherals/jpeg/jpeg_encode/sdkconfig.ci.default b/examples/peripherals/jpeg/jpeg_encode/sdkconfig.ci.default new file mode 100644 index 00000000000..f70e1f1910d --- /dev/null +++ b/examples/peripherals/jpeg/jpeg_encode/sdkconfig.ci.default @@ -0,0 +1 @@ +# Default CI build, inherits sdkconfig.defaults diff --git a/examples/peripherals/jpeg/jpeg_encode/sdkconfig.ci.flash_enc b/examples/peripherals/jpeg/jpeg_encode/sdkconfig.ci.flash_enc new file mode 100644 index 00000000000..2412c23e67a --- /dev/null +++ b/examples/peripherals/jpeg/jpeg_encode/sdkconfig.ci.flash_enc @@ -0,0 +1,7 @@ +CONFIG_PARTITION_TABLE_OFFSET=0x9000 +CONFIG_SECURE_FLASH_ENC_ENABLED=y +CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y +CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC=y +CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y +CONFIG_SPIRAM_ENC_EXEMPT=y +CONFIG_SPIRAM_ENC_EXEMPT_SIZE=4096