|
|
|
@@ -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
|