Embedded Development Board Learning


Introduction to FreeRTOS #1 RTOS for Embedded Systems

Fecha: septiembre 15, 2024

Autor: Guillermo Garcia

Categorías: RTOS Etiquetas:

Introduction to FreeRTOS we will see important points that will allow us to familiarize ourselves with the kernel and the platform.

As an introduction to FreeRTOS we will see some general concepts such as how the kernel files are distributed, the conventions of the functions, how the kernel interacts with the ARM architecture and how to perform the initial configurations.

Introduction to FreeRTOS distribution

Let’s take a look at how the FreeRTOS kernel is distributed. This operating system is highly popular for two main reasons: first, it is very accessible, and second, it comes with a wide range of add-ons that make it a comprehensive environment for any application, especially IoT applications, thanks to its support for AWS connectivity.

In this article, we will focus solely on the kernel, setting aside the other components offered by the FreeRTOS environment.

You can access the kernel’s source code from FreeRTOS’s GitHub page. We will download the FreeRTOS-Kernel project, which contains the source files that make up the kernel.

FreeRTOS-Kernel
    |
    +-examples  Demo application projects
    |
    +-include   The core FreeRTOS kernel header files
    |
    +-portable  Processor specific code.
            |
            +-Compiler x    All the ports supported for compiler x
            +-Compiler y    All the ports supported for compiler y
            +-MemMang       The sample heap implementations
    |
    +-croutine.c
    |
    +-event_groups.c
    |
    +-list.c
    |
    +-queue.c
    |
    +-stream_buffer.c
    |
    +-tasks.c
    |
    +-timers.c

The core RTOS code is located in three files: task.c, queue.c, and list.c. These files can be found in the FreeRTOS-Kernel directory. This directory also contains two optional files, timers.c and croutine.c, which implement the software timer and co-routine functionality, respectively.

The FreeRTOS-Kernel/portable directory holds the source files for portability across different architectures and compilers. Each supported processor architecture requires a small amount of RTOS code specific to that architecture.

This is the portable layer of the RTOS. Fortunately, we don’t have to write this code ourselves, but we do need to choose the folder with the appropriate source files for our architecture and microcontroller.

The last important directory is the memory manager for the kernel, located in FreeRTOS-Kernel/portable/MemMang, where you can find sample heap allocation schemes.

Now, we know that to compile the kernel, we need to add all the source files—task.c, queue.c, list.c, timers.c, and croutine.c—to our compilation environment. We also need to select a port.c file for our architecture and compiler, and choose a memory management file, such as heap_1.c.

Introduction to FreeRTOS functions

In the source code, you’ll notice a series of conventions that help us more easily identify how the code is structured. These naming and formatting conventions are designed to make the code more readable and maintainable. By following these conventions, it becomes easier to understand the purpose of various functions, variables, and data structures within the FreeRTOS kernel, ensuring consistency across different parts of the codebase.

FreeRTOS data types

In the kernel, it only conditions two types of data that are used for very specific things. Let’s look at these two types.

  • TickType_t : FreeRTOS configures a periodic interrupt called the tick interrupt. The number of tick interrupts that have occurred since the FreeRTOS application started is called the tick count. The tick count is used as a measure of time. The time between two tick interrupts is called the tick period. TickType_t can be an unsigned 16-bit type, an unsigned 32-bit type, or an unsigned 64-bit type, depending on the setting of configTICK_TYPE_WIDTH_IN_BITS in FreeRTOSConfig.h.

  • BaseType_t : This is always defined as the most efficient data type for the architecture. Typically, this is a 64-bit type on a 64-bit architecture, a 32-bit type on a 32-bit architecture, a 16-bit type on a 16-bit architecture, and an 8-bit type on an 8-bit architecture. BaseType_t is generally used for return types that take only a very limited range of values, and for pdTRUE/pdFALSE type Booleans.

FreeRTOS variable names

Variables are prefixed with their type: c for char, s for int16_t (short), l for int32_t (long), and x for
BaseType_t and any other non-standard types (structures, task handles, queue handles, etc.).
If a variable is unsigned, it is also prefixed with a u. If a variable is a pointer, it is also prefixed with a p.

FreeRTOS function names

Functions are prefixed with both the type they return and the file they are defined within. For example:

vTaskPrioritySet() returns a void and is defined within tasks.c.

xQueueReceive() returns a variable of type BaseType_t and is defined within queue.c.

pvTimerGetTimerID() returns a pointer to void and is defined within timers.c.

File scope (private) functions are prefixed with ‘prv’.

Common macro definitions

Macros are simple but it is important to know them because the vast majority of APIs use definitions. Let’s look at the Prefixes and the location of the definition.

port (for example, portMAX_DELAY) portable.h

task (for example, taskENTER_CRITICAL()) task.h

pd (for example, pdTRUE) projdefs.h

config (for example, configUSE_PREEMPTION) FreeRTOSConfig.h

