From 50073a24b57bdf43a3963afdd32d9d20270032d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 11:50:35 -1000 Subject: [PATCH 01/33] [esp8266] Add crash handler for post-mortem diagnostics Add a crash handler for ESP8266 that captures and logs crash data from previous boots, matching the existing ESP32 and RP2040 implementations. Uses the SDK's rst_info (always available after reboot) for basic crash info and Arduino's custom_crash_callback to scan the stack for return addresses stored in RTC user memory. --- esphome/components/api/api_connection.h | 6 + esphome/components/esp8266/__init__.py | 1 + esphome/components/esp8266/core.cpp | 9 +- esphome/components/esp8266/crash_handler.cpp | 238 +++++++++++++++++++ esphome/components/esp8266/crash_handler.h | 23 ++ esphome/components/logger/logger_esp8266.cpp | 7 + esphome/core/defines.h | 1 + 7 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 esphome/components/esp8266/crash_handler.cpp create mode 100644 esphome/components/esp8266/crash_handler.h diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 13d5273ecb..5a86240ab5 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 2081145096..fcd3499b15 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 159ec20e77..5db5b064d4 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 0000000000..4b04d851d6 --- /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 0000000000..78ba4711bf --- /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 b9507e707a..5797b03ba7 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 faa8c6d4b0..02168b8fe5 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 From 8dc30bc54d0e1fd6bb46fa26e2e10faf1332a033 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 11:52:02 -1000 Subject: [PATCH 02/33] Reduce RTC memory footprint from 32 to 20 bytes Pack backtrace count into the magic word and reduce max backtrace entries from 6 to 4, saving 3 RTC blocks (12 bytes). --- esphome/components/esp8266/crash_handler.cpp | 36 +++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 4b04d851d6..095ad65196 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -36,23 +36,28 @@ static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & // 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; +// We use blocks 187-191 (last 5 blocks, 20 bytes) to minimize conflicts. +static constexpr uint8_t RTC_CRASH_BASE = 187; +static constexpr size_t MAX_BACKTRACE = 4; -// 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) +// Magic word packs sentinel, version, and count into one uint32_t: +// bits[31:16] = 0xDEAD (sentinel) +// bits[15:8] = version (1) +// bits[7:0] = backtrace count +static constexpr uint32_t CRASH_SENTINEL = 0xDEAD0000; +static constexpr uint32_t CRASH_VERSION = 0x00000100; // version 1 in bits[15:8] +static constexpr uint32_t CRASH_SENTINEL_MASK = 0xFFFF0000; +static constexpr uint32_t CRASH_VERSION_MASK = 0x0000FF00; +static constexpr uint32_t CRASH_COUNT_MASK = 0x000000FF; + +// Struct layout: 5 RTC blocks (20 bytes): +// [0] = magic (sentinel | version | count) +// [1..4] = up to 4 code addresses from stack scanning 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"); +static_assert(sizeof(RtcCrashData) == 20, "RtcCrashData must fit in 5 RTC blocks"); namespace esphome::esp8266 { @@ -144,9 +149,9 @@ static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) { 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) + if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION) return 0; - uint8_t count = rtc_data.backtrace_count; + uint8_t count = magic & CRASH_COUNT_MASK; if (count > max_entries) count = max_entries; for (uint8_t i = 0; i < count; i++) { @@ -228,8 +233,7 @@ extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info * /*rst_info*/, } } - data.backtrace_count = count; - data.magic = CRASH_MAGIC_V1; + data.magic = CRASH_SENTINEL | CRASH_VERSION | count; system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data)); } From 0d93f2fcd1dc3eca246b385fa24f81fd44326adb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 11:54:01 -1000 Subject: [PATCH 03/33] Increase max backtrace to 8 entries for deep ESP8266 stacks ESP8266 call stacks are typically deep due to Arduino/LWIP/WiFi layers. 8 entries (36 bytes, 9 RTC blocks) better captures the WiFi -> LWIP -> socket -> ESPHome -> component call chain. --- esphome/components/esp8266/crash_handler.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 095ad65196..cfd903cdc5 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -36,9 +36,9 @@ static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & // RTC user memory layout for crash backtrace data. // User-accessible RTC memory: blocks 64-191 (each block = 4 bytes). -// We use blocks 187-191 (last 5 blocks, 20 bytes) to minimize conflicts. -static constexpr uint8_t RTC_CRASH_BASE = 187; -static constexpr size_t MAX_BACKTRACE = 4; +// We use blocks 183-191 (last 9 blocks, 36 bytes) to minimize conflicts. +static constexpr uint8_t RTC_CRASH_BASE = 183; +static constexpr size_t MAX_BACKTRACE = 8; // Magic word packs sentinel, version, and count into one uint32_t: // bits[31:16] = 0xDEAD (sentinel) @@ -50,14 +50,14 @@ static constexpr uint32_t CRASH_SENTINEL_MASK = 0xFFFF0000; static constexpr uint32_t CRASH_VERSION_MASK = 0x0000FF00; static constexpr uint32_t CRASH_COUNT_MASK = 0x000000FF; -// Struct layout: 5 RTC blocks (20 bytes): +// Struct layout: 9 RTC blocks (36 bytes): // [0] = magic (sentinel | version | count) -// [1..4] = up to 4 code addresses from stack scanning +// [1..8] = up to 8 code addresses from stack scanning struct RtcCrashData { uint32_t magic; uint32_t backtrace[MAX_BACKTRACE]; }; -static_assert(sizeof(RtcCrashData) == 20, "RtcCrashData must fit in 5 RTC blocks"); +static_assert(sizeof(RtcCrashData) == 36, "RtcCrashData must fit in 9 RTC blocks"); namespace esphome::esp8266 { From eb7ce1b6845470a369edf78e0b2a3dbe6135b047 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 11:54:37 -1000 Subject: [PATCH 04/33] Replace magic numbers with named constexpr constants --- esphome/components/esp8266/crash_handler.cpp | 25 +++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index cfd903cdc5..eb5d333cd9 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -16,23 +16,26 @@ extern "C" { extern struct rst_info resetInfo; } +// Xtensa windowed-ABI: bits[31:30] encode call type (CALL0=00, CALL4=01, +// CALL8=10, CALL12=11). Mask and force bit 30 to recover the real address. +static constexpr uint32_t XTENSA_ADDR_MASK = 0x3FFFFFFF; +static constexpr uint32_t XTENSA_CODE_BASE = 0x40000000; + +// ESP8266 memory map boundaries for code regions +static constexpr uint32_t IRAM_START = 0x40100000; +static constexpr uint32_t IRAM_END = 0x40108000; // 32KB +static constexpr uint32_t IROM_START = 0x40200000; +static constexpr uint32_t IROM_END = 0x40400000; // 2MB conservative upper bound + // 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); + uint32_t addr = (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; + return (addr >= IRAM_START && addr < IRAM_END) || (addr >= IROM_START && addr < IROM_END); } // 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; } +static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; } // RTC user memory layout for crash backtrace data. // User-accessible RTC memory: blocks 64-191 (each block = 4 bytes). From 73326bce07dd5ee58d32c974c2fdb6b08b442417 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 11:56:43 -1000 Subject: [PATCH 05/33] Fix stale docstring and extract stack scan limit constant --- esphome/components/esp8266/crash_handler.cpp | 6 +++--- esphome/components/esp8266/crash_handler.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index eb5d333cd9..5aeb6f6f2d 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -42,6 +42,7 @@ static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & // We use blocks 183-191 (last 9 blocks, 36 bytes) to minimize conflicts. static constexpr uint8_t RTC_CRASH_BASE = 183; static constexpr size_t MAX_BACKTRACE = 8; +static constexpr size_t STACK_SCAN_WORDS = 64; // Scan up to 256 bytes of stack // Magic word packs sentinel, version, and count into one uint32_t: // bits[31:16] = 0xDEAD (sentinel) @@ -225,9 +226,8 @@ extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info * /*rst_info*/, 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; + if (end > scan + STACK_SCAN_WORDS) + end = scan + STACK_SCAN_WORDS; for (; scan < end && count < MAX_BACKTRACE; scan++) { uint32_t val = *scan; diff --git a/esphome/components/esp8266/crash_handler.h b/esphome/components/esp8266/crash_handler.h index 78ba4711bf..def94fbbe9 100644 --- a/esphome/components/esp8266/crash_handler.h +++ b/esphome/components/esp8266/crash_handler.h @@ -8,7 +8,7 @@ namespace esphome::esp8266 { -/// Read crash data from rst_info and RTC user memory, then clear RTC data. +/// Check if previous boot was a crash and set validity flag. void crash_handler_read_and_clear(); /// Log crash data if a crash was detected on previous boot. From a89a4e13536acafeae31566b7e9c402f2af4dd37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 11:59:02 -1000 Subject: [PATCH 06/33] Always log EXCVADDR for exception resets A null EXCVADDR (0x00000000) is the key diagnostic for null pointer crashes (LoadProhibited/StoreProhibited). Suppressing it when zero hides the most useful information. --- esphome/components/esp8266/crash_handler.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 5aeb6f6f2d..9ffe0ff021 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -194,7 +194,8 @@ void crash_handler_log() { if (resetInfo.epc3 != 0) { ESP_LOGE(TAG, " EPC3: 0x%08" PRIX32, resetInfo.epc3); } - if (resetInfo.excvaddr != 0) { + if (resetInfo.reason == REASON_EXCEPTION_RST) { + // Always log EXCVADDR for exceptions — 0x00000000 IS the diagnostic for null pointer crashes ESP_LOGE(TAG, " EXCVADDR: 0x%08" PRIX32 " (faulting address)", resetInfo.excvaddr); } if (resetInfo.depc != 0) { From ac76f1fc3d68c48e784b8754e97b3a4b29acb7ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:02:05 -1000 Subject: [PATCH 07/33] Add return address verification and scan full stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Verify each candidate is a real return address by checking that the instruction at addr-3 is a CALL/CALLX (Xtensa 3-byte call opcodes). This filtering happens at log time when flash is readable, similar to the ESP32 RISC-V handler's is_return_addr() approach. - Scan the entire stack instead of just 64 words — with filtering, false positives from stale stack data are eliminated. - Skip epc1 during scanning (already reported as fault PC). - Increase to 16 RTC slots (72 bytes) for broader capture before filtering. Bump hint buffer to 256 for more addresses. --- esphome/components/esp8266/crash_handler.cpp | 96 +++++++++++++++----- 1 file changed, 74 insertions(+), 22 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 9ffe0ff021..8126cc2ba2 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -27,6 +27,14 @@ static constexpr uint32_t IRAM_END = 0x40108000; // 32KB static constexpr uint32_t IROM_START = 0x40200000; static constexpr uint32_t IROM_END = 0x40400000; // 2MB conservative upper bound +// Xtensa CALL instruction opcodes (3-byte instructions). +// A return address on the stack points to the instruction AFTER a CALL, +// so the CALL instruction is at addr-3. +static constexpr uint8_t XTENSA_CALL_OPCODE = 0x05; // CALL0/4/8/12: bits[3:0] = 0x5 +static constexpr uint8_t XTENSA_CALLX_OPCODE = 0x00; // CALLX0/4/8/12: bits[3:0] = 0x0 +static constexpr uint8_t XTENSA_CALLX_MIN = 0xC0; // CALLX: bits[19:16] >= 0xC (byte 2 upper nibble) +static constexpr uint8_t XTENSA_OPCODE_MASK = 0x0F; + // Check if a value looks like a code address in IRAM or flash-mapped IROM. // 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) { @@ -37,12 +45,42 @@ static inline bool IRAM_ATTR is_code_addr(uint32_t val) { // 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 & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; } +// Read a byte safely from any code address (IRAM or IROM). +// ESP8266 flash requires aligned 32-bit reads; byte extraction avoids alignment faults. +static inline uint8_t safe_read_code_byte(uint32_t addr) { + uint32_t aligned = addr & ~3u; + uint32_t word = *reinterpret_cast(aligned); + return (word >> ((addr & 3u) * 8)) & 0xFF; +} + +// Check if a code address is a real return address by verifying the preceding +// instruction is a CALL or CALLX. Called at log time (not during panic) so +// flash cache is available and both IRAM and IROM are safely readable. +// +// On Xtensa, CALL0/4/8/12 and CALLX0/4/8/12 are 3-byte instructions. +// A return address points to the instruction after the CALL, so we check addr-3. +static inline bool is_return_addr(uint32_t addr) { + if (!is_code_addr(addr) || addr < 3) + return false; + uint8_t b0 = safe_read_code_byte(addr - 3); + // Direct CALL0/4/8/12: bits[3:0] == 0x5 + if ((b0 & XTENSA_OPCODE_MASK) == XTENSA_CALL_OPCODE) + return true; + // CALLX0/4/8/12: bits[3:0] == 0x0, byte[2] upper nibble >= 0xC + if ((b0 & XTENSA_OPCODE_MASK) == XTENSA_CALLX_OPCODE) { + uint8_t b2 = safe_read_code_byte(addr - 1); + if ((b2 & 0xF0) >= XTENSA_CALLX_MIN) + return true; + } + return false; +} + // RTC user memory layout for crash backtrace data. // User-accessible RTC memory: blocks 64-191 (each block = 4 bytes). -// We use blocks 183-191 (last 9 blocks, 36 bytes) to minimize conflicts. -static constexpr uint8_t RTC_CRASH_BASE = 183; -static constexpr size_t MAX_BACKTRACE = 8; -static constexpr size_t STACK_SCAN_WORDS = 64; // Scan up to 256 bytes of stack +// We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts. +// Store 16 raw candidates, filter to real return addresses at log time. +static constexpr uint8_t RTC_CRASH_BASE = 174; +static constexpr size_t MAX_BACKTRACE = 16; // Magic word packs sentinel, version, and count into one uint32_t: // bits[31:16] = 0xDEAD (sentinel) @@ -54,14 +92,16 @@ static constexpr uint32_t CRASH_SENTINEL_MASK = 0xFFFF0000; static constexpr uint32_t CRASH_VERSION_MASK = 0x0000FF00; static constexpr uint32_t CRASH_COUNT_MASK = 0x000000FF; -// Struct layout: 9 RTC blocks (36 bytes): +// Struct layout: 18 RTC blocks (72 bytes): // [0] = magic (sentinel | version | count) -// [1..8] = up to 8 code addresses from stack scanning +// [1..16] = up to 16 code addresses from stack scanning +// [17] = epc1 at crash time (to skip duplicates at log time) struct RtcCrashData { uint32_t magic; uint32_t backtrace[MAX_BACKTRACE]; + uint32_t epc1; // Fault PC, used to filter duplicates }; -static_assert(sizeof(RtcCrashData) == 36, "RtcCrashData must fit in 9 RTC blocks"); +static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC blocks"); namespace esphome::esp8266 { @@ -155,13 +195,20 @@ static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) { uint32_t magic = rtc_data.magic; if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION) return 0; - uint8_t count = magic & CRASH_COUNT_MASK; - if (count > max_entries) - count = max_entries; - for (uint8_t i = 0; i < count; i++) { - backtrace[i] = rtc_data.backtrace[i]; + uint8_t raw_count = magic & CRASH_COUNT_MASK; + if (raw_count > MAX_BACKTRACE) + raw_count = MAX_BACKTRACE; + // Filter: only keep entries that are real return addresses (preceded by CALL instruction). + // Also skip any that match epc1 (already reported as the fault PC). + uint8_t out = 0; + for (uint8_t i = 0; i < raw_count && out < max_entries; i++) { + uint32_t addr = rtc_data.backtrace[i]; + if (addr == rtc_data.epc1) + continue; + if (is_return_addr(addr)) + backtrace[out++] = addr; } - return count; + return out; } // Intentionally uses separate ESP_LOGE calls per line instead of combining into @@ -173,7 +220,7 @@ void crash_handler_log() { if (!s_crash_valid) return; - // Read backtrace from RTC into stack-local buffer (no persistent RAM cost). + // Read and filter 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]; @@ -202,10 +249,10 @@ void crash_handler_log() { 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]); + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (backtrace)", i, backtrace[i]); } // Build addr2line hint with all captured addresses for easy copy-paste - char hint[200]; + char hint[256]; 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++) { @@ -219,24 +266,29 @@ void crash_handler_log() { // --- 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) { +// the device restarts. We scan the full stack for return addresses and store +// them in RTC user memory (which survives software reset). Filtering for +// real return addresses (preceded by CALL instructions) happens at log time +// when flash is accessible. +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); - if (end > scan + STACK_SCAN_WORDS) - end = scan + STACK_SCAN_WORDS; + uint32_t epc1 = rst_info->epc1; for (; scan < end && count < MAX_BACKTRACE; scan++) { uint32_t val = *scan; if (is_code_addr(val)) { - data.backtrace[count++] = recover_code_addr(val); + uint32_t addr = recover_code_addr(val); + // Skip epc1 — already reported as the fault PC + if (addr != epc1) + data.backtrace[count++] = addr; } } + data.epc1 = epc1; data.magic = CRASH_SENTINEL | CRASH_VERSION | count; system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data)); From 5aac3e1317c6d5dd82d0ad98792b70e5f76af859 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:06:46 -1000 Subject: [PATCH 08/33] Replace switch tables with PROGMEM_STRING_TABLE to save 132B RAM GCC generates CSWTCH jump tables in RAM rodata on ESP8266 for switch statements. Convert exception cause and reset reason lookups to PROGMEM_STRING_TABLE which keeps data in flash. Exception causes are split into two tables (0-9 and 12-29) to stay under the 255-byte blob limit. Added static_asserts on reset reason enum values. --- esphome/components/esp8266/crash_handler.cpp | 116 +++++++++---------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 8126cc2ba2..bfe78f2f34 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -6,6 +6,7 @@ #include "crash_handler.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include @@ -120,70 +121,69 @@ void crash_handler_read_and_clear() { // Xtensa exception cause names (shared with ESP32, same ISA). // Keep in sync with Xtensa ISA reference manual Table 4-64. +// Split into two PROGMEM_STRING_TABLEs to stay under 255-byte blob limit. +// clang-format off +PROGMEM_STRING_TABLE(ExcCauseLow, // Causes 0-9 + "IllegalInstruction", // 0 + "Syscall", // 1 + "InstructionFetchError", // 2 + "LoadStoreError", // 3 + "Level1Interrupt", // 4 + "Alloca", // 5 + "IntegerDivideByZero", // 6 + "PCValue", // 7 + "Privileged", // 8 + "LoadStoreAlignment" // 9 +); +PROGMEM_STRING_TABLE(ExcCauseHigh, // Causes 12-29 (offset by 12) + "InstrPDAddrError", // 12 + "LoadStorePIFDataError", // 13 + "InstrPIFAddrError", // 14 + "LoadStorePIFAddrError", // 15 + "InstTLBMiss", // 16 + "InstTLBMultiHit", // 17 + "InstFetchPrivilege", // 18 + "", // 19 (unused) + "InstrFetchProhibited", // 20 + "", // 21 (unused) + "", // 22 (unused) + "", // 23 (unused) + "LoadStoreTLBMiss", // 24 + "LoadStoreTLBMultihit", // 25 + "LoadStorePrivilege", // 26 + "", // 27 (unused) + "LoadProhibited", // 28 + "StoreProhibited" // 29 +); +// clang-format on + 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: + if (cause <= 9) + return ExcCauseLow::get_log_str(cause, ExcCauseLow::LAST_INDEX); + if (cause >= 12 && cause <= 29) { + const LogString *str = ExcCauseHigh::get_log_str(cause - 12, ExcCauseHigh::LAST_INDEX); + // Empty strings are gap entries — return nullptr so caller knows cause is unknown + if (LOG_STR_ARG(str)[0] == '\0') return nullptr; + return str; } + return nullptr; } +// clang-format off +PROGMEM_STRING_TABLE(ResetReasonStrings, + "Unknown", // 0 = fallback + "Hardware Watchdog", // 1 = REASON_WDT_RST + "Exception", // 2 = REASON_EXCEPTION_RST + "Software Watchdog" // 3 = REASON_SOFT_WDT_RST +); +// clang-format on +static_assert(REASON_WDT_RST == 1, "Reset reason enum values must match table indices"); +static_assert(REASON_EXCEPTION_RST == 2, "Reset reason enum values must match table indices"); +static_assert(REASON_SOFT_WDT_RST == 3, "Reset reason enum values must match table indices"); + 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"); - } + return ResetReasonStrings::get_log_str(static_cast(reason), 0); } // Read backtrace from RTC user memory into caller-provided buffer. From c1c45cfd23c1f892cf2e958e3f542f81f89dde81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:07:30 -1000 Subject: [PATCH 09/33] Derive magic word masks from bit positions at compile time --- esphome/components/esp8266/crash_handler.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index bfe78f2f34..0bd58cc545 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -84,14 +84,20 @@ static constexpr uint8_t RTC_CRASH_BASE = 174; static constexpr size_t MAX_BACKTRACE = 16; // Magic word packs sentinel, version, and count into one uint32_t: -// bits[31:16] = 0xDEAD (sentinel) -// bits[15:8] = version (1) +// bits[31:16] = sentinel +// bits[15:8] = version // bits[7:0] = backtrace count -static constexpr uint32_t CRASH_SENTINEL = 0xDEAD0000; -static constexpr uint32_t CRASH_VERSION = 0x00000100; // version 1 in bits[15:8] -static constexpr uint32_t CRASH_SENTINEL_MASK = 0xFFFF0000; -static constexpr uint32_t CRASH_VERSION_MASK = 0x0000FF00; -static constexpr uint32_t CRASH_COUNT_MASK = 0x000000FF; +static constexpr uint8_t CRASH_SENTINEL_BITS = 16; +static constexpr uint8_t CRASH_VERSION_BITS = 8; + +static constexpr uint16_t CRASH_SENTINEL_VALUE = 0xDEAD; +static constexpr uint8_t CRASH_VERSION_VALUE = 1; + +static constexpr uint32_t CRASH_SENTINEL = static_cast(CRASH_SENTINEL_VALUE) << CRASH_SENTINEL_BITS; +static constexpr uint32_t CRASH_VERSION = static_cast(CRASH_VERSION_VALUE) << CRASH_VERSION_BITS; +static constexpr uint32_t CRASH_SENTINEL_MASK = static_cast(0xFFFF) << CRASH_SENTINEL_BITS; +static constexpr uint32_t CRASH_VERSION_MASK = static_cast(0xFF) << CRASH_VERSION_BITS; +static constexpr uint32_t CRASH_COUNT_MASK = 0xFF; // Struct layout: 18 RTC blocks (72 bytes): // [0] = magic (sentinel | version | count) From a328726bfd8fcaaa1644126ec4e199e62816e14f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:08:05 -1000 Subject: [PATCH 10/33] Derive exception cause range bounds from table sizes --- esphome/components/esp8266/crash_handler.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 0bd58cc545..9f9eacf76c 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -163,11 +163,13 @@ PROGMEM_STRING_TABLE(ExcCauseHigh, // Causes 12-29 (offset by 12) ); // clang-format on +static constexpr uint32_t EXC_CAUSE_HIGH_BASE = 12; // First cause code in ExcCauseHigh table + static const LogString *get_exception_cause(uint32_t cause) { - if (cause <= 9) + if (cause < ExcCauseLow::COUNT) return ExcCauseLow::get_log_str(cause, ExcCauseLow::LAST_INDEX); - if (cause >= 12 && cause <= 29) { - const LogString *str = ExcCauseHigh::get_log_str(cause - 12, ExcCauseHigh::LAST_INDEX); + if (cause >= EXC_CAUSE_HIGH_BASE && cause < EXC_CAUSE_HIGH_BASE + ExcCauseHigh::COUNT) { + const LogString *str = ExcCauseHigh::get_log_str(cause - EXC_CAUSE_HIGH_BASE, ExcCauseHigh::LAST_INDEX); // Empty strings are gap entries — return nullptr so caller knows cause is unknown if (LOG_STR_ARG(str)[0] == '\0') return nullptr; From b99a4aab98ce6fd53d68cf86a25d168c76f70e09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:11:38 -1000 Subject: [PATCH 11/33] Fix crash in is_return_addr reading unmapped flash Two issues: 1. safe_read_code_byte used volatile cast which prevented the compiler from using flash-safe l32i instructions, causing LoadStoreError on IROM addresses. Replace with progmem_read_byte. 2. IROM upper bound (0x40400000 = 2MB) was too generous for devices with smaller flash (e.g. 1MB), causing both false positives in the backtrace and faults when is_return_addr tried to verify CALL instructions at unmapped addresses. Use _irom0_text_start/end linker symbols for precise firmware code bounds. --- esphome/components/esp8266/crash_handler.cpp | 24 ++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 9f9eacf76c..e2c01ac9df 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -25,8 +25,16 @@ static constexpr uint32_t XTENSA_CODE_BASE = 0x40000000; // ESP8266 memory map boundaries for code regions static constexpr uint32_t IRAM_START = 0x40100000; static constexpr uint32_t IRAM_END = 0x40108000; // 32KB -static constexpr uint32_t IROM_START = 0x40200000; -static constexpr uint32_t IROM_END = 0x40400000; // 2MB conservative upper bound + +// Linker symbols for the actual firmware IROM section. +// Using these instead of a conservative upper bound (0x40400000) prevents +// false positives from stale stack values beyond the actual flash mapping, +// and avoids LoadStoreError faults when is_return_addr() tries to read +// unmapped flash to verify CALL instructions. +extern "C" { +extern uint8_t _irom0_text_start; // NOLINT(bugprone-reserved-identifier,readability-identifier-naming) +extern uint8_t _irom0_text_end; // NOLINT(bugprone-reserved-identifier,readability-identifier-naming) +} // Xtensa CALL instruction opcodes (3-byte instructions). // A return address on the stack points to the instruction AFTER a CALL, @@ -38,20 +46,22 @@ static constexpr uint8_t XTENSA_OPCODE_MASK = 0x0F; // Check if a value looks like a code address in IRAM or flash-mapped IROM. // Must be IRAM_ATTR since it's called from custom_crash_callback (exception context). +// Using linker symbols (&_irom0_text_start/end) is safe in IRAM — they're link-time +// constants, no flash read needed. static inline bool IRAM_ATTR is_code_addr(uint32_t val) { uint32_t addr = (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; - return (addr >= IRAM_START && addr < IRAM_END) || (addr >= IROM_START && addr < IROM_END); + return (addr >= IRAM_START && addr < IRAM_END) || (addr >= reinterpret_cast(&_irom0_text_start) && + addr < reinterpret_cast(&_irom0_text_end)); } // 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 & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; } // Read a byte safely from any code address (IRAM or IROM). -// ESP8266 flash requires aligned 32-bit reads; byte extraction avoids alignment faults. +// Uses progmem_read_byte which handles ESP8266 flash alignment requirements +// (SPI flash cache requires special access patterns for byte reads). static inline uint8_t safe_read_code_byte(uint32_t addr) { - uint32_t aligned = addr & ~3u; - uint32_t word = *reinterpret_cast(aligned); - return (word >> ((addr & 3u) * 8)) & 0xFF; + return progmem_read_byte(reinterpret_cast(addr)); } // Check if a code address is a real return address by verifying the preceding From af6e10816d9f0efdc8d83e72ae6312bd4e50dd9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:12:06 -1000 Subject: [PATCH 12/33] Fix namespace for progmem_read_byte --- esphome/components/esp8266/crash_handler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index e2c01ac9df..52a26482c8 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -58,10 +58,10 @@ static inline bool IRAM_ATTR is_code_addr(uint32_t val) { static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; } // Read a byte safely from any code address (IRAM or IROM). -// Uses progmem_read_byte which handles ESP8266 flash alignment requirements +// Uses esphome::progmem_read_byte which handles ESP8266 flash alignment requirements // (SPI flash cache requires special access patterns for byte reads). static inline uint8_t safe_read_code_byte(uint32_t addr) { - return progmem_read_byte(reinterpret_cast(addr)); + return esphome::progmem_read_byte(reinterpret_cast(addr)); } // Check if a code address is a real return address by verifying the preceding From 6cdcdbb7985a01c7db91552bcb175ab6fc745443 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:24:05 -1000 Subject: [PATCH 13/33] Remove flash-reading return address verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading IROM addresses from IROM code causes LoadStoreError on ESP8266 due to the direct-mapped flash cache — the reading code and target address can share a cache line, evicting the function mid-execution. Remove is_return_addr() and rely on linker-symbol IROM bounds (_irom0_text_start/_end) to eliminate false positives instead. This is less precise but crash-safe. --- esphome/components/esp8266/crash_handler.cpp | 52 +++----------------- 1 file changed, 8 insertions(+), 44 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 52a26482c8..33bd4b0dff 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -6,7 +6,6 @@ #include "crash_handler.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/progmem.h" #include @@ -36,18 +35,11 @@ extern uint8_t _irom0_text_start; // NOLINT(bugprone-reserved-identifier,readab extern uint8_t _irom0_text_end; // NOLINT(bugprone-reserved-identifier,readability-identifier-naming) } -// Xtensa CALL instruction opcodes (3-byte instructions). -// A return address on the stack points to the instruction AFTER a CALL, -// so the CALL instruction is at addr-3. -static constexpr uint8_t XTENSA_CALL_OPCODE = 0x05; // CALL0/4/8/12: bits[3:0] = 0x5 -static constexpr uint8_t XTENSA_CALLX_OPCODE = 0x00; // CALLX0/4/8/12: bits[3:0] = 0x0 -static constexpr uint8_t XTENSA_CALLX_MIN = 0xC0; // CALLX: bits[19:16] >= 0xC (byte 2 upper nibble) -static constexpr uint8_t XTENSA_OPCODE_MASK = 0x0F; - // Check if a value looks like a code address in IRAM or flash-mapped IROM. // Must be IRAM_ATTR since it's called from custom_crash_callback (exception context). // Using linker symbols (&_irom0_text_start/end) is safe in IRAM — they're link-time -// constants, no flash read needed. +// constants, no flash read needed. This gives precise bounds matching the actual +// firmware, eliminating false positives from addresses beyond the flash mapping. static inline bool IRAM_ATTR is_code_addr(uint32_t val) { uint32_t addr = (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; return (addr >= IRAM_START && addr < IRAM_END) || (addr >= reinterpret_cast(&_irom0_text_start) && @@ -57,35 +49,6 @@ static inline bool IRAM_ATTR is_code_addr(uint32_t val) { // 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 & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; } -// Read a byte safely from any code address (IRAM or IROM). -// Uses esphome::progmem_read_byte which handles ESP8266 flash alignment requirements -// (SPI flash cache requires special access patterns for byte reads). -static inline uint8_t safe_read_code_byte(uint32_t addr) { - return esphome::progmem_read_byte(reinterpret_cast(addr)); -} - -// Check if a code address is a real return address by verifying the preceding -// instruction is a CALL or CALLX. Called at log time (not during panic) so -// flash cache is available and both IRAM and IROM are safely readable. -// -// On Xtensa, CALL0/4/8/12 and CALLX0/4/8/12 are 3-byte instructions. -// A return address points to the instruction after the CALL, so we check addr-3. -static inline bool is_return_addr(uint32_t addr) { - if (!is_code_addr(addr) || addr < 3) - return false; - uint8_t b0 = safe_read_code_byte(addr - 3); - // Direct CALL0/4/8/12: bits[3:0] == 0x5 - if ((b0 & XTENSA_OPCODE_MASK) == XTENSA_CALL_OPCODE) - return true; - // CALLX0/4/8/12: bits[3:0] == 0x0, byte[2] upper nibble >= 0xC - if ((b0 & XTENSA_OPCODE_MASK) == XTENSA_CALLX_OPCODE) { - uint8_t b2 = safe_read_code_byte(addr - 1); - if ((b2 & 0xF0) >= XTENSA_CALLX_MIN) - return true; - } - return false; -} - // RTC user memory layout for crash backtrace data. // User-accessible RTC memory: blocks 64-191 (each block = 4 bytes). // We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts. @@ -216,14 +179,15 @@ static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) { uint8_t raw_count = magic & CRASH_COUNT_MASK; if (raw_count > MAX_BACKTRACE) raw_count = MAX_BACKTRACE; - // Filter: only keep entries that are real return addresses (preceded by CALL instruction). - // Also skip any that match epc1 (already reported as the fault PC). + // Skip any that match epc1 (already reported as the fault PC). + // Note: we cannot verify CALL instructions at addr-3 on ESP8266 because + // reading from IROM causes LoadStoreError due to flash cache conflicts + // (the reading code and target can share a direct-mapped cache line). + // The linker-symbol IROM bounds already eliminate most false positives. uint8_t out = 0; for (uint8_t i = 0; i < raw_count && out < max_entries; i++) { uint32_t addr = rtc_data.backtrace[i]; - if (addr == rtc_data.epc1) - continue; - if (is_return_addr(addr)) + if (addr != rtc_data.epc1) backtrace[out++] = addr; } return out; From 58fd6ceff839a0b6819c2e1c888640069b5fa642 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:24:30 -1000 Subject: [PATCH 14/33] Restore progmem.h include needed for PROGMEM_STRING_TABLE --- esphome/components/esp8266/crash_handler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 33bd4b0dff..7e0ddbd2de 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -6,6 +6,7 @@ #include "crash_handler.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include From 8a8540b247bd0de99b4d832dadffc4b60381a6cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:33:32 -1000 Subject: [PATCH 15/33] Use if-else chains instead of switch for string lookups Switch statements generate CSWTCH jump tables in RAM on ESP8266. PROGMEM_STRING_TABLE causes LoadStoreError from flash cache conflicts in API subscribe paths. If-else with LOG_STR avoids both: strings stay in flash via PSTR, and comparison branches don't need a data table. Zero RAM overhead confirmed via nm. --- esphome/components/esp8266/crash_handler.cpp | 117 +++++++++---------- 1 file changed, 56 insertions(+), 61 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 7e0ddbd2de..3e62859bcb 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -6,7 +6,6 @@ #include "crash_handler.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/progmem.h" #include @@ -101,71 +100,67 @@ void crash_handler_read_and_clear() { // Xtensa exception cause names (shared with ESP32, same ISA). // Keep in sync with Xtensa ISA reference manual Table 4-64. -// Split into two PROGMEM_STRING_TABLEs to stay under 255-byte blob limit. -// clang-format off -PROGMEM_STRING_TABLE(ExcCauseLow, // Causes 0-9 - "IllegalInstruction", // 0 - "Syscall", // 1 - "InstructionFetchError", // 2 - "LoadStoreError", // 3 - "Level1Interrupt", // 4 - "Alloca", // 5 - "IntegerDivideByZero", // 6 - "PCValue", // 7 - "Privileged", // 8 - "LoadStoreAlignment" // 9 -); -PROGMEM_STRING_TABLE(ExcCauseHigh, // Causes 12-29 (offset by 12) - "InstrPDAddrError", // 12 - "LoadStorePIFDataError", // 13 - "InstrPIFAddrError", // 14 - "LoadStorePIFAddrError", // 15 - "InstTLBMiss", // 16 - "InstTLBMultiHit", // 17 - "InstFetchPrivilege", // 18 - "", // 19 (unused) - "InstrFetchProhibited", // 20 - "", // 21 (unused) - "", // 22 (unused) - "", // 23 (unused) - "LoadStoreTLBMiss", // 24 - "LoadStoreTLBMultihit", // 25 - "LoadStorePrivilege", // 26 - "", // 27 (unused) - "LoadProhibited", // 28 - "StoreProhibited" // 29 -); -// clang-format on - -static constexpr uint32_t EXC_CAUSE_HIGH_BASE = 12; // First cause code in ExcCauseHigh table - +// Uses if-else instead of switch to avoid CSWTCH jump tables (RAM on ESP8266). +// PROGMEM_STRING_TABLE also crashes due to flash cache conflicts in API paths. +// LOG_STR strings are in flash via PSTR; if-else generates comparison branches only. static const LogString *get_exception_cause(uint32_t cause) { - if (cause < ExcCauseLow::COUNT) - return ExcCauseLow::get_log_str(cause, ExcCauseLow::LAST_INDEX); - if (cause >= EXC_CAUSE_HIGH_BASE && cause < EXC_CAUSE_HIGH_BASE + ExcCauseHigh::COUNT) { - const LogString *str = ExcCauseHigh::get_log_str(cause - EXC_CAUSE_HIGH_BASE, ExcCauseHigh::LAST_INDEX); - // Empty strings are gap entries — return nullptr so caller knows cause is unknown - if (LOG_STR_ARG(str)[0] == '\0') - return nullptr; - return str; - } + if (cause == 0) + return LOG_STR("IllegalInstruction"); + if (cause == 1) + return LOG_STR("Syscall"); + if (cause == 2) + return LOG_STR("InstructionFetchError"); + if (cause == 3) + return LOG_STR("LoadStoreError"); + if (cause == 4) + return LOG_STR("Level1Interrupt"); + if (cause == 5) + return LOG_STR("Alloca"); + if (cause == 6) + return LOG_STR("IntegerDivideByZero"); + if (cause == 7) + return LOG_STR("PCValue"); + if (cause == 8) + return LOG_STR("Privileged"); + if (cause == 9) + return LOG_STR("LoadStoreAlignment"); + if (cause == 12) + return LOG_STR("InstrPDAddrError"); + if (cause == 13) + return LOG_STR("LoadStorePIFDataError"); + if (cause == 14) + return LOG_STR("InstrPIFAddrError"); + if (cause == 15) + return LOG_STR("LoadStorePIFAddrError"); + if (cause == 16) + return LOG_STR("InstTLBMiss"); + if (cause == 17) + return LOG_STR("InstTLBMultiHit"); + if (cause == 18) + return LOG_STR("InstFetchPrivilege"); + if (cause == 20) + return LOG_STR("InstrFetchProhibited"); + if (cause == 24) + return LOG_STR("LoadStoreTLBMiss"); + if (cause == 25) + return LOG_STR("LoadStoreTLBMultihit"); + if (cause == 26) + return LOG_STR("LoadStorePrivilege"); + if (cause == 28) + return LOG_STR("LoadProhibited"); + if (cause == 29) + return LOG_STR("StoreProhibited"); return nullptr; } -// clang-format off -PROGMEM_STRING_TABLE(ResetReasonStrings, - "Unknown", // 0 = fallback - "Hardware Watchdog", // 1 = REASON_WDT_RST - "Exception", // 2 = REASON_EXCEPTION_RST - "Software Watchdog" // 3 = REASON_SOFT_WDT_RST -); -// clang-format on -static_assert(REASON_WDT_RST == 1, "Reset reason enum values must match table indices"); -static_assert(REASON_EXCEPTION_RST == 2, "Reset reason enum values must match table indices"); -static_assert(REASON_SOFT_WDT_RST == 3, "Reset reason enum values must match table indices"); - static const LogString *get_reset_reason(uint32_t reason) { - return ResetReasonStrings::get_log_str(static_cast(reason), 0); + if (reason == REASON_WDT_RST) + return LOG_STR("Hardware Watchdog"); + if (reason == REASON_EXCEPTION_RST) + return LOG_STR("Exception"); + if (reason == REASON_SOFT_WDT_RST) + return LOG_STR("Software Watchdog"); + return LOG_STR("Unknown"); } // Read backtrace from RTC user memory into caller-provided buffer. From 9035410fa9cc5050cd9158b948c9168d3050b245 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:33:46 -1000 Subject: [PATCH 16/33] Remove PROGMEM_STRING_TABLE mention from comment --- esphome/components/esp8266/crash_handler.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 3e62859bcb..1f52de6529 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -100,9 +100,9 @@ void crash_handler_read_and_clear() { // Xtensa exception cause names (shared with ESP32, same ISA). // Keep in sync with Xtensa ISA reference manual Table 4-64. -// Uses if-else instead of switch to avoid CSWTCH jump tables (RAM on ESP8266). -// PROGMEM_STRING_TABLE also crashes due to flash cache conflicts in API paths. -// LOG_STR strings are in flash via PSTR; if-else generates comparison branches only. +// Uses if-else with LOG_STR instead of switch to avoid CSWTCH jump tables +// (placed in RAM rodata on ESP8266). LOG_STR stores strings in flash via +// PSTR; if-else generates comparison branches with no data table. static const LogString *get_exception_cause(uint32_t cause) { if (cause == 0) return LOG_STR("IllegalInstruction"); From 9f008ba487a035b760ae5630f9099e3f51ad1cf2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:38:36 -1000 Subject: [PATCH 17/33] Reduce flash by removing rarely-useful register lines Remove EPC2/EPC3 (always 0 on LX106, no level 2/3 interrupts) and DEPC (extremely rare double exception). Combine PC and EXCVADDR into one log line for exception resets. Shrink hint buffer 256->220. Saves ~36 bytes flash in crash_handler_log. --- esphome/components/esp8266/crash_handler.cpp | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 1f52de6529..abda1923bb 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -212,25 +212,17 @@ void crash_handler_log() { } 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.reason == REASON_EXCEPTION_RST) { - // Always log EXCVADDR for exceptions — 0x00000000 IS the diagnostic for null pointer crashes - 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); + // Log PC and EXCVADDR together — 0x00000000 IS the diagnostic for null pointer crashes + ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " EXCVADDR: 0x%08" PRIX32, resetInfo.epc1, resetInfo.excvaddr); + } else { + ESP_LOGE(TAG, " PC: 0x%08" PRIX32, resetInfo.epc1); } for (uint8_t i = 0; i < bt_count; i++) { - ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (backtrace)", i, backtrace[i]); + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32, i, backtrace[i]); } // Build addr2line hint with all captured addresses for easy copy-paste - char hint[256]; + char hint[220]; 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++) { From ca623ace1a7d8c829edadd6aa9f8234e1bfd6393 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:43:18 -1000 Subject: [PATCH 18/33] Remove s_crash_valid and crash_handler_read_and_clear MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resetInfo is a global that persists until next reset — no need to cache the crash validity in a separate bool. Check resetInfo.reason directly in crash_handler_has_data() and crash_handler_log(). This eliminates the only RAM byte from the crash handler and removes the now-empty crash_handler_read_and_clear() function and its call from arch_init(). --- esphome/components/esp8266/core.cpp | 9 +-------- esphome/components/esp8266/crash_handler.cpp | 15 +++++---------- esphome/components/esp8266/crash_handler.h | 3 --- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 5db5b064d4..159ec20e77 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -2,9 +2,6 @@ #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" @@ -31,11 +28,7 @@ void arch_restart() { yield(); } } -void arch_init() { -#ifdef USE_ESP8266_CRASH_HANDLER - esp8266::crash_handler_read_and_clear(); -#endif -} +void arch_init() {} 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 index abda1923bb..4651291e0a 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -87,17 +87,12 @@ 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); +static inline bool is_crash_reason(uint32_t reason) { + return reason == REASON_WDT_RST || reason == REASON_EXCEPTION_RST || reason == REASON_SOFT_WDT_RST; } +bool crash_handler_has_data() { return is_crash_reason(resetInfo.reason); } + // Xtensa exception cause names (shared with ESP32, same ISA). // Keep in sync with Xtensa ISA reference manual Table 4-64. // Uses if-else with LOG_STR instead of switch to avoid CSWTCH jump tables @@ -195,7 +190,7 @@ static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) { // 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) + if (!is_crash_reason(resetInfo.reason)) return; // Read and filter backtrace from RTC into stack-local buffer (no persistent RAM cost). diff --git a/esphome/components/esp8266/crash_handler.h b/esphome/components/esp8266/crash_handler.h index def94fbbe9..2d42d07a7e 100644 --- a/esphome/components/esp8266/crash_handler.h +++ b/esphome/components/esp8266/crash_handler.h @@ -8,9 +8,6 @@ namespace esphome::esp8266 { -/// Check if previous boot was a crash and set validity flag. -void crash_handler_read_and_clear(); - /// Log crash data if a crash was detected on previous boot. void crash_handler_log(); From 57c95909d1ea3ec34f77296fcf20e15c568059ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:46:40 -1000 Subject: [PATCH 19/33] Show exception cause for all crash types WDT resets also populate exccause in rst_info (e.g. Level1Interrupt for stack overflow soft WDT). Show the cause string for all crash reasons, not just REASON_EXCEPTION_RST. Also simplify PC logging to a single line for all crash types. --- esphome/components/esp8266/crash_handler.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 4651291e0a..e8f78e5a69 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -200,19 +200,15 @@ void crash_handler_log() { uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE); ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + // Show exception cause for all crash types — WDT resets also populate exccause const LogString *cause = get_exception_cause(resetInfo.exccause); - if (resetInfo.reason == REASON_EXCEPTION_RST && cause != nullptr) { + if (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))); } - if (resetInfo.reason == REASON_EXCEPTION_RST) { - // Log PC and EXCVADDR together — 0x00000000 IS the diagnostic for null pointer crashes - ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " EXCVADDR: 0x%08" PRIX32, resetInfo.epc1, resetInfo.excvaddr); - } else { - ESP_LOGE(TAG, " PC: 0x%08" PRIX32, resetInfo.epc1); - } + ESP_LOGE(TAG, " PC: 0x%08" PRIX32, resetInfo.epc1); for (uint8_t i = 0; i < bt_count; i++) { ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32, i, backtrace[i]); } From 64be4504ae750ac1528f8c7f4278a1894bde2a02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:49:43 -1000 Subject: [PATCH 20/33] Match ESP8266 Arduino core reset reason names --- esphome/components/esp8266/crash_handler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index e8f78e5a69..02bd34d485 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -150,11 +150,11 @@ static const LogString *get_exception_cause(uint32_t cause) { static const LogString *get_reset_reason(uint32_t reason) { if (reason == REASON_WDT_RST) - return LOG_STR("Hardware Watchdog"); + return LOG_STR("Hardware WDT"); if (reason == REASON_EXCEPTION_RST) return LOG_STR("Exception"); if (reason == REASON_SOFT_WDT_RST) - return LOG_STR("Software Watchdog"); + return LOG_STR("Soft WDT"); return LOG_STR("Unknown"); } From ed7bfa3e0f455fe6fee285c91e2d95e932b365df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:51:34 -1000 Subject: [PATCH 21/33] Detect divide-by-zero from ROM ILL instruction pattern GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at ROM addresses 0x4000dce5/0x4000dd3d instead of IntegerDivideByZero (exccause=6). Patch exccause to match the Arduino core's postmortem handler behavior. --- esphome/components/esp8266/crash_handler.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 02bd34d485..f9308ca75c 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -200,11 +200,17 @@ void crash_handler_log() { uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE); ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); - // Show exception cause for all crash types — WDT resets also populate exccause - const LogString *cause = get_exception_cause(resetInfo.exccause); + // GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific + // ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match + // the Arduino core's postmortem handler behavior. + uint32_t exccause = resetInfo.exccause; + if (exccause == 0 && (resetInfo.epc1 == 0x4000dce5 || resetInfo.epc1 == 0x4000dd3d)) { + exccause = 6; // IntegerDivideByZero + } + const LogString *cause = get_exception_cause(exccause); if (cause != nullptr) { ESP_LOGE(TAG, " Reason: %s - %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)), - LOG_STR_ARG(cause), resetInfo.exccause); + LOG_STR_ARG(cause), exccause); } else { ESP_LOGE(TAG, " Reason: %s", LOG_STR_ARG(get_reset_reason(resetInfo.reason))); } From 9e7c990fc639a72d16d90606a62373f47d9146c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:52:01 -1000 Subject: [PATCH 22/33] Use constexpr for ROM divide-by-zero detection --- esphome/components/esp8266/crash_handler.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index f9308ca75c..6456135f2a 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -203,9 +203,14 @@ void crash_handler_log() { // GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific // ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match // the Arduino core's postmortem handler behavior. + static constexpr uint32_t EXCCAUSE_ILLEGAL_INSTRUCTION = 0; + static constexpr uint32_t EXCCAUSE_INTEGER_DIVIDE_BY_ZERO = 6; + static constexpr uint32_t ROM_DIV_ZERO_ADDR_1 = 0x4000dce5; + static constexpr uint32_t ROM_DIV_ZERO_ADDR_2 = 0x4000dd3d; uint32_t exccause = resetInfo.exccause; - if (exccause == 0 && (resetInfo.epc1 == 0x4000dce5 || resetInfo.epc1 == 0x4000dd3d)) { - exccause = 6; // IntegerDivideByZero + if (exccause == EXCCAUSE_ILLEGAL_INSTRUCTION && + (resetInfo.epc1 == ROM_DIV_ZERO_ADDR_1 || resetInfo.epc1 == ROM_DIV_ZERO_ADDR_2)) { + exccause = EXCCAUSE_INTEGER_DIVIDE_BY_ZERO; } const LogString *cause = get_exception_cause(exccause); if (cause != nullptr) { From d512ce24dadf46c550ef5d3e646a1b8d9f6d3956 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:54:42 -1000 Subject: [PATCH 23/33] Share TAG with other esp8266 component files --- esphome/components/esp8266/crash_handler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 6456135f2a..437dc88c54 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -85,7 +85,7 @@ static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC block namespace esphome::esp8266 { -static const char *const TAG = "esp8266.crash"; +static const char *const TAG = "esp8266"; static inline bool is_crash_reason(uint32_t reason) { return reason == REASON_WDT_RST || reason == REASON_EXCEPTION_RST || reason == REASON_SOFT_WDT_RST; From 10b52df32bfd023a4fce68fc23c0cab11d353da2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:57:20 -1000 Subject: [PATCH 24/33] Fix stale comments referencing removed is_return_addr --- esphome/components/esp8266/crash_handler.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 437dc88c54..c91239ce55 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -27,9 +27,7 @@ static constexpr uint32_t IRAM_END = 0x40108000; // 32KB // Linker symbols for the actual firmware IROM section. // Using these instead of a conservative upper bound (0x40400000) prevents -// false positives from stale stack values beyond the actual flash mapping, -// and avoids LoadStoreError faults when is_return_addr() tries to read -// unmapped flash to verify CALL instructions. +// false positives from stale stack values beyond the actual flash mapping. extern "C" { extern uint8_t _irom0_text_start; // NOLINT(bugprone-reserved-identifier,readability-identifier-naming) extern uint8_t _irom0_text_end; // NOLINT(bugprone-reserved-identifier,readability-identifier-naming) @@ -238,10 +236,8 @@ void crash_handler_log() { // --- 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 full stack for return addresses and store -// them in RTC user memory (which survives software reset). Filtering for -// real return addresses (preceded by CALL instructions) happens at log time -// when flash is accessible. +// the device restarts. We scan the full stack for code 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; From 8241e385eb2e7a188d45ddefc8c22114126b2a8e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 12:58:08 -1000 Subject: [PATCH 25/33] Remove redundant IRAM_ATTR from static inline helpers --- esphome/components/esp8266/crash_handler.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index c91239ce55..78b7e176fc 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -34,18 +34,16 @@ extern uint8_t _irom0_text_end; // NOLINT(bugprone-reserved-identifier,readab } // Check if a value looks like a code address in IRAM or flash-mapped IROM. -// Must be IRAM_ATTR since it's called from custom_crash_callback (exception context). -// Using linker symbols (&_irom0_text_start/end) is safe in IRAM — they're link-time -// constants, no flash read needed. This gives precise bounds matching the actual -// firmware, eliminating false positives from addresses beyond the flash mapping. -static inline bool IRAM_ATTR is_code_addr(uint32_t val) { +// Inlined into custom_crash_callback (IRAM_ATTR), so no separate IRAM placement needed. +// Linker symbols are link-time constants — safe to reference from any context. +static inline bool is_code_addr(uint32_t val) { uint32_t addr = (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; return (addr >= IRAM_START && addr < IRAM_END) || (addr >= reinterpret_cast(&_irom0_text_start) && addr < reinterpret_cast(&_irom0_text_end)); } // 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 & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; } +static inline uint32_t recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; } // RTC user memory layout for crash backtrace data. // User-accessible RTC memory: blocks 64-191 (each block = 4 bytes). From 9cdc6f52e04a9894d59e1a481b979787adc05b3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 13:00:14 -1000 Subject: [PATCH 26/33] Skip zero-init of RtcCrashData to save 15 bytes IRAM --- esphome/components/esp8266/crash_handler.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 78b7e176fc..2833e0943d 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -237,7 +237,9 @@ void crash_handler_log() { // the device restarts. We scan the full stack for code 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 = {}; + // No zero-init — only magic, epc1, and backtrace[0..count-1] are read. + // Saves the IRAM cost of a 72-byte zero-init loop. + RtcCrashData data; // NOLINT(cppcoreguidelines-pro-type-member-init) uint8_t count = 0; auto *scan = reinterpret_cast(stack); From 71649f6c1f91fcf32ba5e6a6ee6cfbcb5edf5aea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 13:05:40 -1000 Subject: [PATCH 27/33] Drop impossible exception causes and shorten strings The LX106 (ESP8266) has no MMU, TLB, PIF, or privilege levels, so exception causes 1, 5, 7, 8, 12-18, and 24-26 can never occur. Remove them and shorten remaining cause names. Also drop Syscall, Alloca, PCValue, Privileged which are unreachable on this core. --- esphome/components/esp8266/crash_handler.cpp | 52 +++++--------------- 1 file changed, 12 insertions(+), 40 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 2833e0943d..72ddc68916 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -89,54 +89,26 @@ static inline bool is_crash_reason(uint32_t reason) { bool crash_handler_has_data() { return is_crash_reason(resetInfo.reason); } -// Xtensa exception cause names (shared with ESP32, same ISA). -// Keep in sync with Xtensa ISA reference manual Table 4-64. -// Uses if-else with LOG_STR instead of switch to avoid CSWTCH jump tables -// (placed in RAM rodata on ESP8266). LOG_STR stores strings in flash via -// PSTR; if-else generates comparison branches with no data table. +// Xtensa exception cause names for the LX106 core (ESP8266). +// Only includes causes that can actually occur on the LX106 — it has no MMU, +// no TLB, no PIF, and no privilege levels, so causes 12-18 and 24-26 are +// impossible and omitted. The numeric cause is always logged as fallback. +// Uses if-else with LOG_STR to avoid CSWTCH jump tables (RAM on ESP8266). static const LogString *get_exception_cause(uint32_t cause) { if (cause == 0) - return LOG_STR("IllegalInstruction"); - if (cause == 1) - return LOG_STR("Syscall"); + return LOG_STR("IllegalInst"); if (cause == 2) - return LOG_STR("InstructionFetchError"); + return LOG_STR("InstFetchErr"); if (cause == 3) - return LOG_STR("LoadStoreError"); + return LOG_STR("LoadStoreErr"); if (cause == 4) - return LOG_STR("Level1Interrupt"); - if (cause == 5) - return LOG_STR("Alloca"); + return LOG_STR("Level1Int"); if (cause == 6) - return LOG_STR("IntegerDivideByZero"); - if (cause == 7) - return LOG_STR("PCValue"); - if (cause == 8) - return LOG_STR("Privileged"); + return LOG_STR("DivByZero"); if (cause == 9) - return LOG_STR("LoadStoreAlignment"); - if (cause == 12) - return LOG_STR("InstrPDAddrError"); - if (cause == 13) - return LOG_STR("LoadStorePIFDataError"); - if (cause == 14) - return LOG_STR("InstrPIFAddrError"); - if (cause == 15) - return LOG_STR("LoadStorePIFAddrError"); - if (cause == 16) - return LOG_STR("InstTLBMiss"); - if (cause == 17) - return LOG_STR("InstTLBMultiHit"); - if (cause == 18) - return LOG_STR("InstFetchPrivilege"); + return LOG_STR("Alignment"); if (cause == 20) - return LOG_STR("InstrFetchProhibited"); - if (cause == 24) - return LOG_STR("LoadStoreTLBMiss"); - if (cause == 25) - return LOG_STR("LoadStoreTLBMultihit"); - if (cause == 26) - return LOG_STR("LoadStorePrivilege"); + return LOG_STR("InstFetchProhibited"); if (cause == 28) return LOG_STR("LoadProhibited"); if (cause == 29) From 4630c045bd3ff8d28f329baad4364a2b1c88ba07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 13:06:27 -1000 Subject: [PATCH 28/33] Shorten Prohibited to Prohibit in exception cause names --- esphome/components/esp8266/crash_handler.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 72ddc68916..d74f845380 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -108,11 +108,11 @@ static const LogString *get_exception_cause(uint32_t cause) { if (cause == 9) return LOG_STR("Alignment"); if (cause == 20) - return LOG_STR("InstFetchProhibited"); + return LOG_STR("InstFetchProhibit"); if (cause == 28) - return LOG_STR("LoadProhibited"); + return LOG_STR("LoadProhibit"); if (cause == 29) - return LOG_STR("StoreProhibited"); + return LOG_STR("StoreProhibit"); return nullptr; } From 1ecd45d14d6d3c7993b7f5e29253f42b90fbd14b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 13:18:48 -1000 Subject: [PATCH 29/33] Drop addr2line hint line The ESPHome CLI already decodes addresses inline (shown as WARNING Decoded lines). The hint was redundant and cost ~100 bytes of flash plus 220 bytes of stack for the format buffer. --- esphome/components/esp8266/crash_handler.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index d74f845380..3e35233a65 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -4,7 +4,6 @@ #ifdef USE_ESP8266_CRASH_HANDLER #include "crash_handler.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -191,14 +190,6 @@ void crash_handler_log() { for (uint8_t i = 0; i < bt_count; i++) { ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32, i, backtrace[i]); } - // Build addr2line hint with all captured addresses for easy copy-paste - char hint[220]; - 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 From 061484989639680532754e345111e4d161adc4bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 13:28:29 -1000 Subject: [PATCH 30/33] Fix clang-tidy errors from CI - Match Arduino core's declaration of _irom0_text_start/_end as void functions (mmu_iram.h declares them this way) - Use C-style casts with NOLINT for int-to-ptr conversions in custom_crash_callback (clang-tidy performance-no-int-to-ptr) --- esphome/components/esp8266/crash_handler.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index 3e35233a65..cc9d5c400f 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -27,9 +27,10 @@ static constexpr uint32_t IRAM_END = 0x40108000; // 32KB // Linker symbols for the actual firmware IROM section. // Using these instead of a conservative upper bound (0x40400000) prevents // false positives from stale stack values beyond the actual flash mapping. +// Declared as void functions to match the Arduino core's mmu_iram.h declarations. extern "C" { -extern uint8_t _irom0_text_start; // NOLINT(bugprone-reserved-identifier,readability-identifier-naming) -extern uint8_t _irom0_text_end; // NOLINT(bugprone-reserved-identifier,readability-identifier-naming) +extern void _irom0_text_start(void); // NOLINT(bugprone-reserved-identifier,readability-identifier-naming) +extern void _irom0_text_end(void); // NOLINT(bugprone-reserved-identifier,readability-identifier-naming) } // Check if a value looks like a code address in IRAM or flash-mapped IROM. @@ -37,8 +38,8 @@ extern uint8_t _irom0_text_end; // NOLINT(bugprone-reserved-identifier,readab // Linker symbols are link-time constants — safe to reference from any context. static inline bool is_code_addr(uint32_t val) { uint32_t addr = (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; - return (addr >= IRAM_START && addr < IRAM_END) || (addr >= reinterpret_cast(&_irom0_text_start) && - addr < reinterpret_cast(&_irom0_text_end)); + return (addr >= IRAM_START && addr < IRAM_END) || + (addr >= (uint32_t) _irom0_text_start && addr < (uint32_t) _irom0_text_end); } // Recover the actual code address from a windowed-ABI return address on the stack. @@ -205,8 +206,8 @@ extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint3 RtcCrashData data; // NOLINT(cppcoreguidelines-pro-type-member-init) uint8_t count = 0; - auto *scan = reinterpret_cast(stack); - auto *end = reinterpret_cast(stack_end); + auto *scan = (uint32_t *) stack; // NOLINT(performance-no-int-to-ptr) + auto *end = (uint32_t *) stack_end; // NOLINT(performance-no-int-to-ptr) uint32_t epc1 = rst_info->epc1; for (; scan < end && count < MAX_BACKTRACE; scan++) { From 82d178486b860ea09a4b86a7fd5776bad53b7700 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 13:33:01 -1000 Subject: [PATCH 31/33] Address review feedback - Add IRAM_ATTR to is_code_addr/recover_code_addr as safety net in case the compiler doesn't inline them - Restore EXCVADDR logging for exception resets (faulting address is the key diagnostic for LoadProhibit/StoreProhibit) - Add comment noting Xtensa stack pointer alignment assumption --- esphome/components/esp8266/crash_handler.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp index cc9d5c400f..afc80866ca 100644 --- a/esphome/components/esp8266/crash_handler.cpp +++ b/esphome/components/esp8266/crash_handler.cpp @@ -34,16 +34,16 @@ extern void _irom0_text_end(void); // NOLINT(bugprone-reserved-identifier,rea } // Check if a value looks like a code address in IRAM or flash-mapped IROM. -// Inlined into custom_crash_callback (IRAM_ATTR), so no separate IRAM placement needed. -// Linker symbols are link-time constants — safe to reference from any context. -static inline bool is_code_addr(uint32_t val) { +// IRAM_ATTR as safety net — normally inlined into custom_crash_callback, but +// ensures correctness if the compiler ever chooses not to inline. +static inline bool IRAM_ATTR is_code_addr(uint32_t val) { uint32_t addr = (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; return (addr >= IRAM_START && addr < IRAM_END) || (addr >= (uint32_t) _irom0_text_start && addr < (uint32_t) _irom0_text_end); } // Recover the actual code address from a windowed-ABI return address on the stack. -static inline uint32_t recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; } +static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; } // RTC user memory layout for crash backtrace data. // User-accessible RTC memory: blocks 64-191 (each block = 4 bytes). @@ -188,6 +188,9 @@ void crash_handler_log() { ESP_LOGE(TAG, " Reason: %s", LOG_STR_ARG(get_reset_reason(resetInfo.reason))); } ESP_LOGE(TAG, " PC: 0x%08" PRIX32, resetInfo.epc1); + if (resetInfo.reason == REASON_EXCEPTION_RST) { + ESP_LOGE(TAG, " EXCVADDR: 0x%08" PRIX32, resetInfo.excvaddr); + } for (uint8_t i = 0; i < bt_count; i++) { ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32, i, backtrace[i]); } @@ -206,6 +209,7 @@ extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint3 RtcCrashData data; // NOLINT(cppcoreguidelines-pro-type-member-init) uint8_t count = 0; + // Stack pointer from the Xtensa exception frame is always 4-byte aligned. auto *scan = (uint32_t *) stack; // NOLINT(performance-no-int-to-ptr) auto *end = (uint32_t *) stack_end; // NOLINT(performance-no-int-to-ptr) uint32_t epc1 = rst_info->epc1; From 42f9b4225f0beb1f64c45af8c264136ed3075d93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 13:33:27 -1000 Subject: [PATCH 32/33] Add RTC memory overlap comment in preferences.cpp --- esphome/components/esp8266/preferences.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 906fed2b29..79d0aa25c2 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -22,6 +22,9 @@ static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WO // RTC memory layout for preferences: // - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 96-127) // - Normal region: RTC words 32-127 (mapped from preference offset 0-95) +// Note: The crash handler (crash_handler.cpp) uses RTC blocks 174-191 (words 110-127) +// for stack backtrace data written only during crashes. Preferences fill from the start +// so this only conflicts if 110+ words of RTC preferences are allocated. static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 96; // Words 32-127 for normal prefs static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 128 From 2657bdc97a64246b270cb8ff46cc92202996cf50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 13:35:01 -1000 Subject: [PATCH 33/33] Reserve crash handler RTC region from preferences Reduce preferences normal region from 96 to 78 words (words 32-109) to formally reserve words 110-127 for the crash handler backtrace. Preferences that don't fit in RTC fall back to flash storage, so this has no functional impact for typical configurations. --- esphome/components/esp8266/preferences.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 79d0aa25c2..f444f03555 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -19,15 +19,13 @@ static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; -// RTC memory layout for preferences: -// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 96-127) -// - Normal region: RTC words 32-127 (mapped from preference offset 0-95) -// Note: The crash handler (crash_handler.cpp) uses RTC blocks 174-191 (words 110-127) -// for stack backtrace data written only during crashes. Preferences fill from the start -// so this only conflicts if 110+ words of RTC preferences are allocated. +// RTC memory layout: +// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 78-109) +// - Normal region: RTC words 32-109 (mapped from preference offset 0-77) +// - Crash handler: RTC words 110-127 (reserved for crash_handler.cpp backtrace data) static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot -static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 96; // Words 32-127 for normal prefs -static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 128 +static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 78; // Words 32-109 for normal prefs +static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 110 // Maximum preference size in words (limited by uint8_t length_words field) static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;