Skip to main content

Operating systems

Event driven development with FreeRTOS

FreeRTOS porting and event-driven development with FreeRTOS.

Table of Contents

  1. Introduction
  2. The Active Object Pattern
  3. Demonstration

1 - Introduction

One of the benefits of using FreeRTOS as the operating system for a real time application is that we can create tasks to perform different functions for our project. We can have tasks that handle the access to peripherals, sensors and other devices.

However, tasks may need to access the same resources, such as memory, variables or hardware. This can lead to conflicts and errors if not done properly. In this article, we will learn how to share resources between tasks in a safe and event-driven way.

I will introduce you to one such framework: FreeAct. FreeAct is a C++ framework that works with FreeRTOS, one of the most popular RTOS for embedded systems. FreeAct leverages the message mechanism and the subscriber-publisher pattern to implement event-driven model. I would like to thank Dr. Miro Samek for this incredible framework.

2 - The Active Object Pattern

It basically creates a task that continuously waits for events in a private queue. The task also has a dispatcher, which is a function that can handle and execute different actions depending on the type of event.

This is an example code of an Active Object written in C language, using FreeRTOS tasks and queues.


/* thread function for all Active Objects (FreeRTOS task signature) */
static void Active_eventLoop(void *pvParameters) {
    Active *me = (Active *)pvParameters;
    static Event const initEvt = { INIT_SIG };

    configASSERT(me); /* Active object must be provided */

    /* initialize the AO */
    (*me->dispatch)(me, &initEvt);

    for (;;) {   /* for-ever "superloop" */
        Event const *e; /* pointer to event object ("message") */

        /* wait for any event and receive it into object 'e' */
        xQueueReceive(me->queue, &e, portMAX_DELAY); /* BLOCKING! */
        configASSERT(e != (Event const *)0);

        /* dispatch event to the active object 'me' */
        (*me->dispatch)(me, e); /* NO BLOCKING! */
    }
}

The characteristics of this design pattern are:

  • Each task has its own private data, a private queue and resources that are not shared with other tasks. This eliminates the need for mutexes or other synchronization mechanisms that can cause deadlock or race conditions.
  • Tasks communicate with each other by posting asynchronous events to their queues. Events are messages that indicate what happened and may contain parameters, such as a radio packet. Events are processed in the order they are received by the tasks.
  • Tasks have a common structure that consists of a blocking message waiter and a non-blocking event handler (also known as a dispatcher function). The message waiter waits for an event to arrive in the queue and passes it to the event handler. The event handler executes the appropriate code based on the event type and parameters. The event handler must not block the execution of the task, otherwise it will delay the processing of other events.
  • The event-driven design pattern follows the principle of inversion of control, which means that the tasks do not control when their code is executed, but rather they are controlled by the events. This makes the tasks more responsive and adaptable to changing conditions.

This is an example of dispatcher code of an application that blinks a LED and print information about button press.

static void BlinkyButton_dispatch(BlinkyButton * const me, Event const * const e) {
    switch (e->sig) {
        case INIT_SIG: /* intentionally fall through... */
        case TIMEOUT_SIG: {
            if (!me->isLedOn) { /* LED not on */
                BSP_led0_on();
                me->isLedOn = true;
                TimeEvent_arm(&me->te, (200 / portTICK_RATE_MS));
            }
            else {  /* LED is on */
                BSP_led0_off();
                me->isLedOn = false;
                TimeEvent_arm(&me->te, (800 / portTICK_RATE_MS));
            }
            break;
        }
        case BUTTON_PRESSED_SIG: {
            printf("Button Pressed\n");
            break;
        }
        case BUTTON_RELEASED_SIG: {
            printf("Button Released\n");
            break;
        }
        default: {
            break;
        }
    }
}

This is a visualization of how Active Objects are structured.

3 - Demonstration

These next steps will show you how to use FreeAct framework using FreeRTOS in a Raspberry Pi Pico board.

  • Clone the raspberry pi pico Docker image repository and run it.
git clone https://github.com/lukstep/raspberry-pi-pico-docker-sdk.git

cd raspberry-pi-pico-docker-sdk

docker build . --tag pico-sdk

docker run -d -it --name pico-sdk --mount type=bind,source=${PWD},target=/home/dev pico-sdk

docker exec -it pico-sdk /bin/sh
  • Attach VSCode to SDK Docker container.
image-2
  • Now, clone an FreeRTOS example repository for Raspberry Pi Pico.
git clone https://github.com/LearnEmbeddedSystems/rp2040-freertos-template.git
git submodule update --init --recursive
  • Build the example project.
mkdir build
cd build/
cmake .. && make -j8
  • At this point you can flash the binary to your board and check that led is blinking.
