Developer Doc

RTOS Fundamentals

Multitasking

The kernel is the core component within an operating system. Operating systems such as Linux employ kernels that allow users access to the computer seemingly simultaneously. Multiple users can execute multiple programs apparently concurrently.

Each executing program is a task (or thread) under control of the operating system. If an operating system can execute multiple tasks in this manner it is said to be multitasking.

The use of a multitasking operating system can simplify the design of what would otherwise be a complex software application:

  • The multitasking and inter-task communications features of the operating system allow the complex application to be partitioned into a set of smaller and more manageable tasks.

  • The partitioning can result in easier software testing, work breakdown within teams, and code reuse.

  • Complex timing and sequencing details can be removed from the application code and become the responsibility of the operating system.

Multitasking Vs Concurrency

A conventional processor can only execute a single task at a time - but by rapidly switching between tasks a multitasking operating system can make it appear as if each task is executing concurrently. This is depicted by the diagram below which shows the execution pattern of three tasks with respect to time. The task names are color coded and written down the left hand. Time moves from left to right, with the colored lines showing which task is executing at any particular time. The upper diagram demonstrates the perceived concurrent execution pattern, and the lower the actual multitasking execution pattern.

Scheduling

The scheduler is the part of the kernel responsible for deciding which task should be executing at any particular time. The kernel can suspend and later resume a task many times during the task lifetime.

The scheduling policy is the algorithm used by the scheduler to decide which task to execute at any point in time. The policy of a (non real time) multi user system will most likely allow each task a "fair" proportion of processor time. The policy used in real time / embedded systems is described later.

In addition to being suspended involuntarily by the kernel a task can choose to suspend itself. It will do this if it either wants to delay (sleep) for a fixed period, or wait (block) for a resource to become available (eg a serial port) or an event to occur (eg a key press). A blocked or sleeping task is not able to execute, and will not be allocated any processing time.

Referring to the numbers in the diagram above:

  • At (1) task 1 is executing.

  • At (2) the kernel suspends (swaps out) task 1 ...

  • ... and at (3) resumes task 2.

  • While task 2 is executing (4), it locks a processor peripheral for its own exclusive access.

  • At (5) the kernel suspends task 2 ...

  • ... and at (6) resumes task 3.

  • Task 3 tries to access the same processor peripheral, finding it locked task 3 cannot continue so suspends itself at (7).

  • At (8) the kernel resumes task 1.

  • Etc.

  • The next time task 2 is executing (9) it finishes with the processor peripheral and unlocks it.

  • The next time task 3 is executing (10) it finds it can now access the processor peripheral and this time executes until suspended by the kernel.

Context Switching

As a task executes it utilizes the processor / microcontroller registers and accesses RAM and ROM just as any other program. These resources together (the processor registers, stack, etc.) comprise the task execution context.

A task is a sequential piece of code - it does not know when it is going to get suspended (swapped out or switched out) or resumed (swapped in or switched in) by the kernel and does not even know when this has happened. Consider the example of a task being suspended immediately before executing an instruction that sums the values contained within two processor registers. While the task is suspended other tasks will execute and may modify the processor register values. Upon resumption the task will not know that the processor registers have been altered - if it used the modified values the summation would result in an incorrect value.

To prevent this type of error it is essential that upon resumption a task has a context identical to that immediately prior to its suspension. The operating system kernel is responsible for ensuring this is the case - and does so by saving the context of a task as it is suspended. When the task is resumed its saved context is restored by the operating system kernel prior to its execution. The process of saving the context of a task being suspended and restoring the context of a task being resumed is called context switching.

Implementation

The RTOS Tick

When sleeping, an RTOS task will specify a time after which it requires 'waking'. When blocking, an RTOS task can specify a maximum time it wishes to wait.

The FreeRTOS real time kernel measures time using a tick count variable. A timer interrupt (the RTOS tick interrupt) increments the tick count with strict temporal accuracy - allowing the real time kernel to measure time to a resolution of the chosen timer interrupt frequency.

Each time the tick count is incremented the real time kernel must check to see if it is now time to unblock or wake a task. It is possible that a task woken or unblocked during the tick ISR will have a priority higher than that of the interrupted task. If this is the case the tick ISR should return to the newly woken/unblocked task - effectively interrupting one task but returning to another. This is depicted below:

Referring to the numbers in the diagram above:

  • At (1) the RTOS idle task is executing.

  • At (2) the RTOS tick occurs, and control transfers to the tick ISR (3).

  • The RTOS tick ISR makes vControlTask ready to run, and as vControlTask has a higher priority than the RTOS idle task, switches the context to that of vControlTask.

  • As the execution context is now that of vControlTask, exiting the ISR (4) returns control to vControlTask, which starts executing (5).

A context switch occurring in this way is said to be Preemptive, as the interrupted task is preempted without suspending itself voluntarily.

The AVR port of FreeRTOS uses a compare match event on timer 1 to generate the RTOS tick. The following pages describe how the RTOS tick ISR is implemented using the WinAVR development tools.

Tick Resolution

As described on the RTOS Tick documentation page, RTOS time is based upon a periodic tick interrupt. When an RTOS task wants to wait for a specific time to pass, for example by calling vTaskDelay(), or for an event to occur, for example by specifying a block time in a call to xQueueReceive(), the task specifies the maximum time to block (that is, to wait, without using any CPU cycles) as a tick count. The macro pdMS_TO_TICKS() converts milliseconds to ticks. The requested block time starts at the time the API is called, which will be between two tick interrupts. The tick count is an integer that can't count partial tick periods, so the time between the API call and the next tick interrupt is counted as the first tick of the delay (block) period. This will result in slight differences in the observed block time (in wall-clock time) between different API calls that request the same block time. The diagrams below demonstrate this using a call to vTaskDelay( 2 ) as an example, assuming a tick period of 1ms.

The first diagram shows the situation when the call to vTaskDelay() occurs immediately after a tick interrupt. The observed block time will be nearly 2 milliseconds (most of the time between Tick 1 and Tick 2, plus all of the time between Tick 2 and Tick 3).

The second diagram shows the situation when the call to vTaskDelay() instead occurs immediately before a tick interrupt. The observed block time will be just over 1 millisecond (a small part of the time between Tick 1 and Tick 2, plus all the time between Tick 2 and Tick 3).

With these examples in mind, it can be seen that the actual time delayed when a delay of N ticks is specified will always fall between (N-1 ticks * tick_period) and (N * tick_period). For this specific example, that means a total delay between 1.0000000001ms and 1.99999999999ms.

A few important notes regarding delay time:

  • Tick resolution is always up to the tick count specified as a parameter, but no less than (N-1) ticks. Essentially, the range is from N-1 ticks to N ticks (non-inclusive).

  • To guarantee a minimum delay of N time, you'll need to delay for (N / (tick_period)) + 1 ticks.

  • The duration (in time) of a delay is based on the point between RTOS ticks when the delay is called — the closer the call to the upcoming tick, the shorter the delay will be in time.

About

Why FreeRTOS

FreeRTOS provides the best of all worlds: FreeRTOS is truly free and supported, even when used in commercial applications. The FreeRTOS open source MIT license does not require you to expose your proprietary IP. You can take a product to market using FreeRTOS without even talking to us, let alone paying any fees, and thousands of people do just that. If, at any time, you would like to receive additional backup, or if your legal team require additional written guarantees or indemnification, then there is a simple low cost commercial upgrade path. Your peace of mind comes with the knowledge that you can opt to take the commercial route at any time you choose.

