ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
crash_handler.cpp
Go to the documentation of this file.
1#ifdef USE_ESP32
2
4#ifdef USE_ESP32_CRASH_HANDLER
5
6#include "crash_handler.h"
8#include "esphome/core/log.h"
9
10#include <cinttypes>
11#include <cstring>
12#include <esp_attr.h>
13#include <esp_private/panic_internal.h>
14#include <soc/soc.h>
15
16#if CONFIG_IDF_TARGET_ARCH_XTENSA
17#include <esp_cpu_utils.h>
18#include <esp_debug_helpers.h>
19#include <xtensa_context.h>
20#elif CONFIG_IDF_TARGET_ARCH_RISCV
21#include <riscv/rvruntime-frames.h>
22#endif
23
24static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF;
25static constexpr size_t MAX_BACKTRACE = 16;
26
27// Check if an address looks like code (flash-mapped or IRAM).
28// Must be safe to call from panic context (no flash access needed).
29static inline bool IRAM_ATTR is_code_addr(uint32_t addr) {
30 return (addr >= SOC_IROM_LOW && addr < SOC_IROM_HIGH) || (addr >= SOC_IRAM_LOW && addr < SOC_IRAM_HIGH);
31}
32
33#if CONFIG_IDF_TARGET_ARCH_RISCV
34// Check if a code address is a real return address by verifying the preceding
35// instruction is a JAL or JALR with rd=ra (x1). Called at log time (not during
36// panic) so flash cache is available and both IRAM and IROM are safely readable.
37static inline bool is_return_addr(uint32_t addr) {
38 if (!is_code_addr(addr) || addr < 4)
39 return false;
40 // A return address on the stack points to the instruction after a call.
41 // Check for 4-byte JAL/JALR call instruction before this address.
42 // Use memcpy for alignment safety — RISC-V C extension means code addresses
43 // are only 2-byte aligned, so addr-4 may not be 4-byte aligned.
44 uint32_t inst;
45 // NOLINTNEXTLINE(performance-no-int-to-ptr) - reading code memory at a raw address is the point
46 memcpy(&inst, (const void *) (addr - 4), sizeof(inst));
47 // RISC-V instruction encoding: bits [6:0] = opcode, bits [11:7] = rd
48 uint32_t opcode = inst & 0x7f; // Extract 7-bit opcode
49 uint32_t rd = inst & 0xf80; // Extract rd field (bits 11:7)
50 // Match JAL (0x6f) or JALR (0x67) with rd=ra (x1, encoded as 0x80 = 1<<7)
51 if ((opcode == 0x6f || opcode == 0x67) && rd == 0x80)
52 return true;
53 // Check for 2-byte compressed c.jalr before this address (C extension).
54 // c.jalr saves to ra implicitly: funct4=1001, rs1!=0, rs2=0, op=10
55 if (addr >= 2) {
56 // NOLINTNEXTLINE(performance-no-int-to-ptr) - reading code memory at a raw address is the point
57 uint16_t c_inst = *(uint16_t *) (addr - 2);
58 if ((c_inst & 0xf07f) == 0x9002 && (c_inst & 0x0f80) != 0)
59 return true;
60 }
61 return false;
62}
63#endif
64
65// --- Architecture-specific backtrace helpers ---
66// These run from IRAM during panic (no flash access).
67
68#if CONFIG_IDF_TARGET_ARCH_XTENSA
69// Walk Xtensa backtrace from an exception frame, writing PCs to out[].
70// Returns number of entries written.
71static uint8_t IRAM_ATTR walk_xtensa_backtrace(XtExcFrame *frame, uint32_t *out, uint8_t max) {
72 esp_backtrace_frame_t bt_frame = {
73 .pc = (uint32_t) frame->pc,
74 .sp = (uint32_t) frame->a1,
75 .next_pc = (uint32_t) frame->a0,
76 .exc_frame = frame,
77 };
78 uint8_t count = 0;
79 uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc);
80 if (is_code_addr(first_pc)) {
81 out[count++] = first_pc;
82 }
83 while (count < max && bt_frame.next_pc != 0) {
84 if (!esp_backtrace_get_next_frame(&bt_frame))
85 break;
86 uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc);
87 if (is_code_addr(pc)) {
88 out[count++] = pc;
89 }
90 }
91 return count;
92}
93#endif
94
95#if CONFIG_IDF_TARGET_ARCH_RISCV
96// Capture RISC-V backtrace: MEPC + RA from registers, then stack scan.
97// Returns total count; *reg_count receives number of register-sourced entries.
98static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *out, uint8_t max, uint8_t *reg_count) {
99 uint8_t count = 0;
100 if (is_code_addr(frame->mepc)) {
101 out[count++] = frame->mepc;
102 }
103 if (is_code_addr(frame->ra) && frame->ra != frame->mepc) {
104 out[count++] = frame->ra;
105 }
106 *reg_count = count;
107 // NOLINTNEXTLINE(performance-no-int-to-ptr) - walking the raw stack by address is the point
108 auto *scan_start = (uint32_t *) frame->sp;
109 for (uint32_t i = 0; i < 64 && count < max; i++) {
111 if (is_code_addr(val) && val != frame->mepc && val != frame->ra) {
112 out[count++] = val;
113 }
114 }
115 return count;
116}
117#endif
118
119// Raw crash data written by the panic handler wrapper.
120// Lives in .noinit so it survives software reset but contains garbage after power cycle.
121// Validated by magic marker. Static linkage since it's only used within this file.
122// Version field is first so future firmware can always identify the struct layout.
123// Magic is second to validate the data. Remaining fields can change between versions.
124// Version is uint32_t because it would be padded to 4 bytes anyway before the next
125// uint32_t field, so we use the full width rather than wasting 3 bytes of padding.
126static constexpr uint32_t CRASH_DATA_VERSION = 4;
127struct RawCrashData {
128 uint32_t version;
129 uint32_t magic;
130 uint32_t pc;
131 uint8_t backtrace_count;
132 uint8_t reg_frame_count; // Number of entries from registers (not stack-scanned)
133 uint8_t exception; // panic_exception_t enum (FAULT/ABORT/IWDT/TWDT/DEBUG)
134 uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic)
135 uint32_t backtrace[MAX_BACKTRACE];
136 uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V)
137 uint32_t fault_addr; // Faulting memory address: excvaddr (Xtensa) or mtval (RISC-V)
138 uint32_t build_time; // ESPHOME_BUILD_TIME of the firmware that captured this record
139 uint8_t crashed_core;
140#if SOC_CPU_CORES_NUM > 1
141 static_assert(SOC_CPU_CORES_NUM == 2, "Dual-core logic assumes exactly 2 cores");
142 uint8_t other_backtrace_count;
143 uint8_t other_reg_frame_count;
144 uint32_t other_backtrace[MAX_BACKTRACE];
145#endif
146};
147static RawCrashData __attribute__((section(".noinit")))
148s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
149
150// Whether crash data was found and validated this boot.
151static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
152
153namespace esphome::esp32 {
154
155static const char *const TAG = "esp32.crash";
156
157// RAM copy of the build timestamp. The generated constant lives in flash,
158// which the panic handler must not read (cache may be disabled during
159// cache-error panics), so the wrapper stamps the record from this mirror
160// instead. Filled during C++ dynamic initialization, well before arch_init();
161// ESPHOME_BUILD_TIME itself is constant-initialized, so the read is ordered.
162// Unqualified name on purpose: the runtime header declares it in namespace
163// esphome, while the static-analysis stub defines it as a macro.
164// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
165static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME);
166
168 if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
169 s_crash_data_valid = true;
170 // Clamp counts to prevent out-of-bounds reads from corrupt .noinit data
171 if (s_raw_crash_data.backtrace_count > MAX_BACKTRACE)
172 s_raw_crash_data.backtrace_count = MAX_BACKTRACE;
173 if (s_raw_crash_data.reg_frame_count > s_raw_crash_data.backtrace_count)
174 s_raw_crash_data.reg_frame_count = s_raw_crash_data.backtrace_count;
175 if (s_raw_crash_data.exception > 4) // panic_exception_t max value
176 s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT
177 if (s_raw_crash_data.pseudo_excause > 1)
178 s_raw_crash_data.pseudo_excause = 0;
179 if (s_raw_crash_data.crashed_core >= SOC_CPU_CORES_NUM)
180 s_raw_crash_data.crashed_core = 0;
181#if SOC_CPU_CORES_NUM > 1
182 if (s_raw_crash_data.other_backtrace_count > MAX_BACKTRACE)
183 s_raw_crash_data.other_backtrace_count = MAX_BACKTRACE;
184 if (s_raw_crash_data.other_reg_frame_count > s_raw_crash_data.other_backtrace_count)
185 s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count;
186#endif
187 }
188 // Don't clear magic here — crash data must survive OTA rollback reboots.
189 // Magic is cleared by crash_handler_clear() after an API client receives the data.
190}
191
192bool crash_handler_has_data() { return s_crash_data_valid; }
193
195 // Only clear the magic so data doesn't survive the next reboot.
196 // Keep s_crash_data_valid so crash_handler_log() still works for
197 // additional API clients connecting during this boot session.
198 s_raw_crash_data.magic = 0;
199}
200
201// Look up the exception cause as a human-readable string.
202// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays
203// not exposed via any public API.
204static const char *get_exception_reason() {
205#if CONFIG_IDF_TARGET_ARCH_XTENSA
206 if (s_raw_crash_data.pseudo_excause) {
207 // SoC-level panic: watchdog, cache error, etc.
208 // Keep in sync with ESP-IDF's PANIC_RSN_* defines
209 static const char *const PSEUDO_REASON[] = {
210 "Unknown reason", // 0
211 "Unhandled debug exception", // 1
212 "Double exception", // 2
213 "Unhandled kernel exception", // 3
214 "Coprocessor exception", // 4
215 "Interrupt wdt timeout on CPU0", // 5
216 "Interrupt wdt timeout on CPU1", // 6
217 "Cache error", // 7
218 };
219 uint32_t cause = s_raw_crash_data.cause;
220 if (cause < sizeof(PSEUDO_REASON) / sizeof(PSEUDO_REASON[0]))
221 return PSEUDO_REASON[cause];
222 return PSEUDO_REASON[0];
223 }
224 // Real Xtensa exception
225 static const char *const REASON[] = {
226 "IllegalInstruction",
227 "Syscall",
228 "InstructionFetchError",
229 "LoadStoreError",
230 "Level1Interrupt",
231 "Alloca",
232 "IntegerDivideByZero",
233 "PCValue",
234 "Privileged",
235 "LoadStoreAlignment",
236 nullptr,
237 nullptr,
238 "InstrPDAddrError",
239 "LoadStorePIFDataError",
240 "InstrPIFAddrError",
241 "LoadStorePIFAddrError",
242 "InstTLBMiss",
243 "InstTLBMultiHit",
244 "InstFetchPrivilege",
245 nullptr,
246 "InstrFetchProhibited",
247 nullptr,
248 nullptr,
249 nullptr,
250 "LoadStoreTLBMiss",
251 "LoadStoreTLBMultihit",
252 "LoadStorePrivilege",
253 nullptr,
254 "LoadProhibited",
255 "StoreProhibited",
256 nullptr,
257 nullptr,
258 "Cp0Dis",
259 "Cp1Dis",
260 "Cp2Dis",
261 "Cp3Dis",
262 "Cp4Dis",
263 "Cp5Dis",
264 "Cp6Dis",
265 "Cp7Dis",
266 };
267 uint32_t cause = s_raw_crash_data.cause;
268 if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr)
269 return REASON[cause];
270#elif CONFIG_IDF_TARGET_ARCH_RISCV
271 // For SoC-level panics (watchdog, cache error), mcause holds IDF-internal
272 // interrupt numbers, not standard RISC-V cause codes. The exception type
273 // field already identifies these, so just return null to use the type name.
274 if (s_raw_crash_data.pseudo_excause)
275 return nullptr;
276 static const char *const REASON[] = {
277 "Instruction address misaligned",
278 "Instruction access fault",
279 "Illegal instruction",
280 "Breakpoint",
281 "Load address misaligned",
282 "Load access fault",
283 "Store address misaligned",
284 "Store access fault",
285 "Environment call from U-mode",
286 "Environment call from S-mode",
287 nullptr,
288 "Environment call from M-mode",
289 "Instruction page fault",
290 "Load page fault",
291 nullptr,
292 "Store page fault",
293 };
294 uint32_t cause = s_raw_crash_data.cause;
295 if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr)
296 return REASON[cause];
297#endif
298 return "Unknown";
299}
300
301// Exception type names matching panic_exception_t enum
302static const char *get_exception_type() {
303 static const char *const TYPES[] = {
304 "Debug exception", // PANIC_EXCEPTION_DEBUG
305 "Interrupt wdt", // PANIC_EXCEPTION_IWDT
306 "Task wdt", // PANIC_EXCEPTION_TWDT
307 "Abort", // PANIC_EXCEPTION_ABORT
308 "Fault", // PANIC_EXCEPTION_FAULT
309 };
310 uint8_t exc = s_raw_crash_data.exception;
311 if (exc < sizeof(TYPES) / sizeof(TYPES[0]))
312 return TYPES[exc];
313 return "Unknown";
314}
315
316// Log backtrace entries, filtering stack-scanned addresses on RISC-V.
317static void log_backtrace(const uint32_t *addrs, uint8_t count, uint8_t reg_frame_count) {
318 uint8_t bt_num = 0;
319 for (uint8_t i = 0; i < count; i++) {
320 uint32_t addr = addrs[i];
321#if CONFIG_IDF_TARGET_ARCH_RISCV
322 if (i >= reg_frame_count && !is_return_addr(addr))
323 continue;
324 const char *source = (i < reg_frame_count) ? "backtrace" : "stack scan";
325#else
326 const char *source = "backtrace";
327#endif
328 ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source);
329 }
330}
331
332// Append backtrace addresses to the addr2line hint buffer.
333static int append_addrs_to_hint(char *buf, int size, int pos, const uint32_t *addrs, uint8_t count,
334 uint8_t reg_frame_count) {
335 for (uint8_t i = 0; i < count && pos < size - 12; i++) {
336 uint32_t addr = addrs[i];
337#if CONFIG_IDF_TARGET_ARCH_RISCV
338 if (i >= reg_frame_count && !is_return_addr(addr))
339 continue;
340#endif
341 pos += snprintf(buf + pos, size - pos, " 0x%08" PRIX32, addr);
342 }
343 return pos;
344}
345
346// Register holding the faulting memory address, named as in ESP-IDF's live
347// register dump. The lowercase form is for old-build reports, where the
348// stacktrace decoders must not match the line.
349#if CONFIG_IDF_TARGET_ARCH_XTENSA
350static const char *const FAULT_ADDR_REG = "EXCVADDR";
351static const char *const FAULT_ADDR_REG_LOWER = "excvaddr";
352#elif CONFIG_IDF_TARGET_ARCH_RISCV
353static const char *const FAULT_ADDR_REG = "MTVAL";
354static const char *const FAULT_ADDR_REG_LOWER = "mtval";
355#endif
356
357// Whether the fault address is meaningful — real CPU faults only, not
358// aborts/watchdogs or SoC-level pseudo exceptions.
359static bool has_fault_addr() {
360 return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause;
361}
362
363// The record was captured by a different firmware build (it survives soft
364// resets, including the OTA reboot), so symbolizing its addresses against the
365// current ELF would produce misleading symbols. Print them with lowercase
366// labels the stacktrace decoders deliberately do not match, and skip the
367// addr2line hint. One line per address so nothing is lost to a shared buffer.
368// No is_return_addr() filtering here: it would inspect the current build's
369// code bytes, which say nothing about addresses captured by the old build.
370static uint8_t log_foreign_backtrace(const uint32_t *addrs, uint8_t count, uint8_t bt_num) {
371 for (uint8_t i = 0; i < count; i++) {
372 ESP_LOGE(TAG, " bt%d: 0x%08" PRIX32, bt_num++, addrs[i]);
373 }
374 return bt_num;
375}
376
377static void log_foreign_addresses() {
378 ESP_LOGE(TAG, " Captured by a different firmware build; addresses belong to that build's ELF");
379 ESP_LOGE(TAG, " pc: 0x%08" PRIX32, s_raw_crash_data.pc);
380 if (has_fault_addr()) {
381 ESP_LOGE(TAG, " %s: 0x%08" PRIX32, FAULT_ADDR_REG_LOWER, s_raw_crash_data.fault_addr);
382 }
383 uint8_t bt_num = log_foreign_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, 0);
384#if SOC_CPU_CORES_NUM > 1
385 if (s_raw_crash_data.other_backtrace_count > 0) {
386 // Lowercase like the address labels: carries no address, matches no decoder.
387 ESP_LOGE(TAG, " other core (%d):", 1 - s_raw_crash_data.crashed_core);
388 log_foreign_backtrace(s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count, bt_num);
389 }
390#else
391 (void) bt_num; // Single-core targets have no second list to continue numbering into.
392#endif
393}
394
395// Intentionally uses separate ESP_LOGE calls per line instead of combining into
396// one multi-line log message. This ensures each address appears as its own line
397// on the serial console, making it possible to see partial output if the device
398// crashes again during boot, and allowing the CLI's process_stacktrace to match
399// and decode each address individually.
401 if (!s_crash_data_valid)
402 return;
403
404 ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
405 const char *reason = get_exception_reason();
406 if (reason != nullptr) {
407 ESP_LOGE(TAG, " Reason: %s - %s (cause %" PRIu32 ")", get_exception_type(), reason, s_raw_crash_data.cause);
408 } else {
409 ESP_LOGE(TAG, " Reason: %s", get_exception_type());
410 }
411 ESP_LOGE(TAG, " Crashed core: %d", s_raw_crash_data.crashed_core);
412 if (s_raw_crash_data.build_time != s_current_build_time) {
413 // Captured by a different firmware build: the record survives soft resets
414 // including the OTA reboot, so its addresses belong to a previous ELF.
415 log_foreign_addresses();
416 return;
417 }
418 ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc);
419 // Uses the same register name as ESP-IDF's live register dump so the CLI
420 // decodes the address when it happens to be a code address.
421 if (has_fault_addr()) {
422 ESP_LOGE(TAG, " %s: 0x%08" PRIX32 " (faulting address)", FAULT_ADDR_REG, s_raw_crash_data.fault_addr);
423 }
424 log_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, s_raw_crash_data.reg_frame_count);
425
426#if SOC_CPU_CORES_NUM > 1
427 if (s_raw_crash_data.other_backtrace_count > 0) {
428 int other_core = 1 - s_raw_crash_data.crashed_core;
429 ESP_LOGE(TAG, " Other core (%d) backtrace:", other_core);
430 log_backtrace(s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count,
431 s_raw_crash_data.other_reg_frame_count);
432 }
433#endif
434
435 // Build addr2line hints for easy copy-paste. One line per core: the two
436 // backtraces are separate stacks, and a combined list decodes as one
437 // impossible call chain (and can overflow the buffer, dropping addresses).
438 static const char *const ADDR2LINE_CMD = "addr2line -pfiaC -e firmware.elf";
439 char hint[256];
440 int pos = snprintf(hint, sizeof(hint), "Use: %s 0x%08" PRIX32, ADDR2LINE_CMD, s_raw_crash_data.pc);
441 append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count,
442 s_raw_crash_data.reg_frame_count);
443 ESP_LOGE(TAG, "%s", hint);
444#if SOC_CPU_CORES_NUM > 1
445 if (s_raw_crash_data.other_backtrace_count > 0) {
446 pos = snprintf(hint, sizeof(hint), "Other core: %s", ADDR2LINE_CMD);
447 append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace,
448 s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count);
449 ESP_LOGE(TAG, "%s", hint);
450 }
451#endif
452}
453
454} // namespace esphome::esp32
455
456// --- Panic handler wrapper ---
457// Intercepts esp_panic_handler() via --wrap linker flag to capture crash data
458// into NOINIT memory before the normal panic handler runs.
459//
460extern "C" {
461// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
462// Names are mandated by the --wrap linker mechanism
463extern void __real_esp_panic_handler(panic_info_t *info);
464
465void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
466 // Save the faulting PC and exception info
467 s_raw_crash_data.pc = (uint32_t) info->addr;
468 s_raw_crash_data.backtrace_count = 0;
469 s_raw_crash_data.reg_frame_count = 0;
470 s_raw_crash_data.exception = (uint8_t) info->exception;
471 s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0;
472 s_raw_crash_data.crashed_core = (uint8_t) info->core;
473 // Zero unconditionally so a null frame doesn't leave stale .noinit data from a previous boot
474 s_raw_crash_data.cause = 0;
475 s_raw_crash_data.fault_addr = 0;
476 // Record which build's ELF the captured addresses belong to (RAM read, panic-safe).
477 // Still 0 if the panic precedes C++ dynamic initialization, so such a crash
478 // reports as a foreign build — conservative: addresses are shown raw instead
479 // of decoded.
480 s_raw_crash_data.build_time = esphome::esp32::s_current_build_time;
481#if SOC_CPU_CORES_NUM > 1
482 s_raw_crash_data.other_backtrace_count = 0;
483 s_raw_crash_data.other_reg_frame_count = 0;
484#endif
485
486#if CONFIG_IDF_TARGET_ARCH_XTENSA
487 // Xtensa: walk the backtrace using the public API
488 if (info->frame != nullptr) {
489 auto *xt_frame = (XtExcFrame *) info->frame;
490 s_raw_crash_data.cause = xt_frame->exccause;
491 s_raw_crash_data.fault_addr = xt_frame->excvaddr;
492 s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE);
493 }
494
495#if SOC_CPU_CORES_NUM > 1
496 // Capture the other core's backtrace from the global frame array.
497 // Both cores save their frames to g_exc_frames[] before esp_panic_handler
498 // is called, so the other core's frame is available here.
499 if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) {
500 int other_core = 1 - info->core;
501 auto *other_frame = (XtExcFrame *) g_exc_frames[other_core];
502 if (other_frame != nullptr) {
503 s_raw_crash_data.other_backtrace_count =
504 walk_xtensa_backtrace(other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE);
505 }
506 }
507#endif
508
509#elif CONFIG_IDF_TARGET_ARCH_RISCV
510 // RISC-V: capture MEPC + RA, then scan stack for code addresses
511 if (info->frame != nullptr) {
512 auto *rv_frame = (RvExcFrame *) info->frame;
513 s_raw_crash_data.cause = rv_frame->mcause;
514 s_raw_crash_data.fault_addr = rv_frame->mtval;
515 s_raw_crash_data.backtrace_count =
516 capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count);
517 }
518
519#if SOC_CPU_CORES_NUM > 1
520 // Capture the other core's backtrace from the global frame array.
521 if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) {
522 int other_core = 1 - info->core;
523 auto *other_frame = (RvExcFrame *) g_exc_frames[other_core];
524 if (other_frame != nullptr) {
525 s_raw_crash_data.other_backtrace_count = capture_riscv_backtrace(
526 other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE, &s_raw_crash_data.other_reg_frame_count);
527 }
528 }
529#endif
530#endif
531
532 // Write version and magic last — ensures all data is written before we mark it valid
533 s_raw_crash_data.version = CRASH_DATA_VERSION;
534 s_raw_crash_data.magic = CRASH_MAGIC;
535
536 // Call the real panic handler (prints to UART, does core dump, reboots, etc.)
538}
539
540// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
541} // extern "C"
542
543#endif // USE_ESP32_CRASH_HANDLER
544#endif // USE_ESP32
struct @66::@67 __attribute__
Wake the main loop task from an ISR. ISR-safe.
Definition main_task.h:32
void __real_esp_panic_handler(panic_info_t *info)
void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info)
mopeka_std_values val[3]
bool crash_handler_has_data()
Returns true if crash data was found this boot.
void crash_handler_log()
Log crash data if a crash was detected on previous boot.
void crash_handler_read_and_clear()
Read and validate crash data from NOINIT memory.
void crash_handler_clear()
Clear the magic marker and mark crash data as consumed.
size_t size_t pos
Definition helpers.h:1062
uint32_t * scan_start
static void uint32_t
uint32_t pc