How to Set Up Raspberry Pi Pico C/C++ Toolchain on Windows with VS Code -  Shawn Hymel
  • Let's add the FreeAct framework to the project
  • In FreeAct.c file, use portCHECK_IF_IN_ISR macro instead of xPortIsInsideInterrupt function, because it is not implemented for the target yet.

Modify the lines 139 and 159 of the file.

  • Create bsp files for the target (bsp_rp2040.c and bsp.h)
/*****************************************************************************
* Project: FreeAct Example for Raspberry Pi Pico board
* Board: RP2040
*****************************************************************************/
#ifndef BSP_H
#define BSP_H

#include "FreeAct.h"

void BSP_init(void);
void BSP_start(void);
void BSP_led0_on(void);
void BSP_led0_off(void);
void BSP_led0_toggle(void);

enum Signals {
    TIMEOUT_SIG = USER_SIG,
    BUTTON_PRESSED_SIG,
    STOP_LED_SIG,
    RESUME_LED_SIG,
};

extern Active *AO_button;

#endif /* BSP_H */
/*****************************************************************************
* Project: FreeAct Example for Raspberry Pi Pico board
* Board: RP2040
*****************************************************************************/

#include "FreeAct.h" /* Free Active Object interface */
#include "hardware/gpio.h"
#include "pico/stdlib.h"
#include <assert.h>
#include "bsp.h"
/* add other drivers if necessary... */

/* LEDs and Push-buttons on the EMF32-SLSTK3401A board ---------------------*/
const uint LED_PIN = PICO_DEFAULT_LED_PIN;

/* LEDs and Push-buttons on the EMF32-SLSTK3401A board ---------------------*/
const uint BUTTON_PIN = 2;

/* Function Prototype ======================================================*/
void vApplicationTickHook(void);
void vApplicationIdleHook(void);
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName);
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
                                   StackType_t **ppxIdleTaskStackBuffer,
                                   uint32_t *pulIdleTaskStackSize);
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
                                    StackType_t **ppxTimerTaskStackBuffer,
                                    uint32_t *pulTimerTaskStackSize);

void gpio_callback(uint gpio, uint32_t events);

/* Hooks ===================================================================*/
/* Application hooks used in this project ==================================*/
/* NOTE: only the "FromISR" API variants are allowed in vApplicationTickHook*/
void vApplicationTickHook(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    /* notify FreeRTOS to perform context switch from ISR, if needed */
    portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
}
/*..........................................................................*/
void vApplicationIdleHook(void) {
#ifdef NDEBUG
    /* Put the CPU and peripherals to the low-power mode.
    * you might need to customize the clock management for your application,
    * see the datasheet for your particular Cortex-M3 MCU.
    */
#endif
}
/*..........................................................................*/
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
    (void)xTask;
    (void)pcTaskName;
    /* ERROR!!! */
}
/*..........................................................................*/
/* configSUPPORT_STATIC_ALLOCATION is set to 1, so the application must
 * provide an implementation of vApplicationGetIdleTaskMemory() to provide
 * the memory that is used by the Idle task.
 */
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
                                   StackType_t **ppxIdleTaskStackBuffer,
                                   uint32_t *pulIdleTaskStackSize)
{
    /* If the buffers to be provided to the Idle task are declared inside
     * this function then they must be declared static - otherwise they will
     * be allocated on the stack and so not exists after this function exits.
     */
    static StaticTask_t xIdleTaskTCB;
    static StackType_t  uxIdleTaskStack[configMINIMAL_STACK_SIZE];

    /* Pass out a pointer to the StaticTask_t structure in which the
     * Idle task's state will be stored.
     */
    *ppxIdleTaskTCBBuffer = &xIdleTaskTCB;

    /* Pass out the array that will be used as the Idle task's stack. */
    *ppxIdleTaskStackBuffer = &uxIdleTaskStack[0];

    /* Pass out the size of the array pointed to by *ppxIdleTaskStackBuffer.
     * Note that, as the array is necessarily of type StackType_t,
     * configMINIMAL_STACK_SIZE is specified in words, not bytes.
     */
    *pulIdleTaskStackSize = sizeof(uxIdleTaskStack) / sizeof(uxIdleTaskStack[0]);
}

/* configSUPPORT_STATIC_ALLOCATION is set to 1, so the application must
 * provide an implementation of vApplicationGetTimerTaskMemory() to provide
 * the memory that is used by the Timer task.
 */
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
                                    StackType_t **ppxTimerTaskStackBuffer,
                                    uint32_t *pulTimerTaskStackSize) {
    /* If the buffers to be provided to the Timer task are declared inside
     * this function then they must be declared static - otherwise they will
     * be allocated on the stack and so not exists after this function exits.
     */
    static StaticTask_t xTimerTask_TCB;
    static StackType_t  uxTimerTaskStack[configTIMER_TASK_STACK_DEPTH];

    /* Pass out a pointer to the StaticTask_t structure in which the
     * Timer task's state will be stored.
     */
    *ppxTimerTaskTCBBuffer   = &xTimerTask_TCB;

    /* Pass out the array that will be used as the Timer task's stack. */
    *ppxTimerTaskStackBuffer = &uxTimerTaskStack[0];

    /* Pass out the size of the array pointed to by *ppxTimerTaskStackBuffer.
     * Note that, as the array is necessarily of type StackType_t,
     * configTIMER_TASK_STACK_DEPTH is specified in words, not bytes.
     */
    *pulTimerTaskStackSize   = (uint32_t)configTIMER_TASK_STACK_DEPTH;
}

