ESPHome 2026.9.1
Loading...
Searching...
No Matches
crash_handler.cpp
Go to the documentation of this file.
1#ifdef USE_RP2
2
4#ifdef USE_RP2_CRASH_HANDLER
5
6#include "crash_handler.h"
7#include "esphome/core/log.h"
8
9#include <cinttypes>
10#include <hardware/regs/addressmap.h>
11#include <hardware/structs/watchdog.h>
12#include <hardware/watchdog.h>
13
14// Cortex-M0+ exception frame offsets (words)
15// When a fault occurs, the CPU pushes: R0, R1, R2, R3, R12, LR, PC, xPSR
16static constexpr uint32_t EF_LR = 5;
17static constexpr uint32_t EF_PC = 6;
18
19// Version encoded in the magic value: upper 16 bits are sentinel (0xDEAD),
20// lower 16 bits are the version number. This avoids using a separate scratch
21// register for versioning (we only have 8 total). Future firmware reads the
22// sentinel to confirm it's crash data, then the version to know the layout.
23static constexpr uint32_t CRASH_MAGIC_SENTINEL = 0xDEAD0000;
24static constexpr uint32_t CRASH_DATA_VERSION = 1;
25static constexpr uint32_t CRASH_MAGIC_V1 = CRASH_MAGIC_SENTINEL | CRASH_DATA_VERSION;
26
27// We only have 8 scratch registers (32 bytes) that survive watchdog reboot.
28// Use them for the most important data, then scan the stack for code addresses.
29//
30// Scratch register layout:
31// [0] = versioned magic (upper 16 bits = 0xDEAD sentinel, lower 16 bits = version)
32// [1] = PC (program counter at fault)
33// [2] = LR (link register from exception frame)
34// [3] = SP (stack pointer at fault)
35// [4..7] = up to 4 additional code addresses found by scanning the stack
36// (return addresses from callers, giving a deeper backtrace)
37
38// Flash is mapped at XIP_BASE (0x10000000). We use a conservative upper bound
39// to keep false positives low during stack scanning. Wider ranges would match
40// more stale data on the stack that happens to look like code addresses.
41#if defined(PICO_RP2350)
42static constexpr uint32_t FLASH_SCAN_END = XIP_BASE + 0x400000; // 4MB — RP2350 typical max
43#else
44static constexpr uint32_t FLASH_SCAN_END = XIP_BASE + 0x200000; // 2MB — RP2040 typical max
45#endif
46
47static inline bool is_code_addr(uint32_t val) {
48 uint32_t cleared = val & ~1u; // Clear Thumb bit
49 return cleared >= XIP_BASE && cleared < FLASH_SCAN_END;
50}
51
52static constexpr size_t MAX_BACKTRACE = 4;
53
54namespace esphome::rp2 {
55
56static const char *const TAG = "rp2.crash";
57
58// Filled from the watchdog scratch registers on the first read.
59static struct CrashData {
60 bool valid;
61 uint32_t pc;
62 uint32_t lr;
63 uint32_t sp;
64 uint32_t backtrace[MAX_BACKTRACE];
65 uint8_t backtrace_count;
66} s_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
67
68// Logger::pre_setup() logs the record before App.pre_setup() reaches
69// arch_init(), so the first caller reads it and later calls are no-ops.
70// The read clears the scratch registers, so it must not run twice, and
71// arch_init() keeps its call so the read precedes watchdog_enable(), which
72// overwrites scratch[4].
73static bool s_crash_data_read = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
74
77 return s_crash_data.valid;
78}
79
81 if (s_crash_data_read)
82 return;
83 s_crash_data_read = true;
84 s_crash_data.valid = false;
85 uint32_t magic = watchdog_hw->scratch[0];
86 if ((magic & 0xFFFF0000) == CRASH_MAGIC_SENTINEL && (magic & 0xFFFF) == CRASH_DATA_VERSION) {
87 s_crash_data.valid = true;
88 s_crash_data.pc = watchdog_hw->scratch[1];
89 s_crash_data.lr = watchdog_hw->scratch[2];
90 s_crash_data.sp = watchdog_hw->scratch[3];
91 s_crash_data.backtrace_count = 0;
92 for (size_t i = 0; i < MAX_BACKTRACE; i++) {
93 uint32_t addr = watchdog_hw->scratch[4 + i];
94 if (addr == 0)
95 break;
96 s_crash_data.backtrace[i] = addr;
97 s_crash_data.backtrace_count++;
98 }
99 }
100 // Clear scratch registers regardless
101 for (int i = 0; i < 8; i++) {
102 watchdog_hw->scratch[i] = 0;
103 }
104}
105
106// Intentionally uses separate ESP_LOGE calls per line instead of combining into
107// one multi-line log message. This ensures each address appears as its own line
108// on the serial console (miniterm), making it possible to see partial output if
109// the device crashes again during boot, and allowing the CLI's process_stacktrace
110// to match and decode each address individually.
113 return;
114
115 ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
116 ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_crash_data.pc);
117 ESP_LOGE(TAG, " LR: 0x%08" PRIX32 " (return address)", s_crash_data.lr);
118 ESP_LOGE(TAG, " SP: 0x%08" PRIX32, s_crash_data.sp);
119 for (uint8_t i = 0; i < s_crash_data.backtrace_count; i++) {
120 ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (stack backtrace)", i, s_crash_data.backtrace[i]);
121 }
122 // Build addr2line hint with all captured addresses for easy copy-paste
123 char hint[160];
124 int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32 " 0x%08" PRIX32,
125 s_crash_data.pc, s_crash_data.lr);
126 for (uint8_t i = 0; i < s_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) {
127 pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, s_crash_data.backtrace[i]);
128 }
129 ESP_LOGE(TAG, "%s", hint);
130}
131
132} // namespace esphome::rp2
133
134// --- HardFault handler ---
135// Overrides the weak isr_hardfault from arduino-pico's crt0.S.
136// On Cortex-M0+, the CPU pushes {R0,R1,R2,R3,R12,LR,PC,xPSR} onto the
137// active stack (MSP or PSP). We determine which stack was active,
138// extract key registers, store them in watchdog scratch registers
139// (which survive watchdog reboot), then trigger a reboot.
140
141// Check if a pointer falls within SRAM (valid for stack access).
142// SRAM_BASE and SRAM_END are chip-specific SDK defines:
143// RP2040: 0x20000000 - 0x20042000 (264KB)
144// RP2350: 0x20000000 - 0x20082000 (520KB)
145static inline bool is_valid_sram_ptr(const uint32_t *ptr) {
146 auto addr = reinterpret_cast<uintptr_t>(ptr);
147 // Exception frame is 8 words (32 bytes), so frame+7 must also be in SRAM.
148 // Check alignment (must be word-aligned) and that the full frame fits.
149 return (addr % 4 == 0) && addr >= SRAM_BASE && (addr + 32) <= SRAM_END;
150}
151
152// C handler called from the asm wrapper with the exception frame pointer.
153static void __attribute__((used, noreturn)) hard_fault_handler_c(uint32_t *frame, uint32_t /*exc_return*/) {
154 // watchdog_reboot() overwrites scratch[4]-[7], so we must call it first
155 // then write ALL our data after. The 10ms timeout gives us plenty of time.
156 watchdog_reboot(0, 0, 10);
157
158 // Validate frame pointer before dereferencing. If the HardFault was caused
159 // by a stacking error or corrupted SP, frame may be invalid. Write a minimal
160 // crash marker so we at least know a crash occurred.
161 if (!is_valid_sram_ptr(frame)) {
162 watchdog_hw->scratch[0] = CRASH_MAGIC_V1;
163 watchdog_hw->scratch[1] = 0; // PC unknown
164 watchdog_hw->scratch[2] = 0; // LR unknown
165 watchdog_hw->scratch[3] = reinterpret_cast<uintptr_t>(frame); // Record the bad SP for diagnosis
166 for (uint32_t i = 0; i < MAX_BACKTRACE; i++) {
167 watchdog_hw->scratch[4 + i] = 0;
168 }
169 while (true) {
170 __asm volatile("nop");
171 }
172 }
173
174 // Pre-fault SP: the exception frame is 8 words pushed onto the stack,
175 // so the SP before the fault was frame + 8 words. If xPSR bit 9 is set,
176 // the hardware pushed an extra alignment word to maintain 8-byte stack
177 // alignment (ARMv6-M/ARMv7-M spec), so add 1 more word.
178 static constexpr uint32_t EF_XPSR = 7;
179 uint32_t extra_align = (frame[EF_XPSR] & (1u << 9)) ? 1 : 0;
181 uint32_t pre_fault_sp = reinterpret_cast<uintptr_t>(post_frame);
182
183 // Write key registers
184 watchdog_hw->scratch[0] = CRASH_MAGIC_V1;
185 watchdog_hw->scratch[1] = frame[EF_PC];
186 watchdog_hw->scratch[2] = frame[EF_LR];
187 watchdog_hw->scratch[3] = pre_fault_sp;
188
189 // Scan stack for code addresses to build a deeper backtrace.
190 // The exception frame is 8 words (32 bytes) at 'frame', plus an optional
191 // alignment word. Walk up to 64 words looking for return addresses.
193 // SRAM_END is chip-specific: 0x20042000 (RP2040) or 0x20082000 (RP2350)
194 uint32_t *stack_top = reinterpret_cast<uint32_t *>(SRAM_END);
195 // Scan up to 64 words (256 bytes) — covers typical nested call frames
196 // without scanning too much stale stack data that could produce false positives.
198
199 for (uint32_t *p = scan_start; p < stack_top && p < scan_start + 64 && bt_count < MAX_BACKTRACE; p++) {
200 uint32_t val = *p;
201 // Check if this looks like a code address in flash
202 // Skip if it's the same as PC or LR we already saved
203 if (is_code_addr(val) && val != frame[EF_PC] && val != frame[EF_LR]) {
204 watchdog_hw->scratch[4 + bt_count] = val;
205 bt_count++;
206 }
207 }
208 // Zero remaining slots
209 for (uint32_t i = bt_count; i < MAX_BACKTRACE; i++) {
210 watchdog_hw->scratch[4 + i] = 0;
211 }
212
213 while (true) {
214 __asm volatile("nop");
215 }
216}
217
218// Naked asm wrapper - Cortex-M0+ compatible (no ITE/conditional execution).
219// Determines active stack pointer and branches to C handler.
220// Uses literal pool (.word) for addresses since M0+ has limited immediate encoding.
221//
222// Based on the standard Cortex-M0+ HardFault handler pattern described in:
223// - ARM Application Note AN209: "Using Cortex-M3/M4/M7 Fault Exceptions"
224// (adapted for M0+ which lacks conditional execution instructions)
225// - Memfault: "How to debug a HardFault on an ARM Cortex-M MCU"
226// https://interrupt.memfault.com/blog/cortex-m-hardfault-debug
227// - Raspberry Pi Forums: "Cortex-M0+ Hard Fault handler porting"
228// https://www.eevblog.com/forum/microcontrollers/cortex-m0-hard-fault-handler-porting/
229//
230// The key M0+ adaptation: replaces ITE/MRSEQ/MRSNE (Cortex-M3+) with
231// MOVS+TST+BEQ branch sequence, and uses a literal pool for the C handler address.
232extern "C" void __attribute__((naked, used)) isr_hardfault() {
233 __asm volatile("movs r0, #4 \n" // Prepare bit 2 mask
234 "mov r1, lr \n" // r1 = EXC_RETURN
235 "tst r1, r0 \n" // Test bit 2
236 "beq 1f \n" // If 0, was using MSP
237 "mrs r0, psp \n" // Bit 2 set = PSP was active
238 "b 2f \n"
239 "1: \n"
240 "mrs r0, msp \n" // Bit 2 clear = MSP was active
241 "2: \n"
242 // r0 = exception frame pointer, r1 = EXC_RETURN (still in r1)
243 "ldr r2, 3f \n" // Load C handler address from literal pool
244 "bx r2 \n" // Branch to handler (r0=frame, r1=exc_return)
245 ".align 2 \n"
246 "3: .word %c0 \n" // Literal pool: address of C handler
247 :
248 : "i"(hard_fault_handler_c));
249}
250
251#endif // USE_RP2_CRASH_HANDLER
252#endif // USE_RP2
struct @66::@67 __attribute__
Wake the main loop task from an ISR. ISR-safe.
Definition main_task.h:32
mopeka_std_values val[3]
bool crash_handler_has_data()
Returns true if crash data was found this boot, reading it first if needed.
void crash_handler_read_and_clear()
Read crash data from watchdog scratch registers and clear them.
void crash_handler_log()
Log crash data if a crash was detected on previous boot.
size_t size_t pos
Definition helpers.h:1123
uint32_t * scan_start
static void uint32_t
uint32_t * stack_top
uint32_t bt_count
uint32_t extra_align
uint32_t pre_fault_sp
uint32_t * post_frame