From c77feed4a7cb700d2d64a745d4abffb693127490 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 3 Nov 2026 09:54:02 -0600 Subject: [PATCH] nRF52: say more when an assert resets The assert line alone can't tell an out-of-memory crash apart from heap corruption or a stack overflow, and for a failed operator new it always names alloc.cpp. - After the assert line, log the FreeRTOS task (the log's [thread] tag is the last OSThread to run), its stack high-water mark, free heap and the largest free block, or that the free list failed a bounds-checked walk. - A failed operator new logs the requested size and the caller's address first, for addr2line. - MemGet::getMaxAllocHeap() walks newlib-nano's free list on nRF52, so the periodic heap line reports the largest block there. That line no longer prints "min 0" where the minimum isn't tracked. Co-Authored-By: Claude Opus 5.5 --- src/Power.cpp | 4 ++- src/memGet.cpp | 55 +++++++++++++++++++++++++++++++ src/memGet.h | 2 ++ src/platform/nrf52/alloc.cpp | 14 ++++++-- src/platform/nrf52/main-nrf52.cpp | 23 +++++++++++++ 5 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index 3e84437f92..9f0117081d 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -1327,8 +1327,10 @@ void Power::logHeapUsage() char detail[64] = ""; const uint32_t minFree = memGet.getMinFreeHeap(); const uint32_t maxAlloc = memGet.getMaxAllocHeap(); - if (minFree || maxAlloc) + if (minFree) snprintf(detail, sizeof(detail), ", min %u, largest block %u", minFree, maxAlloc); + else if (maxAlloc) + snprintf(detail, sizeof(detail), ", largest block %u", maxAlloc); const uint32_t psramTotal = memGet.getPsramSize(); if (psramTotal) diff --git a/src/memGet.cpp b/src/memGet.cpp index 4283bb2659..dd2c79397f 100644 --- a/src/memGet.cpp +++ b/src/memGet.cpp @@ -37,6 +37,43 @@ static uint32_t sbrkHeadroom() MemGet memGet; +#ifdef ARCH_NRF52 +#include // sbrk + +extern "C" char __HeapBase[], __HeapLimit[]; +// newlib-nano's free list: each chunk starts with its size (header included) and the next free chunk +struct NanoFreeChunk { + long size; + NanoFreeChunk *next; +}; +extern "C" NanoFreeChunk *__malloc_free_list; + +/// Walk newlib-nano's free list for the largest free chunk; false if a link leaves the heap or the list doesn't end. +static bool walkNrf52FreeList(uint32_t &largest) +{ + largest = 0; + bool intact = true; + uint32_t chunks = 0; + vTaskSuspendAll(); + for (const NanoFreeChunk *c = __malloc_free_list; c; c = c->next) { + const char *p = reinterpret_cast(c); + if (p < __HeapBase || p >= __HeapLimit || (reinterpret_cast(p) & 3) || c->size <= 0 || + c->size > __HeapLimit - p || ++chunks > 2048) { + intact = false; + break; + } + if ((uint32_t)c->size > largest) + largest = c->size; + } + const char *brk = static_cast(sbrk(0)); + (void)xTaskResumeAll(); + // The never-claimed space above the break is one more block + if (brk >= __HeapBase && brk <= __HeapLimit && (uint32_t)(__HeapLimit - brk) > largest) + largest = __HeapLimit - brk; + return intact; +} +#endif + /** * Returns the amount of free heap memory in bytes. * @return uint32_t The amount of free heap memory in bytes. @@ -100,11 +137,29 @@ uint32_t MemGet::getMaxAllocHeap() { #ifdef ARCH_ESP32 return ESP.getMaxAllocHeap(); +#elif defined(ARCH_NRF52) + uint32_t largest; + walkNrf52FreeList(largest); + return largest; #else return 0; #endif } +/** + * Sanity-walks the allocator's free list, where the platform allows it. + * @return false if the list looks corrupt, true if it is intact or can't be checked. + */ +bool MemGet::heapLooksIntact() +{ +#ifdef ARCH_NRF52 + uint32_t largest; + return walkNrf52FreeList(largest); +#else + return true; +#endif +} + /** * Returns the amount of free psram memory in bytes. * diff --git a/src/memGet.h b/src/memGet.h index 69348b4dad..fb20e1a0ad 100644 --- a/src/memGet.h +++ b/src/memGet.h @@ -13,6 +13,8 @@ class MemGet uint32_t getMinFreeHeap(); // Largest block malloc() could still return, or 0 where the platform can't report it uint32_t getMaxAllocHeap(); + // False if the allocator's free list fails a sanity walk (heap corruption); true where it can't be checked + bool heapLooksIntact(); uint32_t getFreePsram(); uint32_t getPsramSize(); }; diff --git a/src/platform/nrf52/alloc.cpp b/src/platform/nrf52/alloc.cpp index 0a610bbe36..df2a38409b 100644 --- a/src/platform/nrf52/alloc.cpp +++ b/src/platform/nrf52/alloc.cpp @@ -7,16 +7,26 @@ * Custom new/delete to panic if out out memory */ -void *operator new(size_t size) +// The assert below can only name this file, so log the request and who made it first +static void __attribute__((noinline)) logAllocFailure(size_t size, void *caller) +{ + LOG_ERROR("new(%u) failed, caller 0x%08lx", (unsigned)size, (unsigned long)caller); +} + +void *__attribute__((noinline)) operator new(size_t size) { auto p = rtos_malloc(size); + if (!p) + logAllocFailure(size, __builtin_return_address(0)); assert(p); return p; } -void *operator new[](size_t size) +void *__attribute__((noinline)) operator new[](size_t size) { auto p = rtos_malloc(size); + if (!p) + logAllocFailure(size, __builtin_return_address(0)); assert(p); return p; } diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 5da8c01274..4b6e88b318 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -1,5 +1,6 @@ #include "UptimeClock.h" #include "configuration.h" +#include "memGet.h" #include "mesh/Throttle.h" #include #include @@ -197,10 +198,32 @@ static void drainConsoleBeforeReset() delay(20); } +// What file:line can't say: the task that asserted (the log's [thread] tag is the last OSThread to run), how close +// its stack came to overflowing, and whether the heap was exhausted, fragmented or corrupt. +static void logAssertContext() +{ + if (__get_IPSR() != 0) { + LOG_ERROR("assert context: exception %lu", (unsigned long)__get_IPSR()); + return; + } + if (xTaskGetSchedulerState() != taskSCHEDULER_RUNNING) + return; + const char *task = pcTaskGetName(NULL); + const unsigned stackFree = uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t); + // mallinfo() behind getFreeHeap() walks the same list unchecked, so only call it once the list checks out + if (!memGet.heapLooksIntact()) { + LOG_ERROR("assert context: task %s, stack free %u B, heap free list corrupt", task, stackFree); + return; + } + LOG_ERROR("assert context: task %s, stack free %u B, heap free %u, largest block %u", task, stackFree, memGet.getFreeHeap(), + memGet.getMaxAllocHeap()); +} + // handle standard gcc assert failures void __attribute__((noreturn)) __assert_func(const char *file, int line, const char *func, const char *failedexpr) { LOG_ERROR("assert failed %s: %d, %s, test=%s", file, line, func, failedexpr); + logAssertContext(); drainConsoleBeforeReset(); // debugger_break(); FIXME doesn't work, possibly for segger // Reboot cpu