/* BSP functions ===========================================================*/
void BSP_init(void) {
    /* NOTE: SystemInit() already called from the startup code
    *  but SystemCoreClock needs to be updated
    */
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    BSP_led0_on();

    gpio_init(BUTTON_PIN);
    gpio_set_dir(BUTTON_PIN, GPIO_IN);
    gpio_set_pulls(BUTTON_PIN, true, false);

    stdio_init_all();

    gpio_set_irq_enabled_with_callback(BUTTON_PIN, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, &gpio_callback);
}
/*..........................................................................*/
void BSP_led0_off(void) {
    gpio_put(LED_PIN, 0);
}
/*..........................................................................*/
void BSP_led0_on(void) {
    gpio_put(LED_PIN, 1);
}

void BSP_led0_toggle(void) {
    bool level = gpio_get_out_level(LED_PIN);
    gpio_put(LED_PIN, !level);
}
/*..........................................................................*/
void BSP_start(void) {
    /* set up the SysTick timer to fire at BSP_TICKS_PER_SEC rate */
    /* SysTick_Config(SystemCoreClock / BSP_TICKS_PER_SEC); done in FreeRTOS */

    /* assign all priority bits for preemption-prio. and none to sub-prio. */
    /* ... */

    /* enable IRQs... */
    /* ... */
}
/*..........................................................................*/
/* error-handling function called by exception handlers in the startup code */
void assert_failed(char const *module, int loc); /* prototype */
void assert_failed(char const *module, int loc) {
    /* NOTE: add here your application-specific error handling */
    (void)module;
    (void)loc;
#ifndef NDEBUG /* debug build? */
    /* light-up both LEDs */
    gpio_put(LED_PIN, 1);

    /* tie the CPU in this endless loop and wait for the debugger... */
    while (1) {
    }
#else /* production build */
    /* TODO: do whatever is necessary to put the system in a fail-safe state */
    /* important!!! */
    NVIC_SystemReset(); /* reset the CPU */
#endif
}

enum itrEvents {
    LEVEL_LOW,
    LEVEL_HIGH,
    EDGE_FALL,
    EDGE_RISE,
    MAX_GPIO_EVENTS
};

void gpio_callback(uint gpio, uint32_t events) {
    // Put the GPIO event(s) that just happened into event_str
    // so we can print it
     for (uint i = 0; i < MAX_GPIO_EVENTS; i++) {
        uint mask = (1 << i);
        if (events & mask) {
            if(i == EDGE_FALL) {
              BaseType_t xHigherPriorityTaskWoken = pdFALSE;
              // Post the event
              static Event const buttonPressedEvt = {BUTTON_PRESSED_SIG};
              Active_postFromISR(AO_button, &buttonPressedEvt,
                                 &xHigherPriorityTaskWoken);
            }
        }
     }
}

/*****************************************************************************
* NOTE1:
* Only ISRs prioritized at or below the
* configMAX_SYSCALL_INTERRUPT_PRIORITY level (i.e.,
* with the numerical values of priorities equal or higher than
* configMAX_SYSCALL_INTERRUPT_PRIORITY) are allowed to call any
* QP/FreeRTOS services. These ISRs are "kernel-aware".
*
* Only ISRs prioritized at or below the configMAX_SYSCALL_INTERRUPT_PRIORITY
* level (i.e., with the numerical values of priorities equal or higher than
* configMAX_SYSCALL_INTERRUPT_PRIORITY) are allowed to call any QF services.
* These ISRs are "kernel-aware".
*
* Conversely, any ISRs prioritized above the
* configMAX_SYSCALL_INTERRUPT_PRIORITY priority level (i.e., with
* the numerical values of priorities less than
* configMAX_SYSCALL_INTERRUPT_PRIORITY) are never disabled and are
* not aware of the kernel. Such "kernel-unaware" ISRs cannot call any
* QP/FreeRTOS services. The only mechanism by which a "kernel-unaware" ISR
* can communicate with the QF framework is by triggering a "kernel-aware"
* ISR, which can post/publish events.
*
* For more information, see article "Running the RTOS on a ARM Cortex-M Core"
* http://www.freertos.org/RTOS-Cortex-M3-M4.html
*/

  • Configure FreeRTOSConfig.h