From 63d18fad59ed16a66b4d79aa86173851adc868b0 Mon Sep 17 00:00:00 2001 From: Marius Vikhammer Date: Fri, 3 Apr 2026 14:01:22 +0800 Subject: [PATCH 1/2] test(esp_hw_support): add esp_cpu api test app Add a dedicated esp_cpu test app that covers the supported helper APIs, interrupt helpers, debug helper validation, and destructive trigger cases. Made-with: Cursor --- .../test_apps/.build-test-rules.yml | 2 + .../test_apps/cpu/CMakeLists.txt | 14 ++ .../esp_hw_support/test_apps/cpu/README.md | 12 ++ .../test_apps/cpu/main/CMakeLists.txt | 14 ++ .../test_apps/cpu/main/test_app_main.c | 17 ++ .../test_apps/cpu/main/test_cpu_basic.c | 146 ++++++++++++++++ .../test_apps/cpu/main/test_cpu_breakpoint.c | 36 ++++ .../test_apps/cpu/main/test_cpu_interrupts.c | 158 ++++++++++++++++++ .../cpu/main/test_cpu_non_recoverable.c | 92 ++++++++++ .../test_apps/cpu/main/test_cpu_reset.c | 27 +++ .../test_apps/cpu/main/test_cpu_stall.c | 73 ++++++++ .../test_apps/cpu/main/test_cpu_watchpoint.c | 58 +++++++ .../test_apps/cpu/pytest_cpu.py | 59 +++++++ .../test_apps/cpu/sdkconfig.ci.default | 2 + .../test_apps/cpu/sdkconfig.defaults | 4 + 15 files changed, 714 insertions(+) create mode 100644 components/esp_hw_support/test_apps/cpu/CMakeLists.txt create mode 100644 components/esp_hw_support/test_apps/cpu/README.md create mode 100644 components/esp_hw_support/test_apps/cpu/main/CMakeLists.txt create mode 100644 components/esp_hw_support/test_apps/cpu/main/test_app_main.c create mode 100644 components/esp_hw_support/test_apps/cpu/main/test_cpu_basic.c create mode 100644 components/esp_hw_support/test_apps/cpu/main/test_cpu_breakpoint.c create mode 100644 components/esp_hw_support/test_apps/cpu/main/test_cpu_interrupts.c create mode 100644 components/esp_hw_support/test_apps/cpu/main/test_cpu_non_recoverable.c create mode 100644 components/esp_hw_support/test_apps/cpu/main/test_cpu_reset.c create mode 100644 components/esp_hw_support/test_apps/cpu/main/test_cpu_stall.c create mode 100644 components/esp_hw_support/test_apps/cpu/main/test_cpu_watchpoint.c create mode 100644 components/esp_hw_support/test_apps/cpu/pytest_cpu.py create mode 100644 components/esp_hw_support/test_apps/cpu/sdkconfig.ci.default create mode 100644 components/esp_hw_support/test_apps/cpu/sdkconfig.defaults diff --git a/components/esp_hw_support/test_apps/.build-test-rules.yml b/components/esp_hw_support/test_apps/.build-test-rules.yml index 27fc95c570..d8a173d2eb 100644 --- a/components/esp_hw_support/test_apps/.build-test-rules.yml +++ b/components/esp_hw_support/test_apps/.build-test-rules.yml @@ -1,5 +1,7 @@ # Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps + +components/esp_hw_support/test_apps/cpu: components/esp_hw_support/test_apps/esp_hw_support_unity_tests: components/esp_hw_support/test_apps/host_test_linux: diff --git a/components/esp_hw_support/test_apps/cpu/CMakeLists.txt b/components/esp_hw_support/test_apps/cpu/CMakeLists.txt new file mode 100644 index 0000000000..c9ed69e40f --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/CMakeLists.txt @@ -0,0 +1,14 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.22) + +set(COMPONENTS main) +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/test_apps/components") + +list(PREPEND SDKCONFIG_DEFAULTS "$ENV{IDF_PATH}/tools/test_apps/configs/sdkconfig.debug_helpers" "sdkconfig.defaults") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +set(COMPONENTS main) + +project(cpu) diff --git a/components/esp_hw_support/test_apps/cpu/README.md b/components/esp_hw_support/test_apps/cpu/README.md new file mode 100644 index 0000000000..2a4f1e20bc --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/README.md @@ -0,0 +1,12 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- | + +This test app collects `esp_cpu.h` coverage in one place. + +It currently includes: + +- safe Unity tests for basic CPU helpers and interrupt helpers +- a multi-stage reset test for `esp_cpu_reset()` +- a thread-pointer roundtrip test and per-core `esp_cpu_get_core_id()` coverage +- non-destructive sanity checks for breakpoint and watchpoint setup APIs +- non-recoverable breakpoint and watchpoint trigger tests, plus a multi-core stall test where supported diff --git a/components/esp_hw_support/test_apps/cpu/main/CMakeLists.txt b/components/esp_hw_support/test_apps/cpu/main/CMakeLists.txt new file mode 100644 index 0000000000..ddb540eb4f --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/main/CMakeLists.txt @@ -0,0 +1,14 @@ +set(srcs "test_app_main.c" + "test_cpu_basic.c" + "test_cpu_interrupts.c" + "test_cpu_reset.c" + "test_cpu_watchpoint.c" + "test_cpu_breakpoint.c" + "test_cpu_non_recoverable.c" + "test_cpu_stall.c") + +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +# the component can be registered as WHOLE_ARCHIVE +idf_component_register(SRCS ${srcs} + REQUIRES unity test_utils esp_hw_support esp_system + WHOLE_ARCHIVE) diff --git a/components/esp_hw_support/test_apps/cpu/main/test_app_main.c b/components/esp_hw_support/test_apps/cpu/main/test_app_main.c new file mode 100644 index 0000000000..3e8f00fad5 --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/main/test_app_main.c @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "esp_log.h" + +static const char *TAG = "cpu_test_app"; + +void app_main(void) +{ + ESP_LOGI(TAG, "Running esp_cpu test app"); + unity_run_menu(); +} diff --git a/components/esp_hw_support/test_apps/cpu/main/test_cpu_basic.c b/components/esp_hw_support/test_apps/cpu/main/test_cpu_basic.c new file mode 100644 index 0000000000..3f2f671da9 --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/main/test_cpu_basic.c @@ -0,0 +1,146 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "unity.h" +#include "soc/soc_caps.h" +#include "esp_cpu.h" +#include "test_utils.h" + +static intptr_t __attribute__((noinline)) get_call_addr_delta(void) +{ + intptr_t return_addr = (intptr_t)__builtin_return_address(0); + intptr_t call_addr = esp_cpu_get_call_addr(return_addr); + return return_addr - call_addr; +} + +typedef struct { + TaskHandle_t waiter; + int observed_core_id; +} core_id_task_ctx_t; + +static void core_id_task(void *arg) +{ + core_id_task_ctx_t *ctx = (core_id_task_ctx_t *)arg; + + ctx->observed_core_id = esp_cpu_get_core_id(); + xTaskNotifyGive(ctx->waiter); + vTaskDelete(NULL); +} + +TEST_CASE("CPU basic: current core id matches pinned task core", "[cpu][cpu_basic]") +{ + const TaskHandle_t waiter = xTaskGetCurrentTaskHandle(); + core_id_task_ctx_t task_ctx[SOC_CPU_CORES_NUM] = {0}; + + for (int core_id = 0; core_id < SOC_CPU_CORES_NUM; ++core_id) { + task_ctx[core_id].waiter = waiter; + task_ctx[core_id].observed_core_id = -1; + TEST_ASSERT_EQUAL(pdPASS, xTaskCreatePinnedToCore(core_id_task, + "cpu_core_id", + 2048, + &task_ctx[core_id], + UNITY_FREERTOS_PRIORITY + 1, + NULL, + core_id)); + } + + for (int core_id = 0; core_id < SOC_CPU_CORES_NUM; ++core_id) { + /* Use pdFALSE (counting mode) so accumulated notifications don't cause + * a false failure if both tasks complete before the first wait. */ + TEST_ASSERT_NOT_EQUAL(0, ulTaskNotifyTake(pdFALSE, pdMS_TO_TICKS(1000))); + } + + for (int core_id = 0; core_id < SOC_CPU_CORES_NUM; ++core_id) { + TEST_ASSERT_EQUAL(core_id, task_ctx[core_id].observed_core_id); + } +} + +TEST_CASE("CPU basic: stack pointer is near current stack frame", "[cpu][cpu_basic]") +{ + uint32_t local_marker = 0; + uintptr_t sp = (uintptr_t)esp_cpu_get_sp(); + uintptr_t local = (uintptr_t)&local_marker; + uintptr_t delta = (sp > local) ? (sp - local) : (local - sp); + + TEST_ASSERT_TRUE(delta < 4096); +} + +TEST_CASE("CPU basic: cycle count increases", "[cpu][cpu_basic]") +{ + esp_cpu_cycle_count_t start = esp_cpu_get_cycle_count(); + for (volatile int i = 0; i < 256; i++) { + __asm__ __volatile__("nop"); + } + esp_cpu_cycle_count_t end = esp_cpu_get_cycle_count(); + + TEST_ASSERT_TRUE((end - start) > 0); +} + +TEST_CASE("CPU basic: current privilege level is sane", "[cpu][cpu_basic]") +{ + int privilege = esp_cpu_get_curr_privilege_level(); + +#if __XTENSA__ + TEST_ASSERT_EQUAL(-1, privilege); +#else + TEST_ASSERT_TRUE(privilege >= 0); +#endif +} + +TEST_CASE("CPU basic: pc to addr conversion matches architecture", "[cpu][cpu_basic]") +{ + uintptr_t addr = (uintptr_t)esp_cpu_pc_to_addr(0x00001234U); + +#if __XTENSA__ + TEST_ASSERT_EQUAL_HEX32(0x40001234U, addr); +#else + TEST_ASSERT_EQUAL_HEX32(0x00001234U, addr); +#endif +} + +TEST_CASE("CPU basic: call addr delta matches architecture", "[cpu][cpu_basic]") +{ +#if __XTENSA__ + TEST_ASSERT_EQUAL(3, get_call_addr_delta()); +#else + TEST_ASSERT_EQUAL(4, get_call_addr_delta()); +#endif +} + +TEST_CASE("CPU basic: compare and set succeeds on matching value", "[cpu][cpu_basic]") +{ + volatile uint32_t value = 1; + + TEST_ASSERT_TRUE(esp_cpu_compare_and_set(&value, 1, 2)); + TEST_ASSERT_EQUAL_UINT32(2, value); +} + +TEST_CASE("CPU basic: compare and set fails on mismatched value", "[cpu][cpu_basic]") +{ + volatile uint32_t value = 2; + + TEST_ASSERT_FALSE(esp_cpu_compare_and_set(&value, 1, 3)); + TEST_ASSERT_EQUAL_UINT32(2, value); +} + +TEST_CASE("CPU basic: thread pointer can be set and restored", "[cpu][cpu_basic]") +{ + uintptr_t marker_words[4] = {0}; + void *original = esp_cpu_get_threadptr(); + void *readback; + void *restored; + + esp_cpu_set_threadptr((void *)marker_words); + readback = esp_cpu_get_threadptr(); + esp_cpu_set_threadptr(original); + restored = esp_cpu_get_threadptr(); + + TEST_ASSERT_EQUAL_PTR(marker_words, readback); + TEST_ASSERT_EQUAL_PTR(original, restored); +} diff --git a/components/esp_hw_support/test_apps/cpu/main/test_cpu_breakpoint.c b/components/esp_hw_support/test_apps/cpu/main/test_cpu_breakpoint.c new file mode 100644 index 0000000000..1f04a9706e --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/main/test_cpu_breakpoint.c @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "unity.h" +#include "soc/soc_caps.h" +#include "esp_cpu.h" + +#if SOC_CPU_BREAKPOINTS_NUM > 0 +static void __attribute__((noinline)) breakpoint_target_function(void) +{ + __asm__ __volatile__("" ::: "memory"); +} + +TEST_CASE("CPU breakpoint: all breakpoints can be set and cleared", "[cpu][cpu_breakpoint]") +{ + for (int bp_num = 0; bp_num < SOC_CPU_BREAKPOINTS_NUM; ++bp_num) { + TEST_ASSERT_EQUAL(ESP_OK, esp_cpu_set_breakpoint(bp_num, (const void *)breakpoint_target_function)); + } + + for (int bp_num = 0; bp_num < SOC_CPU_BREAKPOINTS_NUM; ++bp_num) { + TEST_ASSERT_EQUAL(ESP_OK, esp_cpu_clear_breakpoint(bp_num)); + } +} + +TEST_CASE("CPU breakpoint: invalid index is rejected", "[cpu][cpu_breakpoint]") +{ + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_cpu_set_breakpoint(-1, (const void *)breakpoint_target_function)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_cpu_clear_breakpoint(-1)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_cpu_set_breakpoint(SOC_CPU_BREAKPOINTS_NUM, (const void *)breakpoint_target_function)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_cpu_clear_breakpoint(SOC_CPU_BREAKPOINTS_NUM)); +} + +#endif diff --git a/components/esp_hw_support/test_apps/cpu/main/test_cpu_interrupts.c b/components/esp_hw_support/test_apps/cpu/main/test_cpu_interrupts.c new file mode 100644 index 0000000000..51b840ce7a --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/main/test_cpu_interrupts.c @@ -0,0 +1,158 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "unity.h" +#include "soc/soc_caps.h" +#include "esp_bit_defs.h" +#include "esp_cpu.h" + +static void dummy_handler(void *arg) +{ + volatile uint32_t *counter = (volatile uint32_t *)arg; + if (counter != NULL) { + (*counter)++; + } +} + +static bool is_valid_desc_type(esp_cpu_intr_type_t type) +{ + return type == ESP_CPU_INTR_TYPE_LEVEL + || type == ESP_CPU_INTR_TYPE_EDGE + || type == ESP_CPU_INTR_TYPE_NA; +} + +static int find_test_interrupt(void) +{ + const int core_id = esp_cpu_get_core_id(); + + for (int intr_num = 0; intr_num < SOC_CPU_INTR_NUM; intr_num++) { + esp_cpu_intr_desc_t intr_desc; + esp_cpu_intr_get_desc(core_id, intr_num, &intr_desc); + + if (intr_desc.flags & (ESP_CPU_INTR_DESC_FLAG_SPECIAL | ESP_CPU_INTR_DESC_FLAG_RESVD)) { + continue; + } + if (esp_cpu_intr_has_handler(intr_num)) { + continue; + } + return intr_num; + } + return -1; +} + +TEST_CASE("CPU intr: descriptor is valid for all interrupt lines", "[cpu][cpu_intr]") +{ + const int core_id = esp_cpu_get_core_id(); + const uint32_t valid_flags = ESP_CPU_INTR_DESC_FLAG_SPECIAL | ESP_CPU_INTR_DESC_FLAG_RESVD; + + for (int intr_num = 0; intr_num < SOC_CPU_INTR_NUM; intr_num++) { + esp_cpu_intr_desc_t intr_desc; + esp_cpu_intr_get_desc(core_id, intr_num, &intr_desc); + + TEST_ASSERT_TRUE(is_valid_desc_type(intr_desc.type)); + TEST_ASSERT_TRUE(intr_desc.priority >= -1); + TEST_ASSERT_EQUAL_HEX32(0, intr_desc.flags & ~valid_flags); + } +} + +TEST_CASE("CPU intr: handler and arg can be set and cleared", "[cpu][cpu_intr]") +{ + int intr_num = find_test_interrupt(); + if (intr_num < 0) { + TEST_IGNORE_MESSAGE("No free interrupt line was found for the handler test"); + } + + volatile uint32_t counter = 0; + + esp_cpu_intr_set_handler(intr_num, dummy_handler, (void *)&counter); + TEST_ASSERT_TRUE(esp_cpu_intr_has_handler(intr_num)); +#if !__XTENSA__ + TEST_ASSERT_EQUAL_PTR(&counter, esp_cpu_intr_get_handler_arg(intr_num)); +#endif + + esp_cpu_intr_set_handler(intr_num, NULL, NULL); + TEST_ASSERT_FALSE(esp_cpu_intr_has_handler(intr_num)); +} + +TEST_CASE("CPU intr: enabled mask can be toggled for a free line", "[cpu][cpu_intr]") +{ + int intr_num = find_test_interrupt(); + if (intr_num < 0) { + TEST_IGNORE_MESSAGE("No free interrupt line was found for the enabled mask test"); + } + + const uint32_t bit = BIT(intr_num); + const uint32_t old_mask = esp_cpu_intr_get_enabled_mask(); + + esp_cpu_intr_disable(bit); + TEST_ASSERT_EQUAL_HEX32(0, esp_cpu_intr_get_enabled_mask() & bit); + + esp_cpu_intr_enable(bit); + TEST_ASSERT_NOT_EQUAL(0, esp_cpu_intr_get_enabled_mask() & bit); + + if ((old_mask & bit) == 0) { + esp_cpu_intr_disable(bit); + } else { + esp_cpu_intr_enable(bit); + } +} + +#if SOC_CPU_HAS_FLEXIBLE_INTC +TEST_CASE("CPU intr: type can be changed on a free line", "[cpu][cpu_intr]") +{ + int intr_num = find_test_interrupt(); + if (intr_num < 0) { + TEST_IGNORE_MESSAGE("No free interrupt line was found for the type test"); + } + + esp_cpu_intr_type_t old_type = esp_cpu_intr_get_type(intr_num); + esp_cpu_intr_type_t new_type = (old_type == ESP_CPU_INTR_TYPE_EDGE) ? ESP_CPU_INTR_TYPE_LEVEL : ESP_CPU_INTR_TYPE_EDGE; + + esp_cpu_intr_set_type(intr_num, new_type); + TEST_ASSERT_EQUAL(new_type, esp_cpu_intr_get_type(intr_num)); + esp_cpu_intr_set_type(intr_num, old_type); +} + +TEST_CASE("CPU intr: priority can be changed on a free line", "[cpu][cpu_intr]") +{ + int intr_num = find_test_interrupt(); + if (intr_num < 0) { + TEST_IGNORE_MESSAGE("No free interrupt line was found for the priority test"); + } + + int old_priority = esp_cpu_intr_get_priority(intr_num); + int new_priority = (old_priority == 1) ? 2 : 1; + + esp_cpu_intr_set_priority(intr_num, new_priority); + TEST_ASSERT_EQUAL(new_priority, esp_cpu_intr_get_priority(intr_num)); + esp_cpu_intr_set_priority(intr_num, old_priority); +} +#endif + +TEST_CASE("CPU intr: edge ack is callable", "[cpu][cpu_intr]") +{ + int intr_num = find_test_interrupt(); + if (intr_num < 0) { + TEST_IGNORE_MESSAGE("No free interrupt line was found for the edge ack test"); + } + +#if SOC_CPU_HAS_FLEXIBLE_INTC + esp_cpu_intr_type_t old_type = esp_cpu_intr_get_type(intr_num); + esp_cpu_intr_set_type(intr_num, ESP_CPU_INTR_TYPE_EDGE); + esp_cpu_intr_edge_ack(intr_num); + esp_cpu_intr_set_type(intr_num, old_type); +#else + const int core_id = esp_cpu_get_core_id(); + esp_cpu_intr_desc_t intr_desc; + esp_cpu_intr_get_desc(core_id, intr_num, &intr_desc); + if (intr_desc.type != ESP_CPU_INTR_TYPE_EDGE) { + TEST_IGNORE_MESSAGE("No free edge interrupt line was found for the edge ack test"); + } + esp_cpu_intr_edge_ack(intr_num); +#endif +} diff --git a/components/esp_hw_support/test_apps/cpu/main/test_cpu_non_recoverable.c b/components/esp_hw_support/test_apps/cpu/main/test_cpu_non_recoverable.c new file mode 100644 index 0000000000..f279a80375 --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/main/test_cpu_non_recoverable.c @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "unity.h" +#include "unity_test_runner.h" +#include "soc/soc_caps.h" +#include "esp_cpu.h" +#include "esp_system.h" + +#if __XTENSA__ +#define CPU_DEBUG_EXCEPTION_REASON "Unhandled debug exception" +#else +#define CPU_DEBUG_EXCEPTION_REASON "Breakpoint" +#endif + +#if SOC_CPU_BREAKPOINTS_NUM > 0 +static void __attribute__((noinline)) breakpoint_target_function(void) +{ + __asm__ __volatile__("" ::: "memory"); +} + +static void trigger_breakpoint_panic(void) +{ + TEST_ASSERT_EQUAL(ESP_OK, esp_cpu_set_breakpoint(0, (const void *)breakpoint_target_function)); + breakpoint_target_function(); + while (true) { + } +} + +static void check_breakpoint_reset_reason(void) +{ + TEST_ASSERT_EQUAL(ESP_RST_PANIC, esp_reset_reason()); +} + +TEST_CASE_MULTIPLE_STAGES("CPU breakpoint: trigger causes panic", + "[cpu][cpu_breakpoint][cpu_non_recoverable][reset=" CPU_DEBUG_EXCEPTION_REASON ",SW_CPU_RESET]", + trigger_breakpoint_panic, check_breakpoint_reset_reason) +#endif + +#if SOC_CPU_WATCHPOINTS_NUM > 0 +static volatile uint32_t s_watchpoint_target; +static volatile uint32_t s_watchpoint_sink; +#define WATCHPOINT_TARGET_ADDR ((const void *)&s_watchpoint_target) + +static void __attribute__((noinline)) trigger_watchpoint_store(void) +{ + s_watchpoint_target++; +} + +static void __attribute__((noinline)) trigger_watchpoint_load(void) +{ + s_watchpoint_sink = s_watchpoint_target; +} + +static void trigger_watchpoint_store_panic(void) +{ + s_watchpoint_target = 0; + TEST_ASSERT_EQUAL(ESP_OK, + esp_cpu_set_watchpoint(0, WATCHPOINT_TARGET_ADDR, sizeof(s_watchpoint_target), ESP_CPU_WATCHPOINT_STORE)); + trigger_watchpoint_store(); + while (true) { + } +} + +static void check_watchpoint_reset_reason(void) +{ + TEST_ASSERT_EQUAL(ESP_RST_PANIC, esp_reset_reason()); +} + +TEST_CASE_MULTIPLE_STAGES("CPU watchpoint: store trigger causes panic", + "[cpu][cpu_watchpoint][cpu_non_recoverable][reset=" CPU_DEBUG_EXCEPTION_REASON ",SW_CPU_RESET]", + trigger_watchpoint_store_panic, check_watchpoint_reset_reason) + +static void trigger_watchpoint_load_panic(void) +{ + s_watchpoint_target = 0x12345678; + s_watchpoint_sink = 0; + TEST_ASSERT_EQUAL(ESP_OK, + esp_cpu_set_watchpoint(0, WATCHPOINT_TARGET_ADDR, sizeof(s_watchpoint_target), ESP_CPU_WATCHPOINT_LOAD)); + trigger_watchpoint_load(); + while (true) { + } +} + +TEST_CASE_MULTIPLE_STAGES("CPU watchpoint: load trigger causes panic", + "[cpu][cpu_watchpoint][cpu_non_recoverable][reset=" CPU_DEBUG_EXCEPTION_REASON ",SW_CPU_RESET]", + trigger_watchpoint_load_panic, check_watchpoint_reset_reason) +#endif diff --git a/components/esp_hw_support/test_apps/cpu/main/test_cpu_reset.c b/components/esp_hw_support/test_apps/cpu/main/test_cpu_reset.c new file mode 100644 index 0000000000..8befce967c --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/main/test_cpu_reset.c @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "esp_cpu.h" +#include "esp_system.h" + +#if SOC_CPU_CORES_NUM == 1 +static void do_cpu_reset(void) +{ + esp_cpu_reset(esp_cpu_get_core_id()); + while (true) { + } +} + +static void check_cpu_reset_reason(void) +{ + TEST_ASSERT_EQUAL(ESP_RST_SW, esp_reset_reason()); +} + +TEST_CASE_MULTIPLE_STAGES("CPU reset: current core reset reboots the chip", "[cpu][cpu_reset][reset=SW_CPU_RESET]", + do_cpu_reset, check_cpu_reset_reason) +#endif diff --git a/components/esp_hw_support/test_apps/cpu/main/test_cpu_stall.c b/components/esp_hw_support/test_apps/cpu/main/test_cpu_stall.c new file mode 100644 index 0000000000..0ae55b1a2a --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/main/test_cpu_stall.c @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "unity.h" +#include "soc/soc_caps.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_rom_sys.h" +#include "esp_cpu.h" +#include "test_utils.h" + +#if SOC_CPU_CORES_NUM > 1 +static volatile bool s_stall_task_run; +static volatile bool s_stall_task_started; +static volatile uint32_t s_stall_counter; + +static void stall_counter_task(void *arg) +{ + s_stall_task_started = true; + while (s_stall_task_run) { + s_stall_counter++; + } + vTaskDelete(NULL); +} + +TEST_CASE("CPU stall: other core stops and resumes", "[cpu][cpu_stall]") +{ + const int current_core = esp_cpu_get_core_id(); + const int other_core = !current_core; + TaskHandle_t task_handle = NULL; + + s_stall_task_run = true; + s_stall_task_started = false; + s_stall_counter = 0; + + TEST_ASSERT_EQUAL(pdPASS, + xTaskCreatePinnedToCore(stall_counter_task, "cpu_stall_counter", 2048, NULL, + UNITY_FREERTOS_PRIORITY + 1, &task_handle, other_core)); + + for (int i = 0; i < 1000 && !s_stall_task_started; i++) { + esp_rom_delay_us(100); + } + TEST_ASSERT_TRUE(s_stall_task_started); + + uint32_t start_count = s_stall_counter; + for (int i = 0; i < 1000 && s_stall_counter == start_count; i++) { + esp_rom_delay_us(100); + } + TEST_ASSERT_TRUE(s_stall_counter > start_count); + + esp_cpu_stall(other_core); + esp_rom_delay_us(1000); + uint32_t stalled_count = s_stall_counter; + esp_rom_delay_us(20000); + uint32_t stalled_count_late = s_stall_counter; + esp_cpu_unstall(other_core); + + TEST_ASSERT_EQUAL_UINT32(stalled_count, stalled_count_late); + + for (int i = 0; i < 1000 && s_stall_counter == stalled_count_late; i++) { + esp_rom_delay_us(100); + } + TEST_ASSERT_TRUE(s_stall_counter > stalled_count_late); + + s_stall_task_run = false; + vTaskDelay(1); +} +#endif diff --git a/components/esp_hw_support/test_apps/cpu/main/test_cpu_watchpoint.c b/components/esp_hw_support/test_apps/cpu/main/test_cpu_watchpoint.c new file mode 100644 index 0000000000..9207e96678 --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/main/test_cpu_watchpoint.c @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "unity.h" +#include "soc/soc_caps.h" +#include "esp_cpu.h" + +#if SOC_CPU_WATCHPOINTS_NUM > 0 +static volatile uint32_t s_watchpoint_target; +#define WATCHPOINT_TARGET_ADDR ((const void *)&s_watchpoint_target) + +TEST_CASE("CPU watchpoint: invalid index is rejected", "[cpu][cpu_watchpoint]") +{ + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, + esp_cpu_set_watchpoint(-1, WATCHPOINT_TARGET_ADDR, sizeof(s_watchpoint_target), ESP_CPU_WATCHPOINT_ACCESS)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, + esp_cpu_set_watchpoint(SOC_CPU_WATCHPOINTS_NUM, WATCHPOINT_TARGET_ADDR, sizeof(s_watchpoint_target), ESP_CPU_WATCHPOINT_ACCESS)); +} + +TEST_CASE("CPU watchpoint: invalid non-power-of-two size is rejected", "[cpu][cpu_watchpoint]") +{ + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, + esp_cpu_set_watchpoint(0, WATCHPOINT_TARGET_ADDR, 0, ESP_CPU_WATCHPOINT_ACCESS)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, + esp_cpu_set_watchpoint(0, WATCHPOINT_TARGET_ADDR, 3, ESP_CPU_WATCHPOINT_ACCESS)); +} + +TEST_CASE("CPU watchpoint: misaligned address is rejected", "[cpu][cpu_watchpoint]") +{ + const void *misaligned_addr = (const void *)((uintptr_t)&s_watchpoint_target + 1); + + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, + esp_cpu_set_watchpoint(0, misaligned_addr, sizeof(s_watchpoint_target), ESP_CPU_WATCHPOINT_ACCESS)); +} + +TEST_CASE("CPU watchpoint: all watchpoints can be set and cleared", "[cpu][cpu_watchpoint]") +{ + for (int wp_num = 0; wp_num < SOC_CPU_WATCHPOINTS_NUM; ++wp_num) { + TEST_ASSERT_EQUAL(ESP_OK, + esp_cpu_set_watchpoint(wp_num, WATCHPOINT_TARGET_ADDR, sizeof(s_watchpoint_target), ESP_CPU_WATCHPOINT_ACCESS)); + } + + for (int wp_num = 0; wp_num < SOC_CPU_WATCHPOINTS_NUM; ++wp_num) { + TEST_ASSERT_EQUAL(ESP_OK, esp_cpu_clear_watchpoint(wp_num)); + } +} + +TEST_CASE("CPU watchpoint: invalid clear index is rejected", "[cpu][cpu_watchpoint]") +{ + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_cpu_clear_watchpoint(-1)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_cpu_clear_watchpoint(SOC_CPU_WATCHPOINTS_NUM)); +} + +#endif diff --git a/components/esp_hw_support/test_apps/cpu/pytest_cpu.py b/components/esp_hw_support/test_apps/cpu/pytest_cpu.py new file mode 100644 index 0000000000..64074e219c --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/pytest_cpu.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded_idf import IdfDut +from pytest_embedded_idf.utils import idf_parametrize + + +def _run_non_recoverable_case(dut: IdfDut, test_name: str, expected_reason: str) -> None: + dut.skip_decode_panic = True + dut.expect_exact('Press ENTER to see the list of tests') + dut.write(f'"{test_name}"') + # TEST_CASE_MULTIPLE_STAGES shows a stage sub-menu before running; select stage 1 to trigger the fault + dut.expect_exact('(1)') + dut.write('1') + + actual_reason = dut.expect(r"Core\s+\d+\s+panic'ed \(([^)]+)\)", timeout=10).group(1).decode() + assert actual_reason == expected_reason, f"expected panic reason '{expected_reason}', got '{actual_reason}'" + + dut.expect_exact('Rebooting...') + + +@pytest.mark.generic +@idf_parametrize( + 'config,target', + [('default', 'supported_targets')], + indirect=['config', 'target'], +) +def test_cpu(dut: IdfDut) -> None: + dut.run_all_single_board_cases(group='!cpu_reset&!cpu_non_recoverable', timeout=120) + + +@pytest.mark.generic +@idf_parametrize( + 'config,target', + [('default', 'supported_targets')], + indirect=['config', 'target'], +) +def test_cpu_reset(dut: IdfDut) -> None: + dut.run_all_single_board_cases(group='cpu_reset', timeout=120) + + +@pytest.mark.generic +@idf_parametrize( + 'config,target', + [('default', 'supported_targets')], + indirect=['config', 'target'], +) +def test_cpu_non_recoverable(dut: IdfDut) -> None: + expected_reason = 'Unhandled debug exception' if dut.target in {'esp32', 'esp32s2', 'esp32s3'} else 'Breakpoint' + present_cases = {case.name for case in dut.test_menu} + + for case_name in ( + 'CPU breakpoint: trigger causes panic', + 'CPU watchpoint: store trigger causes panic', + 'CPU watchpoint: load trigger causes panic', + ): + if case_name in present_cases: + _run_non_recoverable_case(dut, case_name, expected_reason) diff --git a/components/esp_hw_support/test_apps/cpu/sdkconfig.ci.default b/components/esp_hw_support/test_apps/cpu/sdkconfig.ci.default new file mode 100644 index 0000000000..984bdcfe2e --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/sdkconfig.ci.default @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 diff --git a/components/esp_hw_support/test_apps/cpu/sdkconfig.defaults b/components/esp_hw_support/test_apps/cpu/sdkconfig.defaults new file mode 100644 index 0000000000..708fb346cb --- /dev/null +++ b/components/esp_hw_support/test_apps/cpu/sdkconfig.defaults @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +CONFIG_ESP_TASK_WDT_EN=n From a4b817d0fe6b527a4526ce7b8a87fd889447395f Mon Sep 17 00:00:00 2001 From: Marius Vikhammer Date: Fri, 3 Apr 2026 17:21:11 +0800 Subject: [PATCH 2/2] fix(cpu): fix CSR_PRV_MODE not defined for S31 --- components/riscv/include/riscv/csr.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/riscv/include/riscv/csr.h b/components/riscv/include/riscv/csr.h index cbb8773da1..04c72e1a88 100644 --- a/components/riscv/include/riscv/csr.h +++ b/components/riscv/include/riscv/csr.h @@ -205,7 +205,7 @@ extern "C" { /* Espressif's custom CSR for the current privilege mode */ #if CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 #define CSR_PRV_MODE 0xC10 -#elif CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C61 || CONFIG_IDF_TARGET_ESP32P4 +#elif CONFIG_IDF_TARGET_ESP32C5 || CONFIG_IDF_TARGET_ESP32C61 || CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S31 #define CSR_PRV_MODE 0x810 #endif