From 15540b39f7d4f1ce28eac6eeec1e0fcb07f40e73 Mon Sep 17 00:00:00 2001 From: Konstantin Kondrashov Date: Tue, 10 Feb 2026 13:42:32 +0200 Subject: [PATCH] fix(esp32): Fix access to MALLOC_CAP_IRAM_8BIT byte array in loop The Xtensa load/store handler did not properly handle 8/16-bit memory access to IRAM regions configured with MALLOC_CAP_IRAM_8BIT (and CONFIG_ESP32_IRAM_AS_8BIT_ACCESSIBLE_MEMORY=y) from a loop (LBEG/LEND/LCOUNT) context. This caused the loop to exit after the first access, instead of continuing to iterate as intended. Closes https://github.com/espressif/esp-idf/issues/14127 --- .../port/test_xtensa_loadstore_handler.c | 156 +++++++++++++++++- .../xtensa/include/xtensa/xtensa_zol_macros.h | 53 ++++++ components/xtensa/xtensa_loadstore_handler.S | 11 +- components/xtensa/xtensa_vectors.S | 26 +-- 4 files changed, 223 insertions(+), 23 deletions(-) create mode 100644 components/xtensa/include/xtensa/xtensa_zol_macros.h diff --git a/components/freertos/test_apps/freertos/port/test_xtensa_loadstore_handler.c b/components/freertos/test_apps/freertos/port/test_xtensa_loadstore_handler.c index 054379e2b49..704ce3dab24 100644 --- a/components/freertos/test_apps/freertos/port/test_xtensa_loadstore_handler.c +++ b/components/freertos/test_apps/freertos/port/test_xtensa_loadstore_handler.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -8,14 +8,37 @@ Test for LoadStore exception handlers. This test performs unaligned load and store in 32bit aligned addresses */ +#include "sdkconfig.h" + +#if CONFIG_IDF_TARGET_ARCH_XTENSA && CONFIG_ESP32_IRAM_AS_8BIT_ACCESSIBLE_MEMORY + #include #include #include -#include "sdkconfig.h" +#include "esp_attr.h" #include "esp_random.h" +#include "esp_intr_alloc.h" +#include "xtensa_api.h" +#include "esp_rom_sys.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include #include "unity.h" +#include "esp_log_buffer.h" + +#define SW_ISR_NUM_L1 7 // CPU interrupt number for internal SW0 (level 1) +#define SW_ISR_NUM_L3 29 // CPU interrupt number for internal SW1 (level 3) + +typedef struct { + uint8_t b8; + uint16_t b16; + uint8_t b8_2; + uint32_t b32; +} iram_data_t; + +static volatile iram_data_t *s_iram; +static volatile int isr_hits; -#if CONFIG_IDF_TARGET_ARCH_XTENSA && CONFIG_ESP32_IRAM_AS_8BIT_ACCESSIBLE_MEMORY TEST_CASE("LoadStore Exception handler", "[freertos]") { int32_t val0 = 0xDEADBEEF; @@ -125,4 +148,131 @@ TEST_CASE("LoadStore Exception handler", "[freertos]") TEST_ASSERT_TRUE(heap_caps_check_integrity_all(true)); heap_caps_free(arr); } + +static void IRAM_ATTR level_isr(void *arg) +{ + const int cpu_intr_num = *(const int *)arg; + + // Clear the SW interrupt source + xt_set_intclear(1 << cpu_intr_num); + + // Schedule another pending level interrupt while still inside this ISR + // to keep dispatch_c_isr in the interrupt loop handling. The pending + // interrupt will be handled after this ISR returns. + if (isr_hits == 0) { + xt_set_intset(1 << cpu_intr_num); + } + + // 8/16-bit accesses to s_iram will trigger LoadStore/Alignment exceptions which changes the EXCSAVE_1 value + s_iram->b16++; + s_iram->b8 = 0x5A; + s_iram->b8_2 = 0xA5; + + isr_hits++; +} + +static void run_sw_isr_level(int int_level) +{ + memset((void *)s_iram, 0, sizeof(iram_data_t)); + isr_hits = 0; + + intr_handle_t handle; + int cpu_intr_num = (int_level == 1) ? SW_ISR_NUM_L1 : SW_ISR_NUM_L3; + TEST_ASSERT_EQUAL(ESP_OK, esp_intr_alloc((int_level == 1) ? ETS_INTERNAL_SW0_INTR_SOURCE : ETS_INTERNAL_SW1_INTR_SOURCE, + (int_level == 1) ? ESP_INTR_FLAG_LEVEL1 : ESP_INTR_FLAG_LEVEL3, + level_isr, + (void *)&cpu_intr_num, + &handle)); + + // Trigger the first interrupt; ISR will queue one more while running + xt_set_intset(1 << cpu_intr_num); + + // Wait for the ISR to be invoked twice + TickType_t start = xTaskGetTickCount(); + while (isr_hits < 2 && (xTaskGetTickCount() - start) < pdMS_TO_TICKS(500)) { + vTaskDelay(1); + } + + TEST_ASSERT_EQUAL(2, isr_hits); + TEST_ASSERT_EQUAL_HEX8(0x5A, s_iram->b8); + TEST_ASSERT_EQUAL_HEX8(0xA5, s_iram->b8_2); + TEST_ASSERT_EQUAL_UINT16(2, s_iram->b16); + TEST_ASSERT_EQUAL_UINT32(0, s_iram->b32); + + esp_intr_free(handle); +} + +TEST_CASE("LoadStore: 8/16-bit field access in IRAM from ISRs when pending interrupts are present", "[freertos]") +{ + s_iram = heap_caps_calloc(1, sizeof(*s_iram), MALLOC_CAP_IRAM_8BIT); + TEST_ASSERT_NOT_NULL(s_iram); + + esp_rom_printf("Running test in level-1 ISR...\n"); + run_sw_isr_level(1); + esp_rom_printf("test passed\n"); + + esp_rom_printf("Running test in level-3 ISR...\n"); + run_sw_isr_level(3); + esp_rom_printf("test passed\n"); + + heap_caps_free((void *)s_iram); + vTaskDelay(pdMS_TO_TICKS(100)); // Wait for memory to be freed, to avoid affecting other tests. +} + +TEST_CASE("LoadStore: zero-overhead loop continues after IRAM 8-bit store", "[freertos]") +{ + const unsigned len = 32; + + uint8_t *dst = heap_caps_calloc(len, 1, MALLOC_CAP_IRAM_8BIT); + TEST_ASSERT_NOT_NULL(dst); + + uint8_t *src = heap_caps_calloc(len, 1, MALLOC_CAP_IRAM_8BIT); + TEST_ASSERT_NOT_NULL(src); + + for (unsigned i = 0; i < len; i++) { + src[i] = i + 1; + dst[i] = 0xCC; + } + + ESP_LOG_BUFFER_HEX("dst before", dst, len); + ESP_LOG_BUFFER_HEX("src ", src, len); + + /* + * Use Xtensa zero-overhead loop where the final instruction (LEND) + * is an 8-bit store into IRAM. With CONFIG_ESP32_IRAM_AS_8BIT_ACCESSIBLE_MEMORY + * each store triggers the LoadStoreError handler. The handler must mimic + * the loop hardware to keep iterating; otherwise the loop exits after one + * iteration. This assembly below performs a simple byte copy from src to dst, + * and the test verifies that all bytes are copied correctly, + * indicating that the loop continued to execute after each store exception. + */ + uint32_t tmp_val; + uint8_t *dst_it = dst; + uint8_t *src_it = src; + unsigned cnt = len; + __asm__ volatile( + "addi %0, %0, -1\n" /* dst-- */ + "addi %1, %1, -1\n" /* src-- */ + "loopnez %2, .endLoop\n" + "addi %1, %1, 1\n" /* src++ */ + "l8ui %3, %1, 0\n" /* tmp_val = src[] */ + "addi %0, %0, 1\n" /* dst++ */ + "s8i %3, %0, 0\n" /* dst[] = tmp_val, LEND: store is last in loop body */ + ".endLoop:\n" + : "+r"(dst_it), "+r"(src_it), "+r"(cnt), "=&r"(tmp_val) + : + : "memory" + ); + + ESP_LOG_BUFFER_HEX("dst after", dst, len); + + for (unsigned i = 0; i < len; i++) { + TEST_ASSERT_EQUAL_HEX8(src[i], dst[i]); + } + + heap_caps_free(dst); + heap_caps_free(src); + + vTaskDelay(pdMS_TO_TICKS(100)); // Wait for free to complete before running other tests. +} #endif // CONFIG_IDF_TARGET_ARCH_XTENSA && CONFIG_ESP32_IRAM_AS_8BIT_ACCESSIBLE_MEMORY diff --git a/components/xtensa/include/xtensa/xtensa_zol_macros.h b/components/xtensa/include/xtensa/xtensa_zol_macros.h new file mode 100644 index 00000000000..424a7e4ecd2 --- /dev/null +++ b/components/xtensa/include/xtensa/xtensa_zol_macros.h @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + + /* + * Zero-overhead loop adjustment helpers for Xtensa assembly code. + * + * Used by exception handlers to emulate loop hardware when an exception + * occurs at LEND so execution resumes at LBEG with LCOUNT decremented. + */ + +#ifndef XTENSA_ZOL_MACROS_H +#define XTENSA_ZOL_MACROS_H + +#if __XTENSA__ +#include "xtensa/config/core-isa.h" +#include "xtensa/config/xt_specreg.h" + +/** + * Adjust loop counter and return address for exceptions at LEND. + * + * When an exception occurs on instruction at LEND address of a zero-overhead loop, + * we must decrement LCOUNT and set EPC back to LBEG so the loop continues + * iterating after the exception is handled. Otherwise, the loop exits prematurely. + * + * if (EPC == LEND && LCOUNT != 0) { + * LCOUNT--; + * EPC = LBEG; + * } + * + * param[in/out] epc_reg - register containing EPC, updated to LBEG if needed + * param[in/out] tmp_reg - temporary register for intermediate values + * + * Return: Use epc_reg to set corrected EPC value. + */ +.macro XT_ZOL_EPC_LCOUNT_RESTORE epc_reg tmp_reg + #if XCHAL_HAVE_LOOPS + rsr \tmp_reg, XT_REG_LEND + bne \epc_reg, \tmp_reg, 1f + rsr \tmp_reg, XT_REG_LCOUNT + beqz \tmp_reg, 1f + addi \tmp_reg, \tmp_reg, -1 + wsr \tmp_reg, XT_REG_LCOUNT + rsr \epc_reg, XT_REG_LBEG +1: + #endif +.endm + +#endif /* __XTENSA__ */ + +#endif /* XTENSA_ZOL_MACROS_H */ diff --git a/components/xtensa/xtensa_loadstore_handler.S b/components/xtensa/xtensa_loadstore_handler.S index 859a6ae81ca..6c9eb95776a 100644 --- a/components/xtensa/xtensa_loadstore_handler.S +++ b/components/xtensa/xtensa_loadstore_handler.S @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -37,6 +37,7 @@ */ #include "xtensa_rtos.h" +#include "xtensa/xtensa_zol_macros.h" #include "sdkconfig.h" #include "soc/soc.h" @@ -124,10 +125,11 @@ LoadStoreErrorHandler: 2: /* a4 contains the value */ + wsr a0, sar rsr a3, epc1 addi a3, a3, 3 + XT_ZOL_EPC_LCOUNT_RESTORE a3, a0 // If faulted at LEND, rewinds loop and leaves adjusted PC in a3 wsr a3, epc1 - wsr a0, sar rsr a0, excsave1 extui a2, a2, 3, 5 @@ -169,6 +171,7 @@ LoadStoreErrorHandler: /* a4 contains the value */ rsr a6, epc1 addi a6, a6, 3 + XT_ZOL_EPC_LCOUNT_RESTORE a6, a5 // If faulted at LEND, rewinds loop and leaves adjusted PC in a6 wsr a6, epc1 ssa8b a3 @@ -321,8 +324,9 @@ AlignmentErrorHandler: srai a4, a4, 16 // a4 contains the value 1: - wsr a3, epc1 wsr a0, sar + XT_ZOL_EPC_LCOUNT_RESTORE a3, a0 // If faulted at LEND, rewinds loop and leaves adjusted PC in a3 + wsr a3, epc1 rsr a0, excsave1 extui a2, a2, 4, 4 @@ -379,6 +383,7 @@ AlignmentErrorHandler: slli a6, a5, 16 // 0xffff0000 1: + XT_ZOL_EPC_LCOUNT_RESTORE a7, a5 // If faulted at LEND, rewinds loop and leaves adjusted PC in a7 wsr a7, epc1 movi a5, ~3 and a5, a3, a5 // a5 has the aligned address diff --git a/components/xtensa/xtensa_vectors.S b/components/xtensa/xtensa_vectors.S index bcaa6b97a9b..c5258146c1b 100644 --- a/components/xtensa/xtensa_vectors.S +++ b/components/xtensa/xtensa_vectors.S @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: MIT * - * SPDX-FileContributor: 2016-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileContributor: 2016-2026 Espressif Systems (Shanghai) CO LTD */ /* * Copyright (c) 2015-2019 Cadence Design Systems, Inc. @@ -99,6 +99,7 @@ */ #include "xtensa_rtos.h" +#include "xtensa/xtensa_zol_macros.h" #include "esp_private/panic_reason.h" #include "sdkconfig.h" #include "soc/soc.h" @@ -120,7 +121,7 @@ frame just in case the exception dispatcher's SP does not point to the exception frame (which is the case when switching from task to interrupt stack). - Clearing the pseudo base-save area is uncessary as the interrupt dispatcher + Clearing the pseudo base-save area is unnecessary as the interrupt dispatcher will restore the current SP to that of the pre-exception SP. -------------------------------------------------------------------------------- */ @@ -627,7 +628,7 @@ _UserExceptionVector: /* -------------------------------------------------------------------------------- Insert some waypoints for jumping beyond the signed 8-bit range of - conditional branch instructions, so the conditional branchces to specific + conditional branch instructions, so the conditional branches to specific exception handlers are not taken in the mainline. Saves some cycles in the mainline. -------------------------------------------------------------------------------- @@ -741,7 +742,7 @@ _xt_handle_exc: rsr a0, EXCVADDR s32i a0, sp, XT_STK_EXCVADDR - /* Set up PS for C, reenable debug and NMI interrupts, and clear EXCM. */ + /* Set up PS for C, re-enable debug and NMI interrupts, and clear EXCM. */ #ifdef __XTENSA_CALL0_ABI__ movi a0, PS_INTLEVEL(XCHAL_DEBUGLEVEL - 2) | PS_UM #else @@ -880,22 +881,13 @@ _xt_syscall_exc: #endif /* - Grab the interruptee's PC and skip over the 'syscall' instruction. - If it's at the end of a zero-overhead loop and it's not on the last - iteration, decrement loop counter and skip to beginning of loop. + Adjust EPC to point to next instruction, so when we return to user code + it will be at the instruction after the 'syscall' that caused the exception. */ rsr a2, EPC_1 /* a2 = PC of 'syscall' */ addi a3, a2, 3 /* ++PC */ - #if XCHAL_HAVE_LOOPS - rsr a0, LEND /* if (PC == LEND */ - bne a3, a0, 1f - rsr a0, LCOUNT /* && LCOUNT != 0) */ - beqz a0, 1f /* { */ - addi a0, a0, -1 /* --LCOUNT */ - rsr a3, LBEG /* PC = LBEG */ - wsr a0, LCOUNT /* } */ - #endif -1: wsr a3, EPC_1 /* update PC */ + XT_ZOL_EPC_LCOUNT_RESTORE a3, a0 /* If faulted at LEND, rewinds loop and leaves adjusted PC in a3 */ + wsr a3, EPC_1 /* update PC to next instruction */ /* Restore interruptee's context and return from exception. */ #ifdef __XTENSA_CALL0_ABI__