err (for example, errQUEUE_FULL) projdefs.h

The macros defined are used throughout the FreeRTOS source code.

pdTRUE 1

pdFALSE 0

pdPASS 1

pdFAIL 0

Introduction to FreeRTOS Scheduler types

We have talked about the types of schedulers that an RTOS has in the first RTOS Introduction for Embedded Systems, now let’s see what scheduling policies we can use with the FreeRTOS kernel.

Scheduling is the software deciding which task to run at what time. FreeRTOS has two modes of operation when it comes to handling task priorities: With and without preemption. Which mode to use can be set in the configuration file. When using the mode without preemption, also called cooperative mode, it is up to the developer to make tasks that yield the CPU through the use of blocking functions and the taskYIELD() function.

When using a preemptive scheduler, a task will automatically yield the CPU when a task of higher priority becomes unblocked. However, there is one exception: When a higher priority task blocks from an ISR, the taskYIELD_FROM_ISR() function has to be called at the end of the ISR for a task switch to occur.

If configUSE_TIME_SLICING is set to 1, the scheduler will also preempt tasks of equal priority at each time the tick occurs. Time slicing is not available in cooperative mode.

In both modes, the scheduler will always switch to the highest priority unblocked task. If there are multiple tasks unblocked with the same priority, the scheduler will choose to execute each task in turn. This is commonly referred to as round robin scheduling.

  • In the preemptive mode, higher priority tasks are immediately switched to when they get unblocked.
  • In the cooperative mode, the release of a semaphore might unblock a higher priority task, but the actual task switch will only happen when the currently executing task calls the taskYIELD() function or enters a blocking state.

Configuration File

The FreeRTOSConfig.h file is a key configuration file in any project using FreeRTOS. It contains a number of macros that allow you to customize the behavior of the real-time operating system (RTOS) according to the project requirements. Through this file, you can enable or disable specific FreeRTOS features such as task management, support for queues, semaphores, timers, and priorities, as well as define critical parameters such as the stack size for tasks, the maximum number of priorities, or the frequency of the system timer. In short, FreeRTOSConfig.h gives you control over how FreeRTOS is tuned and behaves in your application, optimizing the use of system resources.

We will see more details in future articles, for now we know where to go if we want to make any particular configuration.

How FreeRTOS works with ARM Cortex M

This is the highlight of this article we’ll explore how the kernel leverages hardware to perform multitasking and context switching, concepts we discussed in the article RTOS Introduction for Embedded Systems. Understanding how the kernel interacts with the hardware is crucial for grasping the inner workings of FreeRTOS and how it efficiently manages multiple tasks.

We will explore how context switching works on ARM Cortex-M, for this we need to explore a set of primitives that the architecture provides.

When choosing a port for FreeRTOS we must make sure that it is the right one for our architecture, this is because the kernel uses certain features of the ARM Cortex-M architecture to be able to multitask.

FreeRTOS scheduler implementation

The scheduler code is a combination of two components found in the FreeRTOS source code distribution.

Splitting FreeRTOS kernel components

The port.c layer of FreeRTOS uses the SysTick and PendSV interrupts that are available on ARM Cortex-M architectures for specific purposes, let’s see how the scheduler makes use of these tools.

ARM Cortex M architectures can handle exceptions for different purposes clearly. Before understanding what exceptions are we must know that the ARM processor works under a scheme of two operating modes that change in relation to the exceptions.

The processor supports two modes of operation, Thread mode and Handler mode:

Division of ARM Cortex M CPU operating modes
  • The processor enters Handler mode as a result of an exception. All code is privileged in
    Handler mode
  • The processor enters Thread mode on Reset, or as a result of an exception return.
    Privileged and Unprivileged code can run in Thread mode.

As users we don’t have to worry about managing the CPU operating modes, what we really want to know is that all applications will run in the processor’s Thread mode. All exceptions run in Handler mode.

Now that we know this we can say that an exception is something that disrupts the normal operation of the program by changing the operating mode of the processor.

There are two types of exceptions in ARM

  • System exception
  • Interrupts

What are system exceptions? They are generated by the processor itself.

What are interrupts? They are generated externally to the processor.

The architecture code in port.c makes use of system exceptions to generate interrupts to perform the context change. Let’s see what characteristics each exception has.

Systick

System Tick Timer exception generates by a timer peripheral which is included in the processor. This can be used by an RTOS or can be used as a simple timer peripheral.

Once the SysTick is configured to trigger periodically the FreeRTOS scheduler performs a check to see if a context switch is required by calling xTaskIncrementTick this occurs in the FreeRTOS port.c source file:

