The Pico SDK’s memory allocation routines are fully reentrant and can normally be used safely from multiple cores without worrying about race conditions. Once FreeRTOS enters the picture, however, things get more complicated.
Delegating memory management to FreeRTOS
FreeRTOS maintains its own heap and allocation routines (pvPortMalloc() and pvPortFree()), offering several heap management strategies. The most common is Heap 4, which minimizes fragmentation to reduce long-term out-of-memory errors.
Typically, the system reserves a fixed heap size at startup (via configTOTAL_HEAP_SIZE), and multitasking code then uses the pvPort* routines to allocate and free memory.
In practice, this creates two separate heaps—one managed by the SDK, the other by FreeRTOS—introducing unnecessary complexity. You must now predict and manage how much memory belongs to each heap, and deal with fragmentation and hard-to-debug crashes that can result from it.
My preferred approach is to delegate all memory management to FreeRTOS by overriding the system’s default allocation functions (malloc, free, new, delete) and redirecting them to their pvPort* equivalents.
This keeps memory handling consistent and lets us take advantage of FreeRTOS’s runtime diagnostics for heap integrity and allocation failure handling.
Configuring the Pico SDK
To implement this, add the following settings to your project’s CMakeLists.txt:
-
SKIP_PICO_MALLOC — Prevents the SDK from defining its own malloc() and free() wrappers, allowing you to supply your own.
-
PICO_CXX_DISABLE_ALLOCATION_OVERRIDES — Disables the SDK’s default C++ new and delete operators, enabling you to override them.
We can then write our custom allocation wrappers, which you will find in lib/sys/memory.cpp.
A bare-metal monkey wrench
In this setup, core 1 runs bare-metal code outside FreeRTOS’s control, which creates a problem: the FreeRTOS heap manager has no awareness of memory operations performed on that core.
For example, pvPortMalloc() guards the heap with a critical section to prevent concurrent access from other FreeRTOS tasks. But since core 1 operates outside the OS, those protections don’t apply. If core 1 modifies the heap while FreeRTOS is active on core 0, the system will eventually crash—often in ways that are difficult to reproduce due to timing interactions between the cores.
Possible solutions
There are probably several solutions to this problem, but I landed on a couple different ones:
- Static allocation on core 1
If core 1 is used exclusively for deterministic, time-critical tasks, it likely doesn’t need dynamic memory. In this case, simply avoid heap use altogether on that core. No race protection is required.
- Delegated allocation via T76_USE_GLOBAL_LOCKS
If this is not acceptable, the T76_USE_GLOBAL_LOCKS macro causes the memory wrappers to spawn a core 0 task that listens for allocation commands on a bare-metal queue. Core 1 then sends its allocation requests to this task, ensuring that all heap access occurs under FreeRTOS supervision.
T76_USE_GLOBAL_LOCKS is primarily designed for circumstances where we want to be able to allocate memory at startup, when the performance hit on core 1 operations is not important (although note that allocations will block on core 1 until the FreeRTOS scheduler starts running). Core 0 performance will remain relatively unaffected, since memory operations performed inside it will just translate into direct calls to pvPort* functions.
You will find these memory additions in this pull request.
Marco Tabini
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.