Project

Introduction to FreeRTOS on the nRF51

September 24, 2015 by Travis Fagerness

This article demonstrates porting FreeRTOS to the nRF51 with a softdevice so you can use an RTOS with the BLE functionality.

This article demonstrates porting FreeRTOS to the nRF51 with a softdevice so you can use an RTOS with the BLE functionality.

Overview

This is part of a series of articles on the nRF51. The nRF51 is a system-on-chip with a Cortex M0 and a BLE radio chip all in one. This article demonstrates how to use the BLE functionality with an RTOS.

Previous articles: 
BLE using nRF51: ARM-GCC Build Environment​
BLE using nRF51: Creating a BLE Peripheral

Requirements

FreeRTOS

FreeRTOS is an operating system intended for small microcontrollers. RTOS stands for "real-time operating system." What does real-time mean? Many embedded devices require some sort of response to the data they are manipulating on the order of milliseconds. Therefore, the operating systems such as Windows and Linux cannot respond fast enough because they allow other applications time to run. This can cause non-deterministic behavior, which is undesirable in an embedded system. Most RTOS implementations offer the following in one way or another:

Feature Description
Tasks A task (sometimes called a process or thread) is a piece of code that executes in its own context. It doesn't depend on other tasks or the OS. This allows several tasks to run in parallel.
Scheduler The scheduler decides which task can run at a given time. It also maintains each tasks context and memory.
Semaphore A semaphore is a way to share resources and communicate between tasks.
Timers A timer is way to continuously call a function at a given interval or to time a single event.

There are many more features in most RTOS's, such as built in USB drivers and memory management.

Running on nRF51

Download the following project to get started using FreeRTOS on the nRF51. This project assumes you have setup your build environment.

freertos_nrf51.zip

FreeRTOS has been ported to many different processors. Fortunately, one of them is the Cortex-M0, which is the CPU inside the nRF51. Download FreeRTOS from the link above and extract it to a known location. Make note of this location because it is required in the makefile below. The port for the Cortex-M0 must be modified, because it relies on SysTick to run the RTOS. The nRF51 doesn't have the SysTick implemented in the CPU, so you have to use the softdevice timer. An RTOS typically requires a timer to increment "ticks," which is just a way to keep time and schedule tasks. A tick of 1ms is common and is what the code below will do. I have modified the timer function in the following code:
[FreeRTOS dir]\Source\portable\GCC\ARM_CM0\port.c

In this file search for the following function:
void prvSetupTimerInterrupt( void )

And change it from this:

