mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 18:50:34 +03:00
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:
@@ -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)
|
||||
|
||||
+15
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user