From dd57685c42ecced534e848cf49469d1dc159e345 Mon Sep 17 00:00:00 2001 From: Meet Patel Date: Thu, 28 May 2026 15:38:27 +0530 Subject: [PATCH] feat(esp_system): add esp_backtrace_print_all_tasks support for RISC-V Extends esp_backtrace_print_all_tasks() to RISC-V targets. For running tasks, registers are captured using UNW_GET_CONTEXT(); for suspended tasks, the saved RvExcFrame on the task stack is used. Dual-core support follows the same IPC pattern as the Xtensa implementation. Backtrace output adapts to the configured backtrace method: EH-frame, frame-pointer, or register dump (with a hint to enable frame-pointer mode). --- .../esp_system/include/esp_debug_helpers.h | 22 +- .../include/esp_private/fp_unwind.h | 32 ++- .../port/arch/riscv/debug_helpers.c | 213 +++++++++++++++--- .../main/test_backtrace.c | 35 ++- .../pytest_esp_system_unity_tests.py | 15 ++ 5 files changed, 279 insertions(+), 38 deletions(-) diff --git a/components/esp_system/include/esp_debug_helpers.h b/components/esp_system/include/esp_debug_helpers.h index d74a37d1aea..4280943af8e 100644 --- a/components/esp_system/include/esp_debug_helpers.h +++ b/components/esp_system/include/esp_debug_helpers.h @@ -103,10 +103,11 @@ esp_err_t esp_backtrace_print_from_frame(int depth, const esp_backtrace_frame_t* * * @param depth The maximum number of stack frames to print (should be > 0) * - * @note On RISC-V targets printing backtrace at run-time is only available if - * CONFIG_ESP_SYSTEM_USE_EH_FRAME is selected. Otherwise we simply print - * a register dump. Function assumes it is called in a context where the - * calling task will not migrate to another core, e.g. interrupts disabled/panic handler. + * @note On RISC-V targets printing a backtrace at run-time is only available if + * CONFIG_ESP_SYSTEM_USE_EH_FRAME or CONFIG_ESP_SYSTEM_USE_FRAME_POINTER is + * selected. Otherwise we simply print a register dump. Function assumes it is + * called in a context where the calling task will not migrate to another core, + * e.g. interrupt context or panic handler. * * @return * - ESP_OK Backtrace successfully printed to completion or to depth limit @@ -121,10 +122,19 @@ esp_err_t esp_backtrace_print(int depth); * * @note Users must ensure that no tasks are created or deleted while this function is running. * @note This function must be called from a task context. + * @note On RISC-V targets, the 'depth' argument is not honored and the output depends on the + * selected backtracing method (ESP_BACKTRACING_METHOD): + * - CONFIG_ESP_SYSTEM_USE_FRAME_POINTER or CONFIG_ESP_SYSTEM_USE_EH_FRAME: a full + * backtrace is printed for each task (the unwinders always unwind to completion). + * - Otherwise (no backtracing method selected): only a register dump is printed. + * Because the underlying RISC-V print routines do not report frame corruption, + * this function does not return ESP_FAIL on corrupt stacks. * * @return - * - ESP_OK All backtraces successfully printed to completion or to depth limit - * - ESP_FAIL One or more backtraces are corrupt + * - ESP_OK All backtraces printed + * - ESP_ERR_INVALID_ARG depth was <= 0 + * - ESP_ERR_NO_MEM Failed to allocate the task snapshot buffer + * - ESP_FAIL One or more backtraces are corrupt (Xtensa only) */ esp_err_t esp_backtrace_print_all_tasks(int depth); diff --git a/components/esp_system/include/esp_private/fp_unwind.h b/components/esp_system/include/esp_private/fp_unwind.h index 2444609ec4f..1d1fa512c1d 100644 --- a/components/esp_system/include/esp_private/fp_unwind.h +++ b/components/esp_system/include/esp_private/fp_unwind.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -7,6 +7,8 @@ #ifndef FP_UNWIND_H #define FP_UNWIND_H +#include + #ifdef __cplusplus extern "C" { #endif @@ -20,6 +22,34 @@ extern "C" { */ void esp_fp_print_backtrace(const void *frame_or); +/** + * @brief Generate a call stack starting at the given frame pointer. + * + * @param frame[in] Frame pointer to start unrolling from. + * @param callers[out] Array of callers where 0 will store the most recent caller. Can be NULL. + * @param stacks[out] Array of callers' stacks where 0 will store the most recent caller's stack. Can be NULL. + * @param depth[in] Maximum number of entries to fill in the callers and stacks arrays. + * Both arrays (when provided) must be able to hold at least this many entries. + * + * @returns Number of entries filled in the array. + */ +uint32_t esp_fp_get_callers(uint32_t frame, void** callers, void** stacks, uint32_t depth); + +/** + * @brief Output a single backtrace step to the serial. + * + * Defined as weak so that it can be overridden by the user, e.g. to capture each + * backtrace step and store it or forward it elsewhere instead of printing. + * + * @warning This function is also used to print the backtrace from the panic handler. + * Overriding it therefore changes the backtrace output on a panic as well, + * so any override must remain safe to call from a panic/exception context. + * + * @param pc Program counter of the backtrace step. + * @param sp Stack pointer of the backtrace step. + */ +void esp_fp_generated_step(uint32_t pc, uint32_t sp); + #ifdef __cplusplus } #endif diff --git a/components/esp_system/port/arch/riscv/debug_helpers.c b/components/esp_system/port/arch/riscv/debug_helpers.c index 3788a95cc47..50b250dc43d 100644 --- a/components/esp_system/port/arch/riscv/debug_helpers.c +++ b/components/esp_system/port/arch/riscv/debug_helpers.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -11,31 +11,70 @@ #include "freertos/task.h" #include "freertos/freertos_debug.h" #include "esp_err.h" +#include "esp_check.h" #include "esp_private/esp_system_attr.h" #include "esp_private/esp_cpu_internal.h" +#include "esp_rom_sys.h" +#include "riscv/rvruntime-frames.h" +#include "riscv/libunwind-riscv.h" #include +#include + +#if !CONFIG_FREERTOS_UNICORE +#include "esp_ipc.h" +#endif // !CONFIG_FREERTOS_UNICORE #if CONFIG_ESP_SYSTEM_USE_EH_FRAME #include "esp_private/eh_frame_parser.h" #elif CONFIG_ESP_SYSTEM_USE_FRAME_POINTER -extern void esp_fp_print_backtrace(const void*); +#include "esp_private/fp_unwind.h" -#else // !CONFIG_ESP_SYSTEM_USE_EH_FRAME && ! -/* Function used to print all the registers pointed by the given frame .*/ +#else // !CONFIG_ESP_SYSTEM_USE_EH_FRAME && !CONFIG_ESP_SYSTEM_USE_FRAME_POINTER extern void panic_print_registers(const void *frame, int core); #endif // CONFIG_ESP_SYSTEM_USE_EH_FRAME -/* Targets based on a RISC-V CPU cannot perform backtracing that easily. - * We have two options here: - * - Perform backtracing at runtime thanks to the configuration options - * CONFIG_ESP_SYSTEM_USE_EH_FRAME and CONFIG_ESP_SYSTEM_USE_FRAME_POINTER. - * - Let IDF monitor do the backtracing for us. Used during panic already. - * - * In both cases, this takes time, and we might be in an ISR, we must - * exit this handler as fast as possible, then we will simply print - * the interruptee's registers. - */ +static const char *DEBUG_HELPER_TAG = "DBG HLPR"; + +/** + * @brief Print the backtrace (or register dump) for a single saved CPU frame. + * + * Targets based on a RISC-V CPU cannot perform backtracing that easily. Depending on + * the configured backtracing method (ESP_BACKTRACING_METHOD) this prints either a full + * call stack (EH-frame or frame-pointer mode) or, as a fallback, a register dump that + * can be decoded offline by idf.py monitor. + * + * @param task_name Name of the task being printed. If NULL, no name line is printed + * (used when printing the current stack's backtrace). + * @param frame Saved/captured register frame to start backtracing from + * @param core_id Core whose CSRs are reflected in the register-dump fallback + */ +static void ESP_SYSTEM_IRAM_ATTR print_task_backtrace(const char *task_name, const RvExcFrame *frame, int core_id) +{ + if (task_name) { + esp_rom_printf("%s\r\n", task_name); + } + +#if CONFIG_ESP_SYSTEM_USE_EH_FRAME + (void)core_id; + esp_eh_frame_print_backtrace(frame); +#elif CONFIG_ESP_SYSTEM_USE_FRAME_POINTER + (void)core_id; + esp_fp_print_backtrace(frame); +#else // !CONFIG_ESP_SYSTEM_USE_EH_FRAME && !CONFIG_ESP_SYSTEM_USE_FRAME_POINTER + esp_cpu_frame_t backtrace_frame; + memcpy(&backtrace_frame, frame, sizeof(esp_cpu_frame_t)); + panic_prepare_frame_from_ctx(&backtrace_frame); + panic_print_registers(&backtrace_frame, core_id); + esp_rom_printf("\r\n"); + esp_rom_printf("Please enable CONFIG_ESP_SYSTEM_USE_FRAME_POINTER option to have a full backtrace.\r\n"); +#endif // CONFIG_ESP_SYSTEM_USE_EH_FRAME +} + +/* + * Note: On RISC-V the depth argument is not honored. The frame-pointer and eh_frame + * unwinders always unwind to completion, and the register-dump fallback prints no stack. + */ esp_err_t ESP_SYSTEM_IRAM_ATTR esp_backtrace_print(int depth) { (void)depth; @@ -49,22 +88,136 @@ esp_err_t ESP_SYSTEM_IRAM_ATTR esp_backtrace_print(int depth) return ESP_ERR_NOT_FOUND; } - void *frame = snapshot.pxTopOfStack; - - esp_cpu_frame_t backtrace_frame = {}; - memcpy(&backtrace_frame, frame, sizeof(esp_cpu_frame_t)); - -#if CONFIG_ESP_SYSTEM_USE_EH_FRAME - esp_eh_frame_print_backtrace(frame); -#elif CONFIG_ESP_SYSTEM_USE_FRAME_POINTER - esp_fp_print_backtrace(frame); -#else // CONFIG_ESP_SYSTEM_USE_EH_FRAME - panic_prepare_frame_from_ctx(&backtrace_frame); - - panic_print_registers(&backtrace_frame, current_core); - esp_rom_printf("\r\n"); - esp_rom_printf("Please enable CONFIG_ESP_SYSTEM_USE_FRAME_POINTER option to have a full backtrace.\r\n"); -#endif // CONFIG_ESP_SYSTEM_USE_EH_FRAME + print_task_backtrace(NULL, (const RvExcFrame *)snapshot.pxTopOfStack, current_core); return ESP_OK; } + +typedef struct { +#if !CONFIG_FREERTOS_UNICORE + volatile bool start_tracing; + volatile bool finished_tracing; +#endif // !CONFIG_FREERTOS_UNICORE + struct { + TaskHandle_t task_hdl; + RvExcFrame frame; + } cur_tasks[configNUMBER_OF_CORES]; +} riscv_backtrace_ctrl_t; + +#if !CONFIG_FREERTOS_UNICORE +static void backtrace_other_cores_ipc_func(void *arg) +{ + riscv_backtrace_ctrl_t *ctrl = (riscv_backtrace_ctrl_t *)arg; + + vTaskSuspendAll(); + + BaseType_t core_id = xPortGetCoreID(); + ctrl->cur_tasks[core_id].task_hdl = xTaskGetCurrentTaskHandle(); + UNW_GET_CONTEXT(&ctrl->cur_tasks[core_id].frame); + + /* Ensure the task_hdl and frame writes are visible to the backtracing core + before it observes start_tracing == true. */ + __sync_synchronize(); + ctrl->start_tracing = true; + while (!ctrl->finished_tracing) { + ; + } + + xTaskResumeAll(); +} +#endif // !CONFIG_FREERTOS_UNICORE + +esp_err_t esp_backtrace_print_all_tasks(int depth) +{ + if (depth <= 0) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = ESP_OK; + TaskSnapshot_t *task_snapshots; + riscv_backtrace_ctrl_t ctrl = {0}; + + const UBaseType_t num_tasks = uxTaskGetNumberOfTasks(); + task_snapshots = calloc(num_tasks, sizeof(TaskSnapshot_t)); + ESP_GOTO_ON_FALSE(task_snapshots, ESP_ERR_NO_MEM, malloc_err, DEBUG_HELPER_TAG, "Task snapshot alloc failed"); + +#if !CONFIG_FREERTOS_UNICORE + // Use IPC call to prepare other core for backtracing + ESP_GOTO_ON_ERROR(esp_ipc_call(!xPortGetCoreID(), backtrace_other_cores_ipc_func, (void *)&ctrl), + ipc_err, + DEBUG_HELPER_TAG, + "IPC call failed"); + // Wait for other core to confirm it is ready for backtracing + while (!ctrl.start_tracing) { + ; + } +#endif // !CONFIG_FREERTOS_UNICORE + + // Suspend the scheduler to prevent task switching + vTaskSuspendAll(); + + // Capture the current core's running task context now that task switching is disabled + const BaseType_t cur_core_id = xPortGetCoreID(); + ctrl.cur_tasks[cur_core_id].task_hdl = xTaskGetCurrentTaskHandle(); + UNW_GET_CONTEXT(&ctrl.cur_tasks[cur_core_id].frame); + + // Get snapshot of all tasks in the system + UBaseType_t snapshot_count = uxTaskGetSnapshotAll(task_snapshots, num_tasks, NULL); + const UBaseType_t num_snapshots = MIN(num_tasks, snapshot_count); + + // Print the backtrace of every task in the system + for (UBaseType_t task_idx = 0; task_idx < num_snapshots; task_idx++) { + TaskHandle_t task_hdl = (TaskHandle_t)task_snapshots[task_idx].pxTCB; + const RvExcFrame *frame_ptr = NULL; + RvExcFrame saved_frame; + + // Check if the task is one of the currently running tasks + bool cur_running = false; + BaseType_t running_core_id = 0; + for (BaseType_t i = 0; i < configNUMBER_OF_CORES; i++) { + if (task_hdl == ctrl.cur_tasks[i].task_hdl) { + cur_running = true; + running_core_id = i; + break; + } + } + + if (cur_running) { + /* + * For the currently running task(s) use the context we captured + * via UNW_GET_CONTEXT() rather than pxTopOfStack, which may not + * yet reflect the suspended state. + */ + memcpy(&saved_frame, &ctrl.cur_tasks[running_core_id].frame, sizeof(RvExcFrame)); + frame_ptr = &saved_frame; + } else { + // Set the starting backtrace frame using the task's saved stack pointer + frame_ptr = (const RvExcFrame *)task_snapshots[task_idx].pxTopOfStack; + } + + /* + * The register-dump fallback reflects the CSRs of the core executing this + * function, so report the current core for the dump's "Core N" label. + */ + char *name = pcTaskGetName(task_hdl); + print_task_backtrace(name ? name : "No Name", frame_ptr, (int)cur_core_id); + } + + // Resume the scheduler to allow task switching again + xTaskResumeAll(); + +#if !CONFIG_FREERTOS_UNICORE + // Indicate to the other core that backtracing is complete + ctrl.finished_tracing = true; +#endif // !CONFIG_FREERTOS_UNICORE + + free(task_snapshots); + return ret; + +#if !CONFIG_FREERTOS_UNICORE +ipc_err: + free(task_snapshots); +#endif // !CONFIG_FREERTOS_UNICORE +malloc_err: + return ret; +} diff --git a/components/esp_system/test_apps/esp_system_unity_tests/main/test_backtrace.c b/components/esp_system/test_apps/esp_system_unity_tests/main/test_backtrace.c index c13052d06b5..3359f447603 100644 --- a/components/esp_system/test_apps/esp_system_unity_tests/main/test_backtrace.c +++ b/components/esp_system/test_apps/esp_system_unity_tests/main/test_backtrace.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -147,6 +147,39 @@ TEST_CASE("Test esp_backtrace_print_all_tasks()", "[esp_system]") #endif // CONFIG_IDF_TARGET_ARCH_XTENSA +#if CONFIG_IDF_TARGET_ARCH_RISCV + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_debug_helpers.h" + +#define NUM_TEST_FUNCS_RISCV 2 + +static void backtrace_suspend_func_riscv(void *arg) +{ + vTaskSuspend(NULL); +} + +TEST_CASE("Test esp_backtrace_print_all_tasks() on RISC-V", "[esp_system]") +{ + TaskHandle_t task_handles[NUM_TEST_FUNCS_RISCV]; + + // An invalid depth must be rejected + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_backtrace_print_all_tasks(0)); + + for (int i = 0; i < NUM_TEST_FUNCS_RISCV; i++) { + xTaskCreate(backtrace_suspend_func_riscv, "trace_func", 2048, NULL, UNITY_FREERTOS_PRIORITY + i + 1, &task_handles[i]); + } + vTaskDelay(10); + TEST_ASSERT_EQUAL(ESP_OK, esp_backtrace_print_all_tasks(3)); + + for (int i = 0; i < NUM_TEST_FUNCS_RISCV; i++) { + vTaskDelete(task_handles[i]); + } +} + +#endif // CONFIG_IDF_TARGET_ARCH_RISCV + #if CONFIG_ESP_SYSTEM_USE_FRAME_POINTER void my_putc(char c) diff --git a/components/esp_system/test_apps/esp_system_unity_tests/pytest_esp_system_unity_tests.py b/components/esp_system/test_apps/esp_system_unity_tests/pytest_esp_system_unity_tests.py index 6d73284b552..53dff26b29f 100644 --- a/components/esp_system/test_apps/esp_system_unity_tests/pytest_esp_system_unity_tests.py +++ b/components/esp_system/test_apps/esp_system_unity_tests/pytest_esp_system_unity_tests.py @@ -184,3 +184,18 @@ def test_frame_pointer_backtracing(dut: Dut) -> None: # The backtrace should have two entries dut.expect(r'Backtrace: 0x[0-9a-f]{8}:0x[0-9a-f]{8} 0x[0-9a-f]{8}:0x[0-9a-f]{8}\s*[\r]?\n') dut.expect_exact('Rebooting...') + + +@pytest.mark.generic +@idf_parametrize('config', ['framepointer'], indirect=['config']) +@idf_parametrize('target', ['esp32s31'], indirect=['target']) +def test_print_all_tasks_backtracing(dut: Dut) -> None: + # With frame-pointer mode enabled, esp_backtrace_print_all_tasks() should print a + # valid backtrace for each task. Run on a multi-core RISC-V target so the dual-core + # IPC path is exercised: the task running on each core is captured via UNW_GET_CONTEXT, + # so we expect a well-formed "Backtrace:" line for each of the cores. + dut.expect_exact('Press ENTER to see the list of tests') + dut.write('"Test esp_backtrace_print_all_tasks() on RISC-V"') + for _ in range(2): + dut.expect(r'Backtrace: 0x[0-9a-f]{8}:0x[0-9a-f]{8}') + dut.expect_unity_test_output()