From f1952df485a921d3c47ff1c0210bf489b489b405 Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Sat, 25 Jul 2026 00:59:09 +0800 Subject: [PATCH 1/2] fix(stm32wl): make HardFault UART reporting resilient to a frozen tick count uart_debug_write() (STM32duino SrcWrapper) times its "wait for UART idle" loop, and HAL_UART_Transmit()'s own internal timeout, off HAL_GetTick(), which only advances via the SysTick interrupt. HardFault is fixed at NVIC priority -1 (always the highest in the system), so SysTick can never preempt it: if the UART happens to be genuinely mid-transmission when the fault lands (e.g. a log line was still draining out), HAL_GetTick() is frozen for as long as we're inside the fault handler, the timeout can never trip, and HardFault_Handler_C hangs forever inside HAL_UART_GetState instead of printing the register dump or reaching the SOS blink loop - reproduced on wio-e5 hardware under a burst of incoming DMs. Bypass the framework TX path entirely for the fault handler's own output: write straight to the debug UART peripheral registers, using the DWT cycle counter (a free-running hardware counter that keeps incrementing regardless of interrupt state) to bound the wait instead of HAL_GetTick(). Assisted-by: Claude Sonnet 5 Signed-off-by: Andrew Yong --- src/platform/stm32wl/main-stm32wl.cpp | 35 +++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/platform/stm32wl/main-stm32wl.cpp b/src/platform/stm32wl/main-stm32wl.cpp index d429c5662de..3d47bcfa858 100644 --- a/src/platform/stm32wl/main-stm32wl.cpp +++ b/src/platform/stm32wl/main-stm32wl.cpp @@ -220,7 +220,38 @@ typedef struct __attribute__((packed)) ContextStateFrame { static char hardfault_message_buffer[256]; -// printf directly using srcwrapper's debug UART function. +// Bypasses uart_debug_write(), whose timeout relies on HAL_GetTick(): SysTick can't preempt a fault +// handler (fixed NVIC priority -1), so that timeout never trips here. Use DWT->CYCCNT instead - a +// free-running counter that keeps ticking regardless of interrupt state. +static void faultSafeUartWrite(const uint8_t *data, size_t size) +{ + USART_TypeDef *uart = Serial.getHandle()->Instance; + if (!uart || !(uart->CR1 & USART_CR1_UE)) + return; // Not mapped, or Serial.begin() hasn't run yet - nothing we can do. + + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + + const uint32_t timeoutCycles = SystemCoreClock / 5; // ~200ms - generous for a couple hundred bytes at any sane baud rate + + for (size_t i = 0; i < size; i++) { + uint32_t start = DWT->CYCCNT; + while (!(uart->ISR & USART_ISR_TXE_TXFNF)) { + if ((uint32_t)(DWT->CYCCNT - start) >= timeoutCycles) + return; // Give up rather than hang forever. + } + uart->TDR = data[i]; + } + + // Wait for the last byte to actually leave the shift register before returning, same reasoning. + uint32_t start = DWT->CYCCNT; + while (!(uart->ISR & USART_ISR_TC)) { + if ((uint32_t)(DWT->CYCCNT - start) >= timeoutCycles) + return; + } +} + +// printf directly to the debug UART, fault-handler-safe (see faultSafeUartWrite() above). static void debug_printf(const char *format, ...) { va_list args; @@ -230,7 +261,7 @@ static void debug_printf(const char *format, ...) if (length < 0) return; - uart_debug_write((uint8_t *)hardfault_message_buffer, min((unsigned int)length, sizeof(hardfault_message_buffer) - 1)); + faultSafeUartWrite((uint8_t *)hardfault_message_buffer, min((unsigned int)length, sizeof(hardfault_message_buffer) - 1)); } // N picked by guessing From d72f0c2fa3f83af5dc60e2cf2d1f2c50ad3236a1 Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Sat, 25 Jul 2026 01:28:30 +0800 Subject: [PATCH 2/2] address review: shared deadline across the whole write, trim comment Per-byte timer reset meant a wedged-but-slowly-progressing UART could stretch the total write out to size * timeoutCycles instead of being bounded by one deadline. Share a single start point across the byte loop and the final TC wait instead. Assisted-by: Claude Sonnet 5 Signed-off-by: Andrew Yong --- src/platform/stm32wl/main-stm32wl.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/platform/stm32wl/main-stm32wl.cpp b/src/platform/stm32wl/main-stm32wl.cpp index 3d47bcfa858..b260fd88715 100644 --- a/src/platform/stm32wl/main-stm32wl.cpp +++ b/src/platform/stm32wl/main-stm32wl.cpp @@ -220,9 +220,8 @@ typedef struct __attribute__((packed)) ContextStateFrame { static char hardfault_message_buffer[256]; -// Bypasses uart_debug_write(), whose timeout relies on HAL_GetTick(): SysTick can't preempt a fault -// handler (fixed NVIC priority -1), so that timeout never trips here. Use DWT->CYCCNT instead - a -// free-running counter that keeps ticking regardless of interrupt state. +// Bypasses uart_debug_write()'s HAL_GetTick() timeout (frozen inside a fault handler, since SysTick +// can't preempt it) using DWT->CYCCNT instead, which keeps ticking regardless of interrupt state. static void faultSafeUartWrite(const uint8_t *data, size_t size) { USART_TypeDef *uart = Serial.getHandle()->Instance; @@ -232,10 +231,12 @@ static void faultSafeUartWrite(const uint8_t *data, size_t size) CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + // One shared deadline for the whole write (not per byte), so a wedged UART can't stretch this + // out to size * timeoutCycles. const uint32_t timeoutCycles = SystemCoreClock / 5; // ~200ms - generous for a couple hundred bytes at any sane baud rate + const uint32_t start = DWT->CYCCNT; for (size_t i = 0; i < size; i++) { - uint32_t start = DWT->CYCCNT; while (!(uart->ISR & USART_ISR_TXE_TXFNF)) { if ((uint32_t)(DWT->CYCCNT - start) >= timeoutCycles) return; // Give up rather than hang forever. @@ -244,7 +245,6 @@ static void faultSafeUartWrite(const uint8_t *data, size_t size) } // Wait for the last byte to actually leave the shift register before returning, same reasoning. - uint32_t start = DWT->CYCCNT; while (!(uart->ISR & USART_ISR_TC)) { if ((uint32_t)(DWT->CYCCNT - start) >= timeoutCycles) return;