Event driven development with FreeRTOS
FreeRTOS porting and event-driven development with FreeRTOS.
Table of Contents
- Introduction
- The Active Object Pattern
- 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.
- 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.
- 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
#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
*----------------------------------------------------------*/
/* Scheduler Related */
#define configUSE_PREEMPTION 1
#define configUSE_TICKLESS_IDLE 0
#define configUSE_IDLE_HOOK 1
#define configUSE_TICK_HOOK 0
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES 32
#define configMINIMAL_STACK_SIZE ( configSTACK_DEPTH_TYPE ) 256
#define configUSE_16_BIT_TICKS 0
#define configIDLE_SHOULD_YIELD 1
/* Synchronization Related */
#define configUSE_MUTEXES 1
#define configUSE_RECURSIVE_MUTEXES 1
#define configUSE_APPLICATION_TASK_TAG 0
#define configUSE_COUNTING_SEMAPHORES 1
#define configQUEUE_REGISTRY_SIZE 8
#define configUSE_QUEUE_SETS 1
#define configUSE_TIME_SLICING 1
#define configUSE_NEWLIB_REENTRANT 0
// todo need this for lwip FreeRTOS sys_arch to compile
#define configENABLE_BACKWARD_COMPATIBILITY 1
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5
/* System */
#define configSTACK_DEPTH_TYPE uint32_t
#define configMESSAGE_BUFFER_LENGTH_TYPE size_t
/* Memory allocation related definitions. */
#define configSUPPORT_STATIC_ALLOCATION 1
#define configSUPPORT_DYNAMIC_ALLOCATION 1
#define configTOTAL_HEAP_SIZE (128*1024)
#define configAPPLICATION_ALLOCATED_HEAP 0
/* Hook function related definitions. */
#define configCHECK_FOR_STACK_OVERFLOW 0
#define configUSE_MALLOC_FAILED_HOOK 0
#define configUSE_DAEMON_TASK_STARTUP_HOOK 0
/* Run time and task stats gathering related definitions. */
#define configGENERATE_RUN_TIME_STATS 0
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 0
/* Co-routine related definitions. */
#define configUSE_CO_ROUTINES 0
#define configMAX_CO_ROUTINE_PRIORITIES 1
/* Software timer related definitions. */
#define configUSE_TIMERS 1
#define configTIMER_TASK_PRIORITY ( configMAX_PRIORITIES - 1 )
#define configTIMER_QUEUE_LENGTH 10
#define configTIMER_TASK_STACK_DEPTH 1024
/* Interrupt nesting behaviour configuration. */
/*
#define configKERNEL_INTERRUPT_PRIORITY [dependent of processor]
#define configMAX_SYSCALL_INTERRUPT_PRIORITY [dependent on processor and application]
#define configMAX_API_CALL_INTERRUPT_PRIORITY [dependent on processor and application]
*/
#if FREE_RTOS_KERNEL_SMP // set by the RP2040 SMP port of FreeRTOS
/* SMP port only */
#define configNUMBER_OF_CORES 1
#define configTICK_CORE 0
#define configRUN_MULTIPLE_PRIORITIES 1
#define configUSE_CORE_AFFINITY 1
#endif
/* RP2040 specific */
#define configSUPPORT_PICO_SYNC_INTEROP 1
#define configSUPPORT_PICO_TIME_INTEROP 1
#include <assert.h>
/* Define to trap errors during development. */
#define configASSERT(x) assert(x)
/* Set the following definitions to 1 to include the API function, or zero
to exclude the API function. */
#define INCLUDE_vTaskPrioritySet 1
#define INCLUDE_uxTaskPriorityGet 1
#define INCLUDE_vTaskDelete 1
#define INCLUDE_vTaskSuspend 1
#define INCLUDE_vTaskDelayUntil 1
#define INCLUDE_vTaskDelay 1
#define INCLUDE_xTaskGetSchedulerState 1
#define INCLUDE_xTaskGetCurrentTaskHandle 1
#define INCLUDE_uxTaskGetStackHighWaterMark 1
#define INCLUDE_xTaskGetIdleTaskHandle 1
#define INCLUDE_eTaskGetState 1
#define INCLUDE_xTimerPendFunctionCall 1
#define INCLUDE_xTaskAbortDelay 1
#define INCLUDE_xTaskGetHandle 1
#define INCLUDE_xTaskResumeFromISR 1
#define INCLUDE_xQueueGetMutexHolder 1
/* A header file that defines trace macro can be included here. */
#endif /* FREERTOS_CONFIG_H */
- Add FreeAct.c and bsp_rp2040.c to CmakeLists.txt inside /src directory
add_executable(${ProjectName}
main.c FreeAct.c bsp_rp2040.c
)
target_include_directories(${ProjectName} PRIVATE
${CMAKE_CURRENT_LIST_DIR}
)
target_link_libraries(${ProjectName}
pico_stdlib
FreeRTOS-Kernel-Heap4
)
pico_add_extra_outputs(${ProjectName})
- Modify the main.c file to use Active Object functionality instead of naked tasks
Declare the blinkLed struct, it has an Active oject and a time event object.
typedef struct {
Active super; /* Inherit Active base class */
TimeEvent te;
} blinkLed;
Declare the button struct, it has an Active oject and a private variable to store the state of the led.
typedef struct {
Active super; /* Inherit Active base class */
bool isLedBlinking;
} button;
Implement the blinkLed dispatcher function to toogle the LED every time the event timer times out. It stops blinking the LED when receive STOP_LED_SIG event from button Active Object and resume the blinking when receives RESUME_LED_SIG event.
static void blinkLed_dispatch(blinkLed *const me, Event const *const e) {
switch (e->sig) {
case INIT_SIG: { /* intentionally fall through... */
TimeEvent_arm(&me->te, (200 / portTICK_RATE_MS));
break;
}
case TIMEOUT_SIG: {
BSP_led0_toggle();
TimeEvent_arm(&me->te, (200 / portTICK_RATE_MS));
break;
}
case RESUME_LED_SIG: {
TimeEvent_arm(&me->te, (200 / portTICK_RATE_MS));
break;
}
case STOP_LED_SIG: {
BSP_led0_on();
TimeEvent_disarm(&me->te);
break;
}
}
}
The button dispatcher receives the BUTTON_PRESSED_SIG from bsp and post RESUME and STOP events to LED Active Object.
static void button_dispatch(button *const me, Event const *const e) {
switch (e->sig) {
case INIT_SIG: {/* intentionally fall through... */
break;
}
case BUTTON_PRESSED_SIG: {
me->isLedBlinking = !me->isLedBlinking;
if(me->isLedBlinking) {
static Event const resumeLedEvt = {RESUME_LED_SIG};
Active_post(AO_blinkLed, &resumeLedEvt);
}
else {
static Event const stopLedEvt = {STOP_LED_SIG};
Active_post(AO_blinkLed, &stopLedEvt);
}
break;
}
}
}
Crate the constructor function for the blinkLed and button Active Objects. It will point the dispatcher function, configure the Time Event and initialize private variables.
void blinkLed_ctor(blinkLed * const me) {
Active_ctor(&me->super, (DispatchHandler)&blinkLed_dispatch);
me->te.type = TYPE_ONE_SHOT;
TimeEvent_ctor(&me->te, TIMEOUT_SIG, &me->super);
}
void button_ctor(button * const me) {
Active_ctor(&me->super, (DispatchHandler)&button_dispatch);
me->isLedBlinking = true;
}
Declare the stack necessary for the tasks inside the Active Objects, the queue responsible to store events, the instance of Active Objects and the AO pointer thar points to the Active Object.
static StackType_t blinkLedStack[configMINIMAL_STACK_SIZE];
static Event *blinkLed_queue[10];
static blinkLed blinkled;
Active *AO_blinkLed = &blinkled.super;
static StackType_t buttonStack[configMINIMAL_STACK_SIZE];
static Event *button_queue[10];
static button Button;
Active *AO_button = &Button.super;
Initialize the bsp, the constructors and start the Active Objects.
BSP_init();
button_ctor(&Button);
blinkLed_ctor(&blinkled);
Active_start(AO_blinkLed, 1, blinkLed_queue,
sizeof(blinkLed_queue) / sizeof(blinkLed_queue[0]),
blinkLedStack, sizeof(blinkLedStack), 0);
Active_start(AO_button, 2, button_queue,
sizeof(button_queue) / sizeof(button_queue[0]),
buttonStack, sizeof(buttonStack), 0);
- Compile it.
mkdir build
cd build/
cmake .. && make -j8
- Flash it.
At the build folder, after the compilation, the an uf2 file will be generated to be flashed in the board. So, copy the file to your host computer:
docker cp pico-sdk:/home/dev/rp2040-freertos-template/build/src/rp2040-freeact-example.uf2 .
To enter the BOOTSEL mode, follow these steps:
- Disconnect the Pico from the USB port.
- Press and hold the BOOTSEL button on the Pico.
- While keeping the BOOTSEL button pressed, connect the Pico to the USB port.
- Release the BOOTSEL button only after you see the RPI-RP2 drive on your computer.
- Drag the rp2040-freeact-example.uf2 file to RPI-RP2.
- The RPI-RP2 will reboot and you will se the led blinking.
- If you toogle the GP2 pin, the interrupt will be detect and an event will be sent to Led Active Object to stop or start blinking.
That's it, you're done! :)
Congratulations! Now you are able to develop your applications using event driven design!
Discussion