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 <noreply@anthropic.com>
This commit is contained in:
Jonathan BennettandClaude Opus 5.5 committed 2026-11-03 10:03:33 -06:00
1 parent 5a8f7e424a
commit c77feed4a7
5 files changed
+95 -3

No files matched your search

+3 -1
View File
@@ -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)
+55
View File
@@ -37,6 +37,43 @@ static uint32_t sbrkHeadroom()
MemGet memGet;
#ifdef ARCH_NRF52
#include <unistd.h> // 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<const char *>(c);
if (p < __HeapBase || p >= __HeapLimit || (reinterpret_cast<uintptr_t>(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<const char *>(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.
*
+2
View File
@@ -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();
};
+12 -2
View File
@@ -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;
}
+23
View File
@@ -1,5 +1,6 @@
#include "UptimeClock.h"
#include "configuration.h"
#include "memGet.h"
#include "mesh/Throttle.h"
#include <Adafruit_TinyUSB.h>
#include <Adafruit_nRFCrypto.h>
@@ -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