Here are some reasons why FreeRTOS is a good choice for your next application - FreeRTOS...

  • Provides a single and independent solution for many different architectures and development tools.

  • Is known to be reliable. Confidence is assured by the activities undertaken by the SafeRTOS sister project.

  • Is feature rich and still undergoing continuous active development.

  • Has a minimal ROM, RAM and processing overhead. Typically an RTOS kernel binary image will be in the region of 6K to 12K bytes.

  • Is very simple - the core of the RTOS kernel is contained in only 3 C files. The majority of the many files included in the .zip file download relate only to the numerous demonstration applications.

  • Is truly free for use in commercial applications (see license conditions for details).

  • Has commercial licensing, professional support and porting services available in the form of OPENRTOS from our partner WITTENSTEIN high integrity systems.

  • Has a migration path to SafeRTOS, which includes certifications for the medical, automotive and industrial sectors.

  • Is well established with a large and ever growing user base.

  • Contains a pre-configured example for each port. No need to figure out how to setup a project - just download and compile!

  • Has an excellent, monitored, and active free support forum.

  • Has the assurance that commercial support is available should it be required.

  • Provides ample documentation.

  • Is very scalable, simple and easy to use.

  • FreeRTOS offers a smaller and easier real time processing alternative for applications where eCOS, embedded Linux (or Real Time Linux) and even uCLinux won't fit, are not appropriate, or are not available.

Coding, Testing, & Style

Testing

