diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 13d5273ecbb..5a86240ab56 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -20,6 +20,9 @@ #ifdef USE_RP2040_CRASH_HANDLER #include "esphome/components/rp2040/crash_handler.h" #endif +#ifdef USE_ESP8266_CRASH_HANDLER +#include "esphome/components/esp8266/crash_handler.h" +#endif #include "esphome/core/entity_base.h" #include "esphome/core/string_ref.h" @@ -276,6 +279,9 @@ class APIConnection final : public APIServerConnectionBase { #endif #ifdef USE_RP2040_CRASH_HANDLER rp2040::crash_handler_log(); +#endif +#ifdef USE_ESP8266_CRASH_HANDLER + esp8266::crash_handler_log(); #endif } #ifdef USE_API_HOMEASSISTANT_SERVICES diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 2081145096e..fcd3499b151 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -233,6 +233,7 @@ async def to_code(config): cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") cg.add_define(ThreadModel.SINGLE) + cg.add_define("USE_ESP8266_CRASH_HANDLER") enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT) if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config): diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 159ec20e77b..5db5b064d48 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -2,6 +2,9 @@ #include "core.h" #include "esphome/core/defines.h" +#ifdef USE_ESP8266_CRASH_HANDLER +#include "crash_handler.h" +#endif #include "esphome/core/hal.h" #include "esphome/core/time_64.h" #include "esphome/core/helpers.h" @@ -28,7 +31,11 @@ void arch_restart() { yield(); } } -void arch_init() {} +void arch_init() { +#ifdef USE_ESP8266_CRASH_HANDLER + esp8266::crash_handler_read_and_clear(); +#endif +} void HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp new file mode 100644 index 00000000000..4b04d851d62 --- /dev/null +++ b/esphome/components/esp8266/crash_handler.cpp @@ -0,0 +1,238 @@ +#ifdef USE_ESP8266 + +#include "esphome/core/defines.h" +#ifdef USE_ESP8266_CRASH_HANDLER + +#include "crash_handler.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +extern "C" { +#include + +// Global reset info struct populated by SDK/Arduino core at boot +extern struct rst_info resetInfo; +} + +// Check if a value looks like a code address in IRAM or flash-mapped IROM. +// On Xtensa with windowed register ABI, return addresses stored on the stack +// have bits[31:30] encoding the call type (CALL0=00, CALL4=01, CALL8=10, +// CALL12=11). Code lives at 0x40xxxxxx (bits[31:30]=01), so CALL4 return +// addresses look normal, but CALL8 (0x80...) and CALL12 (0xC0...) need +// masking. We recover the real address with (val & 0x3FFFFFFF) | 0x40000000. +// +// Must be IRAM_ATTR since it's called from custom_crash_callback (exception context). +static inline bool IRAM_ATTR is_code_addr(uint32_t val) { + uint32_t addr = (val & 0x3FFFFFFF) | 0x40000000; + // IRAM: 0x40100000 - 0x40108000 (32KB) + // IROM: 0x40200000 - 0x40400000 (2MB, conservative upper bound) + return (addr >= 0x40100000 && addr < 0x40108000) || (addr >= 0x40200000 && addr < 0x40400000); +} + +// Recover the actual code address from a windowed-ABI return address on the stack. +static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & 0x3FFFFFFF) | 0x40000000; } + +// RTC user memory layout for crash backtrace data. +// User-accessible RTC memory: blocks 64-191 (each block = 4 bytes). +// We use blocks 184-191 (last 8 blocks) to minimize conflicts with other users. +static constexpr uint8_t RTC_CRASH_BASE = 184; +static constexpr uint32_t CRASH_MAGIC_SENTINEL = 0xDEAD0000; +static constexpr uint32_t CRASH_DATA_VERSION = 1; +static constexpr uint32_t CRASH_MAGIC_V1 = CRASH_MAGIC_SENTINEL | CRASH_DATA_VERSION; +static constexpr size_t MAX_BACKTRACE = 6; + +// Struct layout matches 8 RTC blocks (32 bytes): +// [0] = magic (0xDEAD0001) +// [1..6] = up to 6 code addresses from stack scanning +// [7] = backtrace count (lower 8 bits) +struct RtcCrashData { + uint32_t magic; + uint32_t backtrace[MAX_BACKTRACE]; + uint32_t backtrace_count; // Only lower 8 bits used; uint32_t for RTC alignment +}; +static_assert(sizeof(RtcCrashData) == 32, "RtcCrashData must fit in 8 RTC blocks"); + +namespace esphome::esp8266 { + +static const char *const TAG = "esp8266.crash"; + +// Whether the previous boot was a crash. Set once in crash_handler_read_and_clear(). +// resetInfo and RTC backtrace data persist until the next reset, so no caching needed. +static bool s_crash_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +bool crash_handler_has_data() { return s_crash_valid; } + +void crash_handler_read_and_clear() { + uint32_t reason = resetInfo.reason; + s_crash_valid = (reason == REASON_WDT_RST || reason == REASON_EXCEPTION_RST || reason == REASON_SOFT_WDT_RST); +} + +// Xtensa exception cause names (shared with ESP32, same ISA). +// Keep in sync with Xtensa ISA reference manual Table 4-64. +static const LogString *get_exception_cause(uint32_t cause) { + switch (cause) { + case 0: + return LOG_STR("IllegalInstruction"); + case 1: + return LOG_STR("Syscall"); + case 2: + return LOG_STR("InstructionFetchError"); + case 3: + return LOG_STR("LoadStoreError"); + case 4: + return LOG_STR("Level1Interrupt"); + case 5: + return LOG_STR("Alloca"); + case 6: + return LOG_STR("IntegerDivideByZero"); + case 7: + return LOG_STR("PCValue"); + case 8: + return LOG_STR("Privileged"); + case 9: + return LOG_STR("LoadStoreAlignment"); + case 12: + return LOG_STR("InstrPDAddrError"); + case 13: + return LOG_STR("LoadStorePIFDataError"); + case 14: + return LOG_STR("InstrPIFAddrError"); + case 15: + return LOG_STR("LoadStorePIFAddrError"); + case 16: + return LOG_STR("InstTLBMiss"); + case 17: + return LOG_STR("InstTLBMultiHit"); + case 18: + return LOG_STR("InstFetchPrivilege"); + case 20: + return LOG_STR("InstrFetchProhibited"); + case 24: + return LOG_STR("LoadStoreTLBMiss"); + case 25: + return LOG_STR("LoadStoreTLBMultihit"); + case 26: + return LOG_STR("LoadStorePrivilege"); + case 28: + return LOG_STR("LoadProhibited"); + case 29: + return LOG_STR("StoreProhibited"); + default: + return nullptr; + } +} + +static const LogString *get_reset_reason(uint32_t reason) { + switch (reason) { + case REASON_WDT_RST: + return LOG_STR("Hardware Watchdog"); + case REASON_EXCEPTION_RST: + return LOG_STR("Exception"); + case REASON_SOFT_WDT_RST: + return LOG_STR("Software Watchdog"); + default: + return LOG_STR("Unknown"); + } +} + +// Read backtrace from RTC user memory into caller-provided buffer. +// Returns the number of valid backtrace entries (0 if no data found). +static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) { + RtcCrashData rtc_data; + if (!system_rtc_mem_read(RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data))) + return 0; + uint32_t magic = rtc_data.magic; + if ((magic & 0xFFFF0000) != CRASH_MAGIC_SENTINEL || (magic & 0xFFFF) != CRASH_DATA_VERSION) + return 0; + uint8_t count = rtc_data.backtrace_count; + if (count > max_entries) + count = max_entries; + for (uint8_t i = 0; i < count; i++) { + backtrace[i] = rtc_data.backtrace[i]; + } + return count; +} + +// 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_valid) + return; + + // Read backtrace from RTC into stack-local buffer (no persistent RAM cost). + // Both resetInfo and RTC data survive until the next reset, so this can be + // called multiple times (logger init + API subscribe) with the same result. + uint32_t backtrace[MAX_BACKTRACE]; + uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE); + + ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + const LogString *cause = get_exception_cause(resetInfo.exccause); + if (resetInfo.reason == REASON_EXCEPTION_RST && cause != nullptr) { + ESP_LOGE(TAG, " Reason: %s - %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)), + LOG_STR_ARG(cause), resetInfo.exccause); + } else { + ESP_LOGE(TAG, " Reason: %s", LOG_STR_ARG(get_reset_reason(resetInfo.reason))); + } + ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", resetInfo.epc1); + if (resetInfo.epc2 != 0) { + ESP_LOGE(TAG, " EPC2: 0x%08" PRIX32, resetInfo.epc2); + } + if (resetInfo.epc3 != 0) { + ESP_LOGE(TAG, " EPC3: 0x%08" PRIX32, resetInfo.epc3); + } + if (resetInfo.excvaddr != 0) { + ESP_LOGE(TAG, " EXCVADDR: 0x%08" PRIX32 " (faulting address)", resetInfo.excvaddr); + } + if (resetInfo.depc != 0) { + ESP_LOGE(TAG, " DEPC: 0x%08" PRIX32 " (double exception)", resetInfo.depc); + } + for (uint8_t i = 0; i < bt_count; i++) { + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (stack scan)", i, backtrace[i]); + } + // Build addr2line hint with all captured addresses for easy copy-paste + char hint[200]; + size_t pos = + buf_append_printf(hint, sizeof(hint), 0, "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, resetInfo.epc1); + for (uint8_t i = 0; i < bt_count; i++) { + pos = buf_append_printf(hint, sizeof(hint), pos, " 0x%08" PRIX32, backtrace[i]); + } + ESP_LOGE(TAG, "%s", hint); +} + +} // namespace esphome::esp8266 + +// --- Custom crash callback --- +// Overrides the weak custom_crash_callback() from Arduino core's +// core_esp8266_postmortem.cpp. Called during exception handling before +// the device restarts. We scan the stack for return addresses and store +// them in RTC user memory (which survives software reset). +extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info * /*rst_info*/, uint32_t stack, uint32_t stack_end) { + RtcCrashData data = {}; + uint8_t count = 0; + + auto *scan = reinterpret_cast(stack); + auto *end = reinterpret_cast(stack_end); + // Limit scan to 64 words (256 bytes) to avoid excessive scanning + if (end > scan + 64) + end = scan + 64; + + for (; scan < end && count < MAX_BACKTRACE; scan++) { + uint32_t val = *scan; + if (is_code_addr(val)) { + data.backtrace[count++] = recover_code_addr(val); + } + } + + data.backtrace_count = count; + data.magic = CRASH_MAGIC_V1; + + system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data)); +} + +#endif // USE_ESP8266_CRASH_HANDLER +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/crash_handler.h b/esphome/components/esp8266/crash_handler.h new file mode 100644 index 00000000000..78ba4711bfa --- /dev/null +++ b/esphome/components/esp8266/crash_handler.h @@ -0,0 +1,23 @@ +#pragma once + +#ifdef USE_ESP8266 + +#include "esphome/core/defines.h" + +#ifdef USE_ESP8266_CRASH_HANDLER + +namespace esphome::esp8266 { + +/// Read crash data from rst_info and RTC user memory, then clear RTC data. +void crash_handler_read_and_clear(); + +/// Log crash data if a crash was detected on previous boot. +void crash_handler_log(); + +/// Returns true if crash data was found this boot. +bool crash_handler_has_data(); + +} // namespace esphome::esp8266 + +#endif // USE_ESP8266_CRASH_HANDLER +#endif // USE_ESP8266 diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index b9507e707a7..5797b03ba72 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -1,5 +1,9 @@ #ifdef USE_ESP8266 #include "logger.h" +#include "esphome/core/defines.h" +#ifdef USE_ESP8266_CRASH_HANDLER +#include "esphome/components/esp8266/crash_handler.h" +#endif #include "esphome/core/log.h" namespace esphome::logger { @@ -26,6 +30,9 @@ void Logger::pre_setup() { global_logger = this; ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_ESP8266_CRASH_HANDLER + esp8266::crash_handler_log(); +#endif } const LogString *Logger::get_uart_selection_() { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index faa8c6d4b0e..02168b8fe58 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -330,6 +330,7 @@ // ESP8266-specific feature flags #ifdef USE_ESP8266 #define USE_ADC_SENSOR_VCC +#define USE_ESP8266_CRASH_HANDLER #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 2) #define USE_CAPTIVE_PORTAL #define USE_ESP8266_LOGGER_SERIAL