Cube Sucks. But, if you’ve gotta use it, at least it has demos and things right? And you can use FreeRTOS, that’s integrated, right?
Not really. At the time of writing, working with STM32WB, the out of the box FreeRTOS demos will have problems with printf. Or, well, anything that uses a raw malloc() call.
Out of the box, the cube demo code uses FreeRTOS’s heap_4.c for memory management. This is fine. It means that you select a heap size in your FreeRTOSConfig.h file, and the port provides pvMalloc and pvFree that do the right thing. But, if any code, (anywhere!) uses malloc() itself, directly, then…. where does that come from? Well, it ends up in _sbrk() and the out of box implementation in the cube demos is completely busted. They provide you with the same implementation as for the non FreeRTOS demo.
So, looking at a “classic” memory arrangement, with heap starting at the end of BSS/DATA, and stack growing downwards, you have something like this. (_estack
and _end
are in the default out of box linker scripts from ST) FreeRTOS’s heap_4.c declares “ucHEAP
” (you can statically provide it to, if you like) based on your user provided configTOTAL_HEAP_SIZE
which then becomes another (somewhat large) chunk of “DATA” space.
The _sbrk()
implementation basically checks that malloc() is asking for space that’s above _end
and below the current stack pointer. This is fine when you’re not using an RTOS, but! The RTOS tasks have their own stacks, allocated from the RTOS heap! This is good and right, but it means that the out of box sbrk implementation is now absolutely garbage. It will never succeed, as every single tasks’ stack pointer will be below _end.
You have options here. https://nadler.com/embedded/newlibAndFreeRTOS.html has written extensively, though I think they make it all sound really complicated.
Option 1 – Just don’t call malloc (or… call it early…)
This is a garbage option. You either need to make sure you never call malloc, or make sure that you only ever call malloc before you start the FreeRTOS task scheduler. This is safe, as the memory you allocate here will be in safely in the gap above DATA on the diagram, but you must make all malloc() calls before starting the task scheduler. This requires no changes to the _sbrk()
system call implementation
Option 2 – Use newlib completely
This is Nadler’s approach. Switch heap_4 out for heap_3, rewrite your system calls (including _sbrk() to make all of FreeRTOS use standard newlib malloc/free natively. This isn’t a bad option, but it’s fairly messy, isn’t really something the FreeRTOS people like much, and puts a lot of reliance on how well newlib behaves. The upside here is that you only have one memory management mechanism for the entire system here. (I failed to get this working at the time of writing)
Option 3 – Fix _sbrk() only
You can also just fix _sbrk
. The upside here is the change is trivial. The downside is that you now have two heaps. One that you statically give to FreeRTOS via the config options for heap_4.c (ucHEAP
in the DATA section), and a new one, growing upwards on demand, classic style, above DATA, show in red on this diagram. Remember, that in the FreeRTOS context, all of the space marked in purple and red is untouched, or “wasted”. You want to size your ucHEAP that you leave “enough” for untracked raw malloc users, but still big enough for your polite, well behaved FreeRTOS consumers.
The change itself is straightforward, just stop looking at the stack pointer, and look at the linker script provided pointer to the end of ram.
--- /out-of-box/application/sysmem.c 2021-03-26 10:02:54.045633733 +0000
+++ /fixed/application/sysmem.c 2021-04-07 13:58:27.610330156 +0000
@@ -37,18 +37,19 @@
**/
caddr_t _sbrk(int incr)
{
extern char end asm("end");
static char *heap_end;
char *prev_heap_end;
+ extern char _estack;
if (heap_end == 0)
heap_end = &end;
prev_heap_end = heap_end;
- if (heap_end + incr > stack_ptr)
+ if (heap_end + incr > &_estack)
{
errno = ENOMEM;
return (caddr_t) -1;
}
heap_end += incr;
And that’s it. The _estack symbol is already provided in the out of box linker scripts. Risks here are that you are only checking against end of ram. Conceivably, if you ran lots of deeply nested code with lots of malloc() calls before you start the scheduler, you might end up allocating heap space up over your running stack. You’d have to be very tight on memory usage for that to happen though, and if that’s the case, you probably want to be re-evaluating other choices.
Option 4 – Make newlib use FreeRTOS memory
There is another option, which is to make newlib internally use FreeRTOS pvMalloc/pvFree() instead of their own malloc/free. This is the logical inverse of Option 2, and has the same sorts of tradeoffs. It’s probably complicated to get right, but you end up with only one memory management system and one pool.