Files
esphome/esphome/components/esp32/crash_handler.cpp
T

592 lines
24 KiB
C++

#ifdef USE_ESP32
#include "esphome/core/defines.h"
#ifdef USE_ESP32_CRASH_HANDLER
#include "crash_handler.h"
#include "esphome/core/build_info_data.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <cstring>
#include <esp_attr.h>
#include <esp_private/panic_internal.h>
#include <soc/soc.h>
#if CONFIG_IDF_TARGET_ARCH_XTENSA
#include <esp_cpu_utils.h>
#include <esp_debug_helpers.h>
#include <xtensa_context.h>
#elif CONFIG_IDF_TARGET_ARCH_RISCV
#include <riscv/rvruntime-frames.h>
#endif
static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF;
static constexpr size_t MAX_BACKTRACE = 16;
// Check if an address looks like code (flash-mapped or IRAM).
// Must be safe to call from panic context (no flash access needed).
static inline bool IRAM_ATTR is_code_addr(uint32_t addr) {
return (addr >= SOC_IROM_LOW && addr < SOC_IROM_HIGH) || (addr >= SOC_IRAM_LOW && addr < SOC_IRAM_HIGH);
}
#if CONFIG_IDF_TARGET_ARCH_RISCV
// Check if a code address is a real return address by verifying the preceding
// instruction is a JAL or JALR with rd=ra (x1). Called at log time (not during
// panic) so flash cache is available and both IRAM and IROM are safely readable.
static inline bool is_return_addr(uint32_t addr) {
if (!is_code_addr(addr) || addr < 4)
return false;
// A return address on the stack points to the instruction after a call.
// Check for 4-byte JAL/JALR call instruction before this address.
// Use memcpy for alignment safety — RISC-V C extension means code addresses
// are only 2-byte aligned, so addr-4 may not be 4-byte aligned.
uint32_t inst;
// NOLINTNEXTLINE(performance-no-int-to-ptr) - reading code memory at a raw address is the point
memcpy(&inst, (const void *) (addr - 4), sizeof(inst));
// RISC-V instruction encoding: bits [6:0] = opcode, bits [11:7] = rd
uint32_t opcode = inst & 0x7f; // Extract 7-bit opcode
uint32_t rd = inst & 0xf80; // Extract rd field (bits 11:7)
// Match JAL (0x6f) or JALR (0x67) with rd=ra (x1, encoded as 0x80 = 1<<7)
if ((opcode == 0x6f || opcode == 0x67) && rd == 0x80)
return true;
// Check for 2-byte compressed c.jalr before this address (C extension).
// c.jalr saves to ra implicitly: funct4=1001, rs1!=0, rs2=0, op=10
if (addr >= 2) {
// NOLINTNEXTLINE(performance-no-int-to-ptr) - reading code memory at a raw address is the point
uint16_t c_inst = *(uint16_t *) (addr - 2);
if ((c_inst & 0xf07f) == 0x9002 && (c_inst & 0x0f80) != 0)
return true;
}
return false;
}
#endif
// --- Architecture-specific backtrace helpers ---
// These run from IRAM during panic (no flash access).
#if CONFIG_IDF_TARGET_ARCH_XTENSA
// Walk Xtensa backtrace from an exception frame, writing PCs to out[].
// Returns number of entries written.
static uint8_t IRAM_ATTR walk_xtensa_backtrace(XtExcFrame *frame, uint32_t *out, uint8_t max) {
esp_backtrace_frame_t bt_frame = {
.pc = (uint32_t) frame->pc,
.sp = (uint32_t) frame->a1,
.next_pc = (uint32_t) frame->a0,
.exc_frame = frame,
};
uint8_t count = 0;
uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc);
if (is_code_addr(first_pc)) {
out[count++] = first_pc;
}
while (count < max && bt_frame.next_pc != 0) {
if (!esp_backtrace_get_next_frame(&bt_frame))
break;
uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc);
if (is_code_addr(pc)) {
out[count++] = pc;
}
}
return count;
}
#endif
#if CONFIG_IDF_TARGET_ARCH_RISCV
// Capture RISC-V backtrace: MEPC + RA from registers, then stack scan.
// Returns total count; *reg_count receives number of register-sourced entries.
static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *out, uint8_t max, uint8_t *reg_count) {
uint8_t count = 0;
if (is_code_addr(frame->mepc)) {
out[count++] = frame->mepc;
}
if (is_code_addr(frame->ra) && frame->ra != frame->mepc) {
out[count++] = frame->ra;
}
*reg_count = count;
// NOLINTNEXTLINE(performance-no-int-to-ptr) - walking the raw stack by address is the point
auto *scan_start = (uint32_t *) frame->sp;
for (uint32_t i = 0; i < 64 && count < max; i++) {
uint32_t val = scan_start[i];
if (is_code_addr(val) && val != frame->mepc && val != frame->ra) {
out[count++] = val;
}
}
return count;
}
#endif
// Raw crash data written by the panic handler wrapper.
// Lives in .noinit so it survives software reset but contains garbage after power cycle.
// Validated by magic marker. Static linkage since it's only used within this file.
// Version field is first so future firmware can always identify the struct layout.
// Magic is second to validate the data. Remaining fields can change between versions.
// Version is uint32_t because it would be padded to 4 bytes anyway before the next
// uint32_t field, so we use the full width rather than wasting 3 bytes of padding.
static constexpr uint32_t CRASH_DATA_VERSION = 4;
#if CONFIG_IDF_TARGET_ARCH_XTENSA
// EXCCAUSE is a 6-bit register; larger recorded values mean the frame's
// cause/vaddr slots were never written (not a real exception frame).
static constexpr uint32_t XTENSA_EXCCAUSE_COUNT = XCHAL_EXCCAUSE_NUM;
#elif CONFIG_IDF_TARGET_ARCH_RISCV
// Synchronous mcause exception codes are small and have no interrupt bit;
// anything else in a non-pseudo record is a stale slot.
static constexpr uint32_t RISCV_EXCEPTION_CAUSE_COUNT = 32;
#endif
struct RawCrashData {
uint32_t version;
uint32_t magic;
uint32_t pc;
uint8_t backtrace_count;
uint8_t reg_frame_count; // Number of entries from registers (not stack-scanned)
uint8_t exception; // panic_exception_t enum (FAULT/ABORT/IWDT/TWDT/DEBUG)
uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic)
uint32_t backtrace[MAX_BACKTRACE];
uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V)
uint32_t fault_addr; // Faulting memory address: excvaddr (Xtensa) or mtval (RISC-V)
uint32_t build_time; // ESPHOME_BUILD_TIME of the firmware that captured this record
uint8_t crashed_core;
#if SOC_CPU_CORES_NUM > 1
static_assert(SOC_CPU_CORES_NUM == 2, "Dual-core logic assumes exactly 2 cores");
uint8_t other_backtrace_count;
uint8_t other_reg_frame_count;
uint32_t other_backtrace[MAX_BACKTRACE];
#endif
};
static RawCrashData __attribute__((section(".noinit")))
s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Whether crash data was found and validated this boot.
static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
namespace esphome::esp32 {
static const char *const TAG = "esp32.crash";
// RAM copy of the build timestamp. The generated constant lives in flash,
// which the panic handler must not read (cache may be disabled during
// cache-error panics), so the wrapper stamps the record from this mirror
// instead. Filled during C++ dynamic initialization, well before arch_init();
// ESPHOME_BUILD_TIME itself is constant-initialized, so the read is ordered.
// Unqualified name on purpose: the runtime header declares it in namespace
// esphome, while the static-analysis stub defines it as a macro.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME);
void crash_handler_read_and_clear() {
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
s_crash_data_valid = true;
// Clamp counts to prevent out-of-bounds reads from corrupt .noinit data
if (s_raw_crash_data.backtrace_count > MAX_BACKTRACE)
s_raw_crash_data.backtrace_count = MAX_BACKTRACE;
if (s_raw_crash_data.reg_frame_count > s_raw_crash_data.backtrace_count)
s_raw_crash_data.reg_frame_count = s_raw_crash_data.backtrace_count;
if (s_raw_crash_data.exception > 4) // panic_exception_t max value
s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT
if (s_raw_crash_data.pseudo_excause > 1)
s_raw_crash_data.pseudo_excause = 0;
if (s_raw_crash_data.crashed_core >= SOC_CPU_CORES_NUM)
s_raw_crash_data.crashed_core = 0;
#if SOC_CPU_CORES_NUM > 1
if (s_raw_crash_data.other_backtrace_count > MAX_BACKTRACE)
s_raw_crash_data.other_backtrace_count = MAX_BACKTRACE;
if (s_raw_crash_data.other_reg_frame_count > s_raw_crash_data.other_backtrace_count)
s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count;
#endif
}
// Don't clear magic here — crash data must survive OTA rollback reboots.
// Magic is cleared by crash_handler_clear() after an API client receives the data.
}
bool crash_handler_has_data() { return s_crash_data_valid; }
void crash_handler_clear() {
// Only clear the magic so data doesn't survive the next reboot.
// Keep s_crash_data_valid so crash_handler_log() still works for
// additional API clients connecting during this boot session.
s_raw_crash_data.magic = 0;
}
// Whether the cause slot was written by a real exception frame.
static bool cause_slot_was_written() {
#if CONFIG_IDF_TARGET_ARCH_XTENSA
return s_raw_crash_data.cause < XTENSA_EXCCAUSE_COUNT;
#else
return s_raw_crash_data.cause < RISCV_EXCEPTION_CAUSE_COUNT;
#endif
}
// Look up the exception cause as a human-readable string.
// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays
// not exposed via any public API.
static const char *get_exception_reason() {
uint8_t exception = s_raw_crash_data.exception;
if (exception == PANIC_EXCEPTION_ABORT || exception == PANIC_EXCEPTION_TWDT) {
// Abort-class panics carry no cause register
return nullptr;
}
if (!cause_slot_was_written()) {
// Garbage from old-build or corrupt records; report just the type
return nullptr;
}
#if CONFIG_IDF_TARGET_ARCH_XTENSA
if (s_raw_crash_data.pseudo_excause) {
// SoC-level panic: watchdog, cache error, etc.
// Keep in sync with ESP-IDF's PANIC_RSN_* defines
static const char *const PSEUDO_REASON[] = {
"Unknown reason", // 0
"Unhandled debug exception", // 1
"Double exception", // 2
"Unhandled kernel exception", // 3
"Coprocessor exception", // 4
"Interrupt wdt timeout on CPU0", // 5
"Interrupt wdt timeout on CPU1", // 6
"Cache error", // 7
};
uint32_t cause = s_raw_crash_data.cause;
if (cause < sizeof(PSEUDO_REASON) / sizeof(PSEUDO_REASON[0]))
return PSEUDO_REASON[cause];
return PSEUDO_REASON[0];
}
// Real Xtensa exception
static const char *const REASON[] = {
"IllegalInstruction",
"Syscall",
"InstructionFetchError",
"LoadStoreError",
"Level1Interrupt",
"Alloca",
"IntegerDivideByZero",
"PCValue",
"Privileged",
"LoadStoreAlignment",
nullptr,
nullptr,
"InstrPDAddrError",
"LoadStorePIFDataError",
"InstrPIFAddrError",
"LoadStorePIFAddrError",
"InstTLBMiss",
"InstTLBMultiHit",
"InstFetchPrivilege",
nullptr,
"InstrFetchProhibited",
nullptr,
nullptr,
nullptr,
"LoadStoreTLBMiss",
"LoadStoreTLBMultihit",
"LoadStorePrivilege",
nullptr,
"LoadProhibited",
"StoreProhibited",
nullptr,
nullptr,
"Cp0Dis",
"Cp1Dis",
"Cp2Dis",
"Cp3Dis",
"Cp4Dis",
"Cp5Dis",
"Cp6Dis",
"Cp7Dis",
};
uint32_t cause = s_raw_crash_data.cause;
if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr)
return REASON[cause];
#elif CONFIG_IDF_TARGET_ARCH_RISCV
// For SoC-level panics (watchdog, cache error), mcause holds IDF-internal
// interrupt numbers, not standard RISC-V cause codes. The exception type
// field already identifies these, so just return null to use the type name.
if (s_raw_crash_data.pseudo_excause)
return nullptr;
static const char *const REASON[] = {
"Instruction address misaligned",
"Instruction access fault",
"Illegal instruction",
"Breakpoint",
"Load address misaligned",
"Load access fault",
"Store address misaligned",
"Store access fault",
"Environment call from U-mode",
"Environment call from S-mode",
nullptr,
"Environment call from M-mode",
"Instruction page fault",
"Load page fault",
nullptr,
"Store page fault",
};
uint32_t cause = s_raw_crash_data.cause;
if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr)
return REASON[cause];
#endif
return "Unknown";
}
// Exception type names matching panic_exception_t enum
static const char *get_exception_type() {
static const char *const TYPES[] = {
"Debug exception", // PANIC_EXCEPTION_DEBUG
"Interrupt wdt", // PANIC_EXCEPTION_IWDT
"Task wdt", // PANIC_EXCEPTION_TWDT
"Abort", // PANIC_EXCEPTION_ABORT
"Fault", // PANIC_EXCEPTION_FAULT
};
uint8_t exc = s_raw_crash_data.exception;
if (exc < sizeof(TYPES) / sizeof(TYPES[0]))
return TYPES[exc];
return "Unknown";
}
// Log backtrace entries, filtering stack-scanned addresses on RISC-V.
static void log_backtrace(const uint32_t *addrs, uint8_t count, uint8_t reg_frame_count) {
uint8_t bt_num = 0;
for (uint8_t i = 0; i < count; i++) {
uint32_t addr = addrs[i];
#if CONFIG_IDF_TARGET_ARCH_RISCV
if (i >= reg_frame_count && !is_return_addr(addr))
continue;
const char *source = (i < reg_frame_count) ? "backtrace" : "stack scan";
#else
const char *source = "backtrace";
#endif
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source);
}
}
// Append backtrace addresses to the addr2line hint buffer.
static int append_addrs_to_hint(char *buf, int size, int pos, const uint32_t *addrs, uint8_t count,
uint8_t reg_frame_count) {
for (uint8_t i = 0; i < count && pos < size - 12; i++) {
uint32_t addr = addrs[i];
#if CONFIG_IDF_TARGET_ARCH_RISCV
if (i >= reg_frame_count && !is_return_addr(addr))
continue;
#endif
pos += snprintf(buf + pos, size - pos, " 0x%08" PRIX32, addr);
}
return pos;
}
// Register holding the faulting memory address, named as in ESP-IDF's live
// register dump. The lowercase form is for old-build reports, where the
// stacktrace decoders must not match the line.
#if CONFIG_IDF_TARGET_ARCH_XTENSA
static const char *const FAULT_ADDR_REG = "EXCVADDR";
static const char *const FAULT_ADDR_REG_LOWER = "excvaddr";
#elif CONFIG_IDF_TARGET_ARCH_RISCV
static const char *const FAULT_ADDR_REG = "MTVAL";
static const char *const FAULT_ADDR_REG_LOWER = "mtval";
#endif
// Whether the fault address is meaningful: real CPU faults with a validly
// written frame only.
static bool has_fault_addr() {
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause &&
cause_slot_was_written();
}
// The record was captured by a different firmware build (it survives soft
// resets, including the OTA reboot), so symbolizing its addresses against the
// current ELF would produce misleading symbols. Print them with lowercase
// labels the stacktrace decoders deliberately do not match, and skip the
// addr2line hint. One line per address so nothing is lost to a shared buffer.
// No is_return_addr() filtering here: it would inspect the current build's
// code bytes, which say nothing about addresses captured by the old build.
static uint8_t log_foreign_backtrace(const uint32_t *addrs, uint8_t count, uint8_t bt_num) {
for (uint8_t i = 0; i < count; i++) {
ESP_LOGE(TAG, " bt%d: 0x%08" PRIX32, bt_num++, addrs[i]);
}
return bt_num;
}
static void log_foreign_addresses() {
ESP_LOGE(TAG, " Captured by a different firmware build; addresses belong to that build's ELF");
ESP_LOGE(TAG, " pc: 0x%08" PRIX32, s_raw_crash_data.pc);
if (has_fault_addr()) {
ESP_LOGE(TAG, " %s: 0x%08" PRIX32, FAULT_ADDR_REG_LOWER, s_raw_crash_data.fault_addr);
}
uint8_t bt_num = log_foreign_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, 0);
#if SOC_CPU_CORES_NUM > 1
if (s_raw_crash_data.other_backtrace_count > 0) {
// Lowercase like the address labels: carries no address, matches no decoder.
ESP_LOGE(TAG, " other core (%d):", 1 - s_raw_crash_data.crashed_core);
log_foreign_backtrace(s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count, bt_num);
}
#else
(void) bt_num; // Single-core targets have no second list to continue numbering into.
#endif
}
// Intentionally uses separate ESP_LOGE calls per line instead of combining into
// one multi-line log message. This ensures each address appears as its own line
// on the serial console, making it possible to see partial output if the device
// crashes again during boot, and allowing the CLI's process_stacktrace to match
// and decode each address individually.
void crash_handler_log() {
if (!s_crash_data_valid)
return;
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
const char *reason = get_exception_reason();
if (reason != nullptr) {
ESP_LOGE(TAG, " Reason: %s - %s (cause %" PRIu32 ")", get_exception_type(), reason, s_raw_crash_data.cause);
} else {
ESP_LOGE(TAG, " Reason: %s", get_exception_type());
}
ESP_LOGE(TAG, " Crashed core: %d", s_raw_crash_data.crashed_core);
if (s_raw_crash_data.build_time != s_current_build_time) {
// Captured by a different firmware build: the record survives soft resets
// including the OTA reboot, so its addresses belong to a previous ELF.
log_foreign_addresses();
return;
}
ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc);
// Uses the same register name as ESP-IDF's live register dump so the CLI
// decodes the address when it happens to be a code address.
if (has_fault_addr()) {
ESP_LOGE(TAG, " %s: 0x%08" PRIX32 " (faulting address)", FAULT_ADDR_REG, s_raw_crash_data.fault_addr);
}
log_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, s_raw_crash_data.reg_frame_count);
#if SOC_CPU_CORES_NUM > 1
if (s_raw_crash_data.other_backtrace_count > 0) {
int other_core = 1 - s_raw_crash_data.crashed_core;
ESP_LOGE(TAG, " Other core (%d) backtrace:", other_core);
log_backtrace(s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count,
s_raw_crash_data.other_reg_frame_count);
}
#endif
// Build addr2line hints for easy copy-paste. One line per core: the two
// backtraces are separate stacks, and a combined list decodes as one
// impossible call chain (and can overflow the buffer, dropping addresses).
static const char *const ADDR2LINE_CMD = "addr2line -pfiaC -e firmware.elf";
char hint[256];
int pos = snprintf(hint, sizeof(hint), "Use: %s 0x%08" PRIX32, ADDR2LINE_CMD, s_raw_crash_data.pc);
append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count,
s_raw_crash_data.reg_frame_count);
ESP_LOGE(TAG, "%s", hint);
#if SOC_CPU_CORES_NUM > 1
if (s_raw_crash_data.other_backtrace_count > 0) {
pos = snprintf(hint, sizeof(hint), "Other core: %s", ADDR2LINE_CMD);
append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace,
s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count);
ESP_LOGE(TAG, "%s", hint);
}
#endif
}
} // namespace esphome::esp32
// --- Panic handler wrapper ---
// Intercepts esp_panic_handler() via --wrap linker flag to capture crash data
// into NOINIT memory before the normal panic handler runs.
//
extern "C" {
// Set by IDF's task watchdog (task_wdt.c, no header) before it simulates an
// abort; weak so builds without the task watchdog still link.
extern bool g_twdt_isr __attribute__((weak));
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
// Names are mandated by the --wrap linker mechanism
extern void __real_esp_panic_handler(panic_info_t *info);
void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
// Save the faulting PC and exception info
s_raw_crash_data.pc = (uint32_t) info->addr;
s_raw_crash_data.backtrace_count = 0;
s_raw_crash_data.reg_frame_count = 0;
s_raw_crash_data.exception = (uint8_t) info->exception;
s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0;
s_raw_crash_data.crashed_core = (uint8_t) info->core;
if (g_panic_abort) {
// IDF reclassifies to ABORT only inside esp_panic_handler(), after this
// wrapper captured info->exception; correct it here. TWDT is our own
// distinction (IDF never assigns PANIC_EXCEPTION_TWDT). The abort text is
// not stored; the symbolized backtrace already identifies the site.
bool is_twdt = &g_twdt_isr != nullptr && g_twdt_isr;
s_raw_crash_data.exception = (uint8_t) (is_twdt ? PANIC_EXCEPTION_TWDT : PANIC_EXCEPTION_ABORT);
}
// Zero unconditionally so a null frame doesn't leave stale .noinit data from a previous boot
s_raw_crash_data.cause = 0;
s_raw_crash_data.fault_addr = 0;
// Record which build's ELF the captured addresses belong to (RAM read, panic-safe).
// Still 0 if the panic precedes C++ dynamic initialization, so such a crash
// reports as a foreign build — conservative: addresses are shown raw instead
// of decoded.
s_raw_crash_data.build_time = esphome::esp32::s_current_build_time;
#if SOC_CPU_CORES_NUM > 1
s_raw_crash_data.other_backtrace_count = 0;
s_raw_crash_data.other_reg_frame_count = 0;
#endif
#if CONFIG_IDF_TARGET_ARCH_XTENSA
// Xtensa: walk the backtrace using the public API
if (info->frame != nullptr) {
auto *xt_frame = (XtExcFrame *) info->frame;
if (!g_panic_abort) {
// Abort-class frames carry no useful cause/vaddr: TWDT task snapshots
// never wrote them and abort() traps describe only the synthetic trap.
s_raw_crash_data.cause = xt_frame->exccause;
s_raw_crash_data.fault_addr = xt_frame->excvaddr;
}
s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE);
}
#if SOC_CPU_CORES_NUM > 1
// Capture the other core's backtrace from the global frame array.
// Both cores save their frames to g_exc_frames[] before esp_panic_handler
// is called, so the other core's frame is available here.
if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) {
int other_core = 1 - info->core;
auto *other_frame = (XtExcFrame *) g_exc_frames[other_core];
if (other_frame != nullptr) {
s_raw_crash_data.other_backtrace_count =
walk_xtensa_backtrace(other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE);
}
}
#endif
#elif CONFIG_IDF_TARGET_ARCH_RISCV
// RISC-V: capture MEPC + RA, then scan stack for code addresses
if (info->frame != nullptr) {
auto *rv_frame = (RvExcFrame *) info->frame;
if (!g_panic_abort) {
// See the Xtensa branch: abort-class frames carry no valid cause/vaddr.
s_raw_crash_data.cause = rv_frame->mcause;
s_raw_crash_data.fault_addr = rv_frame->mtval;
}
s_raw_crash_data.backtrace_count =
capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count);
}
#if SOC_CPU_CORES_NUM > 1
// Capture the other core's backtrace from the global frame array.
if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) {
int other_core = 1 - info->core;
auto *other_frame = (RvExcFrame *) g_exc_frames[other_core];
if (other_frame != nullptr) {
s_raw_crash_data.other_backtrace_count = capture_riscv_backtrace(
other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE, &s_raw_crash_data.other_reg_frame_count);
}
}
#endif
#endif
// Write version and magic last — ensures all data is written before we mark it valid
s_raw_crash_data.version = CRASH_DATA_VERSION;
s_raw_crash_data.magic = CRASH_MAGIC;
// Call the real panic handler (prints to UART, does core dump, reboots, etc.)
__real_esp_panic_handler(info);
}
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
} // extern "C"
#endif // USE_ESP32_CRASH_HANDLER
#endif // USE_ESP32