Dynamic memory on a microcontroller doesn't have to mean unpredictable memory.
On a desktop, writing:
void *ptr = malloc(size);
and later:
free(ptr);
is almost invisible to most developers.
On a microcontroller with a few kilobytes of RAM, things can get more interesting.
Your firmware can have plenty of total free memory and still fail an allocation because the available memory has become fragmented into pieces that aren't suitable for the next request.
That's the problem I wanted to attack.
So I built EcclesRTMem — a small, plain-C fixed-pool memory allocator designed for embedded systems where predictable memory behavior matters.
GitHub: https://github.com/Igwe-Starking/eccles-rtmem
The problem with the traditional heap
Imagine a 4 KB memory region.
Over time, your application performs operations like:
allocate 64
allocate 128
allocate 32
free 128
allocate 96
free 64
allocate 256
...
Eventually, the heap can look like:
┌──────┬────────┬────┬──────────┬──────┬───────┐
│ USED │ FREE │USED│ FREE │ USED │ FREE │
└──────┴────────┴────┴──────────┴──────┴───────┘
There may be plenty of free memory in total.
But if the next allocation requires one contiguous region and no sufficiently large region exists, the allocation fails.
That's external fragmentation.
On a desktop with gigabytes of RAM, this may be acceptable.
On a tiny MCU, where a few hundred bytes can matter, I wanted another option.
The idea behind EcclesRTMem
Instead of treating RAM as one giant variable-sized heap, EcclesRTMem divides it into fixed-size block pools.
Conceptually:
┌───────────────────────────────────────────┐
│ RAM │
├────────────┬──────────────┬───────────────┤
│ SMALL │ MEDIUM │ LARGE │
│ blocks │ blocks │ blocks │
├────────────┼──────────────┼───────────────┤
│ [ ][ ][ ] │ [ ][ ]│ [ ][ ]│
│ [ ][ ][ ] │ [ ][ ]│ [ ][ ]│
└────────────┴──────────────┴───────────────┘
An allocation request is assigned to an appropriate pool.
For example, a request for 20 bytes might consume a 32-byte block rather than creating a unique 20-byte hole in a general-purpose heap.
Yes, that creates internal fragmentation.
That's intentional.
The trade-off is:
«Waste some space inside blocks in exchange for keeping the overall allocation structure bounded and predictable.»
It isn't trying to replace every "malloc()"
This is important.
EcclesRTMem isn't intended to compete with sophisticated desktop allocators.
If you're running Linux with gigabytes of RAM, there are many reasons to use the standard allocator or another general-purpose allocator.
EcclesRTMem targets a different environment:
- AVR
- ESP32
- STM32
- RP2040/RP2350
- nRF52/Zephyr
- MSP430
- Arduino
- bare-metal firmware
- RTOS applications
- other resource-constrained systems
The project provides platform-specific locking mechanisms so the same allocator can be used across different embedded environments.
Fixed pools, but what about large allocations?
A fixed-size allocator has an obvious problem:
What happens when the requested object is larger than one block?
EcclesRTMem supports contiguous multi-block allocations.
Conceptually:
Large pool
┌────────┬────────┬────────┬────────┬────────┐
│ FREE │ USED │ USED │ USED │ FREE │
└────────┴────────┴────────┴────────┴────────┘
└───────────────┘
one allocation
The allocator still operates on fixed-size blocks, but several adjacent blocks can form one larger allocation.
This keeps the underlying memory model relatively simple while allowing larger objects.
I wanted "free()" to tell me when I screwed up
Memory bugs are painful enough without the allocator silently accepting bad pointers.
EcclesRTMem includes diagnostics for invalid frees, including situations such as:
- double free
- pointer outside the managed region
- invalid alignment
- pointers into the middle of an allocation
So instead of simply thinking:
"Something somewhere corrupted memory."
you can get a much more useful indication of what happened.
That matters enormously when debugging embedded firmware.
Because when your device is sitting in a workshop refusing to boot, "probably heap corruption" isn't particularly helpful.
The configuration is explicit
Embedded memory should be treated as a resource, not magic.
EcclesRTMem lets you configure things such as:
define ECCLES_RT_MEM_SIZE 4096ul
define ECCLES_RT_BLOCK_A_SIZE 32ul
define ECCLES_RT_BLOCK_B_SIZE 128ul
define ECCLES_RT_BLOCK_C_SIZE 512ul
The goal is to know ahead of time:
- how much RAM the allocator consumes
- how many blocks exist
- what sizes those blocks are
- what happens when the pools are exhausted
The allocator has hard configuration boundaries rather than pretending to provide unlimited memory.
Then I attacked it with randomized testing
This is probably the part I'm most interested in.
Memory allocators are incredibly easy to make look correct.
A few tests like:
allocate
free
allocate
free
prove almost nothing.
So the test suite performs randomized allocation/free workloads.
The repository includes stress tests performing 200,000 randomized operations, with canary patterns written into live allocations and checked throughout the test.
The test matrix also runs under:
- AddressSanitizer
- UndefinedBehaviorSanitizer
- different pool configurations
- different memory sizes
- locking/no-lock configurations
- boundary-oriented configurations
And this testing actually found a bug.
A randomized test exposed an out-of-bounds problem around the 255-block boundary.
That bug was fixed.
That's exactly what I want from an allocator test suite.
Not simply:
«"All tests passed."»
But:
«"The tests were aggressive enough to find something that was wrong."»
Concurrency matters on embedded systems
A memory allocator shared by multiple tasks can't simply assume that every environment works the same way.
An RTOS mutex isn't equivalent to disabling interrupts.
And a mutex that works perfectly from a task isn't automatically safe to use from an ISR.
That's why EcclesRTMem has platform-specific locking backends.
The project supports configurations ranging from RTOS environments to bare-metal/superloop applications where locking can be disabled.
One important limitation remains:
RTOS mutex-based backends should not be used from ISRs unless the particular platform/backend explicitly supports that usage.
That's not something I want an allocator to hide from the developer.
The trade-offs
EcclesRTMem isn't magic.
It has costs.
Internal fragmentation
A 33-byte allocation may consume a 64-byte block.
Pool boundaries
Free space in one pool doesn't automatically become usable space in another.
Bounded registry
The current design has a 255-block registry boundary.
Timing still needs hardware measurement
The design aims for predictable behavior, but formal worst-case timing measurements across real hardware are still future work.
These limitations are important because a serious embedded library should tell you where it stops being the right tool.
Why build another allocator?
Because embedded systems have different priorities.
Sometimes you don't need the most memory-efficient allocator possible.
You need one where you can answer questions like:
How much RAM can this subsystem consume?
What happens when it runs out?
Can a freed block be reused without changing the memory topology?
What happens if somebody double-frees a pointer?
What happens after 200,000 allocations?
What happens under concurrent access?
Can I configure the memory budget at compile time?
Those questions are more important to me than simply asking:
«"Does malloc work?"»
Where EcclesRTMem fits
I don't see it as a universal replacement for dynamic memory.
Think of the options as different tools:
Static allocation
│
│ maximum predictability
│
▼
Fixed pools / EcclesRTMem
│
│ bounded dynamic allocation
│
▼
Custom/general-purpose heaps
│
│ maximum flexibility
│
▼
Standard malloc/free
If you know every object's lifetime and size at compile time, static allocation is often excellent.
If you need dynamic lifetimes but want bounded memory behavior, a fixed-pool allocator becomes interesting.
If you need maximum flexibility and can accept more complex heap behavior, a general-purpose allocator may be appropriate.
The point isn't that one strategy wins everywhere.
The point is having the right tool for the system.
What's next?
There are still several things I want to improve.
Among them:
- fragmentation/statistics reporting
- allocation-failure counters
- optional wider block registries
- additional oversized-allocation strategies
- more hardware-verified locking backends
- measured worst-case timing on actual AVR, Cortex-M and ESP32 hardware
The next stage isn't just adding features.
It's measuring the behavior on real hardware.
Final thought
One thing embedded development keeps teaching me is that memory is hardware.
Every byte is physical.
Every synchronization mechanism has timing implications.
Every allocation strategy has failure modes.
And eventually, every abstraction has to answer to the silicon underneath it.
That's why I built EcclesRTMem.
Not to create the world's most sophisticated allocator.
Not to declare "malloc()" obsolete.
But to provide another option for firmware that needs dynamic memory while keeping its memory behavior bounded and understandable.
If you're working with an MCU where RAM is measured in kilobytes rather than gigabytes, you might find it useful.
Check it out
GitHub: https://github.com/Igwe-Starking/eccles-rtmem
If you use it, break it, benchmark it, or find a better way to solve the same problem, I'd genuinely like to know.
The heap doesn't have to be mysterious.
Top comments (0)