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 |
---|---|
| Pointer to task function |
| Descriptive name of task - For debugging purposes |
| Number of indices in the Stack array (see puxStackBuffer) |
| Data passed to task as parameters (similar to |
| The priority of the task (see Task Priorities) |
| 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.* |
| Must point to a variable of type |
*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 |
---|---|
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 |
---|---|
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
== 1See Memory Management section for comparison of Dynamic and Static memory schemes
Scheduling:
configUSE_PREEMPTION
== 1: Use Pre-emptionPreemption allows tasks to be put on hold if a higher priority task is accessible by the scheduler
configUSE_TIME_SLICING
== 1: Use time slicingIf 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 schedulingNeed to determine if available for port being used
configTICK_RATE_HZ
== ((TickType_t)) 100: Set tick rate100hz is the standard value used in most applications - can increase if needed
configIDLE_SHOULD_YIELD
== 1: Idle yields to other tasks of same priorityThis 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