This is the second post in my FreeRTOS series. In part 1, I covered tasks and basic signaling. A deeper dive into FreeRTOS now continues with concepts such as buffers, time and memory. Finally I’ll be taking a look at how to configure FreeRTOS itself. This is the final high-level blog entry before the series moves into actual implementation.
Buffers
FreeRTOS buffers in general allow passing data from the writer (Interrupt Service Routine (ISR) or a task) to the reader (a task). When the writer passes the data to a buffer it is copied and when a reader accesses the buffer, it is also copied. It should be noted that it is not safe to have multiple different writers and readers for one buffer without serialization as buffers use task notifications for signaling. Serialization can be achieved by, for example, protecting the API call for accessing the buffer with a mutex. FreeRTOS provides stream buffers and message buffers, and again here is a little analogy for both of them: message buffers are envelopes with one complete message inside them, whereas stream buffers are plumbing pipes where an arbitrary amount of water goes in at one end and goes out the other. Note that examples below do not use ISR API calls, but there are relevant send and receive functions for ISRs as well. 1
Stream
Is the basic building block for buffers as message buffers are also built on top of them. The amount of data in stream buffers may be unspecified and do not need to have a beginning and / or an end. It is possible to write and read any number of bytes to / from them up to the capacity of the buffer:
void writing_task(const void* data, size_t data_length)
{
xStreamBufferSend(buffer_handle, data, data_length, portMAX_DELAY);
}
void reading_task(void *pvParameters)
{
char read_buffer[256];
for (;;)
{
size_t read_size = xStreamBufferReceive(
buffer_handle, read_buffer, sizeof(read_buffer), portMAX_DELAY
);
// Do something with data in the buffer
}
}
In the example above reading_task starts receiving data from the buffer and will stay in “Not Running” state as long as there is no data available since portMAX_DELAY is used. What happens when there is data available or it arrives while waiting depends on settings via xStreamBufferSetTriggerLevel to the specific buffer_handle. Detailed descriptions about these trigger levels are outside the scope of this post, but typically reading_task goes back to “Running” state once data arrives (writing_task is run), or if there was data available when xStreamBufferReceive was called could return instantly with the data without need to change the state at all. Use case for stream buffers is receiving data from sensors or peripherals, where it arrives as a continuous sequence of bytes 2
Message
Allows individual messages of any size to be written, however it is not possible to read messages partially. This means that messages with lengths of 1, 10, and 100 bytes are written, they can only be read as messages of that size:
void writing_task(const void* data, size_t data_length)
{
xMessageBufferSend(buffer_handle, data, data_length, portMAX_DELAY);
}
void reading_task(void *pvParameters)
{
char read_buffer[256];
for (;;)
{
size_t read_size = xMessageBufferReceive(
buffer_handle, read_buffer, sizeof(read_buffer), portMAX_DELAY
);
// Do something with data in the buffer
}
}
The API looks similar to streams but the difference here is that xMessageBufferReceive will return with a size of zero if the message does not fit read_buffer. Message will stay in the message buffer until there is a read buffer large enough for it. Use cases include UART peripherals with framed packets where it is important to have complete messages, rather than fragmented streams, when incoming packets have different lengths. 3
Time
Time itself does not contain multiple objects in FreeRTOS, but rather the following sections describe how time is measured in FreeRTOS and how it enables timer implementation.
Tick
A tick is a way of measuring time in RTOSes. In FreeRTOS, a periodic interrupt is used to increase the tick count at a fixed rate. This tick count acts as the kernel’s internal time base: each tick represents one fixed time interval, and elapsed time can be calculated by counting how many ticks have passed. For example, if the tick interrupt occurs every 1ms, then 100 ticks represent approximately 100ms of elapsed wall clock time. The frequency at which this interrupt, also known as the tick interrupt, is called determines the resolution of time. Higher frequencies give finer time resolution but are more taxing for the CPU, whereas a lower tick rate reduces the resolution but also lowers the overhead. Using the same logic as previously, if the tick interrupt happens every 10ms, then a task waiting for 55ms is more likely to wait closer to 60ms due to the resolution. 4 5
Here is a code example that converts milliseconds to ticks:
void task(void *pvParameters)
{
for (;;)
{
// Perform a task
vTaskDelay(pdMS_TO_TICKS(500)); // Delay for 500ms
// Task continues running
}
}
The reason why pdMS_TO_TICKS is used is the fact that the tick interrupt rate is configured with configTICK_RATE_HZ in FreeRTOSConfig.h (which we will take a closer look at later in this post). So the value differs depending on the configuration. 6
Timer
Allows execution of a callback function (callback from now on) after a set period of time, or to be more precise, ticks have passed. FreeRTOS software timers are lightweight and they, i.e., do not use CPU cycles until the actual execution of a callback. Timers are executed by a task known as “timer service”, which uses standard FreeRTOS objects such as queues to provide timer functionality. The implementation details inside the task are completely private for the service itself, but the settings can be modified with FreeRTOSConfig.h and the timer itself can be accessed with a set of public API functions:
void auto_reload_callback(TimerHandle_t timer_handle)
{
// Timer expired, perform the code inside this block
}
void one_shot_callback(TimerHandle_t timer_handle)
{
// Timer expired, perform the code inside this block
}
int main(void)
{
TimerHandle_t auto_reload_timer_handle = xTimerCreate(
"AutoReloadTimer", // Timer name
pdMS_TO_TICKS(500), // Timer period
pdTRUE, // Auto-reload enabled
NULL, // Timer ID
auto_reload_callback // Callback function
);
TimerHandle_t one_shot_timer_handle = xTimerCreate(
"OneShotTimer", // Timer name
pdMS_TO_TICKS(10 * 1000), // Timer period
pdFALSE, // Auto-reload disabled
NULL, // Timer ID
one_shot_callback // Callback function
);
xTimerStart(auto_reload_timer_handle, 0);
xTimerStart(one_shot_timer_handle, 0);
}
As seen above, there can be multiple timers for the timer service to keep track of and the timers can be either “auto-reload” (periodic callback execution after xTimerStart call) or “one shot” (single callback execution after xTimerStart call). The callbacks should be kept short, just like ISRs, so that other timers do not miss their deadlines. 7
Memory
In embedded environments, memory management is part of the application design, and the same applies to the FreeRTOS kernel. Each object (task, queue, timer…) requires memory, and it can be provided either dynamically or statically depending on the FreeRTOSConfig.h configuration. By default (configurable with configSUPPORT_DYNAMIC_ALLOCATION) the kernel’s dynamic memory allocation is used, as other types of allocations require configuration changes. 8 9
Kernel’s dynamic allocation
In cases where objects are created dynamically, FreeRTOS uses pvPortMalloc and vPortFree functions to allocate and free memory respectively. These are used instead of directly calling malloc and free functions that are part of the C standard. The reason being that embedded use cases differ wildly in terms of RAM allocation. In some cases standard functions are not available or they are not fit for the purpose (i.e. lack of determinism and predictability, the cornerstones of RTOSes). Abstracting the allocation call behind Port functions allows more options and even the kernel provides five different implementation options for applications:
- Does not free the allocated memory of
pvPortMalloc, essentially disablingvPortFree- Legacy option, static allocation is preferred
- Was designed to be used in applications where true dynamic memory allocation is not allowed
- Allows
vPortFreecalls, but freed memory regions are not combined into larger blocks when they are freed- Legacy option, fourth option is preferred
- Directly calls standard
mallocandfreefunctions in a thread-safe manner - Same as second option, but freed memory is combined
- Same as fourth option, but allows noncontiguous memory areas to be used
All of these options are implemented in heap_<number>.c source files, so heap_1.c contains option 1 and so forth.
Allocating the heap
By default, FreeRTOS declares the heap and the linker then places it in memory. However, defining and setting configAPPLICATION_ALLOCATED_HEAP to 1 and setting configTOTAL_HEAP_SIZE to the desired value in FreeRTOSConfig.h allows the uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; array to be placed inside the application. This array is then used by the kernel as the heap.
Static allocation
Static allocations were not present on FreeRTOS from day one, as can be seen with the heap option 1 above. However, all of the objects described so far in this document can also be created using API functions that allow providing a pre-allocated buffer when configSUPPORT_STATIC_ALLOCATION is defined and set to 1. Thus, for example, a call to xTimerCreate (which allocates a timer state variable from the heap) can be replaced with xTimerCreateStatic. The static version takes exactly the same parameters (name, callback etc.) but has a StaticTimer_t *pxTimerBuffer pointer to a variable as final parameter, which holds the state of the timer. 10
Using static allocation brings many benefits, such as allowing the use of specific memory locations, more determinism (total RAM requirement is known at build time) and not needing to worry about allocation failures. However, dynamic allocation allows reuse of the memory, which could lead to smaller memory footprint. As both allocation methods have their pros and cons depending on the use case, FreeRTOS also allows using both dynamic and static allocations at the same time. An application could, for example, allocate from the heap for timers, but tasks could be created statically using xTaskCreateStatic() or any other combination.
FreeRTOSConfig.h
I have referred to this header file a couple of times in this post already, but let’s walk through what this file actually is and what it allows users of the kernel to do. Long story short, this file:
…tailors the RTOS kernel to the application being built.
So it is application specific, and should not be placed amongst the kernel’s files but instead as its own file for each application. In embedded projects it could be next to board specific files, such as startup.c/cpp that handles the startup code for a specific system. The following is not a complete explanation of every possible configuration option, but an example from one of my projects of the kind of decisions that are made in FreeRTOSConfig.h.
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C"
{
#endif
extern uint32_t SystemCoreClock;
#ifdef __cplusplus
}
#endif
/* Scheduler */
// 1 = preemptive RTOS scheduler,
// 0 = cooperative RTOS scheduler
#define configUSE_PREEMPTION 1
// 1 = switch between tasks with same priority upon tick interrupt,
// 0 = keep running the highest priority task without switching
#define configUSE_TIME_SLICING 1
// Frequency of internal clock for generating tick interrupts
#define configCPU_CLOCK_HZ ((unsigned long) SystemCoreClock)
// Tick interrupt frequency, 1000 = 1ms, 100 = 10ms, 10 = 100ms
#define configTICK_RATE_HZ ((TickType_t) 1000)
/* Task */
// Priority count (range is 1...8)
#define configMAX_PRIORITIES 8
// Idle task stack size
#define configMINIMAL_STACK_SIZE 256
// Length of the name given to a task
#define configMAX_TASK_NAME_LEN 16
// 1 = TickType_t is unsigned 16bit type,
// 0 = TickType_t is unsigned 32bit type
#define configUSE_16_BIT_TICKS 0
/* Memory allocation */
// 1 = Enable dynamic allocation,
// 0 = Disable dynamic allocation
#define configSUPPORT_DYNAMIC_ALLOCATION 1
// 1 = Enable static allocation,
// 0 = Disable static allocation
#define configSUPPORT_STATIC_ALLOCATION 0
/* Handlers */
// 1 = Check that FreeRTOS interrupt handlers are installed using direct routing
// 0 = No verification, use indirect routing
#define configCHECK_HANDLER_INSTALLATION 0
/* Hooks and debug */
// Stack Overflow Detection
// 1 = Boundary Checking, fast
// 2 = Sentinel Value Validation, slower
// 3 = Available for certain ports only and enables ISR checking
#define configCHECK_FOR_STACK_OVERFLOW 2
// 1 = Calls a callback for failing dynamic allocations,
// 0 = Disable malloc failure hook
#define configUSE_MALLOC_FAILED_HOOK 1
// 1 = Calls a callback after each cycle of the idle task
// 0 = Disable idle hook
#define configUSE_IDLE_HOOK 0
// 1 = Calls a callback on a tick interrupt
// 0 = Disable tick hook
#define configUSE_TICK_HOOK 0
// Function for trapping errors during runtime.
#define configASSERT(x) \
do \
{ \
if ((x) == 0) \
{ \
taskDISABLE_INTERRUPTS(); \
for (;;) \
{ \
} \
} \
} while (0)
/* Cortex-M interrupt priority configuration.
* QEMU MPS2-AN500 implements 8 priority bits. */
#define configPRIO_BITS 8
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 255
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 192
#define configKERNEL_INTERRUPT_PRIORITY (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
/* Software timers */
// 1 = Software timers enabled
// 0 = Disable timers
#define configUSE_TIMERS 0
/* Optional API functions */
// 1 = Include vTaskDelay function
// 0 = Omit vTaskDelay function
#define INCLUDE_vTaskDelay 1
As can be seen FreeRTOS is not one fixed configuration, the application decides how the kernel behaves with the features that are enabled. 11
Next
In Part 3 I will start dissecting FreeRTOS tasks in great detail and provide actual code examples.
-sorhanp
References
FreeRTOS Documentation - stream & message buffers: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/04-Stream-and-message-buffers/01-RTOS-stream-and-message-buffers ↩︎
FreeRTOS Documentation - stream buffers: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/04-Stream-and-message-buffers/02-Stream-buffer-example ↩︎
FreeRTOS Documentation - message buffers: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/04-Stream-and-message-buffers/03-Message-buffer-example ↩︎
FreeRTOS Documentation - tick: https://www.freertos.org/Documentation/02-Kernel/05-RTOS-implementation-tutorial/02-Building-blocks/03-The-RTOS-tick ↩︎
FreeRTOS Documentation - tick resolution: https://www.freertos.org/Documentation/02-Kernel/05-RTOS-implementation-tutorial/02-Building-blocks/11-Tick-Resolution ↩︎
FreeRTOS Documentation - vTaskDelay: https://www.freertos.org/Documentation/02-Kernel/04-API-references/02-Task-control/01-vTaskDelay ↩︎
FreeRTOS Documentation - software timers: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/05-Software-timers/01-Software-timers ↩︎
FreeRTOS Documentation - heap memory management: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/09-Memory-management/01-Memory-management ↩︎
FreeRTOS Documentation - Static Vs Dynamic Memory Allocation: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/09-Memory-management/03-Static-vs-Dynamic-memory-allocation ↩︎
FreeRTOS Documentation - xTimerCreateStatic: https://www.freertos.org/Documentation/02-Kernel/04-API-references/11-Software-timers/22-xTimerCreateStatic ↩︎
FreeRTOS Documentation - Customization: https://www.freertos.org/Documentation/02-Kernel/03-Supported-devices/02-Customization ↩︎