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).
This commit is contained in:
Meet Patel
2026-06-01 19:18:16 +05:30
parent 9756d9654c
commit dd57685c42
5 changed files with 279 additions and 38 deletions
@@ -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);
@@ -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 <stdint.h>
#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
@@ -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 <string.h>
#include <sys/param.h>
#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;
}
@@ -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)
@@ -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()