Implementation of FreeRTOS program¶
Learning goals: The implementation of an embedded program in Pico
In this material we explore, how to create an embedded program for the Pico in a task-based manner, allowing multiple tasks to run concurrently in the device’s RTOS.
As hinted in previous material, FreeRTOS’s core idea is that the functionality of the program is implemented as separate tasks that run concurrently based on the device’s performance. The programmer has the freedom to define how the program is divided into tasks and what those tasks do internally. While a simple program can be implemented with just a single task, a more complex program - such as one using multiple peripheral devices - cannot easily be handled within a single task. Properly implemented multitasking significantly improves program's performance because tasks don’t have to wait for each other to complete.
This is where the programmer’s design decisions come into play. Of course, it’s not sensible to create a separate task for every minor detail because that would consume resources allocated to the RTOS. It’s also reasonable to implement the internal functions of a task as functions themselves. For example, calculating the area of a circle doesn’t require a separate task; a simple C function suffices, which we can call from a task when needed.
To achieve concurrent execution of tasks on a single microcontroller, the execution time must be shared among tasks (and the RTOS). Time sharing can be based on time-based allocation, priority-based allocation, or reactive handling of inputs or various signals as they arrive to the device. When sharing execution time, it’s essential to ensure that a task doesn’t take all the time, preventing other device operations from running for extended periods. Such a situation could occur, for instance, when a high-priority task starves lower-priority tasks by consuming all the execution time. Another similar scenario might arise when a task communicates with a (relatively) slow peripheral device and selfishly holds the processor during waiting periods.
How it works in Pico¶
Let's see with an example picture, how different tasks can be made to work in parallel in Pico. In the example the following program has been implemented:
- Two tasks related to the program, Tasks 1 and 2.
- One ready-made task: Communications task, that is expected to be included in the program as a part of a library.
- One interrupt handler, that implements the handling of button presses in the program.
In the picture, the tasks have some execution time and priority. Additionally, we can observe that after the execution of some tasks, they are put to sleep by using
sleep
-function call. We can see that the program tasks run with a higher priority (2) than the library task (1), and that the priority of the interruption is the highest of them all ("infinite").Next, let's see what happens in the program:
- The execution of the program starts from Task 1, that is ran and then set to sleep
- After that, the processor frees up and RTOS gives execution to Task 2, which is also set to sleep
- After this, the RTOS again has the power to decide what happens in the device, and because there are no tasks with priority 2 awake in the program at that time, the RTOS executes a task with priority 1 (communications task).
- When the execution of communications task ends, RTOS notices again a task with priority 2, that is awake (Task 1), and executes it.
- During the execution of Task 1, the microcontroller gets an interrupt signal, and because the priority of an interrupt is infinite, the RTOS switches to execute an interrupt handler in the middle of the execution of Task 1!
- When the interrupt handler has been executed, RTOS resumes running Task 1 where it left off.
- And this is how the execution of the program with three tasks and one interrupt handler continues until the end of the world...
But wait a minute, what happens in an interrupt handler? Now we will have to go a bit ahead of things, because an interrupt is a hardware-based signal that is delivered to the microcontroller, and in contrast, executing other tasks is part of the functionality of software (RTOS). Now (here) the priority inifinity meant that the RTOS could not say anything about the execution of interrupts, but they are handled in the microcontroller itself. (This is not a perfect explanation of using interrupts, but they are covered in in the material later on.)
Ok, one more thing... why does the communications task not have a
sleep
-function call? Here the explanation is that the functionality of the wireless radio (or the way it has been implemented in the example) requires, that the task is always running, and is never sleeping. For this reason, the priority of the communications task is 1, when the other tasks have a priority of 2, so they will gain execution time.In addition, we need to know one more thing about task execution. From the presentation, it might seem that each task runs from start to finish by triggering it once, but that’s not how it works in this course. In reality, we use an internal superloop within the tasks - a
while
loop where the sleep
function is called! This way, the tasks we create once in the program remain active and keep the program execution going continuously. To achieve this, we call the sleep
function at the end of each iteration of the superloop. Within a task, we start a timer, during which it sleeps, allowing other tasks to run. The appropriate sleep time depends on the application and can also be determined by the programmer.Now, what would happen if tasks didn’t have this superloop? Since a task is essentially a C function, it would execute only once, and as a result, the entire embedded program would terminate once all its tasks had run once. Superloops are crucial for keeping the program alive on the device!
RTOS's task-library¶
FreeRTOS makes it possible to implement the program depicted earlier and a multi-tasking environment for the Pico by providing the
task
-library. With this library, tasks can be created as functions, and their execution can be controlled through RTOS.Let's look at a code example describing the functionality of
task
-library#include <FreeRTOS.h>
#include <task.h>
#include <stdio.h>
#include <pico/stdlib.h>
#include <inttypes.h>
// (en) A task needs its own internal stack memory
#define STACKSIZE 512
// (en) Implementation of task function
void myTaskFxn(void * pvParameters) {
// (en) The eternal life of a task
while(1) {
printf("My arguments are %d\n", (int)pvParameters);
// (en) Politely sleeping for a moment
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
int main(void) {
TaskHandle_t myTaskHandle = NULL;
// (en) We create a task
BaseType_t result = xTaskCreate(
myTaskFxn, // (en) Task function
"MY_TASK", // (en) Name of the task
STACKSIZE, // (en) Size of the stack for this task
(void *) 127, // (en) Arguments of the task
2, // (en) Priority of this task
&myTaskHandle // (en) A handle to control the execution of this task
);
if(result != pdPASS) {
printf("Task create failed\n");
return 0;
}
// (en) Greetings to the console
printf("Hello world!\n");
return 0;
}
Let's dissect this example.
In the task function
myTaskFxn
we have the earlier mentioned inifinite loop, and at the end of it, we have nicely included the vTaskDelay
-function that implements the sleep functionality and gives the power back to the RTOS after every iteration.Then, in the
main
-function, as a new thing the execution parameters of a task are set in the function xTaskCreate
. In the example, we give the task two things, that are very important for every task:- We set the the size of the stack memory (let's agree at this stage that the stack is the task's own internal memory area, that we don't have to care about) with the parameter
STACKSIZE
. - The size of the stack memory here is 512 bytes, that is enough for most of the tasks. Exceptions to this rule are presented in the course material.
- We set the priority of the task
- Priority for tasks of the program should always be 2
- Priority for communications task should be 1
- If you feel that the priorities of your program should be modified, please ask about this from the teacher first.
Additionally, we set a handle for the task, that can be used to reference the task in our program. So, a handle is just an identifier for a task.
As an argument for a task can be given one value of type void pointer. Well, we use this as the RTOS wishes, but giving arguments to a task is not necessarily needed in the course, as we are going to use for example global variables to pass values.
Task is created with the
xTaskCreate
-function, that is given the above explained things as parameters.Clock is ticking¶
RTOS offers us a variable
portTICK_PERIOD_MS
, that tells us how many milliseconds one system clock period (tick) takes. Now, we can calculate our desired amount of clock periods with the formula n / portTICK_PERIOD_MS
, where n
is time in milliseconds.Actually, for the hardware enthusiasts, here the clock period of RTOS is just a programmatical constant, and does not match 1:1 the clock component of the device.
To conclude¶
In the course laboratory exercise, students are given a program template that includes ready-made implementations of necessary tasks and parameter definitions, so they don’t need to be read from outside of the material. (optional additional material is provided for reading).
Free Tip 1: If the device gets stuck when running a program, the most common reason is either a task that runs for too long or a missing
vTaskDelay
-call in the task function. In such cases, it’s advisable to redesign the task and break it down into smaller parts.Free Tip 2: Avoid making multiple consecutive sleep calls within the task function, as this solution often causes problems when you add functionality to the program and the previously set timings go wrong. Stick to just one
sleep
-call within the task function, and place the functionalities behind conditional structures and a state machine (see upcoming material).Free Tip 3: From time to time you’ll encounter solutions where different priority levels are used. However, this often leads to device malfunctions when additional functionality is added. It’s better to stay at priority level 2 and reconsider how tasks operate. You can always ask the assistants for guidance.
Free Tip 4: When implementing Pico’s program, it’s not advisable to follow the Arduino-style approach with a single task and a superloop. Doing so would be a disservice, as it’s likely to cause problems in the program logic and timings between functionalities.