FreeRTOS on Kinetis E Cortex M0+ : easy porting tutorial
FreeRTOS is a popular, open-source operating system that can run on a variety of microcontrollers. This post shows how to make a minimal working setup with two tasks on a new MCU without starting from a complete demo code or code generators (like Processor Expert) on an inexpensive development board FRDM-KE06Z from NXP. All examples use static memory allocation. Most of the procedures and tips mentioned here apply equally well to all Cortex-M microcontrollers.
I will assume that you have good understanding of C, installed Kinetis Design Studio some experience with Cortex-M microcontrollers and that you know why you want an RTOS. :)
From programmer's point of view FreeRTOS makes several C function appear as they would be executed all at once. It does it by switching between them many times a second. Each of those functions is called a task. Switching happens within a timer interrupt (in case of Cortex-M it is the systick).
Creating a new KDS project
Let's begin by creating blank project targeted for Kinetis KE06Z. Don't enable Processor Expert.
Default project structure
The project contains MCU headers and CMSIS headers. CMSIS headers and provided mostly by ARM - they provide a common programming interface to the CPU core. FreeRTOS uses them to access systick and do some interrupt control. As the KE06Z is a Cortex-M0+ the headers for M4 and M7 can be removed.
Project after removing unnecessary files
Adding FreeRTOS files
The number of directories and files in FreeRTOS release package can be frightening but only a very small part is necessary.
Let's have a look at FreeRTOS v9.0.0 release directory structure:
FreeRTOS-Plus directory and demos can be totally left out of scope (it is handy to look at the demos - to check if the particulr Cortex type is supported). All important parts are in FreeRTOS directory.
FreeRTOS uses basically two kinds of source files:
portable - common to all chips and architectures
port files - specific to the target chip (ideally just two files)
All portable .c and .h files from FreeRTOS/Source have to be added to the project (don't add anything from the "portable" directory yet - naming is a little confusing as the "portable" directory contains files that are CPU-specific).
Finding the right port for your CPU
While the portable files are written in plain C and are easy to follow, the port itself is written in assembly and essentially does low-level black magic (from C programmer's point of view). Fortunately the port is dependent only on the CPU core and not on the whole microcontroller so for example if MCU with Cortex M0+ from Atmel has a port, then it can also be used with an MCU from ST or Silicon Labs that has the same core. Port files should match the core (example: Cortex M4F is different than Cortex M4 but Cortex M0 and M0+ use the same code - YMMV).
In this particular case: NXP Kinetis KE06 has a Cortex M0+ and the port files are in FreeRTOS/Source/portable/GCC/ARM_CM0.
Portable and port files have to be added to Kinetis project.
That is how the project may look like:
FreeRTOSConfig.h
This is the only file that has to be customized for your project. I have taken one from the CORTEX_M0+_Atmel_SAMD20_XPlained demo (as it matches the core type of my MCU).
First thing to specify is the CPU clock speed: #include <asf.h> has to be removed (it is an Atmel library) and instead MKE06Z4.h has to be included. The configCPU_CLOCK_HZ macro has to be defined simply as DEFAULT_SYSTEM_CLOCK.
Second, a bunch of options (in my case) have to be tweaked. My example uses only static memory allocation.
configUSE_TICK_HOOK is zero
configGENERATE_RUN_TIME_STATS is zero (and accompanying vMainConfigureTimerForRunTimeStats, ulMainGetRunTimeCounterValue, portCONFIGURE_TIMER_FOR_RUN_TIME_STATS, portGET_RUN_TIME_COUNTER_VALUE next to it can be removed)
configUSE_TIMERS is 0
configCHECK_FOR_STACK_OVERFLOW is zero (we will get back to it later)
remote configTOTAL_HEAP_SIZE (we will be using only static memory allocation)
add #define configSUPPORT_STATIC_ALLOCATION 1
add #define configSUPPORT_DYNAMIC_ALLOCATION 0
change configMINIMAL_STACK_SIZE to 128 (it is CPU-native words, in case of this ARM: uint32_t)
#ifndef FREERTOS_CONFIG_H#ifndef FREERTOS_CONFIG_H#define FREERTOS_CONFIG_H/*----------------------------------------------------------- * Application specific definitions. * * These definitions should be adjusted for your particular hardware and * application requirements. * * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE. * * See http://www.freertos.org/a00110.html. *----------------------------------------------------------*/#include #define configUSE_PREEMPTION 1#define configUSE_IDLE_HOOK 0#define configUSE_TICK_HOOK 0#define configCPU_CLOCK_HZ ( DEFAULT_SYSTEM_CLOCK )#define configTICK_RATE_HZ ( ( TickType_t ) 500 )#define configMAX_PRIORITIES ( 5 )#define configMINIMAL_STACK_SIZE ( 128 )#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 8192 ) )#define configMAX_TASK_NAME_LEN ( 5 )#define configUSE_TRACE_FACILITY 1#define configUSE_16_BIT_TICKS 0#define configIDLE_SHOULD_YIELD 1#define configUSE_MUTEXES 1#define configQUEUE_REGISTRY_SIZE 8#define configCHECK_FOR_STACK_OVERFLOW 0#define configUSE_RECURSIVE_MUTEXES 1#define configUSE_MALLOC_FAILED_HOOK 1#define configUSE_APPLICATION_TASK_TAG 0#define configUSE_COUNTING_SEMAPHORES 1#define configUSE_QUEUE_SETS 1#define configSUPPORT_STATIC_ALLOCATION 1#define configSUPPORT_DYNAMIC_ALLOCATION 0/* Run time stats related definitions. */#define configGENERATE_RUN_TIME_STATS 0/* Co-routine definitions. */#define configUSE_CO_ROUTINES 0#define configMAX_CO_ROUTINE_PRIORITIES ( 2 )/* Software timer definitions. */#define configUSE_TIMERS 0#define configTIMER_TASK_PRIORITY ( 2 )#define configTIMER_QUEUE_LENGTH 5#define configTIMER_TASK_STACK_DEPTH ( 80 )/* Set the following definitions to 1 to include the API function, or zeroto exclude the API function. */#define INCLUDE_vTaskPrioritySet 1#define INCLUDE_uxTaskPriorityGet 1#define INCLUDE_vTaskDelete 1#define INCLUDE_vTaskCleanUpResources 1#define INCLUDE_vTaskSuspend 1#define INCLUDE_vTaskDelayUntil 1#define INCLUDE_vTaskDelay 1#define INCLUDE_eTaskGetState 1/* This demo makes use of one or more example stats formatting functions. Theseformat the raw data provided by the uxTaskGetSystemState() function in to humanreadable ASCII form. See the notes in the implementation of vTaskList() withinFreeRTOS/Source/tasks.c for limitations. */#define configUSE_STATS_FORMATTING_FUNCTIONS 1/* Normal assert() semantics without relying on the provision of an assert.hheader file. */#define configASSERT( x ) if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }/* Definitions that map the FreeRTOS port interrupt handlers to their CMSISstandard names - or at least those used in the unmodified vector table. */#define xPortPendSVHandler PendSV_Handler#define xPortSysTickHandler SysTick_Handler/* The size of the global output buffer that is available for use when thereare multiple command interpreters running at once (for example, one on a UARTand one on TCP/IP). This is done to prevent an output buffer being defined byeach implementation - which would waste RAM. In this case, there is only onecommand interpreter running. */#define configCOMMAND_INT_MAX_OUTPUT_SIZE 2048#endif /* FREERTOS_CONFIG_H */
main.c
FreeRTOS requires one task to be always ready to execute (not blocked) - this is the idle task. Idle task (as every task) needs its own stack. Because we are using static memory allocation, vApplicationGetIdleTaskMemory has to be implemented. Static stacks for tasks are simply regular arrays. Very basic main.c:
At this point the project should build cleanly and can be flashed to the microcontroller. It does totally nothing (not even blink a LED!) but runs and should not crash (inspect by pausing and restarting with the debugger). Impressive! :)
Blinking a LED the RTOS way
To get anything done at least one task has to be specified. It must have an endless loop and can't return (however it can terminate itself via FreeRTOS API call). Of course a separate stack has to be allocated for the blink task. Example:
#include"MKE06Z4.h"#include"FreeRTOS/FreeRTOS.h"#include"FreeRTOS/task.h"/* global variables */staticStaticTask_txIdleTaskTCBBuffer;staticStackType_txIdleStack[configMINIMAL_STACK_SIZE];staticStackType_tblink_red_task_stack[configMINIMAL_STACK_SIZE];staticStaticTask_tblink_red_task_handle;/* prototypes */voidvApplicationGetIdleTaskMemory(StaticTask_t**ppxIdleTaskTCBBuffer,StackType_t**ppxIdleTaskStackBuffer,uint32_t*pulIdleTaskStackSize);voidblink_red_task(void*params);/* implementation */voidvApplicationGetIdleTaskMemory(StaticTask_t**ppxIdleTaskTCBBuffer,StackType_t**ppxIdleTaskStackBuffer,uint32_t*pulIdleTaskStackSize){*ppxIdleTaskTCBBuffer=&xIdleTaskTCBBuffer;*ppxIdleTaskStackBuffer=xIdleStack;*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;}intmain(void){/* Initialize GPIO for LEDs */GPIOB_PDDR|=PORT_PUE1_PTGPE5_MASK|PORT_PUE1_PTGPE6_MASK|PORT_PUE1_PTGPE7_MASK;/*set as outputs*/GPIOB_PSOR=PORT_PUE1_PTGPE5_MASK|PORT_PUE1_PTGPE6_MASK|PORT_PUE1_PTGPE7_MASK;/*all leds off*/xTaskCreateStatic(blink_red_task,//taskfunction"blink red task",//tasklogicalnameconfigMINIMAL_STACK_SIZE,//sizeofstackNULL,//noextraparameters1,//priority(0islowest-idletask)blink_red_task_stack,//stackpointer&blink_red_task_handle);//pointertoStaticTask_ttaskhandlevTaskStartScheduler();//thisneverreturnsreturn0;}voidblink_red_task(void*params){while(1){GPIOB_PCOR=PORT_PUE1_PTGPE5_MASK;//redLEDonvTaskDelay(pdMS_TO_TICKS(500));//sleep500msGPIOB_PSOR=PORT_PUE1_PTGPE5_MASK;//redLEDoffvTaskDelay(pdMS_TO_TICKS(500));//sleep500ms}}
Blinking two LEDs
What is better than a blinking LED? Two blinking LEDs!
This example has two tasks that blink separate LEDs with different timings so it is easy to observe that both loops are executed "at the same time" (in reality - chopped and executed alternatively), which is the main point of an operating system. :)
#include"MKE06Z4.h"#include"FreeRTOS/FreeRTOS.h"#include"FreeRTOS/task.h"/* global variables */staticStaticTask_txIdleTaskTCBBuffer;staticStackType_txIdleStack[configMINIMAL_STACK_SIZE];staticStackType_tblink_red_task_stack[configMINIMAL_STACK_SIZE];staticStaticTask_tblink_red_task_handle;staticStackType_tblink_blue_task_stack[configMINIMAL_STACK_SIZE];staticStaticTask_tblink_blue_task_handle;/* prototypes */voidvApplicationGetIdleTaskMemory(StaticTask_t**ppxIdleTaskTCBBuffer,StackType_t**ppxIdleTaskStackBuffer,uint32_t*pulIdleTaskStackSize);voidblink_red_task(void*params);voidblink_blue_task(void*params);/* implementation */voidvApplicationGetIdleTaskMemory(StaticTask_t**ppxIdleTaskTCBBuffer,StackType_t**ppxIdleTaskStackBuffer,uint32_t*pulIdleTaskStackSize){*ppxIdleTaskTCBBuffer=&xIdleTaskTCBBuffer;*ppxIdleTaskStackBuffer=xIdleStack;*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;}intmain(void){/* Initialize GPIO for LEDs */GPIOB_PDDR|=PORT_PUE1_PTGPE5_MASK|PORT_PUE1_PTGPE6_MASK|PORT_PUE1_PTGPE7_MASK;/*set as outputs*/GPIOB_PSOR=PORT_PUE1_PTGPE5_MASK|PORT_PUE1_PTGPE6_MASK|PORT_PUE1_PTGPE7_MASK;/*all leds off*/xTaskCreateStatic(blink_red_task,//taskfunction"blink red task",//tasklogicalnameconfigMINIMAL_STACK_SIZE,//sizeofstackNULL,//noextraparameters1,//priority(0islowest-idletask)blink_red_task_stack,//stackpointer&blink_red_task_handle);//pointertoStaticTask_ttaskhandlexTaskCreateStatic(blink_blue_task,//taskfunction"blink blue task",//tasklogicalnameconfigMINIMAL_STACK_SIZE,//sizeofstackNULL,//noextraparameters1,//priority(0islowest-idletask)blink_blue_task_stack,//stackpointer&blink_blue_task_handle);//pointertoStaticTask_ttaskhandlevTaskStartScheduler();//thisneverreturnsreturn0;}voidblink_red_task(void*params){while(1){GPIOB_PCOR=PORT_PUE1_PTGPE5_MASK;//redLEDonvTaskDelay(pdMS_TO_TICKS(500));//sleep500msGPIOB_PSOR=PORT_PUE1_PTGPE5_MASK;//redLEDoffvTaskDelay(pdMS_TO_TICKS(500));//sleep500ms}}voidblink_blue_task(void*params){while(1){GPIOB_PCOR=PORT_PUE1_PTGPE7_MASK;//blueLEDonvTaskDelay(pdMS_TO_TICKS(100));//sleep100msGPIOB_PSOR=PORT_PUE1_PTGPE7_MASK;//blueLEDoffvTaskDelay(pdMS_TO_TICKS(150));//sleep150ms}}
Remember that the stack size everywhere in FreeRTOS is specified in words, for a 32-bit ARM that is 32 bits (so stack size 1 = 4 bytes).
Stack overflow handling
Memory management (especially with multiple stacks) is an issue in all resource-constrained operating systems. Stack overflows can manifest themselves as "random data corruption somewhere" (stack has "hit" other variables above and changed their values). Fortunately FreeRTOS supports stack instrumentation. configCHECK_FOR_STACK_OVERFLOW has to be defined (to 1 or 2) in FreeRTOSConfig.h to enable vApplicationStackOverflowHook. This function has to be defined somewhere and it is up to the developer to do something about the problem.
When a stack overflow happens this function simply calls a breakpoint. With a debugger attached the CPU will stop, without a debugger connected the breakpoint instruction will trigger system reset. In production grade code I would add storage of some diagnostic information, maybe a controlled system shutdown/cleanup and restart. When any of the stacks overflows and the microcontroller has no MPU or MMU - global data and program state can not be trusted anymore (global data could be corrupted and hold literally anything) so a system reset is mandatory.
Hardware sharing between tasks
is difficult. Whenever accessing global data (hardware registers can be regarded as such) you have to assume that an interrupt (or task switch) can happen at any time. All read-modify-write operations can be interrupted. For example GPIO initilization in sample code would not be safe to carry out in two separate tasks. Simplest solution is a critical section - disabling interrupts for a short while. Another approach is to use mutexes or design a bigger driver that can handle multiple tasks, like an SPI bus driver that supports two devices (eg. flash and accelerometer) on a single bus that can be used by two tasks - the driver must take care to properly multiplex access to the shared bus.
What to do when there is nothing to do - idle hook
What does FreeRTOS do when all tasks are blocked and are waiting for something? It calls the idle hook! Idle hook is enabled by defining configUSE_IDLE_HOOK in the configuration file. It is a regular function that will be called whenever there is nothing better to do (so it can also be never called, depending on the circumstances). Typical usage: put the CPU into sleep mode and/or kick the watchdog (to guarantee that CPU utilization is below 100%):
CPU will sleep until next interrupt happens (either systick or another peripheral). This will reduce total power consumption.
Crashes and hard faults
Most of the faults will be caused by stack management or too small stack sizes. It is also important to give proper (~1KB) stack to the idle task (it also does some FreeRTOS housekeeping). When something bad happens it may be hard to figure out which task caused the problem - my way of debugging is to disable/enable tasks or pieces of code until I can find the tipping point that leads to a crash.