A Simple, Scalable RTOS Initialization Design Pattern

I often find that developers initialize task code in seemingly random places throughout their application. This can make it difficult to make changes to the tasks, but more importantly just difficult to understand what all is happening in the application. It also makes it so that the application is not very scalable or easy to adapt and sometimes results in developers not knowing that a task even exists! There is a simple, scalable RTOS initialization design pattern that I use that resolves all these issues.

The design pattern, which I often follow for as much of my code as possible, is to create a generic initialization function that can loop through a configuration table and initialize all the tasks. Let’s examine how we can create such a design pattern.

Step #1 – Create a Task Initialization Structure

The first step is to examine the task create function for your RTOS and see what parameters are required to initialize a task. This often varies slightly but it usually includes things like:

  • Pointer to the task function
  • Size of the stack
  • Passed parameter pointer
  • Task priority
  • Task handle pointer
  • MPU table pointer

A task initialization structure is just a structure that contains all the parameters required to initialize the task. As I mentioned earlier, this is slightly different between RTOS’s, but an example structure can be seen below for FreeRTOS.

/**
 * Task configuration structure used to create a task configuration table.
 * Note: this is for dynamic memory allocation. We create all the tasks up front
 * dynamically and then never allocate memory again after initialization.
 * todo: This could be updated to allocate tasks statically. 
 */
typedef struct
{
    TaskFunction_t const TaskCodePtr;           /*< Pointer to the task function */
    const char * const TaskName;                /*< String task name             */
    const configSTACK_DEPTH_TYPE StackDepth;    /*< Stack depth                  */
    void * const ParametersPtr;                 /*< Parameter Pointer            */
    UBaseType_t TaskPriority;                   /*< Task Priority                */
    TaskHandle_t * const TaskHandle;            /*< Pointer to task handle       */
}TaskInitParams_t;

Note that we typedef the structure with the name TaskInitParams_t.

Step #2 – Create a Task Configuration Table

Once we have our TaskInitParam_t created, we can create a configuration table that contains all our tasks and the parameters required to initialize them. This is nothing more than creating an array of TaskInitParam_t. Each element in the array contains all the information that we defined in TaskInitParams_t that will then be used to initialize a task. For example, an embedded system may contain a task initialization table that looks like the following:

 /**
 * Task configuration table that contains all the parameters necessary to initialize
 * the system tasks. 
 */
TaskInitParams_t const TaskInitParameters[] = 
{
    // Pointer to the Task function, Task String Name  ,  The task stack depth       ,   Parameter Pointer, Task priority  , Task Handle 
    {(TaskFunction_t)Task_Telemetry,   "Task_Telemetry",    TASK_TELEMETRY_STACK_DEPTH,   &Telemetry, TASK_TELEMETRY_PRIORITY,   NULL       }, 
    {(TaskFunction_t)Task_TxMessaging, "Task_TxMessaging",  TASK_TXMESSAGING_STACK_DEPTH, NULL      , TASK_TXMESSAGING_PRIORITY, NULL       }, 
    {(TaskFunction_t)Task_RxMessaging, "Task_RxMessaging",  TASK_RXMESSAGING_STACK_DEPTH, &Telemetry, TASK_RXMESSAGING_PRIORITY, NULL       }, 
    {(TaskFunction_t)Task_SensorData,  "Task_SensorData",   TASK_SENSOR_STACK_DEPTH,      &Telemetry, TASK_SENSOR_PRIORITY,      NULL       }, 
    {(TaskFunction_t)Task_Diagnostic,  "Task_Diagnostic",   TASK_DIAGNOSTIC_STACK_DEPTH,  &Telemetry, TASK_DIAGNOSTIC_PRIORITY,  NULL       }, 
    {(TaskFunction_t)Task_Application, "Task_Application",  TASK_APPLICATION_STACK_DEPTH, &Telemetry, TASK_APPLICATION_PRIORITY, NULL       }, 
};

You’ll notice that there are a lot of parameters in this table that we have not defined. These are all parameters that we would define in our application for initialising the task. For example, TASK_TELEMETRY_PRIORITY would be defined with many of the other values in a task configuration module.

Step #3 – Create an Initialization Loop

Once the configuration table is created, all that is needed to initialize the tasks is a for loop that loops through the table and then calls the task creation function. Each loop through, we access the structure data and pass it as a parameter to the function. For example, we could initialize the tasks in this example using the following loop:

// Loop through the task table and create each task. 
for(uint8_t TaskCount = 0; TaskCount < TasksToCreate; TaskCount++)
{
    (void)xTaskCreate(TaskInitParameters[TaskCount].TaskCodePtr,
                      TaskInitParameters[TaskCount].TaskName,
                      TaskInitParameters[TaskCount].StackDepth,
                      TaskInitParameters[TaskCount].ParametersPtr,
                      TaskInitParameters[TaskCount].TaskPriority, 
                      TaskInitParameters[TaskCount].TaskHandle);
}

Note that we place (void) in front of xTaskCreate to show that we are ignoring the return value of xTaskCreate. Generally we want to check the return value for a function, but in this case we would create all our tasks during initialization and would not expect to have any memory issues. (Not a good assumption). However, we could rely on the FreeRTOS malloc failed hook to catch any dynamic memory allocation issues during development. Alternatively, we could check the return value and then create a function that checks and recovers if there is an issue. It’s really up to the developer.

Conclusions

This simple RTOS initialization pattern that we’ve just examined is scalable, reusable, and super simple to modify for your purposes. It’s a great example for how developers can leverage configuration-based design. The pattern can be used with any RTOS, but we used the FreeRTOS APIs as an example. Making a change to the task is nothing more than adding or removing an entry in the TaskInitParameters array.

3 thoughts on “A Simple, Scalable RTOS Initialization Design Pattern”

  1. Hi,

    this idea can also be extended to peripherals initialization.
    I.e. one need to put peripherals registers addresses+values into an array and then loop over it.

    Some time ago I saw such a pattern in polish electronics magazine “Elektronika Praktyczna”.

    It also clearly summarizes how the peripherals are initialized, in one common place.

    However, whenever feasible, I prefer to keep the peripherals’ initialization together with the driver code.

    But for simpler applications, this array-based approach might prove cleaner and being easier understood.

    Cheers! 🙂


    Best regards,
    Andrzej Telszewski

    1. Absolutely! I actually use this approach for peripheral intitialization when I am writing the driver myself. Generally I separate the table and the driver so that the table is in a config file and then passed as a pointer to the peripheral init function.

      Thanks for the comment (and technique extrapolation!)

  2. You could validate the return code with an assert during debugging, e.g.
    for (…)
    {
    xReturned = xTaskCreate(…)
    assert (xReturned == pdPASS)
    }

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.