This is the third post in my FreeRTOS series. Part 1 and part 2 were high-level blog entries detailing how FreeRTOS operates, now it is time to roll up our sleeves and start implementing FreeRTOS in an actual application, turning this series into FreeRTOS on Cortex-M7 without hardware. I have already explained my “cortex-m7-common” library on part 8 of Cortex-M7 without hardware, so if you need a refresher check it out. 1
“I will cover FreeRTOS on Cortex-M7 in later series.”
That is a quote from me from part 8, thus this blog entry is keeping the promise. In order to follow along it is recommended to clone this repository:
# Clone
git clone \
--depth 1 \
--branch blog-common --single-branch \
https://gitlab.com/sorhanp/cortex-m7-qemu.git \
cortex-m7-qemu-blog-common
# Go to folder
cd cortex-m7-qemu-blog-common
CMake will fetch all the necessary dependencies, including the FreeRTOS kernel itself by running:
# Configure and build
cmake --preset arm-none-eabi-debug
cmake --build --preset arm-gcc-debug-build
So how is FreeRTOS included into the project? Let us dissect each part on next headings.
Files of the FreeRTOS kernel project
FreeRTOS being open source means that anyone can access the files, so let’s take a quick look at the project. If you have cloned and built the repository the files in question can be found in out/build/arm-none-eabi-debug/_deps/_freertos_kernel-src directory or directly from GitHub. 2
Top level source files
These files provide the bulk of the kernel. Not all of them are required for every project: this one pulls in list.c, queue.c, and tasks.c (mandatory), plus the chosen port and heap files. The rest are described below for reference.
croutine.c
A quote directly from FreeRTOS documentation:
“Note: Co-routines were implemented for use on very small devices, but are very rarely used in the field these days. For that reason, while there are no plans to remove co-routines from the code, there are also no plans to develop them further.” 3
As this is a deprecated feature and limited to backward compatibility, it will not be used in this project.
event_groups.c
The core implementation for event group support. Optional and will not be used if configuration does not include it.
list.c
FreeRTOS implementation of a doubly linked list data structure, and such is mandatory. Used for managing various internal lists that track, for example, statuses of the tasks, and as such is rarely needed by the application.
queue.c
Implements queue data structure along with synchronization primitives such as mutexes and semaphores. Mandatory for most projects.
stream_buffer.c
Optional stream buffers and message buffers implementation.
tasks.c
Contains the scheduler and task management logic. Main building block of the kernel, thus this file must be present in all projects.
timers.c
Software timer support will not be included without configuration.
Portable directory
While the top level source files listed above implement the main features in a platform independent manner, it is this directory that provides architecture specific parts for things like starting the first task, context switching, and handling of interrupts and system tick. As such, each project must either:
- Select one of the available ports
- Create its own port by copying empty
port.candportmacro.hfromportable/templateand implementing the stubs
Existing ports are organized into folders by compiler, followed by a subfolder by processor architecture. For example this project is using GCC (arm-none-eabi-gcc to be specific) for compiling and ARM_CM7 (Arm Cortex-M7) as a processor:
.
├── portable
│ ├── GCC
│ │ ├── ARM_CM7
│ │ │ ├── r0p1
│ │ │ │ ├── port.c
│ │ │ │ └── portmacro.h
In addition to architecture implementations provided by the port.c and portmacro.h listed above, the FreeRTOS kernel relies on following Cortex-M7 CPU exception mechanisms:
- SysTick
- Used to generate the periodic RTOS tick
- PendSV
- Used to perform context switching at a low interrupt priority
- SVC
- Used when starting the first task
These will allow preemptive scheduling necessary for the kernel to operate. More on these later, when we go through the project structure. 4 5 6
Finally the portable directory also includes files related to memory management:
.
├── portable
│ ├── MemMang
│ │ ├── heap_1.c
│ │ ├── heap_2.c
│ │ ├── heap_3.c
│ │ ├── heap_4.c
│ │ ├── heap_5.c
Again each project must either:
- Select one of the available memory management files
- Implement its own
Header files
There are following paths containing header files that are required to be included when building the kernel:
includefolder located at top level where main source files are- Among the headers are files matching to main source files listed previously
- Also contains
FreeRTOS.h
/portable/[compiler]/[architecture]folder for selected portFreeRTOSConfig.hheader file folder
When a project’s source file wants to use the public API provided by FreeRTOS this include is always needed:
#include <FreeRTOS.h>
What follows is then the header file that contains the prototype for the API, so for example:
#include <task.h>
Includes API for task related functions such as the all important vTaskStartScheduler that starts tick processing, essentially making the FreeRTOS run.
CMake
So how to get aforementioned FreeRTOS include path available and kernel source files compiled into the final firmware image? The solution resides in the build system.
FreeRTOS target
On top level CMakeLists.txt file:
# FreeRTOS Kernel
# The kernel CMake build requires a freertos_config INTERFACE target providing the include path
# to FreeRTOSConfig.h along with target compile options etc., FREERTOS_PORT to select the hardware port, and optionally FREERTOS_HEAP
# to choose the heap implementation (1..5).
set(FREERTOS_PORT
"GCC_ARM_CM7" # Cortex M-7 Port
CACHE STRING "FreeRTOS port")
set(FREERTOS_HEAP
"3" # heap_3 = malloc/free wrapper
CACHE STRING "FreeRTOS heap (1..5 or path)")
add_library(freertos_config INTERFACE)
target_compile_options(freertos_config INTERFACE ${CPU_FEATURE_FLAGS})
target_include_directories(freertos_config SYSTEM INTERFACE ${CMAKE_SOURCE_DIR}/${PROJECT_SOURCE_SUBDIRECTORY})
target_compile_definitions(freertos_config INTERFACE projCOVERAGE_TEST=0)
FetchContent_Declare(
_freertos_kernel
GIT_REPOSITORY https://github.com/FreeRTOS/FreeRTOS-Kernel.git
GIT_TAG V11.2.0
GIT_SHALLOW TRUE # Do not fetch full history, makes fetch faster
UPDATE_DISCONNECTED TRUE # Do not contact the network, if the dependency is already present in the build tree
)
FetchContent_MakeAvailable(_freertos_kernel)
# Give the shared FreeRTOS OBJECT library access to kernel headers
target_link_libraries(cortex_m7_freertos PRIVATE freertos_kernel)
Before fetching FreeRTOS at all, there must be certain defines available, otherwise the build system of the FreeRTOS (which is also CMake) will issue an error and does not allow configuration to proceed.
These defines include FREERTOS_PORT (mandatory) and FREERTOS_HEAP (optional) variables which both control which hardware, compiler and heap source files will be used by the kernel during compilation. Along with these a freertos_config library target needs to be defined. It carries the compile flags, the header inclusion path so source files can find FreeRTOSConfig.h, and the projCOVERAGE_TEST=0 define that disables the kernel’s internal coverage-test hooks.
Next FetchContent_Declare is utilized to actually download the FreeRTOS kernel files with a GIT_TAG pinning the desired version, making sure that each build is reproducible. Finally CMake now has freertos_kernel library target that can be linked to other libraries and the firmware image itself. Right now linking is done to the cortex_m7_freertos target, which is the FreeRTOS OBJECT library defined inside my cortex-m7-common library. It holds the shared handlers and hooks the kernel calls into (the SVC/PendSV/SysTick trampolines I get to later in this post).
This is the majority of the work already done, as now we have the files needed to build FreeRTOS and an easy-to-use library target to link against:
# Link to other libraries
target_link_libraries(
${PROJECT_NAME}
PUBLIC
PRIVATE arm::cmsis_core freertos_kernel)
The target_link_libraries links all the necessary include paths and “bakes” the source files to the final firmware image.
CMSIS target
The link line above also adds arm::cmsis_core to the firmware image. The FreeRTOS port for Cortex-M7 calls into CMSIS for things like NVIC (Nested Vectored Interrupt Controller, the Cortex-M block that manages interrupt priorities and dispatch) priority setup and SysTick programming, so without those headers on the include path the port code will not even compile.
CMSIS-Core is fetched the same way as FreeRTOS, via FetchContent:
FetchContent_Declare(
_cmsis
GIT_REPOSITORY https://github.com/ARM-software/CMSIS_6.git
GIT_TAG v6.3.0
GIT_SHALLOW TRUE
UPDATE_DISCONNECTED TRUE)
FetchContent_MakeAvailable(_cmsis)
add_library(cmsis_core INTERFACE)
add_library(arm::cmsis_core ALIAS cmsis_core)
target_include_directories(cmsis_core SYSTEM INTERFACE ${_cmsis_SOURCE_DIR}/CMSIS/Core/Include)
CMSIS-Core is header-only, so a single INTERFACE library is enough to expose it. The SYSTEM keyword on the include directory tells the compiler to treat those headers as third-party, silencing warnings I cannot fix anyway. The arm::cmsis_core alias is then just a tidier name to use when linking.
What CMSIS does will be explained shortly.
FreeRTOS to firmware
Simply linking FreeRTOS sources in CMake is not enough as the kernel requires configuration and setting of the handlers. Here is the structure for the project:
cortex-m7-qemu/
├── cortex-m7-qemu/
│ ├── cmsis_device.hpp # CMSIS-Core shim
│ ├── FreeRTOSConfig.h # FreeRTOS configuration
│ ├── system_qemu_mps2_an500.cpp # Device-specific SystemInit
│ ├── vectors.cpp # MPS2-AN500 vector table
│ ├── main.cpp # FreeRTOS tasks
│ └── syscalls_uart.cpp # Optional UART backend
├── CMakeLists.txt
├── CMakePresets.json
└── toolchain/
└── arm-none-eabi.cmake
Let us go through what each of the source files does so that FreeRTOS actually starts operating inside the firmware. The syscalls_uart.cpp file does not contain any kernel related implementations.
cmsis_device.hpp
The FreeRTOS port for Cortex-M7 does not talk to the hardware directly, but goes through CMSIS-Core, which is Arm’s abstraction layer for Cortex-M devices. SysTick programming, NVIC priority setup, barrier instructions like __DSB (Data Synchronization Barrier) and __ISB (Instruction Synchronization Barrier), all of these come from CMSIS headers.
CMSIS-Core expects each device to provide a small header describing the chip: which Cortex-M core it is, whether there is an FPU, how many NVIC priority bits the silicon implements, and the device-specific IRQ numbers. On real hardware this header comes from the vendor. For QEMU there is no vendor, so I will act as one and provide my own minimal version in cmsis_device.hpp:
#ifndef __CORTEX_M
#define __CORTEX_M (7U)
#endif
#ifndef __FPU_PRESENT
#define __FPU_PRESENT (1U)
#endif
#ifndef __NVIC_PRIO_BITS
#define __NVIC_PRIO_BITS (4U)
#endif
typedef enum IRQn
{
/* Cortex-M7 core exceptions */
NonMaskableInt_IRQn = -14,
HardFault_IRQn = -13,
// ...
PendSV_IRQn = -2,
SysTick_IRQn = -1,
} IRQn_Type;
#include "core_cm7.h"
The IRQ enum is what lets calls like NVIC_SetPriority(PendSV_IRQn, ...) and NVIC_SetPriority(SysTick_IRQn, ...) compile, and those two calls are exactly what the FreeRTOS port needs during initialization. With the shim in place, core_cm7.h happily pulls in everything else.
FreeRTOSConfig.h
I have already explained the majority of the configuration used in my part 2 explanation so I will not explain it again. However what I’d like to do is highlight the following:
#ifdef __cplusplus
extern "C"
{
#endif
extern uint32_t SystemCoreClock;
#ifdef __cplusplus
}
#endif
The SystemCoreClock is a forward declaration and it is intended to hold the core clock frequency. Then it is used to define configCPU_CLOCK_HZ:
/* Scheduler */
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configCPU_CLOCK_HZ ((unsigned long) SystemCoreClock)
#define configTICK_RATE_HZ ((TickType_t) 1000)
This way the frequency is not hard coded to a different value if something changes inside the project. Also it makes this FreeRTOS configuration easy to move between two different projects that both share otherwise the same configuration. Other thing to draw attention is:
/* Handler installation: use indirect routing via naked trampolines in freertos_handlers.cpp */
#define configCHECK_HANDLER_INSTALLATION 0
By default this flag is 1, and the kernel asserts at start-up that the vector table points at the FreeRTOS port handlers (vPortSVCHandler, xPortPendSVHandler, xPortSysTickHandler). Our vector table lists generic SVC_Handler, PendSV_Handler, and SysTick_Handler names instead, so the assertion would fire and the kernel would refuse to start. Setting the flag to 0 disables the check.
Going through generic names in the first place is intentional: the vector table stays decoupled from FreeRTOS, so the same vectors.cpp works whether or not the RTOS is linked in. The indirection itself lives in the freertos_handlers.cpp file provided by cortex-m7-common, and I cover it in the vectors.cpp section below.
system_qemu_mps2_an500.cpp
Definition of SystemCoreClock lives here, paired with the include of cmsis_device.hpp so that the CMSIS-Core core_cm7.h header gets pulled in. Two related files, two different roles: cmsis_device.hpp is the device header shim (constants, types, the IRQ enum) and system_qemu_mps2_an500.cpp is the device implementation that owns the global SystemCoreClock symbol the rest of the firmware (and FreeRTOS) reads from.
On real hardware this file would also contain a SystemInit function that runs clock-tree and bus setup before main. QEMU’s MPS2-AN500 model needs no such setup, so the file stays lean: just the SystemCoreClock definition at the frequency the tick math in FreeRTOSConfig.h is built around.
vectors.cpp
The contents of the file have not changed, and look just like they did at the Cortex-M7 without hardware part 7. Meaning that vector_table array still includes the standard Cortex-M core vectors up to SysTick. Each handler is declared as a weak alias to Default_Handler but like comment suggests:
“…application code (or FreeRTOS handlers) can override any of them.”
Another quote directly from GNU:
“The weak attribute causes a declaration of an external symbol to be emitted as a weak symbol rather than a global. This is primarily useful in defining library functions that can be overridden in user code, though it can also be used with non-function declarations.” 7
So now it is time to talk about freertos_handlers.cpp file, located at out/build/arm-none-eabi-debug/_deps/_cortex_m7_common-src/cortex-m7-common directory or directly from GitLab, that does just that. For example:
// Naked trampoline to the FreeRTOS SVCall handler. The branch instruction preserves the exact register and stack state
// that vPortSVCHandler's inline assembly expects.
extern "C" __attribute__((naked)) void SVC_Handler(void)
{
__asm volatile("b vPortSVCHandler");
}
The PendSV_Handler and SysTick_Handler follow the same pattern, branching to xPortPendSVHandler and xPortSysTickHandler respectively.
As can be seen here the prototype for the SVC_Handler function matches how it is presented in the vectors.cpp file. However, this function does not have the weak attribute so it overrides the previous alias to extern "C" void Default_Handler(void). This means that when the firmware image is run, the “naked trampoline” above is executed, not the Default_Handler. The extern "C" wrapping is what makes this work across the C/C++ boundary: without it, the C++ compiler would mangle the name and the linker would no longer see this definition as the same symbol that vectors.cpp declared as weak.
Here we also stumble upon a new GCC only keyword naked, so let us get another direct quote from GNU:
“It allows the compiler to construct the requisite function declaration, while allowing the body of the function to be assembly code. The specified function does not have prologue/epilogue sequences generated by the compiler. Only basic
asmstatements can safely be included in naked functions.” 7
Which means that the compiler does not generate the usual entry/exit boilerplate to the function causing registers, stack state etc. to be altered. I will explain this design rationale with more detail on the post about FreeRTOS task.
And finally main.cpp
Here is where the actual FreeRTOS execution finally begins. Once the Reset_Handler of the firmware image calls the main function, a startup line is printed and then the FreeRTOS API calls are made:
std::printf("Hello from Cortex-M7 main!\n");
xTaskCreate(Task, "Task", 512, nullptr, 1, nullptr);
vTaskStartScheduler();
xTaskCreate creates a new task, and it takes Task function as the first parameter:
extern "C"
{
static void Task(void*)
{
constexpr int count = 5;
for (int i = 1; i <= count; ++i)
{
std::printf("Hello #%d from Cortex-M7 Task\n", i);
vTaskDelay(pdMS_TO_TICKS(50));
}
std::printf("Exiting...");
exit(0);
}
} // extern "C"
This is the task entry function which is executed once the FreeRTOS scheduler starts. What follows is the name for the task, the number of words (32-bit word size is used, so 512 words is 2048 bytes) to allocate for use as the task’s stack. The first nullptr is the value that gets passed to Task as a parameter, which is then followed by the priority. The last parameter is an output pointer that receives the task handle, so the caller can later refer to the task for things like suspending or deleting it. Not used here, so nullptr is given. 8
After xTaskCreate function returns the next line is a call to vTaskStartScheduler, which starts the RTOS scheduler. This function should not return as the kernel now has control over the task execution. However, if the function returns it usually means that there is not enough memory available. 9
Under the hood vTaskStartScheduler configures SysTick, sets the PendSV and SysTick priorities via CMSIS, and triggers SVC to launch the first task. I will trace those steps in Part 4.
With the task in place when run-qemu target is executed:
cmake --build --preset arm-gcc-debug-build --target run-qemu
Expected output is this:
Hello from Cortex-M7 main!
Hello #1 from Cortex-M7 Task
Hello #2 from Cortex-M7 Task
Hello #3 from Cortex-M7 Task
Hello #4 from Cortex-M7 Task
Hello #5 from Cortex-M7 Task
Exiting...
The full handler chain
Putting the pieces together: when an SVC, PendSV, or SysTick exception fires, the Cortex-M7 NVIC dispatches to the handler named in the vector table. Our vectors.cpp lists generic SVC_Handler, PendSV_Handler, and SysTick_Handler as weak aliases to Default_Handler. At link time freertos_handlers.cpp from cortex-m7-common provides strong definitions of those same names as naked trampolines that branch into the FreeRTOS port functions (vPortSVCHandler, xPortPendSVHandler, xPortSysTickHandler). The strong symbols win, so control flows from the CPU exception straight into the kernel: SysTick drives the tick interrupt, PendSV runs the context switch, and SVC kicks off the first task.
Next
There we have it, the FreeRTOS is running nicely inside the firmware. In Part 4 I will more thoroughly start to analyze FreeRTOS tasks.
-sorhanp
References
Cortex-M7 common library: https://gitlab.com/sorhanp/cortex-m7-common ↩︎
FreeRTOS Kernel Book - Kernel Distribution: https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch02.md ↩︎
FreeRTOS Documentation - Tasks and Co-routines: https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/01-Tasks-and-co-routines/00-Tasks-and-co-routines ↩︎
FreeRTOS Documentation - Creating a New Port: https://docs.freertos.org/Documentation/02-Kernel/03-Supported-devices/01-FreeRTOS-porting-guide ↩︎
FreeRTOS Documentation - Running the RTOS on a ARM Cortex-M Core: https://www.freertos.org/Documentation/02-Kernel/03-Supported-devices/04-Demos/ARM-Cortex/RTOS-Cortex-M3-M4 ↩︎
ARM - ARMv7-M Architecture Reference Manual: https://developer.arm.com/documentation/ddi0403/latest/ ↩︎
GNU GCC - Attributes Specific to GCC: https://gcc.gnu.org/onlinedocs/gcc/Attributes.html ↩︎ ↩︎
FreeRTOS Documentation - xTaskCreate: https://www.freertos.org/Documentation/02-Kernel/04-API-references/01-Task-creation/01-xTaskCreate ↩︎
FreeRTOS Documentation - vTaskStartScheduler: https://www.freertos.org/Documentation/02-Kernel/04-API-references/04-RTOS-kernel-control/03-vTaskStartScheduler ↩︎