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