Comprehensive Guide to Real-Time Systems: Understanding FreeRTOS, Task Scheduling, and Advanced Synchronization

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.

Article content

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).

  • RTA (Real-Time Application): The purple line shows an almost constant response time for each execution. This means that, regardless of load or circumstances, the system guarantees a predictable and stable response time. This behavior is crucial for real-time applications, as it ensures that deadlines are consistently met, thereby guaranteeing the reliability of the system.
  • NRTA (Non-Real-Time Application): The red line shows a variable response time from one execution to the next. For example, at point E1, the response time is high, then it drops significantly at E2, and continues to fluctuate in subsequent iterations. This unpredictability makes the application unreliable in a real-time context, as deadlines may not be met.

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)?

  • RTAs are not fast-executing applications.
  • RTAs are time-deterministic applications, meaning their response time to events is almost constant.
  • There may be small deviations in RTAs' response time, in milliseconds or seconds, which classifies them as soft real-time applications.
  • Hard real-time functions must be completed within a given time limit. Failure to meet this deadline will result in total system failure.

Examples of real-time applications:

  • Missile guidance and airbag deployment: Hard real-time applications requiring immediate responses to avoid critical failures.
  • ABS (Anti-lock Braking System): Reacts in milliseconds to ensure vehicle safety.
  • VOIP: Soft real-time application, tolerating slight delays without major impact.
  • Stock market website: Near real-time updates to track market fluctuations, with tolerance for small delays.

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:

  • Handling of interrupts and internal system exceptions
  • Handling of Critical Sections
  • Scheduling Mechanism, etc.

RTOS vs GPOS

Article content

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:

  • VxWorks: Used in security and IoT applications, provided by Intel/Windriver.
  • QNX Neutrino: Used in industrial automation, medical, and robotics applications.
  • FreeRTOS: Free and popular among developers.
  • Integrity (Green Hills): Specialized in security-related applications.

  1. GPOS vs RTOS: Task Scheduling

GPOS (General Purpose Operating System):

  • Designed to maximize throughput (number of tasks completed per unit of time).
  • May delay high-priority tasks to execute multiple low-priority tasks, thus achieving higher throughput.
  • Less focus on priority, with more emphasis on the total number of tasks processed.

RTOS (Real-Time Operating System):

  • Prioritizes high-priority tasks by executing them as soon as they are ready, interrupting lower-priority tasks.
  • Emphasizes time predictability and meeting deadlines for critical tasks, even if it may reduce throughput.
  • Designed for applications where timeliness is more important than the number of tasks completed.

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.

Article content

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.

  • Behavior in RTOS: RTOS minimizes this latency to respond quickly to critical events. This speed is crucial for time-sensitive applications, such as airbag deployment.
  • Behavior in GPOS: In a GPOS, interrupt latency can vary and increase with system load, making it less suitable for applications requiring immediate responsiveness.

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.

  • Behavior in RTOS: In RTOS, scheduling latency is kept low and predictable, which is essential for ensuring that critical tasks are executed on time.
  • Behavior in GPOS: In a GPOS, scheduling latency can increase with system load. Consequently, the delay between two tasks may vary, which is unsuitable for critical systems that require reliable execution within fixed time frames.

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):

  • RTOS is designed to address priority inversion, as this issue can lead to severe consequences in critical systems.
  • It uses techniques like priority inheritance, which temporarily raises the priority of the lower-priority task so it can quickly finish its work and release the resource needed by the higher-priority task.

GPOS (General Purpose Operating System):

  • In GPOS, priority inversion is less problematic because these systems don’t require real-time responses and focus more on optimizing overall throughput.
  • Therefore, priority inversion can be tolerated in GPOS without a major impact on the system’s overall performance.

What are the features that a RTOS has but a GPOS doesn't?

  • Priority based preemptive scheduling mechanism
  • No or very short Critical sections which disables the preemption
  • Priority inversion avoidance
  • Bounded Interrupt latency
  • Bounded Scheduling latency , etc.

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?

  • Task Creation

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.

Article content

-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.

  • Task implementation

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.

