Compare commits

...
Author SHA1 Message Date
J. Nick Koston 3278ea5a04 Merge branch 'dev' into ln882x-crash-handler 2026-10-27 10:07:54 -10:00
J. Nick Koston 361a9580da [libretiny] Gate the vector-patch crash handler on one derived define
The RTL8720C and LN882X check was copied into hal.cpp, the header and the
source; USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH now names it once.
2026-10-27 09:13:25 -10:00
J. Nick Koston 43cc2a8e3d [libretiny] Arm the crash reboot watchdog before recording the fault
A fault while the record was being written spun on the first-fault guard until
the 10 s watchdog from arch_init(), not the 1 s one.
2026-10-27 09:08:25 -10:00
J. Nick Koston 281c5fcabc [libretiny] Keep the first fault when the SDK fault handler faults too
CmBacktrace on the LN882H walks past the end of RAM and faults inside the
fault handler, which overwrote the record with its own hard fault. Record the
first fault of a boot only and wait for the watchdog on any later one.
2026-10-27 09:00:07 -10:00
J. Nick Koston 79c945c76c Merge remote-tracking branch 'origin/dev' into ln882x-crash-handler 2026-10-27 08:32:17 -10:00
J. Nick Koston 92c54aea77 [libretiny] Report the previous boot's crash on LN882H
Put the fault recorder in front of the SDK's CmBacktrace fault handlers through
the RAM vector table, as on the RTL8720C, and keep the record in the RETENTION
RAM that startup does not clear. The Cortex-M capture is now shared and sizes
the exception frame from EXC_RETURN, since the LN882H has an FPU.
2026-10-27 08:18:45 -10:00
13 changed files with 95 additions and 44 deletions

No files matched your search

