From c0e1a76ac43150e70aefc064ba9b3015e667cffd Mon Sep 17 00:00:00 2001 From: "Michael.B" Date: Thu, 27 Aug 2026 02:21:17 +0800 Subject: [PATCH 1/4] test(spi_flash): add flash_encryption xip_psram repro coverage Add an IDF-15875 regression config that places .ext_ram.bss in PSRAM under XIP, and align flash_encryption disable_test rules with available runners. --- .../spi_flash/test_apps/.build-test-rules.yml | 7 +- .../main/test_flash_encryption.c | 122 ++++++++++++++++++ .../pytest_flash_encrypted.py | 20 +++ .../flash_encryption/sdkconfig.ci.xip_psram | 11 ++ .../sdkconfig.ci.xip_psram.esp32s3 | 7 + 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram create mode 100644 components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram.esp32s3 diff --git a/components/spi_flash/test_apps/.build-test-rules.yml b/components/spi_flash/test_apps/.build-test-rules.yml index f751e468aa6..f3a9839ff7c 100644 --- a/components/spi_flash/test_apps/.build-test-rules.yml +++ b/components/spi_flash/test_apps/.build-test-rules.yml @@ -17,8 +17,13 @@ components/spi_flash/test_apps/esp_flash_stress: - spi_flash components/spi_flash/test_apps/flash_encryption: + disable: + - if: CONFIG_NAME == "xip_psram" and SOC_SPIRAM_XIP_SUPPORTED != 1 disable_test: - - if: IDF_TARGET in ["esp32c2", "esp32s2", "esp32c6", "esp32h2", "esp32p4", "esp32c5", "esp32c61"] + # release/verify/rom_impl pytest covers esp32/esp32c3/esp32s3; other FE-capable + # chips without XIP still lack runners for those configs. XIP-capable chips are + # covered by the xip_psram case via SOC_SPIRAM_XIP_SUPPORTED. + - if: CONFIG_NAME != "xip_psram" and SOC_FLASH_ENC_SUPPORTED == 1 and SOC_SPIRAM_XIP_SUPPORTED != 1 and IDF_TARGET not in ["esp32", "esp32c3"] temporary: true reason: No runners # IDF-5634 diff --git a/components/spi_flash/test_apps/flash_encryption/main/test_flash_encryption.c b/components/spi_flash/test_apps/flash_encryption/main/test_flash_encryption.c index 84470d2907a..3bdb3227120 100644 --- a/components/spi_flash/test_apps/flash_encryption/main/test_flash_encryption.c +++ b/components/spi_flash/test_apps/flash_encryption/main/test_flash_encryption.c @@ -9,6 +9,8 @@ #include "esp_log.h" #include "unity.h" #include "esp_flash.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include #include #include @@ -495,4 +497,124 @@ TEST_CASE("Test flash encrypted write over boundary", "[flash_encryption]") } #endif //CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS +#ifdef CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY +// IDF-15875: when the .ext_ram.bss segment is placed in external PSRAM and the CPU +// frequency switch (run from PSRAM-XIP) corrupts it during boot, this buffer +// would contain non-zero words. Assert it is all-zero after boot. +#define PSRAM_BSS_SIZE 171936 +EXT_RAM_BSS_ATTR static uint8_t s_big_bss[PSRAM_BSS_SIZE]; + +static int count_nonzero_psram_bss_words(int *first, uint32_t *first_v) +{ + volatile uint32_t *w = (volatile uint32_t *)s_big_bss; + const int n = PSRAM_BSS_SIZE / 4; + int nz = 0; + + *first = -1; + *first_v = 0; + + for (int i = 0; i < n; i++) { + uint32_t v = w[i]; + if (v != 0) { + if (*first < 0) { + *first = i; + *first_v = v; + } + nz++; + } + } + + return nz; +} + +static void assert_psram_heap_integrity(const char *label) +{ + bool ok = heap_caps_check_integrity_all(true); + + printf("[%s] heap integrity: %s\n", label, ok ? "OK" : "CORRUPT"); + TEST_ASSERT_TRUE_MESSAGE(ok, + "heap integrity check failed while reproducing flash-encryption xip_psram path"); +} + +static void exercise_flash_encryption_psram_repro_rounds(void) +{ + for (int i = 0; i < 16; i++) { + SemaphoreHandle_t mutex = xSemaphoreCreateMutex(); + uint8_t *internal = heap_caps_malloc(256, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + uint8_t *psram = heap_caps_malloc(1024, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + + TEST_ASSERT_NOT_NULL_MESSAGE(mutex, "failed to create mutex during repro setup"); + TEST_ASSERT_NOT_NULL_MESSAGE(internal, "failed to allocate internal buffer during repro setup"); + TEST_ASSERT_NOT_NULL_MESSAGE(psram, "failed to allocate psram buffer during repro setup"); + + memset(internal, 0x5a, 256); + memset(psram, 0xa5, 1024); + + assert_psram_heap_integrity("after-round"); + + vSemaphoreDelete(mutex); + free(psram); + free(internal); + } +} + +static void touch_psram_bss_and_check(size_t bytes, const char *label) +{ + printf("touching ext_ram .bss: first %u bytes ...\n", (unsigned)bytes); + memset(s_big_bss, 0xa5, bytes); + assert_psram_heap_integrity(label); +} + +static void assert_psram_bss_zero_initialized(const char *stage) +{ + const int n = PSRAM_BSS_SIZE / 4; + int first = -1; + uint32_t first_v = 0; + int nz = count_nonzero_psram_bss_words(&first, &first_v); + + printf("[%s] PSRAM .bss @ %p words=%d NON-ZERO words = %d / %d\n", + stage, (void *)s_big_bss, n, nz, n); + if (nz) { + printf("[%s] first non-zero @word %d (0x%" PRIxPTR ") = 0x%" PRIx32 "\n", + stage, + first, (uintptr_t)s_big_bss + first * 4, first_v); + } + TEST_ASSERT_EQUAL_MESSAGE(0, nz, + "external .bss was not zero-initialized (corrupted during boot, see IDF-15875)"); +} + +TEST_CASE("external PSRAM .bss is zero-initialized after boot", "[flash_encryption][psram][bss]") +{ + assert_psram_bss_zero_initialized("boot"); +} + +TEST_CASE("external PSRAM .bss survives flash-encryption heap activity", "[flash_encryption][psram][bss_repro]") +{ + const size_t touch_steps[] = { + 4 * 1024, + 8 * 1024, + 16 * 1024, + 32 * 1024, + 64 * 1024, + }; + const char *labels[] = { + "after-bss-touch 4KB", + "after-bss-touch 8KB", + "after-bss-touch 16KB", + "after-bss-touch 32KB", + "after-bss-touch 64KB", + }; + + TEST_ASSERT_EQUAL(sizeof(touch_steps) / sizeof(touch_steps[0]), sizeof(labels) / sizeof(labels[0])); + + assert_psram_bss_zero_initialized("boot"); + assert_psram_heap_integrity("boot"); + exercise_flash_encryption_psram_repro_rounds(); + + for (size_t i = 0; i < sizeof(touch_steps) / sizeof(touch_steps[0]); i++) { + touch_psram_bss_and_check(touch_steps[i], labels[i]); + } +} +#endif // CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY + #endif // CONFIG_SECURE_FLASH_ENC_ENABLED diff --git a/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py b/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py index 11e654951b3..2ea10ae78aa 100644 --- a/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py +++ b/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py @@ -3,6 +3,14 @@ import pytest from pytest_embedded import Dut from pytest_embedded_idf.utils import idf_parametrize +from pytest_embedded_idf.utils import soc_filtered_targets + + +def get_xip_psram_marks(target: str) -> tuple[pytest.MarkDecorator, ...]: + if target == 'esp32s3': + return (pytest.mark.flash_encryption_f8r8,) + + return (pytest.mark.flash_encryption,) @pytest.mark.flash_encryption @@ -58,3 +66,15 @@ def test_flash_encryption_f4r8(dut: Dut) -> None: @idf_parametrize('target', ['esp32s3'], indirect=['target']) def test_flash_encryption_f8r8(dut: Dut) -> None: dut.run_all_single_board_cases() + + +@pytest.mark.parametrize( + 'config, target', + [ + pytest.param('xip_psram', target, marks=get_xip_psram_marks(target)) + for target in soc_filtered_targets('SOC_SPIRAM_XIP_SUPPORTED == 1') + ], + indirect=True, +) +def test_flash_encryption_psram(dut: Dut) -> None: + dut.run_all_single_board_cases() diff --git a/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram b/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram new file mode 100644 index 00000000000..f0341170088 --- /dev/null +++ b/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram @@ -0,0 +1,11 @@ +# Flash Encryption + external PSRAM .bss regression config for IDF-15875. +# Places .ext_ram.bss in external PSRAM so a boot-time CPU frequency switch +# under PSRAM-XIP can be exercised; without the fix the external .bss is +# corrupted during boot. +# On esp32p4, CONFIG_CACHE_L2_CACHE_LINE_128B is required to reproduce. +CONFIG_CACHE_L2_CACHE_LINE_128B=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_BOOT_INIT=y +CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y +CONFIG_SPIRAM_SPEED_250M=y +CONFIG_SPIRAM_XIP_FROM_PSRAM=y diff --git a/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram.esp32s3 b/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram.esp32s3 new file mode 100644 index 00000000000..74b824c519a --- /dev/null +++ b/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram.esp32s3 @@ -0,0 +1,7 @@ +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_TYPE_AUTO=y +CONFIG_ESPTOOLPY_OCT_FLASH=y +CONFIG_ESPTOOLPY_FLASHMODE_OPI=y +CONFIG_ESPTOOLPY_FLASHSIZE_32MB=y +CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y +CONFIG_SPIRAM_RODATA=y From a2a4dec210484ec680093ae2bf5a55fa6babaeda Mon Sep 17 00:00:00 2001 From: "Michael.B" Date: Thu, 27 Aug 2026 02:21:17 +0800 Subject: [PATCH 2/4] fix(psram): suspend external cache during CPU freq switch on esp32p4 Suspend the external-memory cache around the boot CPU frequency switch so PSRAM-XIP / write-back cache state cannot be corrupted mid-switch. --- components/esp_system/port/soc/esp32p4/clk.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/components/esp_system/port/soc/esp32p4/clk.c b/components/esp_system/port/soc/esp32p4/clk.c index 6d5bf4defc1..2beb05fcc1f 100644 --- a/components/esp_system/port/soc/esp32p4/clk.c +++ b/components/esp_system/port/soc/esp32p4/clk.c @@ -62,6 +62,7 @@ #include "esp_private/periph_ctrl.h" #include "esp_private/esp_clk.h" #include "esp_private/esp_pmu.h" +#include "esp_private/esp_cache_private.h" #include "esp_rom_uart.h" #include "esp_rom_sys.h" @@ -84,6 +85,22 @@ void IRAM_ATTR esp_rtc_init(void) #endif //SOC_PMU_SUPPORTED } +/* + * Perform the CPU frequency switch with the external memory (PSRAM) cache + * suspended. The frequency switch stalls the HP_ROOT clock and toggles PLL / + * clock gating; when the switch code runs from PSRAM-XIP, a cache-coherency + * transient during the switch can corrupt the external .bss region. Suspending + * the external-memory cache for the duration of the switch keeps the CPU off + * the XIP path, isolating the two. + * Must be IRAM-resident because it runs while the external cache is suspended. + */ +static void IRAM_ATTR esp_clk_cpu_freq_set_config_isolated(const rtc_cpu_freq_config_t *config) +{ + esp_cache_suspend_ext_mem_cache(); + rtc_clk_cpu_freq_set_config(config); + esp_cache_resume_ext_mem_cache(); +} + __attribute__((weak)) void esp_clk_init(void) { assert(rtc_clk_xtal_freq_get() == SOC_XTAL_FREQ_40M); @@ -145,7 +162,7 @@ __attribute__((weak)) void esp_clk_init(void) } if (res) { - rtc_clk_cpu_freq_set_config(&new_config); + esp_clk_cpu_freq_set_config_isolated(&new_config); } // Re calculate the ccount to make time calculation correct. From 6430553caab86986235fd78038f90aabb5a8edff Mon Sep 17 00:00:00 2001 From: "Michael.B" Date: Thu, 27 Aug 2026 02:21:17 +0800 Subject: [PATCH 3/4] test(spi_flash): temp_skip_ci esp32h4 flash_encryption xip_psram Keep SOC_SPIRAM_XIP_SUPPORTED target selection and skip esp32h4 in CI until FE support is ready (IDF-12388). --- .../test_apps/flash_encryption/pytest_flash_encrypted.py | 1 + 1 file changed, 1 insertion(+) diff --git a/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py b/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py index 2ea10ae78aa..d310ea7dfa2 100644 --- a/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py +++ b/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py @@ -68,6 +68,7 @@ def test_flash_encryption_f8r8(dut: Dut) -> None: dut.run_all_single_board_cases() +@pytest.mark.temp_skip_ci(targets=['esp32h4'], reason='not support yet') # TODO: [ESP32H4] IDF-12388 @pytest.mark.parametrize( 'config, target', [ From 16dac308e49902d98a7ca51850b40b5afc772802 Mon Sep 17 00:00:00 2001 From: "Michael.B" Date: Thu, 27 Aug 2026 03:41:30 +0800 Subject: [PATCH 4/4] fix(psram): make IDF-15875 backport build clean on v5.4 Guard external-cache suspend for pure-ram apps where esp_cache_utils is not linked, move esp32p4-only xip_psram sdkconfig options into a target-specific file, and add from __future__ import annotations so pytest collection works on CI Python 3.8. --- components/esp_system/port/soc/esp32p4/clk.c | 5 +++++ .../test_apps/flash_encryption/pytest_flash_encrypted.py | 2 ++ .../test_apps/flash_encryption/sdkconfig.ci.xip_psram | 3 --- .../flash_encryption/sdkconfig.ci.xip_psram.esp32p4 | 3 +++ 4 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram.esp32p4 diff --git a/components/esp_system/port/soc/esp32p4/clk.c b/components/esp_system/port/soc/esp32p4/clk.c index 2beb05fcc1f..ee756bfee94 100644 --- a/components/esp_system/port/soc/esp32p4/clk.c +++ b/components/esp_system/port/soc/esp32p4/clk.c @@ -96,9 +96,14 @@ void IRAM_ATTR esp_rtc_init(void) */ static void IRAM_ATTR esp_clk_cpu_freq_set_config_isolated(const rtc_cpu_freq_config_t *config) { + /* esp_cache_utils.c is not built for pure-ram apps. */ +#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP esp_cache_suspend_ext_mem_cache(); +#endif rtc_clk_cpu_freq_set_config(config); +#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP esp_cache_resume_ext_mem_cache(); +#endif } __attribute__((weak)) void esp_clk_init(void) diff --git a/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py b/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py index d310ea7dfa2..d61df9c5ed4 100644 --- a/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py +++ b/components/spi_flash/test_apps/flash_encryption/pytest_flash_encrypted.py @@ -1,5 +1,7 @@ # SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import pytest from pytest_embedded import Dut from pytest_embedded_idf.utils import idf_parametrize diff --git a/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram b/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram index f0341170088..3a8a55bdbf5 100644 --- a/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram +++ b/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram @@ -2,10 +2,7 @@ # Places .ext_ram.bss in external PSRAM so a boot-time CPU frequency switch # under PSRAM-XIP can be exercised; without the fix the external .bss is # corrupted during boot. -# On esp32p4, CONFIG_CACHE_L2_CACHE_LINE_128B is required to reproduce. -CONFIG_CACHE_L2_CACHE_LINE_128B=y CONFIG_SPIRAM=y CONFIG_SPIRAM_BOOT_INIT=y CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y -CONFIG_SPIRAM_SPEED_250M=y CONFIG_SPIRAM_XIP_FROM_PSRAM=y diff --git a/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram.esp32p4 b/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram.esp32p4 new file mode 100644 index 00000000000..9297379f35e --- /dev/null +++ b/components/spi_flash/test_apps/flash_encryption/sdkconfig.ci.xip_psram.esp32p4 @@ -0,0 +1,3 @@ +# On esp32p4, CONFIG_CACHE_L2_CACHE_LINE_128B is required to reproduce IDF-15875. +CONFIG_CACHE_L2_CACHE_LINE_128B=y +CONFIG_SPIRAM_SPEED_250M=y