void prvSetupTimerInterrupt( void )
{
    /* Configure SysTick to interrupt at the requested rate. */
    *(portNVIC_SYSTICK_LOAD) = ( configCPU_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
    *(portNVIC_SYSTICK_CTRL) = portNVIC_SYSTICK_CLK | portNVIC_SYSTICK_INT | portNVIC_SYSTICK_ENABLE;
}

to the following. This code uses the application timers that are offered by the softdevice. The softdevice is sort of like an RTOS in the sense that it has timers and various functions to control the Bluetooth.

void prvSetupTimerInterrupt( void )
{
    APP_TIMER_INIT(0, 1, 4, false);
    app_timer_create(&freertos_timer,APP_TIMER_MODE_REPEATED,xPortSysTickHandler);
    app_timer_start(freertos_timer, APP_TIMER_TICKS(1, 0), NULL);
}

Add the following header files and variable to the top of the file:

/* port includes */
#include "nrf.h"
#include "app_timer.h"

static app_timer_id_t    freertos_timer;

Configuring FreeRTOS

Configuration Header

The file FreeRTOSConfig.h has several defines that configure the operating system. These options configure how much memory to use, what special functions to use, and many other options. The important ones for this application are the stack size, USE_PREEMPTION and the TICK_RATE_HZ. The timer was setup to 1ms, so the tick rate must be 1000Hz. Pre-emption means that the tasks won't be allocated the same amount of time to run. We want the BLE task to have the highest priority so messages get handled properly. This could change if you were running a time critical algorithm in another task to process data. The memory depends on the system constraints. You can monitor the stack depth using some built in functions, but since we aren't really using any memory in this example a stack size of 70 words is plenty.

#define configUSE_PREEMPTION			1
#define configUSE_IDLE_HOOK			1
#define configTICK_RATE_HZ			( ( TickType_t ) 1000 )
#define configMINIMAL_STACK_SIZE		( ( unsigned short ) 70 )
Hooks

Hooks are callback functions the RTOS calls when some event happens. This could be a failure to allocate memory or that the scheduler is idling. In most embedded systems, it would be important to go to sleep during the idle state, so it's important to have access to when the scheduler does this. In FreeRTOS, you must specify which hooks you want to use in the configuration header. Here are the hooks in freertos_hooks.c, which are just a small subset of what is available.

#include "FreeRTOS.h"
#include "task.h"

void vApplicationMallocFailedHook( void )
{
	taskDISABLE_INTERRUPTS();
	for( ;; );
}

void vApplicationIdleHook( void )
{
}

void vApplicationStackOverflowHook( TaskHandle_t pxTask, char *pcTaskName )
{
	( void ) pcTaskName;
	( void ) pxTask;
	taskDISABLE_INTERRUPTS();
	for( ;; );
}

void vApplicationTickHook( void )
{
#if mainCHECK_INTERRUPT_STACK == 1
extern unsigned long _pvHeapStart[];
	configASSERT( memcmp( ( void * ) _pvHeapStart, ucExpectedInterruptStackValues, sizeof( ucExpectedInterruptStackValues ) ) == 0U );
#endif /* mainCHECK_INTERRUPT_STACK */
}

Creating Tasks

In the project file main.c see, the following lines create two different tasks. One task handles the BLE functionality. It also blinks the green LED during each loop iteration. The second task just blinks the blue LED every 100ms, but it could be modified to do more useful tasks, such as reading sensors and data processing. This data could be fed to the BLE task and reported.

#define BLE_TASK_PRIORITY           ( tskIDLE_PRIORITY + 2 )
#define BLINKY_TASK_PRIORITY        ( tskIDLE_PRIORITY + 1 )
static void task_blinky(void *pvParameters);
static void task_ble_process(void *pvParameters);


xTaskCreate(task_ble_process,"BLE Task",configMINIMAL_STACK_SIZE,NULL,BLE_TASK_PRIORITY,NULL);
xTaskCreate(task_blinky,"Blinky Task",configMINIMAL_STACK_SIZE,NULL,BLINKY_TASK_PRIORITY,NULL);
vTaskStartScheduler();

The function xTaskCreate actually creates the task. It is setup by passing the function to run, a friendly name for debugging, how much memory to use for the stack and the priority. The priority is used by the scheduler to determine which task should run if they both happen to be ready at the same time. Each task has to give up control back to the scheduler or else other tasks can't run. In this example, this is achieved by delaying the task a certain amount of milliseconds, which is equivalent to a typical operating system sleep function. While the task is being delayed, other tasks are allowed to run. The LED task delays for 100ms to toggle the light.

static void task_blinky( void *pvParameters )
{
    while(1)
    {
        nrf_gpio_pin_toggle(LED_RGB_BLUE);
        vTaskDelay(100);
    }
}

The BLE task runs the same code as the BLE peripheral example. This time it uses vTaskDelay() instead of nrf_delay_ms(). If you were to use a busy-wait function such as nrf_delay_ms(), other tasks would not be able to run.

static void task_ble_process( void *pvParameters )
{
    uint8_t i=0;
    uint32_t err_code;
    while(1)
    {
        nrf_gpio_pin_toggle(LED_RGB_GREEN);
        vTaskDelay(500);
        for(i=0;i < CHARACTERISTIC_SIZE;i++)
        {
            char2_data[i] = char2_data[i]+1;
        }
        err_code = custom_service_update_data(m_conn_handle,char2_data);
        APP_ERROR_CHECK(err_code);
    }
}

Building FreeRTOS with GCC

The same makefile is used from the previous BLE peripheral article. FreeRTOS has quite a few source files which would increase the build time if all of them were added to the makefile. You can just add the features that you plan on using by adding their respective c files. The following section is added to the C source file section. Change the variable FREERTOS_PATH to where you have extracted the FreeRTOS source code.

#FreeRTOS C files
FREERTOS_PATH = C:/projects/FreeRTOS
C_SOURCE_FILES += freertos_hooks.c
C_SOURCE_FILES += nrf51_port.c
C_SOURCE_FILES += $(FREERTOS_PATH)/Source/croutine.c
C_SOURCE_FILES += $(FREERTOS_PATH)/Source/event_groups.c
C_SOURCE_FILES += $(FREERTOS_PATH)/Source/list.c
C_SOURCE_FILES += $(FREERTOS_PATH)/Source/queue.c
C_SOURCE_FILES += $(FREERTOS_PATH)/Source/tasks.c
C_SOURCE_FILES += $(FREERTOS_PATH)/Source/timers.c
C_SOURCE_FILES += $(FREERTOS_PATH)/Source/portable/MemMang/heap_4.c

The following is added the header includes:

INC_PATHS += -I$(FREERTOS_PATH)/Source/include
INC_PATHS += -I$(FREERTOS_PATH)/Source/portable/GCC\ARM_CM0

Now type "make" in the console and then "make flash" and you should see the following on the nRF51 dongle. The solid green means the dongle's debug features have connected through USB, it isn't actually a part of the application. The blue light is blinking very fast and the green light is blinking slowly. You can also connect to the device using the same methods as before.

What next?

Now that there is a build system, an operating system, and communication functionality, the nRF Dongle has a lot of capabilities. This platform could be used as a remote data collector that could do signal processing on the incoming data prior to sending it out. Any project that requires multiple things running at the same time is much easier with an RTOS. Have fun!

 

Give this project a try for yourself! Get the BOM.