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. These tasks appear to run concurrently, giving the impression of parallel execution Strictly speaking, this is only true on multicore processors, where each core can execute one task at the same time. On a single-core processor, only one task is active at any given moment. FreeRTOS achieves the illusion of concurrency by assigning each task a small time slice (or computation slot) and switching rapidly between them. In the illustration below, the three tasks seem to run at the same time, but in reality the CPU is alternating between them in very short intervals.
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 simulate 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.
By definition FreeRTOS uses a a fixed-priority preemptive scheduling policy, with round-robin time-slicing of equal priority taskSee FREERtos documentation. The term preemptive means that the scheduler always runs the highest priority task that is able to run (stopping if necessary tasks of lower priority). When there are tasks with the same priority, schedule will switch between tasks of equal priority periodically using a Round Robin scheduling algorithm.
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 Raspberry Pico running FreeRTOS?¶
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
vTaskDelay()-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"). We will see in future sections that this is not all history because different interruptions might have different priorities...
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
vTaskDelay()-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, 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 vTaskDelay 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 vTaskDelay 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!
To better understand how FreeRTOS works, it is useful to describe the different states that a task can occupy. Tasks do not always execute; instead, they move between well-defined states depending on system conditions and events.
- Running: A task is Running when it is actually using the CPU. On a single-core processor only one task can be in this state at a time, while on a multicore system one task per core may be running simultaneously.
- Ready: A Ready task is eligible to run but is waiting its turn because another task of equal or higher priority is currently in the Running state. The task will remain ready until the scheduler selects it to execute.
- Blocked:A task enters the Blocked state when it is waiting for something to happen before it can continue. This may be a delay created with
vTaskDelay()(a timing event) or an external trigger such as a message in a queue, a semaphore, or an event group. Usually, a blocked task also has a timeout value: if the expected event does not occur before the timeout, the task is automatically unblocked. While blocked, the task does not consume any CPU time. - Suspended: A Suspended task is also prevented from running, but unlike the Blocked state there is no timeout. A task enter in suspended state using explicitly
vTaskSuspend(). It stays suspended until another part of the program explicitly callsvTaskResume()to restart it.
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");
// Start the scheduler
vTaskStartScheduler();
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. Note that this is the number of words not bytes!!!. We know that the word size is 32 bits. - The size of the stack memory here is 512 words (2048 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 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. If you open the FreeRTOSConfig.h in your implementation, you will find a defined named:
configTICK_RATE_HZ. This defines the period of the Tick (also named time slice). Actually, this period defines how often the scheduler is called.Modifying defaults¶
As mentioned earlier, FreeRTOS is configured through the FreeRTOSConfig.h file. In general, we do not recommend changing the default values, but some advanced students may want to fine-tune FreeRTOS. All the parameters are described in the [[https://www.freertos.org/Documentation/02-Kernel/03-Supported-devices/02-Customization|Customization
section]] of the FreeRTOS documentation. We strongly advise against making any changes to this file unless you have first discussed it with the course staff.
section]] of the FreeRTOS documentation. We strongly advise against making any changes to this file unless you have first discussed it with the course staff.
Where to find additional help?¶
The API reference of FreeRTOS can be found from Official API Reference
In addition the official site offers an excellent online manual about the FreeRTOS Kernel
Finally, Dan Gross has written very good summary of programming FreeRTOS with Raspberry Pi Pico in 4 different blog posts
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 vTaskDelay 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
vTaskDelay-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.