ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
crash_handler.cpp
Go to the documentation of this file.
1#ifdef USE_ESP8266
2
4#ifdef USE_ESP8266_CRASH_HANDLER
5
6#include "crash_handler.h"
7#include "rtc_layout.h"
8#include "esphome/core/log.h"
9
10#include <cinttypes>
11
12extern "C" {
13#include <user_interface.h>
14
15// Global reset info struct populated by SDK/Arduino core at boot
16extern struct rst_info resetInfo;
17}
18
19// Xtensa windowed-ABI: bits[31:30] encode call type (CALL0=00, CALL4=01,
20// CALL8=10, CALL12=11). Mask and force bit 30 to recover the real address.
21static constexpr uint32_t XTENSA_ADDR_MASK = 0x3FFFFFFF;
22static constexpr uint32_t XTENSA_CODE_BASE = 0x40000000;
23
24// ESP8266 memory map boundaries for code regions
25static constexpr uint32_t IRAM_START = 0x40100000;
26static constexpr uint32_t IRAM_END = 0x40108000; // 32KB
27
28// Linker symbols for the actual firmware IROM section.
29// Using these instead of a conservative upper bound (0x40400000) prevents
30// false positives from stale stack values beyond the actual flash mapping.
31extern "C" {
32// NOLINTBEGIN(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration)
33extern void _irom0_text_start(void);
34extern void _irom0_text_end(void);
35// NOLINTEND(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration)
36}
37
38// Check if a value looks like a code address in IRAM or flash-mapped IROM.
39// IRAM_ATTR as safety net — normally inlined into custom_crash_callback, but
40// ensures correctness if the compiler ever chooses not to inline.
41static inline bool IRAM_ATTR is_code_addr(uint32_t val) {
42 uint32_t addr = (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE;
43 return (addr >= IRAM_START && addr < IRAM_END) ||
44 (addr >= (uint32_t) _irom0_text_start && addr < (uint32_t) _irom0_text_end);
45}
46
47// Recover the actual code address from a windowed-ABI return address on the stack.
48static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; }
49
50// Crash data lives in the last esphome::esp8266::RTC_CRASH_WORDS of user RTC memory (rtc_layout.h).
51// Store 16 raw candidates, filter to real return addresses at log time.
52static constexpr size_t MAX_BACKTRACE = 16;
53
54// Magic word packs sentinel, version, and count into one uint32_t:
55// bits[31:16] = sentinel
56// bits[15:8] = version
57// bits[7:0] = backtrace count
58static constexpr uint8_t CRASH_SENTINEL_BITS = 16;
59static constexpr uint8_t CRASH_VERSION_BITS = 8;
60
61static constexpr uint16_t CRASH_SENTINEL_VALUE = 0xDEAD;
62static constexpr uint8_t CRASH_VERSION_VALUE = 1;
63
64static constexpr uint32_t CRASH_SENTINEL = static_cast<uint32_t>(CRASH_SENTINEL_VALUE) << CRASH_SENTINEL_BITS;
65static constexpr uint32_t CRASH_VERSION = static_cast<uint32_t>(CRASH_VERSION_VALUE) << CRASH_VERSION_BITS;
66static constexpr uint32_t CRASH_SENTINEL_MASK = static_cast<uint32_t>(0xFFFF) << CRASH_SENTINEL_BITS;
67static constexpr uint32_t CRASH_VERSION_MASK = static_cast<uint32_t>(0xFF) << CRASH_VERSION_BITS;
68static constexpr uint32_t CRASH_COUNT_MASK = 0xFF;
69
70// Struct layout: 18 RTC blocks (72 bytes):
71// [0] = magic (sentinel | version | count)
72// [1..16] = up to 16 code addresses from stack scanning
73// [17] = epc1 at crash time (to skip duplicates at log time)
74struct RtcCrashData {
75 uint32_t magic;
76 uint32_t backtrace[MAX_BACKTRACE];
77 uint32_t epc1; // Fault PC, used to filter duplicates
78};
79static_assert(sizeof(RtcCrashData) == esphome::esp8266::RTC_CRASH_WORDS * 4, "RtcCrashData must fill its RTC words");
80
81namespace esphome::esp8266 {
82
83ESPHOME_LOG_TAG(TAG, "esp8266");
84
85static inline bool is_crash_reason(uint32_t reason) {
86 return reason == REASON_WDT_RST || reason == REASON_EXCEPTION_RST || reason == REASON_SOFT_WDT_RST;
87}
88
89bool crash_handler_has_data() { return is_crash_reason(resetInfo.reason); }
90
91// Xtensa exception cause names for the LX106 core (ESP8266).
92// Only includes causes that can actually occur on the LX106 — it has no MMU,
93// no TLB, no PIF, and no privilege levels, so causes 12-18 and 24-26 are
94// impossible and omitted. The numeric cause is always logged as fallback.
95// Uses if-else with LOG_STR to avoid CSWTCH jump tables (RAM on ESP8266).
96static const LogString *get_exception_cause(uint32_t cause) {
97 if (cause == 0)
98 return LOG_STR("IllegalInst");
99 if (cause == 2)
100 return LOG_STR("InstFetchErr");
101 if (cause == 3)
102 return LOG_STR("LoadStoreErr");
103 if (cause == 4)
104 return LOG_STR("Level1Int");
105 if (cause == 6)
106 return LOG_STR("DivByZero");
107 if (cause == 9)
108 return LOG_STR("Alignment");
109 if (cause == 20)
110 return LOG_STR("InstFetchProhibit");
111 if (cause == 28)
112 return LOG_STR("LoadProhibit");
113 if (cause == 29)
114 return LOG_STR("StoreProhibit");
115 return nullptr;
116}
117
118static const LogString *get_reset_reason(uint32_t reason) {
119 if (reason == REASON_EXCEPTION_RST)
120 return LOG_STR("Exception");
121 if (reason == REASON_SOFT_WDT_RST)
122 return LOG_STR("Soft WDT");
123 return LOG_STR("Unknown");
124}
125
126// Read backtrace from RTC user memory into caller-provided buffer.
127// Returns the number of valid backtrace entries (0 if no data found).
128static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) {
129 RtcCrashData rtc_data;
130 if (!system_rtc_mem_read(esphome::esp8266::RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
131 return 0;
132 uint32_t magic = rtc_data.magic;
133 if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION)
134 return 0;
135 uint8_t raw_count = magic & CRASH_COUNT_MASK;
136 if (raw_count > MAX_BACKTRACE)
137 raw_count = MAX_BACKTRACE;
138 // Skip any that match epc1 (already reported as the fault PC).
139 // Note: we cannot verify CALL instructions at addr-3 on ESP8266 because
140 // reading from IROM causes LoadStoreError due to flash cache conflicts
141 // (the reading code and target can share a direct-mapped cache line).
142 // The linker-symbol IROM bounds already eliminate most false positives.
143 uint8_t out = 0;
144 for (uint8_t i = 0; i < raw_count && out < max_entries; i++) {
145 uint32_t addr = rtc_data.backtrace[i];
146 if (addr != rtc_data.epc1)
147 backtrace[out++] = addr;
148 }
149 return out;
150}
151
152// Intentionally uses separate ESP_LOGE calls per line instead of combining into
153// one multi-line log message. This ensures each address appears as its own line
154// on the serial console, making it possible to see partial output if the device
155// crashes again during boot, and allowing the CLI's process_stacktrace to match
156// and decode each address individually.
158 if (!is_crash_reason(resetInfo.reason))
159 return;
160
161 ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
162 if (resetInfo.reason == REASON_WDT_RST) {
163 // A hardware WDT reset happens entirely in hardware: the postmortem hook
164 // never runs, so rst_info epc1/exccause and the RTC backtrace are
165 // leftovers from an earlier crash. Don't misattribute them (#18596).
166 ESP_LOGE(TAG, " Reason: Hardware WDT (no crash state is recorded for hardware WDT resets)");
167 return;
168 }
169
170 // Read and filter backtrace from RTC into stack-local buffer (no persistent RAM cost).
171 // Both resetInfo and RTC data survive until the next reset, so this can be
172 // called multiple times (logger init + API subscribe) with the same result.
173 uint32_t backtrace[MAX_BACKTRACE];
174 uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE);
175 // GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific
176 // ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match
177 // the Arduino core's postmortem handler behavior.
178 static constexpr uint32_t EXCCAUSE_ILLEGAL_INSTRUCTION = 0;
179 static constexpr uint32_t EXCCAUSE_INTEGER_DIVIDE_BY_ZERO = 6;
180 static constexpr uint32_t ROM_DIV_ZERO_ADDR_1 = 0x4000dce5;
181 static constexpr uint32_t ROM_DIV_ZERO_ADDR_2 = 0x4000dd3d;
182 uint32_t exccause = resetInfo.exccause;
183 if (exccause == EXCCAUSE_ILLEGAL_INSTRUCTION &&
184 (resetInfo.epc1 == ROM_DIV_ZERO_ADDR_1 || resetInfo.epc1 == ROM_DIV_ZERO_ADDR_2)) {
185 exccause = EXCCAUSE_INTEGER_DIVIDE_BY_ZERO;
186 }
187 const LogString *cause = get_exception_cause(exccause);
188 if (cause != nullptr) {
189 ESP_LOGE(TAG, " Reason: %s - %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)),
190 LOG_STR_ARG(cause), exccause);
191 } else {
192 ESP_LOGE(TAG, " Reason: %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)), exccause);
193 }
194 ESP_LOGE(TAG, " PC: 0x%08" PRIX32, resetInfo.epc1);
195 if (resetInfo.reason == REASON_EXCEPTION_RST) {
196 ESP_LOGE(TAG, " EXCVADDR: 0x%08" PRIX32, resetInfo.excvaddr);
197 }
198 for (uint8_t i = 0; i < bt_count; i++) {
199 ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32, i, backtrace[i]);
200 }
201}
202
203} // namespace esphome::esp8266
204
205// --- Custom crash callback ---
206// Overrides the weak custom_crash_callback() from Arduino core's
207// core_esp8266_postmortem.cpp. Called during exception handling before
208// the device restarts. We scan the full stack for code addresses and store
209// them in RTC user memory (which survives software reset).
210extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint32_t stack, uint32_t stack_end) {
211 // No zero-init — only magic, epc1, and backtrace[0..count-1] are read.
212 // Saves the IRAM cost of a 72-byte zero-init loop.
213 RtcCrashData data; // NOLINT(cppcoreguidelines-pro-type-member-init)
214 uint8_t count = 0;
215
216 // Stack pointer from the Xtensa exception frame is always 4-byte aligned.
217 auto *scan = (uint32_t *) stack; // NOLINT(performance-no-int-to-ptr)
218 auto *end = (uint32_t *) stack_end; // NOLINT(performance-no-int-to-ptr)
219 uint32_t epc1 = rst_info->epc1;
220
221 for (; scan < end && count < MAX_BACKTRACE; scan++) {
222 uint32_t val = *scan;
223 if (is_code_addr(val)) {
224 uint32_t addr = recover_code_addr(val);
225 // Skip epc1 — already reported as the fault PC
226 if (addr != epc1)
227 data.backtrace[count++] = addr;
228 }
229 }
230
231 data.epc1 = epc1;
232 data.magic = CRASH_SENTINEL | CRASH_VERSION | count;
233
234 system_rtc_mem_write(esphome::esp8266::RTC_CRASH_BASE, &data, sizeof(data));
235}
236
237#endif // USE_ESP8266_CRASH_HANDLER
238#endif // USE_ESP8266
struct rst_info resetInfo
void _irom0_text_end(void)
struct rst_info resetInfo
void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint32_t stack, uint32_t stack_end)
void _irom0_text_start(void)
std::vector< uint8_t > & out
Definition gif.cpp:49
unsigned count
Definition gif.cpp:51
mopeka_std_values val[3]
bool crash_handler_has_data()
Returns true if the previous boot was a crash (exception, WDT, or soft WDT).
void crash_handler_log()
Log crash data if a crash was detected on previous boot.
ESPHOME_LOG_TAG(TAG, "esp8266")
static void uint32_t
uint32_t bt_count
uint8_t end[39]
Definition sun_gtil2.cpp:17