Comprehensive Guide to Real-Time Systems: Understanding FreeRTOS, Task Scheduling, and Advanced Synchronization
What is Real Time Application (RTA)
A real-time is one in which the correctness of computations depends not only on their logical accuracy but also on the time at which the result is produced. If timing constraints are not met, it can lead to system failure.
This means that in a real-time system, meeting deadlines is as important as ensuring correct results.
The curve illustrates the difference in response time between a real-time application (RTA) and a non-real-time application (NRTA) over several execution iterations (E1, E2, E3, E4).
The goal of a real-time system is therefore to maintain a constant response time (like the RTA curve), ensuring precise timing and the reliable execution of critical tasks.
What is a Real-Time Application (RTA)?
Examples of real-time applications:
What is a Real Time OS?
It’s an OS, specially designed to run applications with very precise timing and a high degree of reliability.
To be considered as "real-time", an operating system must have a known maximum time for each of the critical operations that it performs. Some of these operations include:
RTOS vs GPOS
A GPOS (such as Windows, Linux, iOS) is intended for general-purpose use, with no requirement for real-time response.
An RTOS is designed to run applications with strict timing requirements and a high level of reliability. It is optimized to minimize interrupt latency and ensure priority scheduling for critical tasks. Examples of RTOS:
GPOS (General Purpose Operating System):
RTOS (Real-Time Operating System):
2. GPOS vs RTOS: Interrupt Latency
In Computing, Latency means The time that elapses between a stimulus and the response to it.
Task switching latency means, that time gap between A triggering of an event and the time at which the task which takes care of that event is allowed to run on the CPU.
Interrupt Latency
Interrupt latency is the time elapsed between the triggering of an interrupt (IRQ) and the start of the Interrupt Service Routine (ISR) execution on the processor.
Imagine Task-1 is currently running, and an interrupt (IRQ) occurs. Interrupt latency is the time required for the ISR to take control of the processor and handle the interrupt.
Scheduling Latency
Scheduling latency is the time elapsed between the end of the ISR (or the previous task) and the start of the next task, usually the one with the highest priority.
Once the ISR is complete, the system must decide which task to execute next. This latency includes context switching: the system saves the previous task’s state and restores the state of the new task to be executed.
3. GPOS vs RTOS: Priority Inversion
Priority Inversion is an issue that occurs when a higher-priority task is blocked by a lower-priority task holding a needed resource.
RTOS (Real-Time Operating System):
GPOS (General Purpose Operating System):
What are the features that a RTOS has but a GPOS doesn't?
What is Multi-Tasking?
Multi-Tasking involves executing multiple tasks almost simultaneously by sharing the processor's time among them.
Role of the Scheduler: A scheduler organizes task execution by allocating "time slices" to each task, creating the illusion that they are running in parallel, even on a single-core processor.
Multicore Systems: On a multicore processor, each core can execute a different task, allowing true parallelism without the need for complex scheduling.
What is Task?
A task is nothing but just a piece of code which is schedulable
How do we Create and implement a task in FreeRTOS?
BaseType_t xTaskCreate ( TaskFunction_t pvTaskCode,
const char * constpcName,
unsigned short usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask
);
The xTaskCreate API in FreeRTOS dynamically creates a task in memory. Before using any task, it must first be created in memory, along with the associated stack space for that task.
-pvTaskCode: Specifies the name of the function that defines the task's behavior.
-constpcName: Provides a descriptive name for the task.
-usStackDepth: Defines the stack size for this task. Each task function has its own stack space, which is used to store local variables and save the task’s context during context switches.
-pvParameters: Allows passing a pointer to data that the task function may need.
-uxPriority: Sets the priority level of the task.
-pxCreatedTask: Serves as an identifier (or handle) for the created task.
void vATaskFunction (void *pvParameters)
{
for( ;; )
{
-- Task application code here --
}
vTaskDelete(NULL);
}
Local (non-static) variables are unique to each task, so each task has its own copy. Static variables are shared among all tasks using the same function, so changes made by one task affect the others.
Exercice:
Write a program to create 2 tasks Task-1 and Task-2 with same priorities.
When Task-1 executes it should print “Hello World from Task-1”
And when Task-2 executes it should print “Hello World from Task-2”
Case 1: Use ARM Semi-hosting feature to print logs on the console
Case 2: Use UART peripheral of the MCU to print logs
You will find the source code in my GitHub link, under the folder "STM32_HelloWorld".
Semi-hosting is a method that allows microcontrollers to display printf messages on a PC via a debugger like OpenOCD, which captures and displays the messages using low-level features of the microcontroller. This is particularly useful for debugging on Cortex-M0 and Cortex-M0+ microcontrollers, which do not support the ITM (Instrumentation Trace Macrocell) output available on Cortex-M3, Cortex-M4, and Cortex-M7 models.
The result of this exercise, using Tera Term, shows that Task-1 and Task-2, both with the same priority, alternate in printing "Hello World from Task-1" and "Hello World from Task-2." This demonstrates that with equal priority, FreeRTOS schedules tasks in a round-robin manner. Initially, only one task was executing continuously, as there was no forced handover. By adding taskYIELD(), the scheduler is prompted to yield control, allowing the tasks to switch context and alternate execution. This forced context switch enables both Task-1 and Task-2 to run consecutively, producing the expected output.
taskYIELD() : forces a context switch, allowing the scheduler to hand control over to another task of the same priority.
What is SEGGER System View?
SystemView is a software toolkit which is used to analyze the embedded software behavior running on your target.
The embedded software may contain embedded OS or RTOS or it could be non-OS based application.
The SystemView can be used to analyze how your embedded code is behaving on the target.
Example: In the case of FreeRTOS application
It sheds light on what exactly happened in which order, which interrupt has triggered which task switch, which interrupt and task has called which API function of the underlying RTOS.
SystemView should be used to verify that the embedded system behaves as expected and can be used to find problems and inefficiencies, such as superfluous and spurious interrupts, and unexpected task changes.
SystemView toolkit comes in 2 parts:
SystemView Visualization Modes:
In this mode, a J-Link debugger is used to continuously transfer data from the embedded application to the host computer via the Real-Time Transfer (RTT) interface. The application, RTOS events, and other actions are recorded live in the target RAM and then sent to the SystemViewer host software. This allows real-time visualization without any interruptions.
This mode does not require a debugger like J-Link. The recording is manually triggered from within the application to capture only specific parts of the system's behavior. The data is stored in an RTT buffer in memory and then exported as an .SVDat file, which can be analyzed in the SystemViewer host software.
Idle Task
The Idle task is created automatically when the RTOS scheduler is started to ensure there is always at least one task that is able to run.
It is created at the lowest possible priority to ensure it does not use any CPU time if there are higher priority application tasks in the ready state.
Idle task hook function implements a callback from idle task to your application.
You have to enable the idle task hook function feature by setting this config item configUSE_TICK_HOOK to 1 within FreeRTOSConfig.h.
Then implement the below function in your application:
void vApplicationIdleHook( void );
That's it, whenever idle task is allowed to run, your hook function will get called, where you can do some useful stuff like sending the MCU to lower mode to save power.
Time Services Task (Time_svc)
FreeRTOS Scheduler
Scheduler is a piece of kernel code responsible for deciding which task should be executing at any particular time on the CPU.
The Scheduler decides which thread should be executing by examining the priority assigned to each thread by the application writer.
In the simplest design higher priority can be assigned to those threads that implement hard real requirements and lower priorities to threads that implement soft real time requirements.
Why do we need Scheduler?
Scheduling Policies (Scheduler types)
The scheduling policy is the algorithm used by the scheduler to decide which task to execute at any point in time.
FreeRTOS or most of the Real-Time OS would most likely be using Priority-based Pre-emptive Scheduling by default.
The scheduling policy is the algorithm used by the scheduler to decide which task to execute at any point in time.
FreeRTOS scheduler Implementation
In FreeRTOS the scheduler code is actually combination of FreeRTOS Generic code (tasks.c) + Architecture specific codes (port.c)
If you are using ARM Cortex Mx processor then you should be able locate the below interrupt handlers in port.c which are part of the scheduler implementation of freeRTOS.
Three important kernel interrupt handlers responsible for scheduling of tasks.
vTaskStartScheduler()
The xPortStartScheduler() function performs the following tasks:
FreeRTOS Kernel Interrupts
When FreeRTOS runs on ARM Cortex Mx Processor based MCU, below interrupts are used to implement the Scheduling of Tasks.
If SysTick interrupt is used for some other purpose in your application, then you may use any other available timer peripheral.
All interrupts are configured at the lowest interrupt priority possible.
The RTOS Tick-why it is needed?
Example:
vTaskDelay(100): task is going to sleep for 100ms. That means, the kernel has to wake up this task and schedule it on the CPU after 100ms.
Whenever Tick interrupt happens:
Who configures RTOS tick Timer?
The RTOS tick timer configuration process is handled by the xPortStartScheduler() function, which is called within vTaskStartScheduler(). This function, located in port.c, performs three key tasks:
What is Context Switching?
Task State
Exercice:
Create 2 Tasks in your FreeRTOS application led_task and button_task.
Button Task should continuously poll the button status of the board and if pressed it should update the flag variable.
Led Task should turn on the LED if button flag is SET, otherwise it should turn off the LED.
Use same freeRTOS task priorities for both the tasks.
Note:
On nucleo-F446RE board the LED is connected to PA5 pin and button is connected to PC13
If you are using any other board, then please find out where exactly the button and LEDs are connected on your board.
You will find the source code in my GitHub link, under the folder "STM32_FreeRTOS_Led_and_Button".
Exercice:
Write a FreeRTOS application which creates only 1 task : led_task and it should toggle the led when you press the button by checking the button status flag. The button interrupt handler must update the button status flag.
You will find the source code in my GitHub link, under the folder "STM32_FreeRTOS_Led_and_Button_IT".
Exercice:
Write a program which creates 2 tasks task_led and task_button with equal priorities.
When button is pressed, task_button should notify the task_led and task_led should run on the CPU to toggle the LED. Also task_led should print how many times user has pressed the button so far.
task_led should not unnecessarily run on the CPU and it should be in Block mode until it receives the notification from the task_button.
You will find the source code in my GitHub link, under the folder "STM32_FreeRTOS_Tasks_Notify".
RTOS Task Notification
Each RTOS task has a 32-bit notification value which is initialised to zero when the RTOS task is created.
An RTOS task notification is an event sent directly to a task that can unblock the receiving task, and optionally update the receiving task's notification value in a number of different ways. For example, a notification may overwrite the receiving task's notification value, or just set one or more bits in the receiving task's notification value.
Wait and Notify APIs
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait );
-ulBitsToClearOnEntry : Any bits set in ulBitsToClearOnEntry will be cleared in the calling RTOS task's notification value on entry to the xTaskNotifyWait() function (before the task waits for a new notification) provided a notification is not already pending when xTaskNotifyWait() is called. For example, if ulBitsToClearOnEntry is 0x01, then bit 0 of the task's notification value will be cleared on entry to the function. Setting ulBitsToClearOnEntry to 0xffffffff (ULONG_MAX) will clear all the bits in the task's notification value, effectively clearing the value to 0.
-ulBitsToClearOnExit: Any bits set in ulBitsToClearOnExit will be cleared in the calling RTOS task's notification value before xTaskNotifyWait() function exits if a notification was received. The bits are cleared after the RTOS task's notification value has been saved in *pulNotificationValue (see the description of pulNotificationValue below). For example, if ulBitsToClearOnExit is 0x03, then bit 0 and bit 1 of the task's notification value will be cleared before the function exits. Setting ulBitsToClearOnExit to 0xffffffff (ULONG_MAX) will clear all the bits in the task's notification value, effectively clearing the value to 0.
-pulNotificationValue: Used to pass out the RTOS task's notification value. The value copied to *pulNotificationValue is the RTOS task's notification value as it was before any bits were cleared due to the ulBitsToClearOnExit setting. If the notification value is not required then set pulNotificationValue to NULL.
-xTicksToWait: The maximum time to wait in the Blocked state for a notification to be received if a notification is not already pending when xTaskNotifyWait() is called. The RTOS task does not consume any CPU time when it is in the Blocked state. The time is specified in RTOS tick periods. The pdMS_TO_TICKS() macro can be used to convert a time specified in milliseconds into a time specified in ticks.
xTaskNotifyWait() return value:
pdTRUE if a notification was received, or a notification was already pending when xTaskNotifyWait() was called.
pdFALSE if the call to xTaskNotifyWait() timed out before a notification was received.
-Write a 32-bit number to the notification value
-Add one (increment) the notification value
-Set one or more bits in the notification value
-Leave the notification value unchanged
This function must not be called from an interrupt service routine (ISR). Use xTaskNotifyFromISR() instead.
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction );
-xTaskToNotify: The handle of the RTOS task being notified. This is the subject task.
-ulValue: Used to update the notification value of the subject task. See the description of the eAction parameter below
-eAction: An enumerated type that can take one of the values documented in the table below in order to perform the associated action:
eNoAction / eIncrement / eSetValueWithOverwrite / eSetValueWithoutOverwrite
FreeRTOS License
Free and Commercial Use: FreeRTOS is a free RTOS kernel that you can use in commercial applications without paying royalties. You don’t need permission from freertos.org to use it.
Open Source and GPL License: FreeRTOS is licensed under the GNU GPL, which means that if you modify the kernel itself (files provided by freertos.org), you must make these modifications open source. However, applications that use FreeRTOS APIs do not need to be open-sourced.
No Warranty and Limited Technical Support: freertos.org does not provide warranty or technical support. They fix reported bugs, but developing your application remains your responsibility.
Recommended by LinkedIn
Security Standards:
Commercial Versions: SAFERTOS and OpenRTOS:
A derivative version of FreeRTOS, developed to meet the IEC 61508 SIL 3 safety standard requirements.
It is tested and audited for safety-critical applications but is not free and requires a commercial license.
A commercial version of FreeRTOS without GPL references, meaning OpenRTOS users are not required to make kernel modifications open source.
No Security Compliance: OpenRTOS does not meet security standards and is therefore not recommended for safety-critical applications.
Technical Support and Legal Protection: As a commercial version, OpenRTOS includes technical support and some legal protection, particularly for intellectual property (IP).
FreeRTOS API Interface
FreeRTOS Memory Management
RAM and Flash are the two main types of memory in microcontrollers.
RAM is smaller than Flash and is mainly used to store application data (global variables, arrays) and for temporary code execution.
Flash is non-volatile memory that stores the application code, constants, and the interrupt vector table.
RAM is used to:
Flash is mainly used to:
Stack Management
The stack follows a LIFO (Last In, First Out) model.
Heap Management
The heap is a region for dynamic memory allocations, often used for structures like linked lists.
FreeRTOS Stack and heap management
Each task in FreeRTOS has a Task Control Block (TCB) that manages its state.
There are 2 options:
if you use dynamic creation method then they will be created in the heap memory of the RAM
if you create them statically then they will be created in other part of the RAM except heap and stack space
FreeRTOS Heap management Schemes
FreeRTOS Synchronization
Synchronization refers to the idea that multiple processes are to join up or handshake at a certain point, in order to reach an agreement or commit to a certain sequence of action.
How to achieve this signaling?
Mutual Exlusion Services of FreeRTOS
Mutual exclusion means that only a single thread should be able to access the shared resource at any given point of time. This avoids the race conditions between threads acquiring the resource. Usually, you have to lock that resource before using and unlock it after you finish accessing the resource.
Synchronization means that you synchronize/order the access of multiple threads to the shared resource.
Deleting a Task
The API to delete a task is vTaskDelete(). To delete a task, simply provide the task identifier to this function. void vTaskDelete(xTaskHandle pxTaskToDelete);
vTaskDelete() does not directly deallocate the memory used by the deleted task. It only marks the task as deleted or terminated. Memory cleanup is handled by the idle task in FreeRTOS.
In real-time applications, it is rare to delete tasks. Most tasks are designed to run continuously. Instead of deleting them, it is preferable to suspend or block tasks if they are not needed.
Exercice:
Write an application which launches 2 tasks task1 and task2.
task1 priority = 1
task2 priority = 2
task2 should toggle the LED every 1 second and should delete itself when the button is pressed by the user.
task1 should toggle the same LED every 200ms.
You will find the source code in my GitHub link, under the folder "STM32_FreeRTOS_Tasks_Delete".
FreeRTOS Hardware Interrupt Configuration Items
FreeRTOS has configuration elements in FreeRTOSConfig.h to manage hardware interrupt priorities on ARM Cortex-M processors.
The two main configuration elements are:
Kernel Interrupts: Kernel interrupts include:
These interrupts are essential for the operation of the RTOS kernel.
Priority Levels: ARM Cortex-M processors use a configurable number of bits to represent interrupt priority levels, defined by NVIC_PRIO_BITS.
For example, in STM32F4xx microcontrollers, NVIC_PRIO_BITS is often set to 4, allowing for 16 possible priority levels (2^4).
Priority Ordering:
In ARM Cortex-M processors, the higher the priority value (e.g., 0xF0), the lower the priority.
Kernel interrupts like SysTick, PendSV, and SVC are typically set to the lowest possible priority to avoid interfering with other critical tasks.
Use of configKERNEL_INTERRUPT_PRIORITY: This parameter is often set to the lowest available priority for the kernel (e.g., 0xF0 for certain processors), ensuring that system interrupts can be preempted by higher-priority tasks.
In FreeRTOS, task priority management is different from hardware interrupt priority management on an ARM Cortex-M processor.
Interrupt Priority:
On an ARM Cortex-M processor, a lower numerical priority value means a higher priority. For example, an interrupt with priority value 2 has higher priority than an interrupt with priority value 5.
This convention means that the lower the numerical value, the higher the interrupt priority.
Task Priority in FreeRTOS:
Unlike interrupts, in FreeRTOS, a lower numerical priority value indicates a lower priority for the task.
For example, a task with priority 2 is higher priority than a task with priority 0, meaning that Task 2 is more urgent than Task 1.
API to set priority: void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_TYPE uxNewPriority );
API to Get priority: unsigned portBASE_TYPE uxTaskPriorityGet( xTaskHandle pxTask );
Exercise
Write an application which creates 2 tasks task 1 : Priority 2 task 2 : Priority 3 task 2 should toggle the LED at 1 sec duration and task 1 should toggle the LED at 100ms duration.
When application receives button interrupt the priority must be reversed inside the task handlers.
You will find the source code in my GitHub link, under the folder "STM32_FreeRTOS_Tasks_Priority".
Interrupt Safe and Interrupt Un-Safe APIs
Interrupt Un-Safe APIs
FreeRTOS APIs which don’t end with the word “FromISR” are called as interrupt unsafe APIs xTaskCreate(), xQueueSend() xQueueReceive().
If you want to send a data item to the queue from the ISR, then use xQueueSendFromISR() instead of xQueueSend().
xQueueSendFromISR() is an interrupt safe version of xQueueSend().
If you want to Read a data item from the queue being in the ISR, then use xQueueReceiveFromISR() instead of xQueueReceive(). xQueueReceiveFromISR() is an interrupt safe version of xQueueReceive().
xSemaphoreTakeFromISR() is an interrupt safe version of xSemaphoreTake() : which is used to ‘take’ the semaphore
xSemaphoreGiveFromISR() is an interrupt safe version of xSemaphoreGive() : which is used to ‘give’ the semaphore
FreeRTOS Task States
In a simplified model, there are two main states for a task:
Within the non-running state, there are three sub-states:
When a task is blocked, it is temporarily put on hold and does not execute on the CPU. For example, to generate a 10 ms delay, it is preferable to block the task using an API like vTaskDelay rather than running a for loop that would unnecessarily occupy the CPU, preventing other tasks from running.
The Blocked state is useful for:
Implementing delays without engaging the CPU, allowing other tasks to execute.
Synchronization: A task can be blocked while waiting for data from a queue or a semaphore. It does not consume the CPU until the data is available, thus improving efficiency.
Most of the applications don’t use this state. This is very rarely used task state.
When the task is in suspended state, it is not available to the scheduler to schedule it.
There is only one way to put the task in to suspended state, that is by calling the function vTaskSuspend()
The only way to come out from the suspended state is by calling the API vTaskResume()
FreeRTOS Blocking Delay APIs
void vTaskDelay( portTickType xTicksToDelay );
vTaskDelay() places the calling task into the "Blocked state" for a fixed number of tick interrupts. While in the Blocked state, the task will not use any processing time at all, so processing time is only consumed when there is genuine work to be done.
void vTaskDelayUntil( TickType_t *pxPreviousWakeTime, const TickType_t xTimeIncrement );
vTaskDelayUntil() specifies an absolute time at which the task wishes to unblock. [Time is calculated relative to the last wake up time of the task.]
Scheduling Policies
Preemption is the act of temporarily interrupting an already executing task with the intention of removing it from the running state without its co-operation.
In this scheduling policy, each task is given an equal amount of time to execute. No priority is considered for context switching, and all tasks are treated equally.
In FreeRTOS, context switching is managed by the PendSV exception rather than the scheduler code itself. The tick ISR triggers the PendSV exception if a context switch is necessary.
This simple type of scheduling does not take task priority into account, which limits its effectiveness in certain real-time scenarios.
Priority-based scheduling policy prioritizes tasks based on their assigned priority levels.
Higher-priority tasks are executed before lower-priority ones.
If a high-priority task is blocked, the CPU is assigned to lower-priority tasks until the high-priority task becomes ready again.
In FreeRTOS, context switching is triggered by the Tick ISR, which calls the PendSV exception to switch to the appropriate task.
This system ensures that when a high-priority task unblocks, it can immediately take control of the CPU, optimizing processor usage for critical tasks.
This type of scheduling relies on cooperation between tasks.
A running task will only yield the CPU if it enters a blocked state or explicitly calls the taskYield() function.
Unlike preemptive scheduling, the Systick Handler does not interrupt the current task to perform a context switch.
This type of scheduling is simple but can make the system less responsive, as a task of the same priority will not access the CPU until the running task voluntarily releases it.
Cooperative scheduling is not ideal for real-time systems because it does not guarantee adherence to timing constraints.
FreeRTOS Queue Management
A queue is a data structure that can store a limited number of fixed-size elements. The maximum number of elements it can hold is called its length. It generally operates in FIFO (First In, First Out) mode: elements are added at the back (queue) and removed from the front (head).
The main operations are:
FreeRTOS API to Create a Queue
xQueueHandle xQueueCreate( unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxItemSize );
Sending data to the queue
portBASE_TYPE xQueueSendToFront( xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait );
- xQueue: Q handle which we got from Q create API.
- pvItemToQueue: mention the address of the data item which you want to send to Q.
- xTicksToWait: Number of ticks the task who calls this API must wait if the Q is full. Mention zero here if your task doesn't want to wait if the Q is full.
- If you use the macro port_MAX_DELAY, then your task will wait until the Q becomes free for at least one item. If Q never becomes free, then your task will wait forever.
portBASE_TYPE xQueueSendToBack( xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait );
Receiving data from the queue
portBASE_TYPE xQueueReceive(xQueueHandle xQueue, const void * pvBuffer, portTickType xTicksToWait);
- xQueue: The queue handle from which to read the data.
- pvBuffer: A pointer to a buffer where the read data item will be stored.
- xTicksToWait: The number of ticks to wait if the queue is empty. If the queue is still empty after this time, the function will return QUEUE_EMPTY.
- The function returns TRUE if the read is successful and QUEUE_EMPTY if the queue is empty even after xTicksToWait.
portBASE_TYPE xQueuePeek(xQueueHandle xQueue, const void * pvBuffer, portTickType xTicksToWait);
Exercise
Design a FreeRTOS application which implements the below commands:
The command should be sent to the board via UART from the user.
You will find the source code in my GitHub link, under the folder "STM32_FreeRTOS_Queue_Processing".
Symaphore for Synchronization
A semaphore is a kernel object or you can say kernel service, that one or more threads of execution can acquire or release for the purpose of synchronization or mutual exclusion.
A semaphore can be used to signal between two tasks. For instance, Task A (producer) can signal to Task B (consumer) that data is ready. This way, Task B will wait for the signal before attempting to consume the data, avoiding unnecessary checks.
Mutual Exclusion: Mutual exclusion means that if a task is using a shared resource, another task cannot use it until the first task releases it. This prevents conflicts when accessing shared resources (critical sections).
A critical section is a piece of code or shared resource that must be protected from simultaneous access. Although semaphores can be used for mutual exclusion, mutexes (locks) are generally preferred for this purpose because they are specifically designed to avoid design issues that may arise with semaphores.
Difference Between Synchronization and Mutual Exclusion:
Limitations of Semaphores for Mutual Exclusion: While semaphores can be used for mutual exclusion, they are not ideal for this purpose as they can introduce design problems. Mutexes are generally a better choice for managing exclusive access to shared resources.
When a semaphore is created, the kernel assigns it a control block, a unique identifier, a value, and a waiting list for tasks.
The semaphore can be thought of as a set of keys. If a task obtains a key, it can access a resource or perform a specific operation. If it cannot obtain a key, it is blocked and placed in the waiting list.
The semaphore has a defined number of keys (e.g., 4), indicating how many tasks can access the resource simultaneously. If the number of available keys is 0, new tasks are blocked.
When tasks are waiting in the semaphore’s queue, they are released based on their priority (highest priority first) or according to the order of arrival if priorities are equal.
There are two types of semaphores:
When the value is 1, the semaphore (or "key") is available; when the value is 0, it is unavailable. The binary semaphore is primarily used for:
Synchronization: between tasks or between a task and an interrupt.
Mutual exclusion: to protect shared data.
A binary semaphore is initialized and set to "unavailable." The auxiliary task then attempts to acquire the semaphore, but since it is unavailable, it enters a blocked state and waits for the semaphore to be released.
When an interrupt occurs, the ISR (Interrupt Service Routine) performs only the necessary critical tasks and then releases (or "gives") the semaphore, signaling the auxiliary task that it can begin its execution.
The auxiliary task, which was waiting for the semaphore, is unblocked and performs the heavier or time-consuming tasks that were not handled in the ISR.
Once the auxiliary task has completed its work, it tries to acquire the semaphore again, but since it is now unavailable, it returns to the blocked state, ready to be awakened by the next interrupt.
Exercise
Create 2 tasks 1) Manager task 2) Employee task
With manager task being the higher priority.
When manager task runs it should create a “Ticket id” and post it to the queue and signal the employee task to process the “Ticket id”.
When employee task runs it should read from the queue and process the “Ticket id” posted by the manager task.
Use binary semaphore to synchronize between manager and employee task.
You will find the source code in my GitHub link, under the folder "STM32_FreeRTOS_Bin_Sema_Tasks".
Event counting: Each event increments the semaphore's value, allowing a task to process these events later.
Resource management: The value indicates the number of available resources. When a task takes a semaphore, it means it’s accessing a resource; when the value reaches 0, there are no more resources available.
A counting semaphore is initialized with a count value (e.g., 5) and an initial state of 0 (unavailable). When a task tries to acquire the semaphore without it being available, it becomes blocked.
When an interrupt occurs, it triggers the "GiveFromISR()", which increases the semaphore count and releases the blocked task, allowing it to process the event. If multiple interrupts occur in succession, the semaphore count increases with each interrupt, "stacking" the events without losing any. The unblocked task can then process these events one by one in sequence.
The counting semaphore thus efficiently manages frequent or successive interrupts, unlike a binary semaphore which could handle only one event at a time.
Exercice
Create 2 tasks.
Periodic task priority must be higher than the handler task.
Use counting semaphore to process latched events (by handler task) sent by fast triggering interrupts.
Use the counting semaphore to latch events from the interrupts.
You will find the source code in my GitHub link, under the folder "STM32_FreeRTOS_Cnt_Sema_Tasks".
Mutual Exclusion using Binary Semaphore
Access to a resource that is shared either between tasks or between tasks and interrupts needs to be serialized using some techniques to ensure data consistency.
Usually, a common code block which deals with global array, variable or memory address, has the possibility to get corrupted, when many tasks or interrupts are racing around it.
2 ways we can Implement the mutual exclusion in FreeRtos
Mutexes have a major advantage over binary semaphores by enabling priority inheritance, which helps mitigate the impact of priority inversion.
Priority inversion occurs when a high-priority task is blocked because a low-priority task holds a mutex it requires. In this case, the mutex temporarily raises the priority of the low-priority task to match that of the high-priority task, allowing it to release the mutex more quickly. Once the mutex is released, the low-priority task returns to its original priority.
However, using mutexes to avoid priority inversion can increase memory usage and code size. In simpler systems or those with limited memory resources, it's often preferable to use a binary semaphore, as it is lighter and consumes less memory.
Online Courses Available on Tips Engineer Zone
6mohttps://www.tipsengineerzone.in/2024/01/list-of-all-online-engineering-courses.html
Lead Software Engineer | Embedded Software Architect | Automotive Development | Senior Engineering Manager
6moCool article One thing to add if you use STMCubeMx setting up the freeRTOS is quite easy.
Autosar Specialist at Bosch Automotive Service Solutions, LLC
6moVery informative