Article content

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

  • You can analyze how many tasks are running and how much duration they consume on the CPU.
  • ISR entry and exit timings and duration of run on the CPU.
  • You can analyze other behavior of tasks: like blocking, unblocking, notifying, yielding, etc.

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:

  1. PC visualization software: SystemView Host software (Windows / Linux / mac)
  2. SystemView target codes (this is used to collect the target events and send them back to the PC visualization software)

SystemView Visualization Modes:

  • Real-time recording (Continuous recording): With a SEGGER J-Link and its Real Time Transfer (RTT) technology, SystemView can continuously record data, and analyze and visualize it in real time. Real-time mode can be achieved via ST-Link instead of J-Link. For that, J-Link firmware has to be flashed on ST-Link circuitry of STM32 boards. More on this later.

Article content

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.

  • Single-shot recording: You do not need to have a JLINK or STLINK debugger for this. In single-shot mode, the recording is started manually in the application, which allows recording only specific parts that are of interest.

Article content

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.

Article content

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.

  • It is a lowest priority task which is automatically created when the scheduler is started.
  • The idle task is responsible for freeing memory allocated by the RTOS to tasks that have been deleted.
  • When there are no tasks running, Idle task will always run on the CPU.
  • You can give an application hook function in the idle task to send the CPU to low power mode when there are no useful tasks are executing.

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)

  • This is also called as timer daemon task
  • The timer daemon task deals with “Software timers”
  • This task is created automatically when the scheduler is started and if configUSE_TIMERS = 1 in FreeRTOSConfig.h
  • The RTOS uses this daemon to manage FreeRTOS software timers and nothing else.
  • If you don't use software timers in your FreeRTOS application then you need to use this Timer daemon task. For that just make configUSE_TIMERS = 0 in FreeRTOSConfig.h
  • All software timer callback functions execute in the context of the timer daemon task

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?

  • It’s just a piece of code which implements task switching in and task switching out according to the scheduling policy selected.
  • Scheduler is the reason why multiple tasks run on your system efficiently.
  • The basic job of the scheduler is to determine which is the next potential task to run on the CPU.
  • Scheduler has the ability to preempt a running task if you configure so.

Scheduling Policies (Scheduler types)

  1. Simple Pre-emptive Scheduling (Round robin)
  2. Priority-based Pre-Emptive Scheduling
  3. Co-operative Scheduling

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.

  • configUSE_PREEMPTION of FreeRTOSConfig.h configurable item decides the scheduling policy in FreeRTOS.
  • If configUSE_PREEMPTION = 1, then scheduling policy will be priority-based pre-emptive scheduling.
  • If configUSE_PREEMPTION = 0, then scheduling policy will be cooperative scheduling.

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.

  • vPortSVCHandler(): used to launch the very first task Triggered by SVC instruction.
  • xPortPendSVHandler(): used to achieve the context switching between taks Triggered by pending the PendSV System exception of ARM.
  • xPortSystickHandler(): This implements the RTOS tick management. Triggered periodically by Systick timer of ARM cortex Mx processor.

vTaskStartScheduler()

  • This is implemented in tasks.c of the FreeRTOS kernel and used to start the RTOS scheduler.
  • Remember that after calling this function, only the scheduler code is initialized, and all the architecture-specific interrupts will be activated.
  • This function also creates the idle and timer daemon tasks.
  • This function calls xPortStartScheduler() to perform architecture-specific initializations.

The xPortStartScheduler() function performs the following tasks:

  1. Configures the SysTick timer to generate interrupts at a rate defined by configTICK_RATE_HZ in FreeRTOSConfig.h.
  2. Sets the priority for the PendSV and SysTick interrupts.
  3. Starts the first task by executing the SVC (Supervisor Call) instruction.

FreeRTOS Kernel Interrupts

