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