+3 -2
View File
@@ -44,6 +44,7 @@ from .const import (
FAMILY_BK7238,
FAMILY_COMPONENT,
FAMILY_FRIENDLY,
FAMILY_LN882H,
FAMILY_RTL8710B,
FAMILY_RTL8720C,
KEY_BOARD,
@@ -60,8 +61,8 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
# Fault handlers the crash handler wraps (BK7238, BK7231N)
BK_TRAP_HANDLERS = ("bk_trap_udef", "bk_trap_pabt", "bk_trap_dabt")
# Families with a crash handler; the RTL8720C installs it at run time (no wraps).
CRASH_HANDLER_FAMILIES = (FAMILY_BK7231N, FAMILY_BK7238, FAMILY_RTL8720C)
# Families with a crash handler; the RTL8720C and LN882H install it at run time (no wraps).
CRASH_HANDLER_FAMILIES = (FAMILY_BK7231N, FAMILY_BK7238, FAMILY_LN882H, FAMILY_RTL8720C)
CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["preferences"]
+68 -31
View File
@@ -3,11 +3,12 @@
#include "esphome/core/defines.h"
#ifdef USE_LIBRETINY_CRASH_HANDLER
#if !defined(USE_BK72XX) && !defined(USE_LIBRETINY_VARIANT_RTL8720C)
#include "crash_handler.h"
#if !defined(USE_BK72XX) && !defined(USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH)
#error "The LibreTiny crash handler has no fault capture for this chip family"
#endif
#include "crash_handler.h"
#include "core.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
@@ -58,6 +59,18 @@ extern char __ram_code_text_end__[];
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_LIBRETINY_VARIANT_RTL8720C
#ifdef USE_LN882X
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
// Bounds of the code in flash and of the IRAM_ATTR code (linker symbols; the latter from patch_linker.py).
extern char _stext[];
extern char _etext[];
extern char __esphome_sram_text_start[];
extern char __esphome_sram_text_end[];
}
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_LN882X
namespace esphome::libretiny {
ESPHOME_LOG_TAG(TAG, "libretiny.crash");
@@ -69,7 +82,7 @@ static constexpr uint32_t CRASH_MAGIC_V2 = 0xDEAD0002;
static constexpr uint32_t CRASH_REASON_UNDEFINED = 1;
static constexpr uint32_t CRASH_REASON_PREFETCH_ABORT = 2;
static constexpr uint32_t CRASH_REASON_DATA_ABORT = 3;
// RTL8720C: the Cortex-M exception number (3 to 7) plus this offset.
// RTL8720C and LN882H: the Cortex-M exception number (3 to 7) plus this offset.
static constexpr uint32_t CRASH_REASON_CORTEX_M_OFFSET = 1;
PROGMEM_STRING_TABLE(CrashReasonStrings, "Unknown", "Undefined instruction", "Prefetch abort", "Data abort",
"Hard fault", "MemManage fault", "Bus fault", "Usage fault", "Secure fault");
@@ -92,6 +105,9 @@ struct CrashRecord {
#ifdef USE_BK72XX
// Kept in ITCM, see patch_noinit.py.script.
#define CRASH_RECORD_SECTION ".noinit"
#elif defined(USE_LN882X)
// ln882hki_bsp.template.ld puts .noinit in RETENTION RAM, which startup does not clear.
#define CRASH_RECORD_SECTION ".noinit"
#else
// rtl8710c_ram.ld keeps .ram.bss.noinit* out of the startup zeroing.
#define CRASH_RECORD_SECTION ".ram.bss.noinit.esphome_crash"
@@ -146,6 +162,9 @@ static inline bool is_code_addr(uint32_t val) {
#ifdef USE_BK72XX
return (addr >= (uint32_t) _vector_start && addr < (uint32_t) _data_flash_begin) ||
(addr >= (uint32_t) _itcmcode_ram_begin && addr < (uint32_t) _itcmcode_ram_end);
#elif defined(USE_LN882X)
return (addr >= (uint32_t) _stext && addr < (uint32_t) _etext) ||
(addr >= (uint32_t) __esphome_sram_text_start && addr < (uint32_t) __esphome_sram_text_end);
#else
return (addr >= (uint32_t) __xip_code_text_start__ && addr < (uint32_t) __xip_code_text_end__) ||
(addr >= (uint32_t) __ram_entry_text_start__ && addr < (uint32_t) __ram_code_text_end__);
@@ -257,19 +276,29 @@ extern "C" void __wrap_bk_trap_dabt(arm_registers *regs) {
#endif // USE_BK72XX
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
#ifdef USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
// The SDK points the fault vectors (3 HardFault to 7 SecureFault) of its RAM vector table (VTOR) at a
// ROM handler that prints a register dump and then spins. crash_handler_install() puts
// esphome_fault_entry() in front of it, as the SDK's own CONFIG_MIIO patch (app_start.c) does.
// The SDK's RAM vector table (VTOR) points the fault vectors at a handler that prints a register dump and then
// spins: a ROM handler on the RTL8720C, CmBacktrace (hal_interrupt.c) on the LN882H. crash_handler_install()
// puts esphome_fault_entry() in front of it, as the RTL8720C SDK's own CONFIG_MIIO patch (app_start.c) does.
static_assert(LT_HW_WATCHDOG, "the crash reboot relies on the hardware watchdog");
// The ROM dump takes about 0.4 s at 115200 baud; without the re-arm the reset waits for the 10 s
// The RTL8720C ROM dump takes about 0.4 s at 115200 baud; without the re-arm the reset waits for the 10 s
// watchdog from arch_init().
static constexpr uint32_t CRASH_REBOOT_WDT_MS = 1000;
static constexpr uint32_t FIRST_FAULT_VECTOR = 3;
static constexpr uint32_t FAULT_VECTOR_COUNT = 5;
static constexpr uint32_t FIRST_FAULT_VECTOR = 3; // HardFault, then MemManage, BusFault, UsageFault
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
static constexpr uint32_t FAULT_VECTOR_COUNT = 5; // and SecureFault (ARMv8-M)
// DTCM, where the app's stacks and heap live (rtl8710c_ram.ld).
static constexpr uint32_t SRAM_START = 0x10000000;
static constexpr uint32_t SRAM_END = 0x1003FA00;
#else
static constexpr uint32_t FAULT_VECTOR_COUNT = 4;
// RAM0, where the stacks and heap live (ln882hki_bsp.template.ld).
static constexpr uint32_t SRAM_START = 0x20000000;
static constexpr uint32_t SRAM_END = 0x20049C00;
#endif
// Architectural System Control Block registers (ARMv8-M).
// Architectural System Control Block registers (ARMv7-M and ARMv8-M).
static constexpr uintptr_t SCB_VTOR = 0xE000ED08;
static constexpr uintptr_t SCB_CFSR = 0xE000ED28;
static constexpr uintptr_t SCB_MMFAR = 0xE000ED34;
@@ -277,26 +306,34 @@ static constexpr uintptr_t SCB_BFAR = 0xE000ED38;
static constexpr uint32_t CFSR_MMARVALID = 1u << 7;
static constexpr uint32_t CFSR_BFARVALID = 1u << 15;
// DTCM, where the app's stacks and heap live (rtl8710c_ram.ld).
static constexpr uint32_t SRAM_START = 0x10000000;
static constexpr uint32_t SRAM_END = 0x1003FA00;
// Exception frame: r0-r3, r12, lr, pc, xPSR (the RTL8720C has no FPU, so never the extended frame).
// Exception frame: r0-r3, r12, lr, pc, xPSR, then the FPU registers when EXC_RETURN says so (LN882H only).
static constexpr uint32_t FRAME_LR = 5;
static constexpr uint32_t FRAME_PC = 6;
static constexpr uint32_t FRAME_XPSR = 7;
static constexpr uint32_t FRAME_WORDS = 8;
static constexpr uint32_t EXTENDED_FRAME_WORDS = 26;
static constexpr uint32_t EXC_RETURN_STANDARD_FRAME = 1u << 4;
static constexpr uint32_t XPSR_STACK_ALIGNED = 1u << 9;
// The ROM handlers fault_entry() continues into, by vector.
static uint32_t s_rom_fault_vectors[FAULT_VECTOR_COUNT]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// The SDK handlers fault_entry() continues into, by vector.
static uint32_t s_sdk_fault_vectors[FAULT_VECTOR_COUNT]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_fault_recorded = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static inline uint32_t read_reg(uintptr_t addr) {
return *(const volatile uint32_t *) addr; // NOLINT(performance-no-int-to-ptr)
}
// Returns the ROM handler to continue into. pc, lr and sp stay 0 when the frame is outside DTCM.
extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *frame, uint32_t vector) {
// Returns the SDK handler to continue into. pc, lr and sp stay 0 when the frame is outside SRAM.
extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *frame, uint32_t vector,
uint32_t exc_return) {
// A later fault, in the recording below or in the SDK handler (CmBacktrace on the LN882H walks past the end
// of RAM), keeps the first record and waits for the watchdog instead of starting the dump again.
if (s_fault_recorded) {
while (true) {
}
}
s_fault_recorded = true;
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
uint32_t cfsr = read_reg(SCB_CFSR);
uint32_t address = 0;
if (cfsr & CFSR_BFARVALID) {
@@ -309,25 +346,24 @@ extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *f
s_record.fault_address = address;
auto frame_addr = (uint32_t) frame;
uint32_t frame_words = (exc_return & EXC_RETURN_STANDARD_FRAME) ? FRAME_WORDS : EXTENDED_FRAME_WORDS;
uint32_t sp = 0;
if (frame_addr >= SRAM_START && frame_addr <= SRAM_END - FRAME_WORDS * 4 && (frame_addr & 3) == 0) {
if (frame_addr >= SRAM_START && frame_addr <= SRAM_END - frame_words * 4 && (frame_addr & 3) == 0) {
s_record.pc = frame[FRAME_PC];
s_record.lr = frame[FRAME_LR];
sp = frame_addr + FRAME_WORDS * 4 + ((frame[FRAME_XPSR] & XPSR_STACK_ALIGNED) ? 4 : 0);
sp = frame_addr + frame_words * 4 + ((frame[FRAME_XPSR] & XPSR_STACK_ALIGNED) ? 4 : 0);
} else {
s_record.pc = 0;
s_record.lr = 0;
}
s_record.sp = sp;
finish_record(sp, SRAM_START, SRAM_END);
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
return s_rom_fault_vectors[vector - FIRST_FAULT_VECTOR];
return s_sdk_fault_vectors[vector - FIRST_FAULT_VECTOR];
}
// Fault vector: calls esphome_record_fault(frame, exception number), then jumps to the ROM handler it returns
// with r0-r3 and lr restored. Asm because C would push onto MSP before the frame is found, and the ROM handler
// reads EXC_RETURN from lr.
// Fault vector: calls esphome_record_fault(frame, exception number, EXC_RETURN), then jumps to the SDK handler it
// returns with r0-r3 and lr restored. Asm because C would push onto MSP before the frame is found, and the SDK
// handler reads EXC_RETURN from lr.
extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
__asm volatile("push {r0-r3, r4, lr} \n" // r4 only keeps the stack 8-byte aligned
"tst lr, #4 \n"
@@ -338,8 +374,9 @@ extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
"addeq r0, r0, #24 \n" // MSP: skip the six words pushed above
"mrs r1, ipsr \n"
"ubfx r1, r1, #0, #9 \n" // exception number, 3 to 7
"mov r2, lr \n"
"bl esphome_record_fault \n"
"mov r12, r0 \n" // ROM handler address
"mov r12, r0 \n" // SDK handler address
"pop {r0-r3, r4, lr} \n"
"bx r12 \n");
}
@@ -347,12 +384,12 @@ extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
void crash_handler_install() {
auto *vectors = (uint32_t *) read_reg(SCB_VTOR); // NOLINT(performance-no-int-to-ptr)
for (uint32_t i = 0; i < FAULT_VECTOR_COUNT; i++) {
s_rom_fault_vectors[i] = vectors[FIRST_FAULT_VECTOR + i];
s_sdk_fault_vectors[i] = vectors[FIRST_FAULT_VECTOR + i];
vectors[FIRST_FAULT_VECTOR + i] = (uint32_t) &esphome_fault_entry;
}
}
#endif // USE_LIBRETINY_VARIANT_RTL8720C
#endif // USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
} // namespace esphome::libretiny
+6 -1
View File
@@ -6,6 +6,11 @@
#ifdef USE_LIBRETINY_CRASH_HANDLER
#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(USE_LN882X)
// Cortex-M families: the recorder goes in front of the SDK's fault handlers through the RAM vector table.
#define USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
#endif
namespace esphome::libretiny {
/// Log crash data if a crash was detected on previous boot.
@@ -14,7 +19,7 @@ void crash_handler_log();
/// Returns true if crash data was found this boot, reading it first if needed.
bool crash_handler_has_data();
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
#ifdef USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
/// Put the crash recorder in front of the SDK's fault handlers.
void crash_handler_install();
#endif
@@ -156,6 +156,7 @@ COMPONENT_CODEOWNERS = {
# Extra imports per component; the CLI looks up process_stacktrace on the platform package.
COMPONENT_EXTRA_IMPORTS = {
"bk72xx": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
"ln882x": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
"rtl87xx": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
}
+1 -1
View File
@@ -28,7 +28,7 @@ namespace esphome {
void arch_init() {
libretiny::setup_preferences();
lt_wdt_enable(10000L);
#if defined(USE_LIBRETINY_CRASH_HANDLER) && defined(USE_LIBRETINY_VARIANT_RTL8720C)
#ifdef USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
libretiny::crash_handler_install();
#endif
#ifdef USE_BK72XX
+1
View File
@@ -29,6 +29,7 @@ from esphome.components.libretiny.const import (
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj
from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401
from esphome.types import ConfigType
from .boards import LN882X_BOARD_PINS, LN882X_BOARDS
+1 -1
View File
@@ -781,7 +781,7 @@
#endif
#ifdef USE_LIBRETINY
#if defined(USE_BK72XX) || defined(USE_LIBRETINY_VARIANT_RTL8720C)
#if defined(USE_BK72XX) || defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(USE_LN882X)
#define USE_LIBRETINY_CRASH_HANDLER
#endif
#define USE_BK72XX_BLE
+1
View File
@@ -67,6 +67,7 @@ STACKTRACE_GATES: Final[dict[str, str]] = {
),
PLATFORM_RP2: _CRASH_HANDLER_GATE,
PLATFORM_BK72XX: _CRASH_HANDLER_GATE,
PLATFORM_LN882X: _CRASH_HANDLER_GATE,
PLATFORM_RTL87XX: _CRASH_HANDLER_GATE,
PLATFORM_NRF52: r"0x[0-9a-fA-F]{3,}\b|Last crash:",
}
+1 -1
View File
@@ -152,7 +152,7 @@ def _addr2line(tool: str, elf: Path, addr: str) -> str:
def process_crash_handler_line(
config: ConfigType, line: str, backtrace_state: bool
) -> bool:
"""Decode a crash handler report with addr2line; the process_stacktrace hook of rp2, bk72xx and rtl87xx."""
"""Decode a crash handler report with addr2line; the process_stacktrace hook of rp2, bk72xx, rtl87xx and ln882x."""
if CRASH_HANDLER_RE.search(line):
_LOGGER.error("Crash detected - decoding addresses")
return True
@@ -1,4 +1,4 @@
"""The crash handler is built for BK7238, BK7231N (both with an ITCM linker region) and RTL8720C."""
"""The crash handler is built for BK7238, BK7231N (both with an ITCM linker region), RTL8720C and LN882H."""
from __future__ import annotations
@@ -19,7 +19,7 @@ from esphome.core import CORE
("bk7231t.yaml", False, False),
("rtl8720c.yaml", True, False), # installed at run time
("rtl8710b.yaml", False, False),
("ln882x.yaml", False, False),
("ln882x.yaml", True, False), # installed at run time
],
)
def test_crash_handler_family_gate(
+1 -2
View File
@@ -117,7 +117,6 @@ from esphome.const import (
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_HOST,
PLATFORM_LN882X,
PLATFORM_NRF52,
PLATFORM_RP2,
ErrorFormat,
@@ -6304,7 +6303,7 @@ def test_run_miniterm_no_stacktrace_analyzer(
"""Platforms without a stacktrace analyzer log an info and stream anyway."""
mock_serial = MockSerial([b"[I][app:100]: Line 1\r\n", MOCK_SERIAL_END])
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_LN882X}
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_HOST}
config = {
CONF_LOGGER: {
CONF_BAUD_RATE: 115200,
+2 -2
View File
@@ -15,7 +15,7 @@ from unittest.mock import Mock
import pytest
from esphome import platform_hooks
from esphome.const import PLATFORM_ESP32, PLATFORM_LN882X, Platform
from esphome.const import PLATFORM_ESP32, PLATFORM_HOST, Platform
def test_no_unregistered_platform_exposes_a_hook() -> None:
@@ -165,7 +165,7 @@ def test_get_stacktrace_handler_reports_missing_analyzer(
caplog: pytest.LogCaptureFixture,
) -> None:
caplog.set_level("INFO", logger="esphome.platform_hooks")
assert platform_hooks.get_stacktrace_handler(PLATFORM_LN882X) is None
assert platform_hooks.get_stacktrace_handler(PLATFORM_HOST) is None
assert "no compatible analyzer" in caplog.text
# A capability gap is ordinary; it must not warn.
assert not any(r.levelno >= logging.WARNING for r in caplog.records)
+7 -1
View File
@@ -18,6 +18,7 @@ from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_HOST,
PLATFORM_LN882X,
PLATFORM_NRF52,
PLATFORM_RP2,
@@ -71,6 +72,10 @@ CRASH_SAMPLES: dict[str, dict[str, list[str]]] = {
"state_markers": ["CRASH DETECTED ON PREVIOUS BOOT"],
"addresses": ["PC: 0x9B0073A4 (fault location)"],
},
PLATFORM_LN882X: {
"state_markers": ["CRASH DETECTED ON PREVIOUS BOOT"],
"addresses": ["PC: 0x1001A2C4 (fault location)"],
},
PLATFORM_NRF52: {
"state_markers": ["Last crash:"],
"addresses": [
@@ -237,6 +242,7 @@ DECODER_PATTERNS: dict[str, list[str]] = {
PLATFORM_RP2: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
PLATFORM_BK72XX: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
PLATFORM_RTL87XX: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
PLATFORM_LN882X: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
PLATFORM_NRF52: ["STACKTRACE_NRF52_PC_LR_RE"],
}
@@ -554,7 +560,7 @@ def test_processor_registry_miss_disables_at_construction(
caplog.set_level("INFO", logger="esphome.platform_hooks")
with patch.object(stacktrace.platform_hooks, "import_module") as mock_import:
processor = stacktrace.LogLineProcessor(CONFIG, PLATFORM_LN882X)
processor = stacktrace.LogLineProcessor(CONFIG, PLATFORM_HOST)
processor.process_line("PC: 0x40104960")
mock_import.assert_not_called()