This is the first post in my FreeRTOS series, which aims to expand upon my previous series Cortex-M7 without hardware by implementing a Real-Time Operating System on top of the bare-metal project. Before going directly to the FreeRTOS implementation details, this part goes through basics of operating systems in general.
Operating systems
While there are many fancy ways of defining an operating system, such as saying that it is “middleman” between a personal computer (the hardware) and a human (the user) by allowing the user to interface with the hardware. I found this I found IBM’s definition to be most fitting when discussing Real-Time Operating Systems:
…a collection of software that manages a computer’s hardware… 1
While this is true for all kinds of operating systems, I think this is apt as Real-Time Operating Systems tend to be really lightweight and focused on this exact point when comparing to General Purpose Operating Systems. You’ll come to see that with FreeRTOS it is simply software for very specific purposes. Now let us ask and answer two simple questions.
What are Real-Time Operating Systems?
The underlying principles of a Real-Time Operating System (RTOS, or RTOSes in plural) are determinism and predictability. This means that a task is expected to produce the same result consistently and complete within a known or bounded amount of time. Thus RTOSs are often used in systems where the set of tasks is limited, predefined, and subject to strict timing constraints. Usually this is also reflected in the hardware where they run on, which is, along with the software, designed for a specific purpose.2
While all RTOSes share the key concepts, there are also different RTOS types: hard, firm and soft. 3 Here is a quick rundown on each:
Hard RTOSes
When reliability is the most important factor, as a failure could cause a loss of life in worst cases. Medical technology and industrial control systems are among the most common application areas for hard RTOSs.
Firm RTOSes
When some delays do not cause issues for operation and are acceptable up to a certain point. Areas for this type are, for example, networking and multimedia systems, where a loss of packet does not cause an issue.
Soft RTOSes
When the focus is on well-timed execution, but delays still allow system to operate correctly with a hit to performance. Web servers and even some desktop operating systems are in this category.
What are General Purpose Operating Systems?
Windows, Linux and Chrome OS all share something common, they all are General Purpose Operating Systems. As the name says they are, well general, which is to say that they are not specific. You might already see the difference between the two. They often run on “general-purpose computers”, hardware which are designed to perform a wide range of tasks, again something that is very different to hardware where RTOS is used.4
These operating systems might be operating in a deterministic and predictable manner like RTOS, but most of the time they are not. 2 This means that a task might be completed in different timeframe each time it is executed due the scheduling that switches between tasks swiftly, causing unpredictability. 4
Bottom line
It would sound that RTOS is superior choice, as who in the world would want to have hardware that behaves in non-deterministic manner with unpredictable timings? Well it is not that simple and both have their places. General Purpose Operating Systems provide flexibility and hardware support that cannot be matched with RTOS. So you can think that both of them are built for a purpose, like taxis (built for flexibility and able to handle many different requests) and trains (built around predefined routes and schedules) are.
FreeRTOS
That is enough about operating systems in general, let us get to the main topic at hand and use it as practical example of a RTOS. FreeRTOS is a open source RTOS for microcontrollers and small microprocessors. It promises small memory footprint and fast execution times, while also providing optional features such as TCP stack. 5 The reason why I chose this RTOS from a myriad of choices because it is familiar to me while supporting a wide range of architectures and compilers.
It should be noted that FreeRTOS is not an operating system that hosts multiple applications like General Purpose Operating Systems do, but a rather a kernel that gets compiled into a single embedded application so that it provides capabilities like multi-tasking. For the sake of clarity I will refer them as a FreeRTOS application(s) from here onwards.
The following sections each explain one fundamental concept of FreeRTOS applications. Later in the series I will dedicate more effort in explaining each of them, but for now these are the high level concepts.
Tasks
Applications are typically divided into tasks, where each is responsible for a specific function. One task could be, for example, reading sensor data, while other could be then processing it (turning it from data to information) and a third task could store that information into a file for later use. Tasks usually contain an infinite loop and might look something like this:
void sensor_task(void *pvParameters)
{
for (;;)
{
// Read sensor
// Do something with the data received from sensor
}
}
Here the pseudo code shows that infinite loop accesses sensor data and does something with it. 6
Task states
Generally there are two states for a task. It is either “Running” or “Not Running”. While there are other states in the “Not Running” category, such as “Ready”, “Blocked”, and “Suspended”, these are not explained here for simplicity.
When a task is “Running”, the processor is executing the code inside the task. When a task is “Not Running” the task is halted and the task’s execution context is saved to be resumed when it returns to “Running” state. 7
Task priorities
Determine which tasks enter running state first. Since tasks with higher priority take precedence over lower priority ones, allowing time sensitive work to be handled quickly. In the example above, reading the sensor data should have the highest priority (i.e. value of ten) as the sensor might output the data in certain intervals, while both processing and storing can each have lower priority (i.e. value of nine or even lower). When same priority is assigned to two different tasks the CPU time can be shared using a time slicing feature. There is no limit on how many tasks share the same priority. 8
Interrupt Service Routine
Or ISR is a piece of code in applications that is executed once hardware or software sends an interrupt signal. So in FreeRTOS applications tasks perform their assigned functions in a loop constantly, the functions inside ISRs are only executed when interrupt is received. An interrupt could be, for example, indication about data arriving from a peripheral. One thing to keep in mind is that when an interrupt happens and the ISR is run, the currently running task is halted. So ISRs preempt even the highest-priority tasks. As such ISRs should contain as little code as possible. 9
Scheduler
There is no software limit, but rather hardware is the limiting factor, on how many tasks can be created. However not all of them can be run at the same time (FreeRTOS by default is designed for single-core microcontrollers), thus it’s up to the scheduler to decide which task gets the CPU time at any one moment. As stated, the highest priority tasks go to “Running” state first, thus in worst case scenario the scheduler might never let lowest priority tasks perform. When designing tasks it is important to make sure that high-priority tasks would go to “Not Running” state at some point. This can be achieved by designing tasks to switch to “Not Running” when they have nothing useful to do, for example by waiting on a queue, semaphore, event group, notification, or delay. 10
Signaling
There are several ways of signaling in FreeRTOS and next sections cover those. Short analogy before we start: a queue is like placing a letter in a shared mailbox, a semaphore is like raising a flag, an event group is like a control panel with multiple indicator lights, and a task notification is like calling a specific person directly.
Queues
Are one of the main ways tasks and ISRs communicate with each other by using First In First Out (FIFO) buffers that can hold a set amount of items (length of the queue).
One effective way of making sure that highest priority tasks do not starve lower-priority tasks is to have them actually wait for something to be processed instead of busy looping (and blocking any other tasks from performing their functions). This “event-driven” technique is best explained by modifying the previous sensor_task function:
void sensor_task(void *pvParameters)
{
for (;;)
{
SensorData item_from_queue{};
// Start waiting for data
xQueueReceive(queue_handle, &item_from_queue, portMAX_DELAY);
// Access sensor data
}
}
Above we see that the function no longer reads the data constantly, but instead waits for it to arrive, as portMAX_DELAY will cause the task to wait indefinitely. Now it is possible to add another function:
void sensor_isr_function(int sensor_data)
{
SensorData item_to_queue{sensor_data};
BaseType_t px_higher_priority_task_woken = pdFALSE;
// Send data to queue
xQueueSendToBackFromISR(queue_handle, &item_to_queue, &px_higher_priority_task_woken);
portYIELD_FROM_ISR(px_higher_priority_task_woken);
}
This function is called when hardware interrupt indicating arriving sensor data has been fired. It places the data inside a struct and hands it forward for queue processing. This function is lightweight and it simply copies the data to a buffer inside the queue implementation and signals the other task so that it goes to the “Running” state. 11
Semaphores
FreeRTOS provides two different kinds of semaphores with binary 12 and counting 13. Both semaphores are essentially queues that do not move any data between tasks. Binary semaphore’s length is one item while counting semaphore’s length is greater than one.
For both semaphore types, the example function thus becomes:
void sensor_task(void *pvParameters)
{
for (;;)
{
// Start waiting for signal
xSemaphoreTake(binary_or_counting_semaphore_handle, portMAX_DELAY);
// Do tasks: i.e. access sensor data by reading it from other task
}
}
While the ISR function looks like this:
void sensor_isr_function(int sensor_data)
{
BaseType_t px_higher_priority_task_woken = pdFALSE;
// Signal
xSemaphoreGiveFromISR(binary_or_counting_semaphore_handle, &px_higher_priority_task_woken);
portYIELD_FROM_ISR(px_higher_priority_task_woken);
}
Since both of them look identical, one might think that they both perform the same. The length however makes a difference, just like with queues. If the semaphore has a length of one the sensor_task does not know exactly how many times the semaphore has been signaled. Why it matters depends on the use case and will be the subject of another blog post.
Event groups
So far we have always waited for one sensor to signal and then done something with the data, how about when a task needs to have data available from multiple places? This is where event groups come in, which represent each event by a bit, and a task can wait until selected bit(s) is / are set:
// Event bit for sensor 1 data
#define SENSOR_1_READY_BIT (1 << 0)
// Event bit for sensor 2 data
#define SENSOR_2_READY_BIT (1 << 1)
void processing_task(void *pvParameters)
{
for (;;)
{
xEventGroupWaitBits(
event_handle,
SENSOR_1_READY_BIT | SENSOR_2_READY_BIT,
pdTRUE, // Clear bits after they are received
pdTRUE, // Wait for all bits
portMAX_DELAY
);
// Both sensors have data available
// Combine/process sensor data here
}
}
void sensor_1_task(void *pvParameters)
{
for (;;)
{
// Read sensor 1 data
xEventGroupSetBits(event_handle, SENSOR_1_READY_BIT);
}
}
void sensor_2_task(void *pvParameters)
{
for (;;)
{
// Read sensor 2 data
xEventGroupSetBits(event_handle, SENSOR_2_READY_BIT);
}
}
Above example shows processing_task waiting for both sensor_1_task and sensor_2_task before combining their results. As bits could be used to represent almost anything, and another example task might wait until either a button is pressed, data is received, or an error occurs. 14
Notifications
Unlike previously mentioned signaling methods, which required a separate kernel object for communication, notifications are built into each task. This allows a task, or an interrupt, to signal another task directly:
void sensor_task(void *pvParameters)
{
for (;;)
{
// Wait for direct notification, NOTE: there is no separate handle here
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// Do tasks: i.e. access sensor data by reading it from other task
}
}
void sensor_isr_function(int sensor_data)
{
BaseType_t px_higher_priority_task_woken = pdFALSE;
// Notify the specific task directly with the direct task handle
vTaskNotifyGiveFromISR(sensor_task_handle, &px_higher_priority_task_woken);
portYIELD_FROM_ISR(px_higher_priority_task_woken);
}
The difference here seems minimal compared to, say semaphores, however it should be noted that task notifications are direct. While queues, semaphores and event groups are separate objects that can be shared more freely between different parts of the application, notifications are directed to a specific task only. This makes them more lightweight as there is no need to have an object (handle) for signaling. 15
Mutex
Stands for mutual exclusion and like in other programming languages it is used to protect a shared resource. However in FreeRTOS taking the mutex may put the task into the “Not Running” state until the mutex becomes available, so it is also a synchronization primitive, just like signals. Mutexes also share the API with semaphores:
void update()
{
if (xSemaphoreTake(mutex_handle, portMAX_DELAY) == pdTRUE)
{
// Only the task that has taken the mutex can access the protected resource.
// Even if the executing task would get blocked,
// other tasks would have to wait for the mutex to be released.
// Perform some update and release
xSemaphoreGive(mutex_handle);
}
}
void task_a(void *pvParameters)
{
for (;;)
{
update();
}
}
void task_b(void *pvParameters)
{
for (;;)
{
update();
}
}
As seen above, a task that acquires the mutex must also release it and a failure to release would lead to a deadlock situation. 16
Next
In Part 2 I will continue the FreeRTOS overview by looking at additional communication mechanisms such as stream buffers and message buffers. After that, the series will move from high-level concepts into a practical code base, where each FreeRTOS concept is implemented and tested step by step.
-sorhanp
References
IBM - What is an operating system: https://www.ibm.com/think/topics/operating-systems ↩︎
Wind River Software - What Is a Real-Time Operating System: https://www.windriver.com/solutions/learning/rtos ↩︎ ↩︎
IBM - What is a real-time operating system: https://www.ibm.com/think/topics/real-time-operating-system ↩︎
Lenovo - What is a general-purpose computer: https://www.lenovo.com/us/en/glossary/general-purpose-computer/ ↩︎ ↩︎
FreeRTOS - home page https://www.freertos.org/ ↩︎
FreeRTOS Kernel Book - Task Functions: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch04.md#42-task-functions ↩︎
FreeRTOS Kernel Book - Top Level Task States: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch04.md#43-top-level-task-states ↩︎
FreeRTOS Kernel Book - Task Priorities: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch04.md#45-task-priorities ↩︎
FreeRTOS Kernel Book - Interrupt Management: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch07.md#7-interrupt-management ↩︎
FreeRTOS Documentation - scheduling: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/01-Tasks-and-co-routines/04-Task-scheduling ↩︎
FreeRTOS Documentation - queues: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/02-Queues-mutexes-and-semaphores/01-Queues ↩︎
FreeRTOS Documentation - binary semaphores: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/02-Queues-mutexes-and-semaphores/02-Binary-semaphores ↩︎
FreeRTOS Documentation - counting semaphores: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/02-Queues-mutexes-and-semaphores/03-Counting-semaphores ↩︎
FreeRTOS Documentation - event groups: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/06-Event-groups ↩︎
FreeRTOS Kernel Book - Task Notifications: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch10.md ↩︎
FreeRTOS Kernel Book - Mutexes (and Binary Semaphores): https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch08.md#83-mutexes-and-binary-semaphores ↩︎