Intro to FreeRTOS

The idea of this document is to give a primer on Real Time Operating Systems and to create a go-to reference page for FreeRTOS concepts. It is essentially an abridged version of Mastering the FreeRTOS Kernel which is linked below, along with the full API reference sheet.

 

Introduction:

As part of the development for the next iteration of a solar car firmware system, an RTOS implementation is being investigated to help run the firmware the team writes to control the various electrical systems that make up the car. This is in contrast to the bare-metal designs that were used for both the MS12 and MS14 original designs.

What is an RTOS?

A Real Time Operating System (RTOS) is a form of operating system used in real-time or resource limited environments. Similar to mainstream General-Purpose Operating Systems (GPOS) used in PCs, such as Windows, Linux, and MacOS, it handles scheduling, resource management and program execution. The main differences between the two is that an RTOS is designed to deal with soft and hard real-time requirements, and is typically smaller in size and scope as compared its general-purpose counterpart.

From Mastering the FreeRTOS Kernel:

Soft real-time requirements are those that state a time deadline—but breaching the deadline would not render the system useless. For example, responding to keystrokes too slowly might make a system seem annoyingly unresponsive without actually making it unusable.

Hard real-time requirements are those that state a time deadline—and breaching the deadline would result in absolute failure of the system. For example, a driver’s airbag has the potential to do more harm than good if it responded to crash sensor inputs too slowly.

See Real-Time Operating System (Wiki Page), or Introduction to RTOS (video) for more information*.

FreeRTOS is one example of an RTOS. The FreeRTOS ‘Kernel’ (or scheduler as it is alternately called) is a collection of software designed to provide some of the features described above for microcontroller environments. It is free to use in almost any application, commercial or otherwise, under the MIT Open Source License.

*A Note on Documentation
There is a learning curve involved with gaining an understanding of fundamentals. In order to aid the process, both documents and videos have been provided for those who prefer different medium. Some thought has been put into mediating the content to be as direct as possible, but you definitely don’t need to go through all of it. If you find a better resource feel free to edit this page to include it.

Why FreeRTOS?

Although there are a multitude of market RTOS available, FreeRTOS is the leading choice for application in Midnight Sun’s electrical system for the following reasons:

  • Open source - FreeRTOS is open source and free to use, with excellent documentation available for all aspects that it contains

  • Reliability and Safety: Due to it’s tried-and-true nature across many disciplines, it’s safety and reliability are well established

  • Simplicity and Size - The FreeRTOS kernel is comprised of only a few C-files and associated headers, making it easily to integrate into any system. This also creates significantly less program overhead

  • Precedent - FreeRTOS was initially planned for implementation in MSXIV. Additionally, several other international solar car teams have utilized the kernel in previous or current designs

  • Industry Relevance - FreeRTOS is an industry leading RTOS implementation, and has documented use cases in the medical, automotive, and industrial fields and beyond. As such, members who gain experience with this system are better prepared for prospective careers in the embedded space

RTOS Fundamentals

The guide below on implementing FreeRTOS assumes the reader has some familiarity with the concepts below. Additional documentation has been provided for each, and it is advised that these are explored first before continuing. The details for each as they apply to the FreeRTOS and the Midnight Sun project are described in the implementation guide.

RTOS fundamentals on FreeRTOS.org

Additionally, this video series produced by DigiKey gives an in depth explanation of RTOS concepts with FreeRTOS. It is comprised of twelve ~15 minute videos, so may be more depth than is needed at the onset.

Introduction to RTOS Video Series

Tasks and Multitasking:

FreeRTOS uses the term “tasks” to refer to separate executable programs. In concurrent programming for Linux or other systems, these are known as “threads”. One of the responsibilities of the kernel is to control the scheduled execution of these tasks; if more than one task is executing at the same time, then this is a multitasking (multithreading) system.

FreeRTOS multitasking guide (doc) - Single task and Multitask systems (video)

Scheduling

The scheduler is responsible for deciding which task is supposed to be executing at a given time. On single-core processors, only one task can actually be executing at any given time. Multitasking is implemented by the scheduler switching between different tasks, allowing CPU resources to be shared.

To determine which task to execute at a given time, a scheduler implements a scheduling policy. This is an algorithm which pulls a task in or out of execution based on it’s priority or events such as a timer expiry or an external interrupt being triggered.

FreeRTOS Scheduler Guide (doc) - RTOS Scheduling (video)

Context Switching

FreeRTOS Context Switching Intro (doc) - Context Switching in operating systems (video)

Implementing FreeRTOS

The following information is essentially an abridged version of Mastering the FreeRTOS Real-Time Kernel by Richard Barry, which is linked at the top of the page. It is an excellently explained and well-formulated guide. The following is intended to be used for reference, explain concepts as they pertain to the teams solar car system implementation.