void xPortSysTickHandler( void )
{
    /* The SysTick runs at the lowest interrupt priority, so when this interrupt
     * executes all interrupts must be unmasked.  There is therefore no need to
     * save and then restore the interrupt mask value as its value is already
     * known. */
    portDISABLE_INTERRUPTS();
    {
        /* Increment the RTOS tick. */
        if( xTaskIncrementTick() != pdFALSE )
        {
            /* A context switch is required.  Context switching is performed in
             * the PendSV interrupt.  Pend the PendSV interrupt. */
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    portENABLE_INTERRUPTS();
}

Implements RTOS Tick management, which is periodically activated by the Systick ARM timer.

PendSV

PendSV (Pended Service Call) is another exception type that is important for supporting RTOS operations. The PendSV Exception is triggered by setting its pending status by writing to the Interrupt Control and State Register. we can schedule the PendSV exception handler to be executed after all other interrupt processing tasks are done, by making sure that the PendSV has the lowest exception priority level. This is very useful for a context-switching operation, which is a key operation in FreeRTOS.

FreeRTOS port.c source file:

void xPortPendSVHandler( void )
{
    /* This is a naked function. */

    __asm volatile
    (
        "	mrs r0, psp							\n"
        "	isb									\n"
        "										\n"
        "	ldr	r3, pxCurrentTCBConst			\n"/* Get the location of the current TCB. */
        "	ldr	r2, [r3]						\n"
        "										\n"
        "	tst r14, #0x10						\n"/* Is the task using the FPU context?  If so, push high vfp registers. */
        "	it eq								\n"
        "	vstmdbeq r0!, {s16-s31}				\n"
        "										\n"
        "	stmdb r0!, {r4-r11, r14}			\n"/* Save the core registers. */
        "	str r0, [r2]						\n"/* Save the new top of stack into the first member of the TCB. */
        "										\n"
        "	stmdb sp!, {r0, r3}					\n"
        "	mov r0, %0 							\n"
        "	msr basepri, r0						\n"
        "	dsb									\n"
        "	isb									\n"
        "	bl vTaskSwitchContext				\n"
        "	mov r0, #0							\n"
        "	msr basepri, r0						\n"
        "	ldmia sp!, {r0, r3}					\n"
        "										\n"
        "	ldr r1, [r3]						\n"/* The first item in pxCurrentTCB is the task top of stack. */
        "	ldr r0, [r1]						\n"
        "										\n"
        "	ldmia r0!, {r4-r11, r14}			\n"/* Pop the core registers. */
        "										\n"
        "	tst r14, #0x10						\n"/* Is the task using the FPU context?  If so, pop the high vfp registers too. */
        "	it eq								\n"
        "	vldmiaeq r0!, {s16-s31}				\n"
        "										\n"
        "	msr psp, r0							\n"
        "	isb									\n"
        "										\n"
        #ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata workaround. */
            #if WORKAROUND_PMU_CM001 == 1
                "			push { r14 }				\n"
                "			pop { pc }					\n"
            #endif
        #endif
        "										\n"
        "	bx r14								\n"
        "										\n"
        "	.align 4							\n"
        "pxCurrentTCBConst: .word pxCurrentTCB	\n"
        ::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
    );
}

It is used to achieve context switching from a task, when a task requests context switching by calling portYIELD(), the handler is actually triggered by ARM PendSV exceptions.

SVC

The SuperVisor Call (SVC) instruction is used to generate the SVC exception Typically, SVC is used with an embedded RTOS where an application task running in an unprivileged execution state can request services from the RTOS, which runs in the privileged execution state. The SVC exception mechanism provides the transition from unprivileged to privileged.

FreeRTOS port.c source file:

void vPortSVCHandler( void )
{
    __asm volatile (
        "	ldr	r3, pxCurrentTCBConst2		\n"/* Restore the context. */
        "	ldr r1, [r3]					\n"/* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
        "	ldr r0, [r1]					\n"/* The first item in pxCurrentTCB is the task top of stack. */
        "	ldmia r0!, {r4-r11, r14}		\n"/* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
        "	msr psp, r0						\n"/* Restore the task stack pointer. */
        "	isb								\n"
        "	mov r0, #0 						\n"
        "	msr	basepri, r0					\n"
        "	bx r14							\n"
        "									\n"
        "	.align 4						\n"
        "pxCurrentTCBConst2: .word pxCurrentTCB				\n"
        );
}

It is used to start the first task and this controller is activated by the ARM SVC instruction.

Introduction to FreeRTOS Context Switching

Let’s put all the pieces together to understand how context switching happens in FreeRTOS.

Let’s summarize the steps:

  1. Saving the current task state
  2. Updating Task Control Blocks (TCBs)
  3. Selecting the next task
  4. Restoring the next task’s state
Introduction to FreeRTOS context switching

Closing

Understanding how the FreeRTOS kernel interacts with the ARM Cortex M architecture is important to know what type of port.c is suitable for our platform. We will continue with this series in the next article with a guide to compile the kernel for an MCU.



Card image cap
Guillermo Garcia I am an embedded systems software engineer. I like constant learning, IoT systems and sharing knowledge


Comentarios... no existen comentarios.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subscribe


Subscribe to receive the latest content.
Loading