When FreeRTOS runs on ARM Cortex Mx Processor based MCU, below interrupts are used to implement the Scheduling of Tasks.

  1. SVC Interrupt (SVC handler will be used to launch the very first Task)
  2. PendSV Interrupt (PendSV handler is used to carry out context switching between tasks)
  3. SysTick Interrupt (SysTick Handler implements the RTOS Tick Management)

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?

  • The simple answer is to keep track of time elapsed.
  • There is a global variable called “xTickCount,” and it is incremented by one whenever tick interrupt occurs.
  • RTOS Ticking is implemented using SysTick timer of the ARM Cortex Mx processor.
  • Tick interrupt happens at the rate of configTICK_RATE_HZ configured in the FreeRTOSConfig.h

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:

  • The tick ISR runs.
  • All the state tasks are scanned.
  • Determines which is the next potential task to run.
  • If found, triggers the context switching by pending the PendSV interrupt
  • The PendSV handler takes care of switching out of old task and switching in of new task.

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:

  1. Initializes the SysTick interrupt priority to the lowest possible level.
  2. Loads the desired tick rate value (configTICK_RATE_HZ from FreeRTOSConfig.h) into the SysTick timer.
  3. Enables the SysTick timer interrupt, starting the timer for RTOS tick management.

What is Context Switching?

  • Context switching is a process of switching out of one task and switching in of another task on the CPU to execute.
  • In RTOS, Context Switching is taken care by the Scheduler.
  • In FreeRTOS, Context Switching is taken care by the PendSV Handler found in port.c.
  • Whether context switch should happen or not depends upon the scheduling policy of the scheduler.
  • If the scheduler is priority-based pre-emptive scheduler, then for every RTOS tick interrupt, the scheduler will compare the priority of the running task with the priority of ready tasks list. If there is any ready task whose priority is higher than the running task, then context switch will occur.
  • On FreeRTOS, you can also trigger context switch manually using taskYIELD() macro.
  • Context switch also happens immediately whenever a new task unblocks and if its priority is higher than the currently running task.

Task State

  • When a task executes on the Processor it utilizes:Processor core registers.
  • If a Task wants to do any push and pop operations (during function call) then it uses its own dedicated stack memory.

Article content

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

  • xTaskNotifyWait(): if a task calls xTaskNotifyWait(), then it wait an optional timeout until it receives a notification from some other task or interrupt handler.

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.

  • xTaskNotify() is used to send an event directly to and potentially unblock an RTOS task, and optionally update the receiving task's notification value in one of the following ways:

-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.

Security Standards:

  • Not Compliant with Security Standards: FreeRTOS is not designed to meet strict security standards, so it is not recommended for safety-critical applications.

Commercial Versions: SAFERTOS and OpenRTOS:

  • SAFERTOS:

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.

  • OpenRTOS:

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

Article content

  • Application Layer: The top layer represents the user application (Application-1), which consists of multiple tasks (e.g., Task1, Task2, etc.). Each task performs specific functions or routines, utilizing the services provided by FreeRTOS.
  • FreeRTOS Kernel: This is the core of FreeRTOS and sits between the application and hardware layers. It provides essential RTOS services such as: semaphores, queues, timers, tasks, memory management, power management.
  • Device Driver Layer: This layer includes the device drivers responsible for interfacing directly with the hardware. It acts as a bridge, allowing the FreeRTOS kernel to communicate with the hardware components like UART, SPI, GPIO, etc.
  • Hardware Layer: The bottom layer represents the physical hardware (e.g., microcontrollers, sensors, and peripherals).

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:

  • Store global variables and arrays of the application.
  • Download and execute patches if bugs cannot be fixed in Flash.
  • Divided into sections for the stack and heap: Stack: Used for local variables, function arguments, and return addresses. Managed by the Stack Pointer (SP). Heap: Used for dynamic memory allocations, typically via malloc and free in C.

Flash is mainly used to:

  • Permanently store the application code.
  • Retain constants and strings.
  • Contain the vector table for interrupts and exceptions of the microcontroller.

Stack Management

The stack follows a LIFO (Last In, First Out) model.

  • The Stack Pointer (SP) indicates the current location in the stack and adjusts with each PUSH and POP instruction.
  • During a function call, arguments and the return address are pushed onto the stack, then local variables are created.
  • When the function ends, elements are removed from the stack, and the SP returns to its original position.

Heap Management