Types and Naming Conventions

These are specific to the FreeRTOS kernel, but are important to know when using the methods and variables it defines.

TickType_t: Used to hold a Tick Count value and to specify times (defined as a uint32_t on 32-bit architecture)

BaseType_t: The most efficient data type for a given platform. Used for return types that can only take a certain range

Variables names are prefixed with their type ('c' for char, ‘s' for short, 'x’ for BaseType_t, structs or other non-standard types) , and functions are prefixed with their return type and where they are defined. For example:

  • vTaskPrioritySet() returns a void and is defined within task.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.

Memory Management

Due to the greater complexity introduced with an RTOS, memory management, specifically as it pertains to kernel objects such as tasks, queues, events, and timers, must be considered.

FreeRTOS has several management schemes for heap systems, defined in various files, which allow for the usage of dynamic memory allocation. See here if you are unfamiliar with the difference between static and dynamic memory allocation. Dynamic memory allocation has the following advantages

  • 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

However, there is also the option to use static memory allocation, which is generally a safer way to deal with memory in safety-critical systems. Regulations such as MISRA explicitly forbid dynamic memory allocation outside of initialization. Some of the main benefits of using FreeRTOS’s static memory API are as follows:

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

All of the libraries/projects designed for MSXIV used static allocation. As such, FreeRTOS’s static memory allocation will be used.

To use static allocation, configSUPPORT_STATIC_ALLOCATION must be defined in FreeRTOSConfig.h. FreeRTOS provides the following methods (each with a link to further documentation) for its main kernel objects, each suffixed with static,.

Time and the Tick Interrupt

Time in FreeRTOS is governed by the tick interrupt. The tick interrupt fires at a constant rate, defined by configTICK_RATE_HZ, and is used for task switching (at each tick, checks relevant tasks) and for absolute and relative time. The tick period (1/configTICK_RATE_HZ) is the smallest increment of time available, and all time values (period of soft-timer etc) are a multiple of this. Luckily, there are macros to obtain a tick value based on a millisecond value: pdMS_TO_TICKS()

Tasks

Tasks in FreeRTOS are created, modified, and deleted through API calls. Their functionality is implemented by a C function with a pre-defined prototype, which is registered when the task is created. They must return void, and take a void * as their only parameter, as seen below.

Example Task Function:

void ATaskFunction( void *pvParameters ) { /* Variables can be declared just as per a normal function. Each instance of a task created using this example function will have its own copy of the lVariableExample variable. This would not be true if the variable was declared static – in which case only one copy of the variable would exist, and this copy would be shared by each created instance of the task. */ int32_t lVariableExample = 0; /* A task will normally be implemented as an infinite loop. */ for( ;; ) { /* The code to implement the task functionality will go here. */ } /* Should the task implementation ever break out of the above loop, then the task must be deleted before reaching the end of its implementing function. The NULL parameter passed to the vTaskDelete() API function indicates that the task to be deleted is the calling (this) task.*/ vTaskDelete( NULL ); }

A major characteristic of these task implementations is that they must not return. This means that a task function will not include the return keyword, and it must not ever reach the end of the function. As such, a task function is generally implemented with an infinite loop.

Task Creation

Tasks are created with the xTaskCreate API, in our case, we use xTaskCreateStatic

Will return NULL if puxStackBuffer or puxTaskBuffer are null, and the task handle otherwise

TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, const char * const pcName, const uint32_t ulStackDepth, void * const pvParameters, UBaseType_t uxPriority, StackType_t * const puxStackBuffer, StaticTask_t * const pxTaskBuffer );

Parameter

Description

Parameter

Description

pxTaskCode

Pointer to task function

pcName

Descriptive name of task - For debugging purposes

ulStackDepth

Number of indices in the Stack array (see puxStackBuffer)

pvParameters

Data passed to task as parameters (similar to void *context used in MSXIV)

uxPriority

The priority of the task (see Task Priorities)

puxStackBuffer

Pointer to a StackType_t array that must (by convention) be statically declared in your program. This will be used as the stack for the task, and so must be persistent.*

pxTaskBuffer

Must point to a variable of type StaticTask_t. The variable will be used to hold the new task's data structures (TCB), so it must be persistent (not declared on the stack of a function)

*Note: “There is no easy way to determine the stack space required by a task. It is possible to calculate, but most users will simply assign what they think is a reasonable value, then use the features provided by FreeRTOS to ensure that the space allocated is indeed adequate

Task Scheduling

Task priority is indicated by an unsigned integer passed in task creation, which can take any value in the range 0 to configMAX_PRIORITIES - 1 (defined in FreeRTOSConfig.h). 0 is the lowest priority, and configMAX_PRIORITIES - 1 is the highest priority.

