mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 17:05:36 +00:00
Merge branch 'rp2040-crash-handler' into integration
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#ifdef USE_RP2040
|
||||
#include "logger.h"
|
||||
#include "esphome/components/rp2040/crash_handler.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
@@ -25,6 +26,7 @@ void Logger::pre_setup() {
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
rp2040::crash_handler_log();
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg, uint16_t len) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from string import ascii_letters, digits
|
||||
import subprocess
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -269,3 +271,48 @@ def copy_files():
|
||||
path = CORE.relative_src_path("esphome.h")
|
||||
content = read_file(path).rstrip("\n")
|
||||
write_file_if_changed(path, content + '\n#include "pio_includes.h"\n')
|
||||
|
||||
|
||||
# RP2040 crash handler stacktrace decoding
|
||||
# Matches output from esphome/components/rp2040/crash_handler.cpp
|
||||
_CRASH_RE = re.compile(r"CRASH DETECTED ON PREVIOUS BOOT")
|
||||
_CRASH_ADDR_RE = re.compile(
|
||||
r"(?:PC|LR|BT\d):\s+(0x[0-9a-fA-F]{8})\s+\((?:fault location|return address|stack backtrace)\)"
|
||||
)
|
||||
|
||||
|
||||
def _addr2line(tool: str, elf: Path, addr: str) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[tool, "-pfiaC", "-e", str(elf), addr],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return f"{addr} (decode failed)"
|
||||
|
||||
|
||||
def process_stacktrace(config, line: str, backtrace_state: bool) -> bool:
|
||||
"""Decode RP2040 crash handler output using addr2line."""
|
||||
if _CRASH_RE.search(line):
|
||||
_LOGGER.error("RP2040 crash detected - decoding addresses")
|
||||
return True
|
||||
|
||||
if backtrace_state:
|
||||
if match := _CRASH_ADDR_RE.search(line):
|
||||
from esphome.platformio_api import get_idedata
|
||||
|
||||
idedata = get_idedata(config)
|
||||
if idedata.addr2line_path:
|
||||
elf = CORE.relative_pioenvs_path(CORE.name, "firmware.elf")
|
||||
if elf.exists():
|
||||
decoded = _addr2line(idedata.addr2line_path, elf, match.group(1))
|
||||
_LOGGER.error(" %s => %s", match.group(1), decoded)
|
||||
|
||||
# Stop backtrace state after addr2line hint (last line of crash dump)
|
||||
if "addr2line" in line:
|
||||
return False
|
||||
|
||||
return backtrace_state
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_RP2040
|
||||
|
||||
#include "core.h"
|
||||
#include "crash_handler.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -24,6 +25,7 @@ void arch_restart() {
|
||||
}
|
||||
|
||||
void arch_init() {
|
||||
rp2040::crash_handler_read_and_clear();
|
||||
#if USE_RP2040_WATCHDOG_TIMEOUT > 0
|
||||
watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false);
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
#ifdef USE_RP2040
|
||||
|
||||
#include "crash_handler.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <hardware/structs/watchdog.h>
|
||||
#include <hardware/watchdog.h>
|
||||
|
||||
// Cortex-M0+ exception frame offsets (words)
|
||||
// When a fault occurs, the CPU pushes: R0, R1, R2, R3, R12, LR, PC, xPSR
|
||||
#define EF_LR 5
|
||||
#define EF_PC 6
|
||||
|
||||
static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF;
|
||||
|
||||
// We only have 8 scratch registers (32 bytes) that survive watchdog reboot.
|
||||
// Use them for the most important data, then scan the stack for code addresses.
|
||||
//
|
||||
// Scratch register layout:
|
||||
// [0] = magic (CRASH_MAGIC)
|
||||
// [1] = PC (program counter at fault)
|
||||
// [2] = LR (link register from exception frame)
|
||||
// [3] = SP (stack pointer at fault)
|
||||
// [4..7] = up to 4 additional code addresses found by scanning the stack
|
||||
// (return addresses from callers, giving a deeper backtrace)
|
||||
|
||||
// RP2040 flash is mapped at 0x10000000, code lives in first 1MB typically
|
||||
static inline bool is_code_addr(uint32_t val) {
|
||||
// Thumb addresses have bit 0 set, but addr2line wants them without it.
|
||||
// Accept anything in flash range 0x10000000-0x10100000 (1MB)
|
||||
uint32_t cleared = val & ~1u;
|
||||
return cleared >= 0x10000000 && cleared < 0x10100000;
|
||||
}
|
||||
|
||||
static constexpr size_t MAX_BACKTRACE = 4;
|
||||
|
||||
namespace esphome::rp2040 {
|
||||
|
||||
static const char *const TAG = "rp2040.crash";
|
||||
|
||||
static struct {
|
||||
bool valid{false};
|
||||
uint32_t pc;
|
||||
uint32_t lr;
|
||||
uint32_t sp;
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
uint8_t backtrace_count;
|
||||
} s_crash_data;
|
||||
|
||||
void crash_handler_read_and_clear() {
|
||||
if (watchdog_hw->scratch[0] == CRASH_MAGIC) {
|
||||
s_crash_data.valid = true;
|
||||
s_crash_data.pc = watchdog_hw->scratch[1];
|
||||
s_crash_data.lr = watchdog_hw->scratch[2];
|
||||
s_crash_data.sp = watchdog_hw->scratch[3];
|
||||
s_crash_data.backtrace_count = 0;
|
||||
for (size_t i = 0; i < MAX_BACKTRACE; i++) {
|
||||
uint32_t addr = watchdog_hw->scratch[4 + i];
|
||||
if (addr == 0)
|
||||
break;
|
||||
s_crash_data.backtrace[i] = addr;
|
||||
s_crash_data.backtrace_count++;
|
||||
}
|
||||
}
|
||||
// Clear scratch registers regardless
|
||||
for (int i = 0; i < 8; i++) {
|
||||
watchdog_hw->scratch[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void crash_handler_log() {
|
||||
if (!s_crash_data.valid)
|
||||
return;
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
ESP_LOGE(TAG, " PC: 0x%08X (fault location)", s_crash_data.pc);
|
||||
ESP_LOGE(TAG, " LR: 0x%08X (return address)", s_crash_data.lr);
|
||||
ESP_LOGE(TAG, " SP: 0x%08X", s_crash_data.sp);
|
||||
for (uint8_t i = 0; i < s_crash_data.backtrace_count; i++) {
|
||||
ESP_LOGE(TAG, " BT%d: 0x%08X (stack backtrace)", i, s_crash_data.backtrace[i]);
|
||||
}
|
||||
ESP_LOGE(TAG, "Use addr2line -e firmware.elf 0x%08X 0x%08X to decode", s_crash_data.pc, s_crash_data.lr);
|
||||
}
|
||||
|
||||
} // namespace esphome::rp2040
|
||||
|
||||
// --- HardFault handler ---
|
||||
// Overrides the weak isr_hardfault from arduino-pico's crt0.S.
|
||||
// On Cortex-M0+, the CPU pushes {R0,R1,R2,R3,R12,LR,PC,xPSR} onto the
|
||||
// active stack (MSP or PSP). We determine which stack was active,
|
||||
// extract key registers, store them in watchdog scratch registers
|
||||
// (which survive watchdog reboot), then trigger a reboot.
|
||||
|
||||
// C handler called from the asm wrapper with the exception frame pointer.
|
||||
static void __attribute__((used)) hard_fault_handler_c(uint32_t *frame, uint32_t exc_return) {
|
||||
// watchdog_reboot() overwrites scratch[4]-[7], so we must call it first
|
||||
// then write ALL our data after. The 10ms timeout gives us plenty of time.
|
||||
watchdog_reboot(0, 0, 10);
|
||||
|
||||
// Write key registers
|
||||
watchdog_hw->scratch[0] = CRASH_MAGIC;
|
||||
watchdog_hw->scratch[1] = frame[EF_PC];
|
||||
watchdog_hw->scratch[2] = frame[EF_LR];
|
||||
watchdog_hw->scratch[3] = (uint32_t) frame; // SP at fault
|
||||
|
||||
// Scan stack for code addresses to build a deeper backtrace.
|
||||
// The exception frame is 8 words (32 bytes) at 'frame'. The pre-fault
|
||||
// stack starts at frame+8. Walk up to 64 words looking for return addresses.
|
||||
uint32_t *scan_start = frame + 8; // Past exception frame
|
||||
// RP2040 RAM ends at 0x20042000 (264KB SRAM)
|
||||
uint32_t *stack_top = (uint32_t *) 0x20042000;
|
||||
uint32_t bt_count = 0;
|
||||
|
||||
for (uint32_t *p = scan_start; p < stack_top && p < scan_start + 64 && bt_count < MAX_BACKTRACE; p++) {
|
||||
uint32_t val = *p;
|
||||
// Check if this looks like a code address in flash
|
||||
// Skip if it's the same as PC or LR we already saved
|
||||
if (is_code_addr(val) && val != frame[EF_PC] && val != frame[EF_LR]) {
|
||||
watchdog_hw->scratch[4 + bt_count] = val;
|
||||
bt_count++;
|
||||
}
|
||||
}
|
||||
// Zero remaining slots
|
||||
for (uint32_t i = bt_count; i < MAX_BACKTRACE; i++) {
|
||||
watchdog_hw->scratch[4 + i] = 0;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
__asm volatile("nop");
|
||||
}
|
||||
}
|
||||
|
||||
// Naked asm wrapper - Cortex-M0+ compatible (no ITE/conditional execution).
|
||||
// Determines active stack pointer and branches to C handler.
|
||||
// Uses literal pool (.word) for addresses since M0+ has limited immediate encoding.
|
||||
//
|
||||
// Based on the standard Cortex-M0+ HardFault handler pattern described in:
|
||||
// - ARM Application Note AN209: "Using Cortex-M3/M4/M7 Fault Exceptions"
|
||||
// (adapted for M0+ which lacks conditional execution instructions)
|
||||
// - Memfault: "How to debug a HardFault on an ARM Cortex-M MCU"
|
||||
// https://interrupt.memfault.com/blog/cortex-m-hardfault-debug
|
||||
// - Raspberry Pi Forums: "Cortex-M0+ Hard Fault handler porting"
|
||||
// https://www.eevblog.com/forum/microcontrollers/cortex-m0-hard-fault-handler-porting/
|
||||
//
|
||||
// The key M0+ adaptation: replaces ITE/MRSEQ/MRSNE (Cortex-M3+) with
|
||||
// MOVS+TST+BEQ branch sequence, and uses a literal pool for the C handler address.
|
||||
extern "C" void __attribute__((naked, used)) isr_hardfault() {
|
||||
__asm volatile("movs r0, #4 \n" // Prepare bit 2 mask
|
||||
"mov r1, lr \n" // r1 = EXC_RETURN
|
||||
"tst r1, r0 \n" // Test bit 2
|
||||
"beq 1f \n" // If 0, was using MSP
|
||||
"mrs r0, psp \n" // Bit 2 set = PSP was active
|
||||
"b 2f \n"
|
||||
"1: \n"
|
||||
"mrs r0, msp \n" // Bit 2 clear = MSP was active
|
||||
"2: \n"
|
||||
// r0 = exception frame pointer, r1 = EXC_RETURN (still in r1)
|
||||
"ldr r2, 3f \n" // Load C handler address from literal pool
|
||||
"bx r2 \n" // Branch to handler (r0=frame, r1=exc_return)
|
||||
".align 2 \n"
|
||||
"3: .word %c0 \n" // Literal pool: address of C handler
|
||||
:
|
||||
: "i"(hard_fault_handler_c));
|
||||
}
|
||||
|
||||
#endif // USE_RP2040
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_RP2040
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::rp2040 {
|
||||
|
||||
/// Read crash data from watchdog scratch registers and clear them.
|
||||
void crash_handler_read_and_clear();
|
||||
|
||||
/// Log crash data if a crash was detected on previous boot.
|
||||
void crash_handler_log();
|
||||
|
||||
} // namespace esphome::rp2040
|
||||
|
||||
#endif // USE_RP2040
|
||||
Reference in New Issue
Block a user