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
This commit is contained in:
Konstantin Kondrashov
2026-02-10 13:42:32 +02:00
parent bb4c139547
commit b40aae66e3
4 changed files with 124 additions and 16 deletions
@@ -15,6 +15,7 @@
#include <esp_types.h>
#include <stdio.h>
#include <esp_heap_caps.h>
#include "esp_attr.h"
#include "esp_random.h"
#include "esp_intr_alloc.h"
#include "xtensa_api.h"
@@ -23,6 +24,7 @@
#include "freertos/task.h"
#include <string.h>
#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)
@@ -217,4 +219,60 @@ TEST_CASE("LoadStore: 8/16-bit field access in IRAM from ISRs when pending inter
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
@@ -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 */
+8 -3
View File
@@ -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
+5 -13
View File
@@ -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"
@@ -904,22 +905,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, XT_REG_EPC_1 /* a2 = PC of 'syscall' */
addi a3, a2, 3 /* ++PC */
#if XCHAL_HAVE_LOOPS
rsr a0, XT_REG_LEND /* if (PC == LEND */
bne a3, a0, 1f
rsr a0, XT_REG_LCOUNT /* && LCOUNT != 0) */
beqz a0, 1f /* { */
addi a0, a0, -1 /* --LCOUNT */
rsr a3, XT_REG_LBEG /* PC = LBEG */
wsr a0, XT_REG_LCOUNT /* } */
#endif
1: wsr a3, XT_REG_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, XT_REG_EPC_1 /* update PC to next instruction */
/* Restore interruptee's context and return from exception. */
#ifdef __XTENSA_CALL0_ABI__