The scheduler will always ensure that the highest priority task available to run is selected to enter the Running state. Where more than one task of the same priority is able to run, the scheduler will transition each task into and out of the Running state, in turn. Since a task never returns or exits, changing which task is executing happens by removing it from the running state. There are a few methods which handle this:

Task Blocking

A task can pause it’s execution, and ‘block’ until some event occurs. This could be time related (expiry of a timer) or a Synchronization Event.

Blocking occurs when a task calls a blocking API such as vTaskDelay().

Task Suspension

Called with vTaskSuspend(), this will disable a task until vTaskResume() is called. This API is less frequently used

A state diagram with the different possible states is shown below:

The Idle Task

The idle task has a priority of 0 and essentially runs when there are no other tasks in the ready state. This can be configured to be ultra-low power consumption, which is one of the benefits of using FreeRTOS. It is possible to configure an idle task hook to run on each iteration of the idle task, and this may be something to look into.

A visualization of how scheduling works with pre-emption and time-slicing enabled:

Queues

Queues, as implemented by FreeRTOS are very similar to the queue data structure used in normal programming. They implement a first in first out (FIFO) buffer system for storing and accessing data where data is written to the back (tail) and removed from the front (head):

Some things to note:

  • Queues are created with a fixed length. This governs the number of items they can hold

  • Multiple tasks can access a queue. As such, they can be used as transmit information between tasks

  • FreeRTOS uses queue by copy: this means that the data parameter is copied byte-by-byte into the queue. If you use a pointer as the parameter, then you can queue by reference

It is possible for tasks to read from multiple queues, but it is recommended that this is only done if strictly necessary. As such, it is likely that the task model that we implement for board projects will have one associated queue from which it can receive multiple types of information, such as can messages, adc readings, etc. This is very similar to STDIN used for linux programs.

Blocking on Queues

Read and write operations can provide an optional value, ‘block time', to indicate a length of time to wait.

  • Read - If there is nothing in the queue, the task will block until ‘block time’ for data to become available in the queue. It might be put there by an interrupt or another task

  • Write - If the queue is full, the task will block until ‘block time’ waiting for data to be removed. This could occur from another interrupt or task

Queues can have multiple readers/writers, so it is possible for a single queue to have more than one task blocked on it. When this is the case, only one task will be unblocked when data becomes available. The task that is unblocked will always be the highest priority task that is waiting for data. If the blocked tasks have equal priority, then the task that has been waiting for data the longest will be unblocked.

Additionally, queues can be grouped into sets, allowing a task to enter the Blocked state to wait for data on any of the queues in the set.

Queues of Pointers

FreeRTOS queues by default copy all object data into the queue, but sometimes it is preferable to queue references instead when using large objects. However, this can cause some issues if the following precautions are not practiced:

  • The owner of the object memory must be clearly defined:

    • Passing a pointer between tasks creates shared memory between those tasks. As such, it is important that it is not modified by both at once, or it becomes corrupted

Queues as a Mailbox

A ‘mailbox’ as described by FreeRTOS standards is a queue with a length of one, used “to hold data that can be read by any task, or any interrupt service routine. The data does not pass through the mailbox, but instead remains in the mailbox until it is overwritten. The sender overwrites the value in the mailbox. The receiver reads the value from the mailbox, but does not remove the value from the mailbox.”

The xQueueOverwrite() API is used by the sender to put data in the mailbox, overwriting whatever was previously in it. xQueuePeek() is used to read the item in the mailbox without removing it.

 

Some relevant Queue Methods: Full can be found here.

Method

Description

Method

Description

Creates a queue using static memory allocation

Delete a queue, freeing its memory

Send an item to the back of a queue

Send an item to the front of a queue

Receive an item from a queue. You must provide a buffer with sufficient space to receive.

Get the number of items in a queue

Check the number of available spaces

Reset the queue: all indices emptied

Peek the item at the front of the queue

Assigns a name to a queue and adds the queue to the registry*

Removes a queue from the registry

Gets the name of a queue if it has been added to the registry

A version of send to back, that will write even if queue is full

Many of the queue interaction functions return a BaseType_t: this will be one of pdPASS or pdFAIL indicating the success of the operation. Additionally, these methods should not be used in an interrupt service routine: equivalents are given for this purpose.

Software Timers

Software timers (SoftTimers) are a concept that those with experience developing firmware for previous solar car iterations should already be familiar with. Effectively they are a timer used to execute functionality after a pre-determined time change has passed. They call a function, called the 'timer callback', which is associated with the timer when it is created.

Soft timers are handled entirely by the FreeRtos kernel, and their callbacks have the following prototype:

void ATimerCallback( TimerHandle_t xTimer );

