ESPHome 2026.9.0-dev
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;
127#if CONFIG_IDF_TARGET_ARCH_XTENSA
128// EXCCAUSE is a 6-bit register; larger recorded values mean the frame's
129// cause/vaddr slots were never written (not a real exception frame).
130static constexpr uint32_t XTENSA_EXCCAUSE_COUNT = XCHAL_EXCCAUSE_NUM;
131#elif CONFIG_IDF_TARGET_ARCH_RISCV
132// Synchronous mcause exception codes are small and have no interrupt bit;
133// anything else in a non-pseudo record is a stale slot.
134static constexpr uint32_t RISCV_EXCEPTION_CAUSE_COUNT = 32;
135#endif
136struct RawCrashData {
137 uint32_t version;
138 uint32_t magic;
139 uint32_t pc;
140 uint8_t backtrace_count;
141 uint8_t reg_frame_count; // Number of entries from registers (not stack-scanned)
142 uint8_t exception; // panic_exception_t enum (FAULT/ABORT/IWDT/TWDT/DEBUG)
143 uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic)
144 uint32_t backtrace[MAX_BACKTRACE];
145 uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V)
146 uint32_t fault_addr; // Faulting memory address: excvaddr (Xtensa) or mtval (RISC-V)
147 uint32_t build_time; // ESPHOME_BUILD_TIME of the firmware that captured this record
148 uint8_t crashed_core;
149#if SOC_CPU_CORES_NUM > 1
150 static_assert(SOC_CPU_CORES_NUM == 2, "Dual-core logic assumes exactly 2 cores");
151 uint8_t other_backtrace_count;
152 uint8_t other_reg_frame_count;
153 uint32_t other_backtrace[MAX_BACKTRACE];
154#endif
155};
156static RawCrashData __attribute__((section(".noinit")))
157s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
158
159// Whether crash data was found and validated this boot.
160static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
161
162namespace esphome::esp32 {
163
164static const char *const TAG = "esp32.crash";
165
166// RAM copy of the build timestamp. The generated constant lives in flash,
167// which the panic handler must not read (cache may be disabled during
168// cache-error panics), so the wrapper stamps the record from this mirror
169// instead. Filled during C++ dynamic initialization, well before arch_init();
170// ESPHOME_BUILD_TIME itself is constant-initialized, so the read is ordered.
171// Unqualified name on purpose: the runtime header declares it in namespace
172// esphome, while the static-analysis stub defines it as a macro.
173// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
174static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME);
175
177 if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
178 s_crash_data_valid = true;
179 // Clamp counts to prevent out-of-bounds reads from corrupt .noinit data
180 if (s_raw_crash_data.backtrace_count > MAX_BACKTRACE)
181 s_raw_crash_data.backtrace_count = MAX_BACKTRACE;
182 if (s_raw_crash_data.reg_frame_count > s_raw_crash_data.backtrace_count)
183 s_raw_crash_data.reg_frame_count = s_raw_crash_data.backtrace_count;
184 if (s_raw_crash_data.exception > 4) // panic_exception_t max value
185 s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT
186 if (s_raw_crash_data.pseudo_excause > 1)
187 s_raw_crash_data.pseudo_excause = 0;
188 if (s_raw_crash_data.crashed_core >= SOC_CPU_CORES_NUM)
189 s_raw_crash_data.crashed_core = 0;
190#if SOC_CPU_CORES_NUM > 1
191 if (s_raw_crash_data.other_backtrace_count > MAX_BACKTRACE)
192 s_raw_crash_data.other_backtrace_count = MAX_BACKTRACE;
193 if (s_raw_crash_data.other_reg_frame_count > s_raw_crash_data.other_backtrace_count)
194 s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count;
195#endif
196 }
197 // Don't clear magic here — crash data must survive OTA rollback reboots.
198 // Magic is cleared by crash_handler_clear() after an API client receives the data.
199}
200
201bool crash_handler_has_data() { return s_crash_data_valid; }
202
204 // Only clear the magic so data doesn't survive the next reboot.
205 // Keep s_crash_data_valid so crash_handler_log() still works for
206 // additional API clients connecting during this boot session.
207 s_raw_crash_data.magic = 0;
208}
209
210// Whether the cause slot was written by a real exception frame.
211static bool cause_slot_was_written() {
212#if CONFIG_IDF_TARGET_ARCH_XTENSA
213 return s_raw_crash_data.cause < XTENSA_EXCCAUSE_COUNT;
214#else
215 return s_raw_crash_data.cause < RISCV_EXCEPTION_CAUSE_COUNT;
216#endif
217}
218
219// Look up the exception cause as a human-readable string.
220// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays
221// not exposed via any public API.
222static const char *get_exception_reason() {
223 uint8_t exception = s_raw_crash_data.exception;
224 if (exception == PANIC_EXCEPTION_ABORT || exception == PANIC_EXCEPTION_TWDT) {
225 // Abort-class panics carry no cause register
226 return nullptr;
227 }
228 if (!cause_slot_was_written()) {
229 // Garbage from old-build or corrupt records; report just the type
230 return nullptr;
231 }
232#if CONFIG_IDF_TARGET_ARCH_XTENSA
233 if (s_raw_crash_data.pseudo_excause) {
234 // SoC-level panic: watchdog, cache error, etc.
235 // Keep in sync with ESP-IDF's PANIC_RSN_* defines
236 static const char *const PSEUDO_REASON[] = {
237 "Unknown reason", // 0
238 "Unhandled debug exception", // 1
239 "Double exception", // 2
240 "Unhandled kernel exception", // 3
241 "Coprocessor exception", // 4
242 "Interrupt wdt timeout on CPU0", // 5
243 "Interrupt wdt timeout on CPU1", // 6
244 "Cache error", // 7
245 };
246 uint32_t cause = s_raw_crash_data.cause;
247 if (cause < sizeof(PSEUDO_REASON) / sizeof(PSEUDO_REASON[0]))
248 return PSEUDO_REASON[cause];
249 return PSEUDO_REASON[0];
250 }
251 // Real Xtensa exception
252 static const char *const REASON[] = {
253 "IllegalInstruction",
254 "Syscall",
255 "InstructionFetchError",
256 "LoadStoreError",
257 "Level1Interrupt",
258 "Alloca",
259 "IntegerDivideByZero",
260 "PCValue",
261 "Privileged",
262 "LoadStoreAlignment",
263 nullptr,
264 nullptr,
265 "InstrPDAddrError",
266 "LoadStorePIFDataError",
267 "InstrPIFAddrError",
268 "LoadStorePIFAddrError",
269 "InstTLBMiss",
270 "InstTLBMultiHit",
271 "InstFetchPrivilege",
272 nullptr,
273 "InstrFetchProhibited",
274 nullptr,
275 nullptr,
276 nullptr,
277 "LoadStoreTLBMiss",
278 "LoadStoreTLBMultihit",
279 "LoadStorePrivilege",
280 nullptr,
281 "LoadProhibited",
282 "StoreProhibited",
283 nullptr,
284 nullptr,
285 "Cp0Dis",
286 "Cp1Dis",
287 "Cp2Dis",
288 "Cp3Dis",
289 "Cp4Dis",
290 "Cp5Dis",
291 "Cp6Dis",
292 "Cp7Dis",
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#elif CONFIG_IDF_TARGET_ARCH_RISCV
298 // For SoC-level panics (watchdog, cache error), mcause holds IDF-internal
299 // interrupt numbers, not standard RISC-V cause codes. The exception type
300 // field already identifies these, so just return null to use the type name.
301 if (s_raw_crash_data.pseudo_excause)
302 return nullptr;
303 static const char *const REASON[] = {
304 "Instruction address misaligned",
305 "Instruction access fault",
306 "Illegal instruction",
307 "Breakpoint",
308 "Load address misaligned",
309 "Load access fault",
310 "Store address misaligned",
311 "Store access fault",
312 "Environment call from U-mode",
313 "Environment call from S-mode",
314 nullptr,
315 "Environment call from M-mode",
316 "Instruction page fault",
317 "Load page fault",
318 nullptr,
319 "Store page fault",
320 };
321 uint32_t cause = s_raw_crash_data.cause;
322 if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr)
323 return REASON[cause];
324#endif
325 return "Unknown";
326}
327
328// Exception type names matching panic_exception_t enum
329static const char *get_exception_type() {
330 static const char *const TYPES[] = {
331 "Debug exception", // PANIC_EXCEPTION_DEBUG
332 "Interrupt wdt", // PANIC_EXCEPTION_IWDT
333 "Task wdt", // PANIC_EXCEPTION_TWDT
334 "Abort", // PANIC_EXCEPTION_ABORT
335 "Fault", // PANIC_EXCEPTION_FAULT
336 };
337 uint8_t exc = s_raw_crash_data.exception;
338 if (exc < sizeof(TYPES) / sizeof(TYPES[0]))
339 return TYPES[exc];
340 return "Unknown";
341}
342
343// Log backtrace entries, filtering stack-scanned addresses on RISC-V.
344static void log_backtrace(const uint32_t *addrs, uint8_t count, uint8_t reg_frame_count) {
345 uint8_t bt_num = 0;
346 for (uint8_t i = 0; i < count; i++) {
347 uint32_t addr = addrs[i];
348#if CONFIG_IDF_TARGET_ARCH_RISCV
349 if (i >= reg_frame_count && !is_return_addr(addr))
350 continue;
351 const char *source = (i < reg_frame_count) ? "backtrace" : "stack scan";
352#else
353 const char *source = "backtrace";
354#endif
355 ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source);
356 }
357}
358
359// Append backtrace addresses to the addr2line hint buffer.
360static int append_addrs_to_hint(char *buf, int size, int pos, const uint32_t *addrs, uint8_t count,
361 uint8_t reg_frame_count) {
362 for (uint8_t i = 0; i < count && pos < size - 12; i++) {
363 uint32_t addr = addrs[i];
364#if CONFIG_IDF_TARGET_ARCH_RISCV
365 if (i >= reg_frame_count && !is_return_addr(addr))
366 continue;
367#endif
368 pos += snprintf(buf + pos, size - pos, " 0x%08" PRIX32, addr);
369 }
370 return pos;
371}
372
373// Register holding the faulting memory address, named as in ESP-IDF's live
374// register dump. The lowercase form is for old-build reports, where the
375// stacktrace decoders must not match the line.
376#if CONFIG_IDF_TARGET_ARCH_XTENSA
377static const char *const FAULT_ADDR_REG = "EXCVADDR";
378static const char *const FAULT_ADDR_REG_LOWER = "excvaddr";
379#elif CONFIG_IDF_TARGET_ARCH_RISCV
380static const char *const FAULT_ADDR_REG = "MTVAL";
381static const char *const FAULT_ADDR_REG_LOWER = "mtval";
382#endif
383
384// Whether the fault address is meaningful: real CPU faults with a validly
385// written frame only.
386static bool has_fault_addr() {
387 return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause &&
388 cause_slot_was_written();
389}
390
391// The record was captured by a different firmware build (it survives soft
392// resets, including the OTA reboot), so symbolizing its addresses against the
393// current ELF would produce misleading symbols. Print them with lowercase
394// labels the stacktrace decoders deliberately do not match, and skip the
395// addr2line hint. One line per address so nothing is lost to a shared buffer.
396// No is_return_addr() filtering here: it would inspect the current build's
397// code bytes, which say nothing about addresses captured by the old build.
398static uint8_t log_foreign_backtrace(const uint32_t *addrs, uint8_t count, uint8_t bt_num) {
399 for (uint8_t i = 0; i < count; i++) {
400 ESP_LOGE(TAG, " bt%d: 0x%08" PRIX32, bt_num++, addrs[i]);
401 }
402 return bt_num;
403}
404
405static void log_foreign_addresses() {
406 ESP_LOGE(TAG, " Captured by a different firmware build; addresses belong to that build's ELF");
407 ESP_LOGE(TAG, " pc: 0x%08" PRIX32, s_raw_crash_data.pc);
408 if (has_fault_addr()) {
409 ESP_LOGE(TAG, " %s: 0x%08" PRIX32, FAULT_ADDR_REG_LOWER, s_raw_crash_data.fault_addr);
410 }
411 uint8_t bt_num = log_foreign_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, 0);
412#if SOC_CPU_CORES_NUM > 1
413 if (s_raw_crash_data.other_backtrace_count > 0) {
414 // Lowercase like the address labels: carries no address, matches no decoder.
415 ESP_LOGE(TAG, " other core (%d):", 1 - s_raw_crash_data.crashed_core);
416 log_foreign_backtrace(s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count, bt_num);
417 }
418#else
419 (void) bt_num; // Single-core targets have no second list to continue numbering into.
420#endif
421}
422
423// Intentionally uses separate ESP_LOGE calls per line instead of combining into
424// one multi-line log message. This ensures each address appears as its own line
425// on the serial console, making it possible to see partial output if the device
426// crashes again during boot, and allowing the CLI's process_stacktrace to match
427// and decode each address individually.
429 if (!s_crash_data_valid)
430 return;
431
432 ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
433 const char *reason = get_exception_reason();
434 if (reason != nullptr) {
435 ESP_LOGE(TAG, " Reason: %s - %s (cause %" PRIu32 ")", get_exception_type(), reason, s_raw_crash_data.cause);
436 } else {
437 ESP_LOGE(TAG, " Reason: %s", get_exception_type());
438 }
439 ESP_LOGE(TAG, " Crashed core: %d", s_raw_crash_data.crashed_core);
440 if (s_raw_crash_data.build_time != s_current_build_time) {
441 // Captured by a different firmware build: the record survives soft resets
442 // including the OTA reboot, so its addresses belong to a previous ELF.
443 log_foreign_addresses();
444 return;
445 }
446 ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc);
447 // Uses the same register name as ESP-IDF's live register dump so the CLI
448 // decodes the address when it happens to be a code address.
449 if (has_fault_addr()) {
450 ESP_LOGE(TAG, " %s: 0x%08" PRIX32 " (faulting address)", FAULT_ADDR_REG, s_raw_crash_data.fault_addr);
451 }
452 log_backtrace(s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count, s_raw_crash_data.reg_frame_count);
453
454#if SOC_CPU_CORES_NUM > 1
455 if (s_raw_crash_data.other_backtrace_count > 0) {
456 int other_core = 1 - s_raw_crash_data.crashed_core;
457 ESP_LOGE(TAG, " Other core (%d) backtrace:", other_core);
458 log_backtrace(s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count,
459 s_raw_crash_data.other_reg_frame_count);
460 }
461#endif
462
463 // Build addr2line hints for easy copy-paste. One line per core: the two
464 // backtraces are separate stacks, and a combined list decodes as one
465 // impossible call chain (and can overflow the buffer, dropping addresses).
466 static const char *const ADDR2LINE_CMD = "addr2line -pfiaC -e firmware.elf";
467 char hint[256];
468 int pos = snprintf(hint, sizeof(hint), "Use: %s 0x%08" PRIX32, ADDR2LINE_CMD, s_raw_crash_data.pc);
469 append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count,
470 s_raw_crash_data.reg_frame_count);
471 ESP_LOGE(TAG, "%s", hint);
472#if SOC_CPU_CORES_NUM > 1
473 if (s_raw_crash_data.other_backtrace_count > 0) {
474 pos = snprintf(hint, sizeof(hint), "Other core: %s", ADDR2LINE_CMD);
475 append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace,
476 s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count);
477 ESP_LOGE(TAG, "%s", hint);
478 }
479#endif
480}
481
482} // namespace esphome::esp32
483
484// --- Panic handler wrapper ---
485// Intercepts esp_panic_handler() via --wrap linker flag to capture crash data
486// into NOINIT memory before the normal panic handler runs.
487//
488extern "C" {
489// Set by IDF's task watchdog (task_wdt.c, no header) before it simulates an
490// abort; weak so builds without the task watchdog still link.
491extern bool g_twdt_isr __attribute__((weak));
492
493// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
494// Names are mandated by the --wrap linker mechanism
495extern void __real_esp_panic_handler(panic_info_t *info);
496
497void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
498 // Save the faulting PC and exception info
499 s_raw_crash_data.pc = (uint32_t) info->addr;
500 s_raw_crash_data.backtrace_count = 0;
501 s_raw_crash_data.reg_frame_count = 0;
502 s_raw_crash_data.exception = (uint8_t) info->exception;
503 s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0;
504 s_raw_crash_data.crashed_core = (uint8_t) info->core;
505 if (g_panic_abort) {
506 // IDF reclassifies to ABORT only inside esp_panic_handler(), after this
507 // wrapper captured info->exception; correct it here. TWDT is our own
508 // distinction (IDF never assigns PANIC_EXCEPTION_TWDT). The abort text is
509 // not stored; the symbolized backtrace already identifies the site.
510 bool is_twdt = &g_twdt_isr != nullptr && g_twdt_isr;
511 s_raw_crash_data.exception = (uint8_t) (is_twdt ? PANIC_EXCEPTION_TWDT : PANIC_EXCEPTION_ABORT);
512 }
513 // Zero unconditionally so a null frame doesn't leave stale .noinit data from a previous boot
514 s_raw_crash_data.cause = 0;
515 s_raw_crash_data.fault_addr = 0;
516 // Record which build's ELF the captured addresses belong to (RAM read, panic-safe).
517 // Still 0 if the panic precedes C++ dynamic initialization, so such a crash
518 // reports as a foreign build — conservative: addresses are shown raw instead
519 // of decoded.
520 s_raw_crash_data.build_time = esphome::esp32::s_current_build_time;
521#if SOC_CPU_CORES_NUM > 1
522 s_raw_crash_data.other_backtrace_count = 0;
523 s_raw_crash_data.other_reg_frame_count = 0;
524#endif
525
526#if CONFIG_IDF_TARGET_ARCH_XTENSA
527 // Xtensa: walk the backtrace using the public API
528 if (info->frame != nullptr) {
529 auto *xt_frame = (XtExcFrame *) info->frame;
530 if (!g_panic_abort) {
531 // Abort-class frames carry no useful cause/vaddr: TWDT task snapshots
532 // never wrote them and abort() traps describe only the synthetic trap.
533 s_raw_crash_data.cause = xt_frame->exccause;
534 s_raw_crash_data.fault_addr = xt_frame->excvaddr;
535 }
536 s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE);
537 }
538
539#if SOC_CPU_CORES_NUM > 1
540 // Capture the other core's backtrace from the global frame array.
541 // Both cores save their frames to g_exc_frames[] before esp_panic_handler
542 // is called, so the other core's frame is available here.
543 if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) {
544 int other_core = 1 - info->core;
545 auto *other_frame = (XtExcFrame *) g_exc_frames[other_core];
546 if (other_frame != nullptr) {
547 s_raw_crash_data.other_backtrace_count =
548 walk_xtensa_backtrace(other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE);
549 }
550 }
551#endif
552
553#elif CONFIG_IDF_TARGET_ARCH_RISCV
554 // RISC-V: capture MEPC + RA, then scan stack for code addresses
555 if (info->frame != nullptr) {
556 auto *rv_frame = (RvExcFrame *) info->frame;
557 if (!g_panic_abort) {
558 // See the Xtensa branch: abort-class frames carry no valid cause/vaddr.
559 s_raw_crash_data.cause = rv_frame->mcause;
560 s_raw_crash_data.fault_addr = rv_frame->mtval;
561 }
562 s_raw_crash_data.backtrace_count =
563 capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count);
564 }
565
566#if SOC_CPU_CORES_NUM > 1
567 // Capture the other core's backtrace from the global frame array.
568 if (info->core >= 0 && info->core < SOC_CPU_CORES_NUM) {
569 int other_core = 1 - info->core;
570 auto *other_frame = (RvExcFrame *) g_exc_frames[other_core];
571 if (other_frame != nullptr) {
572 s_raw_crash_data.other_backtrace_count = capture_riscv_backtrace(
573 other_frame, s_raw_crash_data.other_backtrace, MAX_BACKTRACE, &s_raw_crash_data.other_reg_frame_count);
574 }
575 }
576#endif
577#endif
578
579 // Write version and magic last — ensures all data is written before we mark it valid
580 s_raw_crash_data.version = CRASH_DATA_VERSION;
581 s_raw_crash_data.magic = CRASH_MAGIC;
582
583 // Call the real panic handler (prints to UART, does core dump, reboots, etc.)
585}
586
587// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
588} // extern "C"
589
590#endif // USE_ESP32_CRASH_HANDLER
591#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:1092
uint32_t * scan_start
static void uint32_t
uint32_t pc