[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.
This commit is contained in:
J. Nick Koston
2026-04-05 11:50:35 -10:00
parent c7a163441e
commit 50073a24b5
7 changed files with 284 additions and 1 deletions
+6
View File
@@ -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
+1
View File
@@ -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):
+8 -1
View File
@@ -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) {
@@ -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 <cinttypes>
extern "C" {
#include <user_interface.h>
// 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<uint32_t *>(stack);
auto *end = reinterpret_cast<uint32_t *>(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
@@ -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
@@ -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_() {
+1
View File
@@ -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