They execute normally (can return, or exit at end at function) and must not enter a blocked state.

Timer Types:

  • One-Shot Timer: These execute and call their callback only once. It will not restart itself, but can be restarted manually. These are created by passing

  • Periodic (Auto-Reload) Timer: Once started, these will execute their callback and then automatically restart their timer. This results in a periodic calling of the callback.

Timers can either be in a dormant or running state. If they are dormant, it means that the timer exists and can be referenced, but will not call its callback after any given time.

 

Timers and their associated callbacks are handled by the RTOS Daemon Task. It is automatically created at scheduler startup, and its priority and task stack size are configurable. *Using static memory for this task will need to be investigated

Timer IDs

Each softtimer has an ID parameter, which is contained in a void pointer. This is entirely user defined, so the use for this will be determined at a later date.

 

Software Timer API methods:

Method

Description

Method

Description

Creates a timer using static memory

Checks to see if a softtimer is running or dormant

Gets the ID of a timer

Gets the name of a timer

Sets whether timer is one-shot or auto-reload

Starts a previously created timer (puts in run state)

Stops a previously started timer (Sends to dormant state)

Changes the time after which the timer will execute. If called on a dormant timer, it will also start it

Deletes a timer - less applicable when using static memory

Restarts a timer

Gets the ID of a timer

Sets the ID of a timer

Gets the task handle of the Daemon task in charge of running the timers

Gets the name of a timer

Gets the period of a timer

Put the RTOS Daemon in charge of executing a function - Unlikely to be used

ISR Specific methods:

Concurrency And FreeRTOS (Semaphores and Mutexes)

 

Interrupt Handling:

In FreeRTOS, the lowest priority interrupts will surpass the highest priority of tasks. Respectively, interrupt handlers should be kept short, as they will have to run to completion before normal operation is resumed.

There are a few FreeRTOS API calls that must not happen inside an interrupt handler. Essentially, anything that is blocking must not be called. For many of the FreeRTOS supplied methods, there is an equivalent function that is suffixed with FromISR. These functions are Interrupt Safe. Functions without this suffix must not be called from inside an ISR.

Additionally, some API functions may make a task move from the blocked state into the ready state. However, even if it has higher priority than the task which was interrupted, a switch to this task will not happen automatically as it would with pre-emption on regular tasks. The interrupt-safe API functions use a parameter, pxHigherPriorityTaskWoken, which will be set to true if a higher priority task has been moved to the ready state. The programmer can call portYIELD_FROM_ISR() with pxHigherPriorityTaskWoken; if its value is equal to pdTrue, a context switch will take place.

Any kind of interrupt driven functionality (ie a GPIO IT callback) should be deferred to its own task. This allows an ISR to execute as quickly as possible, and prevent unwanted behaviour.

Synchronization with Binary semaphores

Binary semaphores can be used by interrupt handlers to unblock a certain task when an interrupt occurs. This is useful when you have processing based on an interrupt that you do not want to handle in an ISR.

Imagine you have an interrupt-driven I2C interface. When you initiate a read or write operation, you want a task to handle all of the functionality resulting from a read or write completion, and you only want the interrupt to signify that the operation is complete. This could be done as follows:

Binary semaphores in this context can be thought of as a queue, in which they start as empty. The task which reads only ever takes from the semaphore (and waits on it to be filled), and the interrupt handler gives to it, which allows the task to resume. This is a bit different from mutex locking in GPOS, but this usage is discussed later.

Design Decisions:

This section details the selection of certain FreeRTOS functionality for the team's specific application. This mainly concerns the inclusion/exclusion of certain files, and settings defined in FreeRTOSConfig.h

Use Static Memory Allocation

  • configSUPPORT_STATIC_ALLOCATION == 1

    • See Memory Management section for comparison of Dynamic and Static memory schemes

Scheduling:

  • configUSE_PREEMPTION == 1: Use Pre-emption

    • Preemption allows tasks to be put on hold if a higher priority task is accessible by the scheduler

  • configUSE_TIME_SLICING == 1: Use time slicing

    • If two tasks of the same priority are available to run, alternate between them with each tick period

  • configUSE_PORT_OPTIMISED_TASK_SELECTION == TBD: Use port-optimized scheduling

    • Need to determine if available for port being used

  • configTICK_RATE_HZ == ((TickType_t)) 100: Set tick rate

    • 100hz is the standard value used in most applications - can increase if needed

  • configIDLE_SHOULD_YIELD == 1: Idle yields to other tasks of same priority

    • This ensures that the idle task does not run unless nothing else is in the ready state

Timers:

RTOS Daemon:

  • configTIMER_QUEUE_LENGTH - TBD

  • configTIMER_TASK_PRIORITY - TBD

  • configTIMER_TASK_STACK_DEPTH - TBD