Summary
'Real Work' begins by fixing up some things in the FreeRTOS and HAL libraries, and implementing the skeleton of the 'Default' task. The Default task performs systems initialization and resource usage monitoring.
Deets
First, I update some FreeRTOS settings to get some memory usage statistics.
Phat Stacks of RAM
There are several FreeRTOS configuration settings I like to make that cause the collection of some statistics that I find useful for tuning memory usage and handling errors. In particular, I like to set INCLUDE_uxTaskGetStackHighWaterMark, which will cause FreeRTOS to pattern-fill the task's stack allotment upon creation, and enable the uxTaskGetStackHighWaterMark() API so that you can query (whenever you want) for what is the least amount of stack space (reported in words, which here are 32-bit quantities). I find this a great help because stack space usage is otherwise a wild guess. You will doubtless one day find yourself in the Hard Fault handler, and quite often it is simply that you have overflowed the stack. Unlike with desktop development, the stack on these processes (er, 'tasks') is fixed size -- you don't get any more than what you created with. And you don't want it to be too big or you're just wasting precious RAM.
To turn on the stack reporting feature you can do that in the CubeMX GUI, or you can explicitly do it in the FreeRTOSConfig.h header that is generated for you. In the GUI, it is under 'Middleware', 'FreeRTOS', and on the flyout pane, under 'Include parameters'. To do it in the header (which is located in the 'Inc' directory), there is a section at the bottom between
/* USER CODE BEGIN Defines */
and
/* USER CODE END Defines */
where you can put your own stuff that will not be overwritten by subsequent runs of CubeMX. If you stick stuff there, you might consider #undef ing things before you #define them to something else, just to be robust.
Enabling the API is the first step, now you have to use it. I generally just define some globals like:
volatile int g_nMinStackFreeDefault;
in main.c, and then down near the bottom where the 'default' task thread function is
void StartDefaultTask(void const * argument)
you will see a user code block demarcated wit
/* USER CODE BEGIN 5 */
and
/* USER CODE END 5 */
which contains an infinite loop. In a way, you can view this infinite loops as a kind of cyclic executive, but instead of it being for the entire project, it is just for this one thread (er, task). Anyway, I simply put some code in there which updates the stats:
g_nMinStackFreeDefault = uxTaskGetStackHighWaterMark ( defaultTaskHandle );
and that's really all there is to it. In debugging use, I might set a breakpoint and see what the value is, but as we'll see later, I also typically provide a command over the Monitor interface -- usually named 'diag' -- which will dump this value out at will. So, for tuning, I will exercise the project thoroughly though all it's features and over time, and then see what what the maximum stack usage. Those numbers will give me some comfort in safely reducing stack space allocation to within some iota of that maximum usage. Also, I check these numbers from time-to-time during project development to see if I need to increase stack, or to see the effects of changes I make to the code (which may reduce stack needs despite being functionally equivalent). It's a great help, and it's something I wind up manually implementing in other projects that do not use FreeRTOS.
I do wrap these features in a #ifdef DEBUG wrapper, so they're not in the Release build of my project, but in truth they cost nearly nothing, and I often just leave them in-place even for Release builds.
Heaps of Phun
Another thing that I typically do straight away is replace the heap manager. The use of heap (i.e. what is used for malloc() and free()) in an embedded environment is somewhat contentious because it introduces non-deterministic behaviour. While I appreciate that opinion, I also appreciate the convenience of having heap allocations, and sometimes it's required, anyway.
Much as with stack tuning, there is a similar motivation for heap usage: how much do I need maximally, and how likely am I to succeed at getting it. Heap is more complicated than stack, though, because stack is guaranteed to provide you what you need subject only to the ultimate size limit. Heap doesn't provide that guarantee. Heap allocation can also fail because of fragmentation, so I also want to be able to observe those things.
System Workbench for STM32 (i.e. the pre-packaged Eclipse and gcc toolchain for ARM) use 'newlib-nano' for the standard library implementation, and there is a heap implementation in that. However, the internals are undocumented and inaccessible from the outside, so it is not practical to probe the heap for usage and fragmentation inspection.
FreeRTOS can be configured to use heap for systems structures. I usually do /not/ use heap for this purpose, but nonetheless when so configured FreeRTOS can provide any of several sample heap implementations that represent tradeoffs in complexity and overhead. One of the most fancy is 'heap 4', which works much like malloc() and free(). However it lacks realloc(). Personally, I have used realloc() as many times as I can count on the fingers of one hand in the past 40 years, but I have used libraries which do use it (e.g. Lua uses it almost exclusively). So I implemented my own heap derived from the FreeRTOS 'heap 4' which adds realloc(). And since I was in there, I also added some debug capabilities, notably a 'heapwalk' that allows inspecting the block assignments so that fragmentation can be assessed. Because I am uncreative, I called it 'heap_x.c'. (FreeRTOS has heap_1.c - heap_5.c, and why risk it with '_6'?)
This alternative heap implementation goes in
Middlewares\Third_Party\FreeRTOS\Source\portable\MemMang
and you delete the existing heap_4.c file.
That will satisfy FreeRTOS's use of the heap, though that is a Pyrrhic victory since I'm not going to be using heap with FreeRTOS, anyway. In FreeRTOS's world, you don't do malloc() or free(), but rather do 'pvPortMalloc()' and 'pvPortFree()' (and now also 'pvPortRealloc()'). So in /your/ code you can use the alternative heap implementation, but this doesn't help with heap usage done by code that you did not author.
A common way of helping with that is to make a header that #define's a macro that would redirect the 'malloc' symbol into an equivalent call to 'pvPortMalloc', and that will work for third-party code for which you have the source, but it still won't help with usage that was done in binaries with which you link (e.g. the standard libraries.
There is a facility that is specific to gcc whereby the linker can be told to 'redirect' a symbol to another symbol. This is called 'wrapping' and is is done with some linker flags. These can be found in Eclipse through the path
Project Properties, C/C++ Build, Settings, MCU GCC Linker, Miscellaneous, Linker flags.
The default contents will be something like '-specs-nosys.specs -specs-nano.specs'. We will add some more. One quirk of this UI is that the edit control does not scroll to the cursor, so if you type more it goes beyond the end off-screen! It's easiest to maximize that dialog while adding stuff. At any rate, add the following to what is there:
-Wl,--wrap,malloc -Wl,--wrap,free -Wl,--wrap,realloc -Wl,--wrap,_malloc_r -Wl,--wrap,_free_r -Wl,--wrap,_realloc_r
(now you see what I mean about the non-scrolling edit control).
When you do this, the linker will translate a link-link time any references to the symbols named (e.g. malloc, free, etc.) into a new symbol with the text '__wrap_' prepended to it. You are expected to provide those implementations. In this way, you can funnel all calls to malloc() and friends into your implementation, even if you do not have control of the source code. The implementation of those wrappers is trivial:
void* __wrap_malloc ( size_t size ) { return pvPortMalloc ( size ); }
void __wrap_free ( void* pv ) { vPortFree ( pv ); }
void* __wrap_realloc ( void* pv, size_t size ) { return pvPortRealloc ( pv, size ); }
void* __wrap__malloc_r ( struct _reent* r, size_t size ) { return pvPortMalloc ( size ); }
void __wrap__free_r ( struct _reent* r, void* pv ) { vPortFree ( pv ); }
void* __wrap__realloc_r ( struct _reent* r, void* pv, size_t size ) { return pvPortRealloc ( pv, size ); }
A few final tweaks I like to do is also provide the block of RAM to be used for heap myself, and set up to fill the heap blocks with a known pattern. This makes it easier for me to browse the heap RAM to see what is going on. You do this in the FreeRTOS.h config at the bottom in the 'USER CODE' block where you can put your own defines (I don't think there is a way in the CubeMX UI to set these):
#undef configAPPLICATION_ALLOCATED_HEAP
#define configAPPLICATION_ALLOCATED_HEAP 1
#undef configMALLOC_FILL
#define configMALLOC_FILL 1
The first one will then require you to define ucHeap somewhere, and the second will cause the heap blocks to be filled with a well-known pattern. In my alternative heap implementation, I also fill them with patterns upon free(), which makes it easier to understand what I'm looking at (rather than have the freed blocks contain their former contents).
I just define the heap block in main like this:
__attribute__((aligned(8)))
uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
You could also use this to put your heap in special memory regions if your chip has such, but we don't in this case.
Task Notifications
As mentioned in the previous post, FreeRTOS offers several Inter-Process Communications (IPC) mechanisms. One of which is FreeRTOS-specific, called 'task notifications'. These are simply a bitfield of flags per-process (er, task) which another task can set to cause the targeted task to be notified of its change. They are very lightweight (just an additional uint32_t in the Task Control Block (TCB) structure), and by using the FreeRTOS APIs all the atomicity and task wakening concerns are handled. The first step is to define them in a header. The definition is arbitrary -- it is a private detail between the sender and the responding task -- but in this project I don't expect to have a bunch so I just have one global definition in a file of my creation named 'task_notification_bits.h' which simply defines an enum:
typedef enum
{
//these are generally used for byte streams events
TNB_DAV = 0x00000001, //data is available (serial receive)
TNB_TBMT = 0x00000002, //transmit buffer is empty (serial send)
TNB_LIGHTSCHANGED = 0x00010000, //the lights have changed
};
I will probably add more bits later as the project evolves. For example, the GPS task might notify the WSPR task that lock has been achieved and location and time are updated, and so it is not safe to start transmitting. Similarly, the Monitor task might notify the WSPR task that it has explicitly set the same things.
Lamps Management
What project would be complete without a blinking, blazing, boisterous LED? Well, on the Blue Pill board we only have one: the green on on-board. We could add more, but I'm not planning to for this project.
Flicking a lamp on or off is trivial:
void _ledOnGn ( void ) { HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET); }
void _ledOffGn ( void ) { HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET); }
What is not-so-trivial is flicking it on for a period of time and having it automatically extinguish. It is straightforward to do something like osDelay(1000) between flicking it on or off, but why do I want to suspect my process (er, task) for a second when it could be doing other things? And if I'm using the LED for debugging, such as flicking it on to indicate parity errors or whatnot, then I almost certainly don't want it to wait. But I also want it to be on long enough that the human eye can observe it. So what I do is repurpose the Default task to managing the LEDs, and provide an API that callers can use to flick the LED on for a specified period and carry on with work unhindered.
The API I create looks like this:
//these structs are used in the calls below and maintain state for the one-shot functions
typedef struct LED_LightTime LED_LightTime;
extern LED_LightTime g_lltGn;
//these methods provide 'one-shot' lighting for a period of time.
//this method is intended to be used anywhere, to light a lamp for a period of time
void LightLamp ( uint32_t durMS, LED_LightTime* pllt, void(*pfnOn)(void) );
//this method is intended to be used only in the default task, to maintain and
//turn off the lamp after it's time has expired.
void ProcessLightOffTime ( uint32_t now, uint32_t* pnRemMin, LED_LightTime* pllt, void(*pfnOff)(void) );
The 'LED_LightTime' struct is the state management structure for handling the LED. There would be one of these for each LED in the system (and we just have one -- the green LED). The 'LightLamp' API takes a time that you want it to stay lit, a reference to the management structure for that lamp, and the function that can turn in on. We defined that function earlier for the green lamp, '_ledOnGn'. The 'ProcessLightOffTime' function is used by the Default task to manage the state of the light, and extinguish it when it's time.
I keep all that stuff in a file named 'lamps.h' and concomitantly 'lamps.c'. In lamps.c, there is implementation:
#include "lamps.h"
#include "main.h" //for project-specific GPIO bit definitions
#include "stm32f1xx_hal.h" //for HAL gpio functions
#include "cmsis_os.h" //for OS (FreeRTOS in this case) stuff
#include "task_notification_bits.h" //bithsz
extern osThreadId defaultTaskHandle; //in main.c
void _ledOnGn ( void ) { HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET); }
void _ledOffGn ( void ) { HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET); }
struct LED_LightTime
{
uint32_t _nStart;
uint32_t _nDur;
};
LED_LightTime g_lltGn = {0};
//turn a light on for a while
//this is used wherever it is desired to turn on a lamp for a period of time.
void LightLamp ( uint32_t durMS, LED_LightTime* pllt, void(*pfnOn)(void) )
{
pllt->_nDur = durMS;
pllt->_nStart = HAL_GetTick();
pfnOn();
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTaskNotifyFromISR ( defaultTaskHandle, TNB_LIGHTSCHANGED, eSetBits, &xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
//turn a light off if it is time, and update remaining time otherwise
//this is used in the task that turns off lamps after a period of time
void ProcessLightOffTime ( uint32_t now, uint32_t* pnRemMin, LED_LightTime* pllt, void(*pfnOff)(void) )
{
if ( 0 != pllt->_nStart )
{
uint32_t dur = now - pllt->_nStart;
if ( dur > pllt->_nDur )
{
pfnOff();
pllt->_nStart = 0;
}
else
{
uint32_t rem = pllt->_nDur - dur;
if ( rem < *pnRemMin )
*pnRemMin = rem;
}
}
}
So, LightLamp() takes note of the start time and the desired duration, turns the light on, and sends a task notification to the Default task that the lights have changed. You invoke it like this:
LightLamp ( 1000, &g_lltGn, _ledOnGn );
and that can be done both from a userland task and also from within an ISR.
This will cause the Default task to awaken, and it will call ProcessLightOffTime to determine if it needs to shut off a lamp. If it's not time to shut off a lamp, ProcessLightOffTime will indicate how much time needs to elapse before a shutoff needs to happen, and the Default task will effectively sleep for that period of time before checking again.
This is realized in the Default task's 'forever' loop. Up to now, we just had that loop sleep for one second, and collect memory statistics. Now we add some more stuff:
//Infinite loop
uint32_t msWait = 1000;
for(;;)
{
//wait on various task notifications
uint32_t ulNotificationValue;
BaseType_t xResult = xTaskNotifyWait( pdFALSE, //Don't clear bits on entry.
0xffffffff, //Clear all bits on exit.
&ulNotificationValue, //Stores the notified value.
pdMS_TO_TICKS(msWait) );
if( xResult == pdPASS )
{
//the lights have changed
if ( ulNotificationValue & TNB_LIGHTSCHANGED )
{
//XXX if you want to, you can do something special
}
}
else //timeout on wait
{
//XXX if you want to, you can do something like a periodic idle timeout
}
#ifdef DEBUG
//heap measurements
g_nHeapFree = xPortGetFreeHeapSize();
g_nMinEverHeapFree = xPortGetMinimumEverFreeHeapSize();
//free stack space measurements
g_nMinStackFreeDefault = uxTaskGetStackHighWaterMark ( defaultTaskHandle );
#endif
//turn out the lights, the party's over
uint32_t now = HAL_GetTick();
uint32_t remMin = 0xffffffff; //nothing yet
ProcessLightOffTime ( now, &remMin, &g_lltGn, _ledOffGn );
//don't wait longer than 3 sec
if ( remMin > 3000 )
remMin = 3000;
msWait = remMin;
}
Here the Default task is made to wait for a task notification event, or a timeout. We use the timeout to keep the periodic polling of the heap and stack up-to-date as before, and the task notification here serves to wake up and check for the lamp timeout action. The period the task sleeps is no longer fixed, but rather is the minimum of 3 seconds or however long is left before a lamp needs to be extinguished. In a way, this is like a mini tickless scheduler. The three seconds is an arbitrary choice for the freshness of the globally registered heap/stack usage statistics, and you can adjust that to taste.
Next
Defining the IO stream abstraction, binding the serial ports to it, and fixing up some HAL oddities.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.