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