This section describes the tests performed on common code (the code located in the FreeRTOS/Source directory that is built by all FreeRTOS kernel ports), and the tests performed on the portable layer code (the code located in subdirectories of the FreeRTOS/Source/portable directory).

  • Common code

    The standard demo/test files attempt to provide 'branch' test coverage whereby the tests ensure both 'true' and 'false' paths through each decision are exercised. (In most cases this actually achieves 'condition' coverage because the kernel's coding style deliberately keeps conditions simple, specifically for this purpose.) 'branch' coverage is measured using GCOV by defining the mtCOVERAGE_TEST_MARKER() macro to a NOP (no operation) instruction in the 'else' path of each 'if()' condition if the 'else' path would otherwise be empty. mtCOVERAGE_TEST_MARKER() is only defined while measuring test coverage - normally it is an empty macro that does not generate any code.

  • Port layer

    Port layer code is tested using 'reg test' tasks, and, for ports that support interrupt nesting, the 'interrupt queue' tasks.

    The 'reg test' tasks create multiple (normally two) tasks that first fill all the CPU registers with known values, then continuously check that every register maintains its expected known value as the other tests execute continuously (soak test). Each reg test task uses unique values.

    The 'interrupt queue' tasks perform tests on interrupts of different priorities that nest at least three deep. Macros are used to insert artificial delays into pertinent points within the code to ensure the desired test coverage is achieved.

It is worth noting that the thoroughness of these tests has been responsible for finding bugs in silicon on multiple occasions.

Memory Safety Proofs

FreeRTOS Core (and FreeRTOS for AWS IoT) libraries include memory safety proofs. The same proofs are being applied to the FreeRTOS kernel and FreeRTOS-Plus-TCP stack, but these two older libraries do not have full coverage yet.

Security Review

Non trivial updates must pass Applied Security (APSEC) review and penetration testing (pentesting) prior to release (services provided by AWS).

Naming Conventions

The RTOS kernel and demo application source code use the following conventions:

  • Variables

    • Variable names use camel case, are unambiguously descriptive, and use full words (no abbreviations, except universally accepted acronyms).

    • Variables of type uint32_t are prefixed ul, where the 'u' denotes 'unsigned' and the 'l' denotes 'long'.

    • Variables of type uint16_t are prefixed us, where the 'u' denotes 'unsigned' and the 's' denotes 'short'.

    • Variables of type uint8_t are prefixed uc, where the 'u' denotes 'unsigned' and the 'c' denotes 'char'.

    • Variables of non stdint types are prefixed x. Examples include BaseType_t and TickType_t, which are portable layer defined typedefs for the type that is the natural or most efficient type for the architecture, and the type used to hold the RTOS tick count, respectively.

    • Unsigned variables of non stdint types have an additional prefix u. For example, variables of type UBaseType_t (unsigned BaseType_t) are prefixed ux.

    • Variables of type size_t are also prefixed x.

    • Enumerated variables are prefixed e

    • Pointers have an additional p prefixed, for example, a pointer to a uint16_t will have prefix pus.

    • In line with MISRA guides, unqualified standard char types are only permitted to hold ASCII characters and are prefixed c.

    • In line with MISRA guides, variables of type char * are only permitted to hold pointers to ASCII strings and are prefixed pc.

  • Functions

    • Function names use camel case, are unambiguously descriptive, and use full words (no abbreviations, except universally accepted acronyms).

    • File scope static (private) functions are prefixed with prv.

    • API functions are prefixed with their return type, as per the convention defined for variables with the addition of the prefix v for void.

    • API function names start with the name of the file in which they are defined. For example vTaskDelete is defined in tasks.c, and has a void return type.

  • Macros

    • Macro are unambiguously descriptive, and use full words (no abbreviations, except universally accepted acronyms).

    • Macros are prefixed with the file in which they are defined. The pre-fix is lower case. For example, configUSE_PREEMPTION is defined in FreeRTOSConfig.h.

    • Other than the prefix, macros are written in all upper case, and use an underscore to separate words.

Data Types

Only stdint.h types and the RTOS's own typedefs are used, with the following exceptions:

  • char

    In line with MISRA guides, unqualified char types are permitted, but only when they are used to hold ASCII characters.

  • char *

    In line with MISRA guides, unqualified character pointers are permitted, but only when they are used to point to ASCII strings. This removes the need to suppress benign compiler warnings when standard library functions that expect char * parameters are used, especially considering some compilers default unqualified char types to be signed while other compilers default unqualified char types to be unsigned.

There are four types that are defined for each port. These are:

  • TickType_t

    Use either configUSE_16_BIT_TICKS or configTICK_TYPE_WIDTH_IN_BITS to control the type (and therefore the bit-width) of TickType_t:

    • If configUSE_16_BIT_TICKS is set to non-zero (true), then TickType_t is defined to be an unsigned 16-bit type. If configUSE_16_BIT_TICKS is set to zero (false), then TickType_t is defined to be an unsigned 32-bit type.

    • If configTICK_TYPE_WIDTH_IN_BITS is set to TICK_TYPE_WIDTH_16_BITS, then TickType_t is defined as an unsigned 16-bit type. If configTICK_TYPE_WIDTH_IN_BITS is set to TICK_TYPE_WIDTH_32_BITS, then TickType_t is defined as an unsigned 32-bit type. If configTICK_TYPE_WIDTH_IN_BITS is set to TICK_TYPE_WIDTH_64_BITS, then TickType_t is defined as an unsigned 64-bit type.

    Only one of configUSE_16_BIT_TICKS and configTICK_TYPE_WIDTH_IN_BITS must be used. See the customisation section of the API documentation for full information.

  • BaseType_t

    This is defined to be the most efficient, natural type for the architecture. For example, on a 32-bit architecture, BaseType_t will be defined to be a 32-bit type. On a 16-bit architecture, BaseType_t will be defined to be a 16-bit type. If BaseType_t is defined to char, then particular care must be taken to ensure signed chars are used for function return values that can be negative to indicate an error.

  • UBaseType_t

    This is an unsigned BaseType_t.

  • StackType_t

    Defined to the type used by the architecture for items stored on the stack. Normally this would be a 16-bit type on 16-bit architectures and a 32-bit type on 32-bit architectures, although there are some exceptions. Used internally by FreeRTOS.

Style Guide

  • Indentation

    Four space characters are used to indent.

  • Comments

    Comments never pass column 80, unless they follow, and describe, a parameter.

    C++ style double slash (//) comments are not used.

  • Layout

    The FreeRTOS source code layout is designed to be as easy to view and read as possible. The code snippets below show first the file layout, then the C code formatting.

/* Library includes come first... */
#include <stdlib.h>
/* ...followed by FreeRTOS includes... */
#include "FreeRTOS.h"
/* ...followed by other includes. */
#include "HardwareSpecifics.h"
/* #defines come next, bracketed where possible. */
#define A_DEFINITION	( 1 )

/*
 * Static (file private) function prototypes appear next, with comments
 * in this style - each line starting with a '*'.
 */
static void prvAFunction( uint32_t ulParameter );

/* File scope variables are the last thing before the function definitions.
Comments for variables are in this style (without each line starting with
a '*'). */
static BaseType_t xMyVariable;

/* The following separator is used after the closing bracket of each function,
with a blank line following it before the start of the next function definition. */

/*-----------------------------------------------------------*/

void vAFunction( void )
{
    /* Function definition goes here - note the separator after the closing
    curly bracket. */
}
/*-----------------------------------------------------------*/

static UBaseType_t prvNextFunction( void )
{
    /* Function definition goes here. */
}
/*-----------------------------------------------------------*/

File LayoutC

/* Variables are given verbose, unabbreviated, descriptive names. */
UBaseType_t uxCurrentNumberOfTasks = 0U;
TickType_t xTickCount = 0U;
uint32_t ulTopReadyPriority = 0UL;

/* Booleans use the microcontroller architecture's most efficient type, which
is defined in the kernel's portable layer as BaseType_t. */
BaseType_t xSchedulerRunning = pdFALSE;

/* Function names are always written on a single line, including the return
type.  As always, there is no space before the opening parenthesis.  There
is a space after an opening parenthesis.  There is a space before a closing
parenthesis.  There is a space after each comma.  Parameters are given
verbose, descriptive names (unlike this example!).  The opening and closing
curly brackets appear on their own lines, lined up underneath each other. */
void vAnExampleFunction( uint32_t ulParameter1, uint16_t usParameter2 )
{
/* Variable declarations are not indented, and use stdint.h defined types. */
uint8_t ucByte;

    /* Code is indented.  Curly brackets are always on their own lines
    and lined up underneath each other. */
    for( ucByte = 0U; ucByte < fileBUFFER_LENGTH; ucByte++ )
    {
        /* Indent again. */
    }
}

/* For, while, do and if constructs follow a similar pattern.  There is no
space before the opening parenthesis.  There is a space after an opening
parenthesis.  There is a space before a closing parenthesis.  There is a
space after each semicolon (if there are any).  There are spaces before and
after each operator.  No reliance is placed on operator precedence -
parenthesis are always used to make precedence explicit.  Magic numbers,
other than zero, are always replaced with a constant or #defined constant.
The opening and closing curly brackets appear on their own lines. */
for( ucByte = 0U; ucByte < fileBUFFER_LENGTH; ucByte++ )
{
}

while( ucByte < fileBUFFER_LENGTH )
{
}

/* There must be no reliance on operator precedence - every condition in a
multi-condition decision must uniquely be bracketed, as must all
sub-expressions.  Ternary and comma operators are not allowed (except for
variable declarations). See the following comment block for a preferred layout
of the following code. */
if( ( ucByte < fileBUFFER_LENGTH ) && ( ucByte != 0U ) )
{
    /* Example of no reliance on operator precedence! */
    ulResult = ( ( ulValue1 + ulValue2 ) - ulValue3 ) * ulValue4;
}

/* To improve code coverage metrics, each condition in a decision should be
tested individually.  For example, the if() statement in the code snipped
immediately above is better written as the code snippet immediately below. */
if( ucByte < fileBUFFER_LENGTH )
{
    if( ucByte != 0U )
    {
        /* Example of no reliance on operator precedence! */
        ulResult = ( ( ulValue1 + ulValue2 ) - ulValue3 ) * ulValue4;
    }
}

/* Conditional compilations are laid out and indented as per any
other code. */
#if( configUSE_TRACE_FACILITY == 1 )
{
    /* Add a counter into the TCB for tracing only. */
    pxNewTCB->uxTCBNumber = uxTaskNumber;
}
#endif

/* A space is placed after an opening square bracket, and before a closing
square bracket. */
ucBuffer[ 0 ] = 0U;
ucBuffer[ fileBUFFER_LENGTH - 1U ] = 0U;

Formatting of C Constructs

Memory Management

Memory can be allocated using the standard C library malloc() and free() functions, but they may not be suitable, or appropriate, for one or more of the following reasons:

  • They are not always available on small embedded systems.

  • Their implementation can be relatively large, taking up valuable code space.

  • They are rarely thread-safe.

  • They are not deterministic; the amount of time taken to execute the functions will differ from call to call.

  • They can suffer from fragmentation

  • They can complicate the linker configuration.

  • They can be the source of difficult to debug errors if the heap space is allowed to grow into memory used by other variables.

When FreeRTOS requires RAM, instead of calling malloc(), it calls pvPortMalloc(). When RAM is being freed, instead of calling free(), the kernel calls vPortFree(). pvPortMalloc() has the same prototype as the standard C library malloc() function, and vPortFree() has the same prototype as the standard C library free() function. pvPortMalloc() and vPortFree() are public functions, so can also be called from application code.

Tasks and Co-routines

Task States

A task can exist in one of the following states:

  • Running

    When a task is actually executing it is said to be in the Running state. It is currently utilising the processor. If the processor on which the RTOS is running only has a single core then there can only be one task in the Running state at any given time.

  • Ready

    Ready tasks are those that are able to execute (they are not in the Blocked or Suspended state) but are not currently executing because a different task of equal or higher priority is already in the Running state.

  • Blocked

    A task is said to be in the Blocked state if it is currently waiting for either a temporal or external event. For example, if a task calls vTaskDelay() it will block (be placed into the Blocked state) until the delay period has expired - a temporal event. Tasks can also block to wait for queue, semaphore, event group, notification or semaphore event. Tasks in the Blocked state normally have a 'timeout' period, after which the task will be timeout, and be unblocked, even if the event the task was waiting for has not occurred.

    Tasks in the Blocked state do not use any processing time and cannot be selected to enter the Running state.

  • Suspended

    Like tasks that are in the Blocked state, tasks in the Suspended state cannot be selected to enter the Running state, but tasks in the Suspended state do not have a time out. Instead, tasks only enter or exit the Suspended state when explicitly commanded to do so through the vTaskSuspend() and xTaskResume() API calls respectively.

Task Priorities

Each task is assigned a priority from 0 to ( configMAX_PRIORITIES - 1 ), where configMAX_PRIORITIES is defined within FreeRTOSConfig.h.

If the port in use implements a port optimised task selection mechanism that uses a 'count leading zeros' type instruction (for task selection in a single instruction) and configUSE_PORT_OPTIMISED_TASK_SELECTION is set to 1 in FreeRTOSConfig.h, then configMAX_PRIORITIES cannot be higher than 32. In all other cases configMAX_PRIORITIES can take any value within reason - but for reasons of RAM usage efficiency should be kept to the minimum value actually necessary.

Low priority numbers denote low priority tasks. The idle task has priority zero (tskIDLE_PRIORITY).

The FreeRTOS scheduler ensures that tasks in the Ready or Running state will always be given processor (CPU) time in preference to tasks of a lower priority that are also in the ready state. In other words, the task placed into the Running state is always the highest priority task that is able to run.

Any number of tasks can share the same priority. If configUSE_TIME_SLICING is not defined, or if configUSE_TIME_SLICING is set to 1, then Ready state tasks of equal priority will share the available processing time using a time sliced round robin scheduling scheme.

Task Scheduling

his page provides an overview of the FreeRTOS scheduling algorithm for single-core, asymmetric multicore (AMP), and symmetric multicore (SMP) RTOS configurations. The scheduling algorithm is the software routine that decides which RTOS task should be in the Running state. There can only be one task in the Running state per processor core at any given time. AMP is where each processor core runs its own instance of FreeRTOS. SMP is where there is one instance of FreeRTOS that schedules RTOS tasks across multiple cores.

The free-to-download FreeRTOS book contains a more complete description of the base FreeRTOS scheduling algorithm, with diagrams and examples.

The default RTOS scheduling policy (single-core)

By default, FreeRTOS uses a fixed-priority preemptive scheduling policy, with round-robin time-slicing of equal priority tasks:

  • "Fixed priority" means the scheduler will not permanently change the priority of a task, although it may temporarily boost the priority of a task due to priority inheritance.

  • "Preemptive" means the scheduler always runs the highest priority RTOS task that is able to run, regardless of when a task becomes able to run. For example, if an interrupt service routine (ISR) changes the highest priority task that is able to run, the scheduler will stop the currently running lower priority task and start the higher priority task - even if that occurs within a time slice. In this case, the lower priority task is said to have been "preempted" by the higher priority task.

  • "Round-robin" means tasks that share a priority take turns entering the Running state.

  • "Time sliced" means the scheduler will switch between tasks of equal priority on each tick interrupt - the time between tick interrupts being one time slice. (The tick interrupt is the periodic interrupt used by the RTOS to measure time.)

Using a prioritised preemptive scheduler - avoiding task starvation

A consequence of always running the highest priority task that is able to run is that a high priority task that never enters the Blocked or Suspended state will permanently starve all lower priority tasks of any execution time. That is one reason why, normally, it is best to create tasks that are event-driven. For example, if a high-priority task is waiting for an event, it should not sit in a loop (poll) for the event because by polling it is always running, and so never in the Blocked or Suspended state. Instead, the task should enter the Blocked state to wait for the event. The event can be sent to the task using one of the many FreeRTOS inter-task communication and synchronisation primitives. Receiving the event automatically removes the higher priority task from the Blocked state. Lower priority tasks will run while the higher priority task is in the Blocked state.

Configuring the RTOS scheduling policy

The following FreeRTOSConfig.h settings change the default scheduling behaviour:

  • configUSE_PREEMPTION

    If configUSE_PREEMPTION is 0 then preemption is off and a context switch will only occur if the Running state task enters the Blocked or Suspended state, the Running state task calls taskYIELD(), or an interrupt service routine (ISR) manually requests a context switch.

  • configUSE_TIME_SLICING

    If configUSE_TIME_SLICING is 0 then time slicing is off, so the scheduler will not switch between equal priority tasks on each tick interrupt.

The FreeRTOS AMP scheduling policy

Asymmetric multiprocessing (AMP) with FreeRTOS is where each core of a multicore device runs its own independent instance of FreeRTOS. The cores do not all need to have the same architecture, but do need to share some memory if the FreeRTOS instances need to communicate with each other.

Each core runs its own instance of FreeRTOS so the scheduling algorithm on any given core is exactly as described above for a single-core system. You can use a stream or message buffer as the inter-core communication primitive so that tasks on one core may enter the Blocked state to wait for data or events sent from a different core.

The FreeRTOS SMP scheduling policy

Symmetric multiprocessing (SMP) with FreeRTOS is where one instance of FreeRTOS schedules RTOS tasks across multiple processor cores. As there is only one instance of FreeRTOS running, only one port of FreeRTOS can be used at a time, so each core must have the same processor architecture and share the same memory space.

The FreeRTOS SMP scheduling policy uses the same algorithm as the single-core scheduling policy but, unlike the single-core and AMP scenarios, SMP results in more than one task being in the Running state at any given time (there is one Running state task per core). That means the assumption no longer holds that a lower priority task will only ever run when there are no higher priority tasks that are able to run. To understand why, consider how the SMP scheduler will select tasks to run on a dual-core microcontroller when, initially, there is one high priority task and two medium priority tasks which are all in the Ready state. The scheduler needs to select two tasks, one for each core. First, the high priority task is the highest priority task that is able to run, so it gets selected for the first core. That leaves two medium priority tasks as the highest priority tasks that are able to run, so one gets selected for the second core. The result is that both a high and medium priority task run simultaneously.

Configuring the SMP RTOS scheduling policy

The following configuration options help when moving code written for single-core or AMP RTOS configurations to an SMP RTOS configuration when that code relies on the assumption that a lower priority task will not run if there is a higher priority task that is able to run.

  • configRUN_MULTIPLE_PRIORITIES

    If configRUN_MULTIPLE_PRIORITIES is set to 0 in FreeRTOSConfig.h then the scheduler will only run multiple tasks at the same time if the tasks have the same priority. This may fix code written with the assumption that only one task will run at a time, but only at the cost of losing some of the benefits of the SMP configuration.

  • configUSE_CORE_AFFINITY

    If configUSE_CORE_AFFINITY is set to 1 in FreeRTOSConfig.h, then the vTaskCoreAffinitySet() API function can be used to define which cores a task can and cannot run on. Using this, the application writer can prevent two tasks that make assumptions about their respective execution order from executing at the same time.

Implementation

A task should have the following structure:

    void vATaskFunction( void *pvParameters )
    {
        for( ;; )
        {
            -- Task application code here. --
        }

        /* Tasks must not attempt to return from their implementing
        function or otherwise exit.  In newer FreeRTOS port
        attempting to do so will result in an configASSERT() being
        called if it is defined.  If it is necessary for a task to
        exit then have the task call vTaskDelete( NULL ) to ensure
        its exit is clean. */
        vTaskDelete( NULL );
    }

The type TaskFunction_t is defined as a function that returns void and takes a void pointer as its only parameter. All functions that implement a task should be of this type. The parameter can be used to pass information of any type into the task - this is demonstrated by several of the standard demo application tasks.

Task functions should never return so are typically implemented as a continuous loop. However, as noted on the page that describes the RTOS scheduling algorithm, normally it is best to create tasks that are event-driven so as not to starve lower priority tasks of processing time, making the structure:

    void vATaskFunction( void *pvParameters )
    {
        for( ;; )
        {
            /* Psudeo code showing a task waiting for an event 
            with a block time. If the event occurs, process it.  
            If the timeout expires before the event occurs, then 
            the system may be in an error state, so handle the
            error.  Here the pseudo code "WaitForEvent()" could 
            replaced with xQueueReceive(), ulTaskNotifyTake(), 
            xEventGroupWaitBits(), or any of the other FreeRTOS 
            communication and synchronisation primitives. */
            if( WaitForEvent( EventObject, TimeOut ) == pdPASS )
            {
                -- Handle event here. --
            }
            else
            {
                -- Clear errors, or take actions here. --
            }
        }

        /* As per the first code listing above. */
        vTaskDelete( NULL );
    }

Again, see the RTOS demo application for numerous examples.

Tasks are created by calling xTaskCreate() or xTaskCreateStatic(), and deleted by calling vTaskDelete().

Task Creation Macros

Task functions can optionally be defined using the portTASK_FUNCTION and portTASK_FUNCTION_PROTO macros. These macro are provided to allow compiler specific syntax to be added to the function definition and prototype respectively. Their use is not required unless specifically stated in documentation for the port being used (currently only the PIC18 fedC port).

The prototype for the function shown above can be written as:

void vATaskFunction( void *pvParameters );
portTASK_FUNCTION_PROTO( vATaskFunction, pvParameters );

Likewise the function above could equally be written as:

portTASK_FUNCTION( vATaskFunction, pvParameters )
    {
        for( ;; )
        {
            -- Task application code here. --
        }
    }

Co-routines

Co-routines are only intended for use on very small processors that have severe RAM constraints, and would not normally be used on 32-bit microcontrollers.

A co-routine can exist in one of the following states:

  • Running

    When a co-routine is actually executing it is said to be in the Running state. It is currently utilising the processor.

  • Ready

    Ready co-routines are those that are able to execute (they are not blocked) but are not currently executing. A co-routine may be in the Ready state because:

    1. Another co-routine of equal or higher priority is already in the Running state, or

    2. A task is in the Running state - this can only be the case if the application uses both tasks and co-routines.

  • Blocked

    A co-routine is said to be in the Blocked state if it is currently waiting for either a temporal or external event. For example, if a co-routine calls crDELAY() it will block (be placed into the Blocked state) until the delay period has expired - a temporal event. Blocked co-routines are not available for scheduling.

Queues, Mutexes, Semaphores

Queues

Queues are the primary form of intertask communications. They can be used to send messages between tasks, and between interrupts and tasks. In most cases they are used as thread safe FIFO (First In First Out) buffers with new data being sent to the back of the queue, although data can also be sent to the front.

User Model: Maximum Simplicity, Maximum Flexibility . . .

The FreeRTOS queue usage model manages to combine simplicity with flexibility - attributes that are normally mutually exclusive. Messages are sent through queues by copy, meaning the data (which can be a pointer to larger buffers) is itself copied into the queue rather than the queue always storing just a reference to the data. This is the best approach because:

  • Small messages that are already contained in C variables (integers, small structures, etc.) can be sent into a queue directly. There is no need to allocate a buffer for the message and then copy the variable into the allocated buffer. Likewise, messages can be read from queues directly into C variables.

    Further, sending to a queue in this way allows the sending task to immediately overwrite the variable or buffer that was sent to the queue, even when the sent message remains in the queue. Because the data contained in the variable was copied into the queue the variable itself is available for re-use. There is no requirement for the task that sends the message and the task that receives the message to agree which task owns the message, and which task is responsible for freeing the message when it is no longer required.

  • Using queues that pass data by copy does not prevent queues from being used to pass data by reference. When the size of a message reaches a point where it is not practical to copy the entire message into the queue byte for byte, define the queue to hold pointers and copy just a pointer to the message into the queue instead. This is exactly how the FreeRTOS-Plus-UDP implementation passes large network buffers around the FreeRTOS IP stack.

  • The kernel takes complete responsibility for allocating the memory used as the queue storage area.

  • Variable sized messages can be sent by defining queues to hold structures that contain a member that points to the queued message, and another member that holds the size of the queued message.

  • A single queue can be used to receive different message types, and messages from multiple locations, by defining the queue to hold a structure that has a member that holds the message type, and another member that holds the message data (or a pointer to the message data). How the data is interpreted depends on the message type. This is exactly how the task that manages the FreeRTOS-Plus-UDP IP stack is able to use a single queue to receive notifications of ARP timer events, packets being received from the Ethernet hardware, packets being received from the application, network down events, etc.

  • The implementation is naturally suited for use in a memory protected environment. A task that is restricted to a protected memory area can pass data to a task that is restricted to a different protected memory area because invoking the RTOS by calling the queue send function will raise the microcontroller privilege level. The queue storage area is only accessed by the RTOS (with full privileges).

  • A separate API is provided for use inside of an interrupt. Separating the API used from an RTOS task from that used from an interrupt service routine means the implementation of the RTOS API functions do not carry the overhead of checking their call context each time they execute. Using a separate interrupt API also means, in most cases, creating RTOS aware interrupt service routines is simpler for end users than when compared to alternative RTOS products.

  • In every way, the API is simpler.

The FreeRTOS tutorial book provides additional information on queues, binary semaphores, mutexes, counting semaphores and recursive semaphores, along with simple worked examples in a set of accompanying example projects.

Blocking on Queues

Queue API functions permit a block time to be specified.

When a task attempts to read from an empty queue the task will be placed into the Blocked state (so it is not consuming any CPU time and other tasks can run) until either data becomes available on the queue, or the block time expires.

When a task attempts to write to a full queue the task will be placed into the Blocked state (so it is not consuming any CPU time and other tasks can run) until either space becomes available in the queue, or the block time expires.

If more than one task block on the same queue then the task with the highest priority will be the task that is unblocked first.

See the Queue Management section of the user documentation for a list of queue related API functions. Searching the files in the FreeRTOS/Demo/Common/Minimal directory will reveal multiple examples of their usage.

Note that interrupts must NOT use API functions that do not end in "FromISR".

Semaphores

In FreeRTOS, semaphores are a synchronization primitive used for task coordination in a real-time operating system (RTOS) environment. Semaphores are often employed to control access to shared resources, manage task execution order, and avoid race conditions.

Here are the key aspects of semaphores in FreeRTOS:

  • Binary Semaphores:

    • Binary semaphores are simple and can have only two states: available (unlocked) or unavailable (locked).

    • They are commonly used for signaling between tasks, where a task can give or take the semaphore.

    Binary semaphores and mutexes are very similar but have some subtle differences: Mutexes include a priority inheritance mechanism, binary semaphores do not. This makes binary semaphores the better choice for implementing synchronisation (between tasks or between tasks and an interrupt), and mutexes the better choice for implementing simple mutual exclusion. The description of how a mutex can be used as a mutual exclusion mechanism holds equally for binary semaphores. This sub section will only describe using binary semaphores for synchronisation.

  • Counting Semaphores:

    • Counting semaphores, unlike binary semaphores, can have a count greater than one.

    • They are useful for scenarios where multiple resources of the same type are available, and tasks need to acquire a specific number of resources.

    Counting semaphores are typically used for two things:

    • Counting events.

      In this usage scenario an event handler will 'give' a semaphore each time an event occurs (incrementing the semaphore count value), and a handler task will 'take' a semaphore each time it processes an event (decrementing the semaphore count value). The count value is therefore the difference between the number of events that have occurred and the number that have been processed. In this case it is desirable for the count value to be zero when the semaphore is created.

    • Resource management.

      In this usage scenario the count value indicates the number of resources available. To obtain control of a resource a task must first obtain a semaphore - decrementing the semaphore count value. When the count value reaches zero there are no free resources. When a task finishes with the resource it 'gives' the semaphore back - incrementing the semaphore count value. In this case it is desirable for the count value to be equal the maximum count value when the semaphore is created.

  • Semaphore Functions:

    • FreeRTOS provides functions to create, delete, give, and take semaphores.

    • The xSemaphoreCreateBinary() function is used to create a binary semaphore, and xSemaphoreCreateCounting() is used for counting semaphores.

  • Semaphore Operations:

    • xSemaphoreGive(): Used to release (give) a semaphore.

    • xSemaphoreTake(): Used to acquire (take) a semaphore. If the semaphore is not available, the task will block until it becomes available.

  • Semaphore Timeout:

    • The xSemaphoreTake() function can be configured with a timeout value. This allows a task to wait for a semaphore for a specified period before giving up.

  • Mutex Semaphores:

    • Mutex (mutual exclusion) semaphores are a special type of semaphore used for exclusive access to a shared resource. They ensure that only one task can access the resource at a time.

Here's a simple example of using a binary semaphore in FreeRTOS:

#include "FreeRTOS.h"
#include "semphr.h"

// Create a binary semaphore
SemaphoreHandle_t mySemaphore = NULL;

void Task1(void *pvParameters) {
    while (1) {
        // Perform some task
        // ...

        // Give the semaphore
        xSemaphoreGive(mySemaphore);

        // Wait for a while
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void Task2(void *pvParameters) {
    while (1) {
        // Wait for the semaphore
        if (xSemaphoreTake(mySemaphore, portMAX_DELAY)) {
            // The semaphore is acquired, perform some task
            // ...
        }
    }
}

int main(void) {
    // Create the semaphore
    mySemaphore = xSemaphoreCreateBinary();

    // Create tasks
    xTaskCreate(Task1, "Task1", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
    xTaskCreate(Task2, "Task2", configMINIMAL_STACK_SIZE, NULL, 2, NULL);

    // Start the FreeRTOS scheduler
    vTaskStartScheduler();

    // Should never reach here
    return 0;
}

In this example, Task1 periodically gives the semaphore, and Task2 waits for the semaphore to perform its task. The use of semaphores ensures proper synchronization between the tasks.

Mutexes

Mutexes are binary semaphores that include a priority inheritance mechanism. Whereas binary semaphores are the better choice for implementing synchronisation (between tasks or between tasks and an interrupt), mutexes are the better choice for implementing simple mutual exclusion (hence 'MUT'ual 'EX'clusion).

When used for mutual exclusion the mutex acts like a token that is used to guard a resource. When a task wishes to access the resource it must first obtain ('take') the token. When it has finished with the resource it must 'give' the token back - allowing other tasks the opportunity to access the same resource.

Mutexes use the same semaphore access API functions so also permit a block time to be specified. The block time indicates the maximum number of 'ticks' that a task should enter the Blocked state when attempting to 'take' a mutex if the mutex is not immediately available. Unlike binary semaphores however - mutexes employ priority inheritance. This means that if a high priority task blocks while attempting to obtain a mutex (token) that is currently held by a lower priority task, then the priority of the task holding the token is temporarily raised to that of the blocking task. This mechanism is designed to ensure the higher priority task is kept in the blocked state for the shortest time possible, and in so doing minimise the 'priority inversion' that has already occurred.

Priority inheritance does not cure priority inversion! It just minimises its effect in some situations. Hard real time applications should be designed such that priority inversion does not happen in the first place.

Mutexes should not be used from an interrupt because:

  • They include a priority inheritance mechanism which only makes sense if the mutex is given and taken from a task, not an interrupt.

  • An interrupt cannot block to wait for a resource that is guarded by a mutex to become available.

Recursive Mutexes

A mutex used recursively can be 'taken' repeatedly by the owner. The mutex doesn't become available again until the owner has called xSemaphoreGiveRecursive() for each successful xSemaphoreTakeRecursive() request. For example, if a task successfully 'takes' the same mutex 5 times then the mutex will not be available to any other task until it has also 'given' the mutex back exactly five times. This type of semaphore uses a priority inheritance mechanism so a task 'taking' a semaphore MUST ALWAYS 'give' the semaphore back once the semaphore it is no longer required.

Mutex type semaphores cannot be used from within interrupt service routines.

Mutexes should not be used from an interrupt because:

They include a priority inheritance mechanism which only makes sense if the mutex is given and taken from a task, not an interrupt. An interrupt cannot block to wait for a resource that is guarded by a mutex to become available.

Software Timers

Timer Service Daemon Task

Timer functionality is optional, and not part of the core FreeRTOS kernel. It is instead provided by a timer service (or daemon) task.

FreeRTOS provides a set of timer related API functions. Many of these functions use a standard FreeRTOS queue to send commands to the timer service task. The queue used for this purpose is called the 'timer command queue'. The 'timer command queue' is private to the FreeRTOS timer implementation, and cannot be accessed directly.

The diagram below demonstrates this scenario. The code on the left represents a function that is part of a user application, and called from a task that is created as part of the same user application. The code on the right represents the timer service task implementation. The timer command queue is the link between the application task and the timer service task. In this demonstrated case, the xTimerReset() API function is called from the application code. This results in a reset command being sent to the timer command queue for processing by the timer service task. The application code only calls the xTimerReset() API function - it does not (and cannot) access the timer command queue directly.

Timer Daemon Configuration

To make the FreeRTOS software timer API available in an application, simply:

  1. Add the FreeRTOS/Source/timers.c source file to your project, and

  2. Define the constants detailed in the table below in the applications FreeRTOSConfig.h header file.

  • configUSE_TIMERS -- Set to 1 to include timer functionality. The timer service task will be automatically created as the RTOS scheduler starts when configUSE_TIMERS is set to 1.

  • configTIMER_TASK_PRIORITY -- Sets the priority of the timer service task. Like all tasks, the timer service task can run at any priority between 0 and ( configMAX_PRIORITIES - 1 ).

  • configTIMER_QUEUE_LENGTH -- This sets the maximum number of unprocessed commands that the timer command queue can hold at any one time.

  • configTIMER_TASK_STACK_DEPTH -- Sets the size of the stack (in words, not bytes) allocated to the timer service task.

One-Shot Vs Auto-Reload

There are two types of timer, one-shot timers, and auto-reload timers. Once started, a one-shot timer will execute its callback function only once. It can be manually re-started, but will not automatically re-start itself. Conversely, once started, an auto-reload timer will automatically re-start itself after each execution of its callback function, resulting in periodic callback execution.

The difference in behaviour between a one-shot timer and an auto-reload timer is demonstrated by the timeline in the diagram below. In this diagram, Timer 1 is a one-shot timer that has a period equal to 100, and Timer 2 is an auto-reload timer that has a period equal to 200.

Resetting a Timer

It is possible to re-set a timer that has already started to run. Resetting a timer results in the timer recalculating its expiry time so the expiry time becomes relative to when the timer was reset, and not when the timer was originally started. This behaviour is demonstrated in the next diagram, where Timer 1 is a one-shot timer that has a period equal to 5 seconds.

In the depicted example, it is assumed that the application switches on an LCD back-light when a key is pressed, and that the back-light remains on until 5 seconds pass without any keys being pressed. Timer 1 is used to switch off the LCD back-light when this 5 seconds has elapsed.

More

Event Groups (or "Flags")

Event Bits (Event Flags)

Event bits are used to indicate if an event has occurred or not. Event bits are often referred to as event flags. For example, an application may:

  • Define a bit (or flag) that means "A message has been received and is ready for processing" when it is set to 1, and "there are no messages waiting to be processed" when it is set to 0.

  • Define a bit (or flag) that means "The application has queued a message that is ready to be sent to a network" when it is set to 1, and "there are no messages queued ready to be sent to the network" when it is set to 0.

  • Define a bit (or flag) that means "It is time to send a heartbeat message onto a network" when it is set to 1, and "it is not yet time to send another heartbeat message" when it is set to 0.

Event Groups

An event group is a set of event bits. Individual event bits within an event group are referenced by a bit number. Expanding the example provided above:

  • The event bit that means "A message has been received and is ready for processing" might be bit number 0 within an event group.

  • The event bit that means "The application has queued a message that is ready to be sent to a network" might be bit number 1 within the same event group.

  • The event bit that means "It is time to send a heartbeat message onto a network" might be bit number 2 within the same event group.

Event Group and Event Bits Data Types

Event groups are referenced by variables of type EventGroupHandle_t.

The number of bits (or flags) implemented within an event group depends on whether configUSE_16_BIT_TICKS or configTICK_TYPE_WIDTH_IN_BITS is used to control the type of TickType_t:

  • The number of bits (or flags) implemented within an event group is 8 if configUSE_16_BIT_TICKS is set to 1, or 24 if configUSE_16_BIT_TICKS is set to 0.

  • The number of bits (or flags) implemented within an event group is 8 if configTICK_TYPE_WIDTH_IN_BITS is set to TICK_TYPE_WIDTH_16_BITS, or 24 if configTICK_TYPE_WIDTH_IN_BITS is set to TICK_TYPE_WIDTH_32_BITS, or 56 if configTICK_TYPE_WIDTH_IN_BITS is set to TICK_TYPE_WIDTH_64_BITS.

The dependency on configUSE_16_BIT_TICKS or configTICK_TYPE_WIDTH_IN_BITS results from the data type used for thread local storage in the internal implementation of RTOS tasks.

All the event bits in an event group are stored in a single unsigned variable of type EventBits_t. Event bit 0 is stored in bit position 0, event bit 1 is stored in bit position 1, and so on.

The image below represents a 24-bit event group that uses three bits to hold the three example events already described. In the image only event bit 2 is set.

Event Group RTOS API Functions

Event group API functions are provided that allow a task to, among other things, set one or more event bits within an event group, clear one or more event bits within an event group, and pend (enter the Blocked state so the task does not consume any processing time) to wait for a set of one or more event bits to become set within an event group.

Event groups can also be used to synchronise tasks, creating what is often referred to as a task 'rendezvous'. A task synchronisation point is a place in application code at which a task will wait in the Blocked state (not consuming any CPU time) until all the other tasks taking part in the synchronisation also reached their synchronisation point.

The Challenges an RTOS Must Overcome When Implementing Event Groups

The two main challenges an RTOS must overcome when implementing event groups are:

  1. Avoiding the creation of race conditions in the user's application:

    An event group implementation will create a race condition in the application if:

    • It is not clear who is responsible for clearing individual bits (or flags).

    • It is not clear when a bit is to be cleared.

    • It is not clear if a bit was set or clear at the time a task exited the API function that tested the bit's value (it could be that another task or interrupt has since changed the bit's state).

    The FreeRTOS event groups implementation removes the potential for race conditions by including built in intelligence to ensure setting, testing and clearing of bits appears atomic. Thread local storage and careful use of API function return values make this possible.

  2. Avoiding non-determinism:

    The event group concept implies non-deterministic behaviour because it is not know how many tasks are blocked on an event group, and therefore it is not known how many conditions will need to be tested or tasks unblocked when an event bit is set.

    The FreeRTOS quality standards do not permit non-deterministic actions to be performed while interrupts are disabled, or from within interrupt service routines. To ensure these strict quality standards are not breached when an event bit is set:

    • The RTOS scheduler's locking mechanism is used to ensure interrupts remain enabled when an event bit is set from an RTOS task.

    • The centralised deferred interrupt mechanism is used to defer the action of setting a bit to a task when an attempt is made to set an event bit from an interrupt service routine.

Example Code

Example code snippets are provided in the API documentation, and a comprehensive example is provided in the EventGroupsDemo.c set of standard demo tasks (the source file is located in the FreeRTOS/Demo/Common/Minimal directory of the main FreeRTOS download).

Source Code Organization

The FreeRTOS download includes source code for every processor port, and every demo application. Placing all the ports in a single download greatly simplifies distribution, but the number of files may seem daunting. The directory structure is however very simple, and the FreeRTOS real time kernel is contained in just 3 files (additional files are required if software timer, event group or co-routine functionality is required).

From the top, the download is split into two sub directories; FreeRTOS, and FreeRTOS-Plus. These are shown below:

+-FreeRTOS-Plus    Contains  components and demo projects.
|
+-FreeRTOS         Contains the FreeRTOS real time kernel source
                   files and demo projects

The FreeRTOS-Plus directory tree contains multiple readme files that describe its contents.

FreeRTOS kernel directory structure

The core FreeRTOS kernel source files and demo projects are contained in two sub directories as shown below:

FreeRTOS
    |
    +-Demo      Contains the demo application projects.
    |
    +-Source    Contains the real time kernel source code.

The core RTOS code is contained in three files, which are called called tasks.c, queue.c and list.c. These three files are in the FreeRTOS/Source directory. The same directory contains two optional files called timers.c and croutine.c which implement software timer and co-routine functionality respectively.

Each supported processor architecture requires a small amount of architecture specific RTOS code. This is the RTOS portable layer, and it is located in the FreeRTOS/Source/Portable/[compiler]/[architecture] sub directories, where [compiler] and [architecture] are the compiler used to create the port, and the architecture on which the port runs, respectively.

For the reasons stated on the memory management page, the sample heap allocation schemes are also located in the portable layer. The various sample heap_x.c files are located in the FreeRTOS/Source/portable/MemMang directory.

Examples of portable layer directories:

  • If using the TriCore 1782 port with the GCC compiler:

    The TriCore specific file (port.c) is in the FreeRTOS/Source/Portable/GCC/TriCore_1782 directory. All the other FreeRTOS/Source/Portable sub directories, other than FreeRTOS/Source/Portable/MemMang, can be ignored or deleted.

  • If using the Renesas RX600 port with the IAR compiler:

    The RX600 specific file (port.c) is in the FreeRTOS/Source/Portable/IAR/RX600 directory. All the other FreeRTOS/Source/Portable sub directories, other than FreeRTOS/Source/Portable/MemMang, can be ignored or deleted.

  • And so on for all the ports ...

The structure of the FreeRTOS/Source directory is shown below.

FreeRTOS
    |
    +-Source        The core FreeRTOS kernel files
        |
        +-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

The FreeRTOS download also contains a demo application for every processor architecture and compiler port. The majority of the demo application code is common to all ports and is contained in the FreeRTOS/Demo/Common/Minimal directory (the code located in the FreeRTOS/Demo/Common/Full directory is legacy, and only used by the PC port).

The remaining FreeRTOS/Demo sub directories contain pre-configured projects used to build individual demo applications. The directories are named to indicate the port to which they relate. Each RTOS port also has its own web page that details the directory in which the demo application for that port can be found.

Examples of demo directories:

  • If building the TriCore GCC demo application that targets the Infineon TriBoard hardware:

    The TriCore demo application project file is located in the FreeRTOS/Demo/TriCore_TC1782_TriBoard_GCC directory. All the other sub directories contained in the FreeRTOS/Demo directory (other than the Common directory) can be ignored or deleted.

  • If building the Renesas RX6000 IAR demo application that targets the RX62N RDK hardware:

    The IAR workspace file is located in the FreeRTOS/Demo/RX600_RX62N-RDK_IAR directory. All the other sub directories contained in the FreeRTOS/Demo directory (other than the Common directory) can be ignored or deleted.

  • And so on for all the ports ...

The structure of the FreeRTOS/Demo directory is shown below.

FreeRTOS
    |
    +-Demo
        |
        +-Common    The demo application files that are used by all the demos.
        +-Dir x     The demo application build files for port x
        +-Dir y     The demo application build files for port y

FreeRTOSConfig.h

Static Vs Dynamic Memory Allocation

FreeRTOS versions prior to V9.0.0 allocate the memory used by the RTOS objects listed below from the special FreeRTOS heap. FreeRTOS V9.0.0 and onwards gives the application writer the ability to instead provide the memory themselves, allowing the following objects to optionally be created without any memory being allocated dynamically:

  • Tasks

  • Software Timers

  • Queues

  • Event Groups

  • Binary Semaphores

  • Counting Semaphores

  • Recursive Semaphores

  • Mutexes

Whether it is preferable to use static or dynamic memory allocation is dependent on the application, and the preference of the application writer. Both methods have pros and cons, and both methods can be used within the same RTOS application.

The simple Win32 example located in the FreeRTOS/Source/WIN32-MSVC-Static-Allocation-Only directory of the main FreeRTOS download demonstrates how a FreeRTOS application can be created without including any of the FreeRTOS heap implementations in a project.

Creating an RTOS Object Using Dynamically Allocated RAM

Creating RTOS objects dynamically has the benefit of greater simplicity, and the potential to minimise the application's maximum RAM usage:

  • Fewer function parameters are required when an object is created.

  • The memory allocation occurs automatically, within the RTOS API functions.

  • The application writer does not need to concern themselves with allocating memory themselves.

  • The RAM used by an RTOS object can be re-used if the object is deleted, potentially reducing the application's maximum RAM footprint.

  • RTOS API functions are provided to return information on heap usage, allowing the heap size to be optimised.

  • The memory allocation scheme used can be chosen to best suite the application, be that heap_1.c for simplicity and determinism often necessary for safety critical applications, heap_4.c for fragmentation protection, heap_5.c to split the heap across multiple RAM regions, or an allocation scheme provided by the application writer themselves.

The following API functions, which are available if configSUPPORT_DYNAMIC_ALLOCATION is set to 1 or left undefined, create RTOS objects using dynamically allocated RAM:

Creating an RTOS Object Using Statically Allocated RAM

Creating RTOS objects using statically allocated RAM has the benefit of providing the application writer with more control:

  • RTOS objects can be placed at specific memory locations.

  • The maximum RAM footprint can be determined at link time, rather than run time.

  • The application writer does not need to concern themselves with graceful handling of memory allocation failures.

  • It allows the RTOS to be used in applications that simply don't allow any dynamic memory allocation (although FreeRTOS includes allocation schemes that can overcome most objections).

The following API functions, which are available if configSUPPORT_STATIC_ALLOCATION is set to 1, allow RTOS objects to be created using memory provided by the application writer. To provide memory the application writer simply needs to declare a variable of the appropriate object type, then pass the address of the variable into the RTOS API function. The StaticAllocation.c standard demo/test task is provided to demonstrate how the functions are used:

Create New Project

It is always recommended that a new FreeRTOS project is created by starting with, and then adapting, one of the provided pre-configured demos. Doing this ensures the new project includes all the necessary source and header files, and installs the necessary interrupt service routines, with no effort on the part of the project's creator.

Anatomy of a FreeRTOS Project

A FreeRTOS application will start up and execute just like a non-RTOS application until vTaskStartScheduler() is called. vTaskStartScheduler() is normally called from the application's main() function. The RTOS only controls the execution sequencing after vTaskStartScheduler() has been called.

It is highly recommended to ensure that code is executing correctly (correct start up code, correct linker configuration, etc.) on the chosen target before attempting to use any RTOS functionality.

Source Files

FreeRTOS is supplied as standard C source files that are be built along with all the other C files in your project. The FreeRTOS source files are distributed in a zip file. The RTOS source code organisation page describes the structure of the files in the zip file.

As a minimum, the following source files must be included in your project:

  • FreeRTOS/Source/tasks.c

  • FreeRTOS/Source/queue.c

  • FreeRTOS/Source/list.c

  • FreeRTOS/Source/portable/[compiler]/[architecture]/port.c.

  • FreeRTOS/Source/portable/MemMang/heap_x.c where 'x' is 1, 2, 3, 4 or 5.

If the directory that contains the port.c file also contains an assembly language file, then the assembly language file must also be used.

Optional Source Files

If you need software timer functionality, then add FreeRTOS/Source/timers.c to your project.

If you need event group functionality, then add FreeRTOS/Source/event_groups.c to your project.

If you need stream buffer or message buffer functionality, then add FreeRTOS/Source/stream_buffer.c to your project.

If you need co-routine functionality, then add FreeRTOS/Source/croutine.c to your project (note co-routines are deprecated and not recommended for new designs).

Header Files

The following directories must be in the compiler's include path (the compiler must be told to search these directories for header files):

  • FreeRTOS/Source/include

  • FreeRTOS/Source/portable/[compiler]/[architecture].

  • Whichever directory contains the FreeRTOSConfig.h file to be used - see the Configuration File paragraph below.

Depending on the port, it may also be necessary for the same directories to be in the assembler's include path.

Configuration File

Every project also requires a file called FreeRTOSConfig.h. FreeRTOSConfig.h tailors the RTOS kernel to the application being built. It is therefore specific to the application, not the RTOS, and should be located in an application directory, not in one of the RTOS kernel source code directories.

If heap_1, heap_2, heap_4 or heap_5 is included in your project, then the FreeRTOSConfig.h definition configTOTAL_HEAP_SIZE will dimension the FreeRTOS heap. Your application will not link if configTOTAL_HEAP_SIZE is set too high.

The FreeRTOSConfig.h definition configMINIMAL_STACK_SIZE sets the size of the stack used by the idle task. If configMINIMAL_STACK_SIZE is set too low, then the idle task will generate stack overflows. It is advised to copy the configMINIMAL_STACK_SIZE setting from an official FreeRTOS demo provided for the same microcontroller architecture. The FreeRTOS demo projects are stored in sub directories of the FreeRTOS/Demo directory. Note that some demo projects are old, so do not contain all the available configuration options.

Interrupt Vectors

[Cortex-M users: Information on installing interrupt handers is provided in the "The application I created compiles, but does not run" FAQ]

Every RTOS port uses a timer to generate a periodic tick interrupt. Many ports use additional interrupts to manage context switching. The interrupts required by an RTOS port are serviced by the provided RTOS port source files.

The method used to install the interrupt handlers provided by the RTOS port is dependent on the port and compiler being used. Refer to, and if necessary copy, the provided official demo application for the port being used. Also refer to the documentation page provided for the official demo application.

Future Reading

Last updated