The heap is a region for dynamic memory allocations, often used for structures like linked lists.

  • Unlike the stack, the heap has no ordered structure and is generally managed by functions like malloc and free in C.

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

Article content

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?

  • Events (or Event Flags)
  • Semaphores (Counting and Binary)
  • Queues and Message Queues
  • Pipes
  • Mailboxes
  • Signals(UNIX like signals)
  • Mutex

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:

  • configKERNEL_INTERRUPT_PRIORITY: Sets the priority for kernel interrupts.
  • configMAX_SYSCALL_INTERRUPT_PRIORITY: Defines the maximum priority level from which FreeRTOS API calls are allowed in an interrupt.

Kernel Interrupts: Kernel interrupts include:

  • SysTick: Used for timekeeping within the RTOS.
  • PendSV: Manages context switching.
  • SVC (Supervisor Call): Initializes the first task.

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:

  • Running state: This is when the task is actively executing on the CPU. At any given moment, only one task can be in the running state, which means it is currently using the CPU.
  • Non-running state: All tasks that are not currently executing are in this state. "Non-running" simply means that the task is not active on the CPU at that moment.

Article content

Within the non-running state, there are three sub-states:

  • Ready: The task is ready to run as soon as it gets CPU time.
  • Blocked: The task is waiting for a condition to be met (e.g., a delay or a resource).

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.

  • Suspended: The task is inactive and will not be scheduled until it is explicitly resumed.

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

  • Preemptive scheduling

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 preemptive scheduling

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.

  • co-operative scheduling

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).

Article content

The main operations are:

  • Enqueue: adding an element to the back of the queue (write).
  • Dequeue: removing an element from the front of the queue (read). When the queue is empty, the head and tail pointers point to the same position.

FreeRTOS API to Create a Queue

xQueueHandle xQueueCreate( unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxItemSize );

  • xQueueHandle: if the creation is successful, the API returns the reference (a pointer) to the created queue, otherwise NULL.
  • uxQueueLength mention here, How many items this queue should hold (i.e. length)
  • uxItemSize here you should mention, what's the size of single item in bytes.

Sending data to the queue

  • xQueueSendToFront(): sends data to the front (head) of 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.

  • xQueueSendToBack(): sends data to the back (tail) of the queue

portBASE_TYPE xQueueSendToBack( xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait );

Receiving data from the queue

  • xQueueReceive(): This API will read a data item from the queue. The data item that is being read will be removed 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.

  • xQueuePeek(): This API also reads from the queue, but unlike xQueueReceive(), this API doesn’t remove the data item from the queue.

portBASE_TYPE xQueuePeek(xQueueHandle xQueue, const void * pvBuffer, portTickType xTicksToWait);

Exercise

Design a FreeRTOS application which implements the below commands:

  • LED_ON
  • LED_OFF
  • LED_TOGGLE_START
  • LED_TOGGLE_STOP
  • LED_STATUS_READ
  • RTC_DATETIME_READ

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:

  • Synchronization: It aims to align tasks so they can interact correctly (e.g., the consumer task waits for a signal from the producer).
  • Mutual Exclusion: It aims to prevent multiple tasks from accessing the same shared resource simultaneously.

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:

  • Binary semaphore : as name indicates, this semaphore works on only 2 values that is 1 and 0

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".

  • Counting semaphore: This type of semaphore can be initialized with a value greater than 1 (e.g., 2, 3, 20). The value indicates the number of available keys. Each time a task takes a key, the value is decremented, and when it returns the key, the value is incremented. Use cases for the counting semaphore include:

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.

  1. Handler task
  2. Periodic task

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

  • Using Binary Semaphore APIs
  • Using Mutex APIs

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.


Alexander Koenig

Lead Software Engineer | Embedded Software Architect | Automotive Development | Senior Engineering Manager

6mo

Cool article One thing to add if you use STMCubeMx setting up the freeRTOS is quite easy.

Karudaiyar Ganapathy

Autosar Specialist at Bosch Automotive Service Solutions, LLC

6mo

Very informative

To view or add a comment, sign in

More articles by NIZAR MOJAB

Insights from the community

Others also viewed

Explore topics