In my article on Multitasking on the Pico I managed to run two parallel tasks using both cores. The next logical step is to run multiple tasks on one core. To manage this, I need an operating system running on that core. The OS must schedule the tasks and deliver mechanisms for inter process communication, e.g. queues.

That’s why I tried to find out whether FreeRTOS works on a Pico. To put it in a nutshell: Yes is works, and here’s the article how to do it.

My project is based on the work of Yuichi Nakamura. He ported the FreeRTOS Kernel to the Pico and provided a nice example. I reduced the number of tasks to two, but added a queue between them. You can find the code in my github repository.

The folder freertos contains a copy of the FreeRTOS Kernel which can be downloaded at https://github.com/FreeRTOS/FreeRTOS-Kernel.

From Nakamuras github site you can download a suitable FreeRTOSConfig.h file as well as a port.c file for the RP2040. Both files should be places in freertos.

My project is stored to folder freertos_single_core. It only contains a CMakeLists.txt file and main.cpp. But more about this file in a minute.

Generating the build files

CMakeLists.txt in the top project folder is pretty much the template file from the Pico github site. I only created a variable called APP to easily change the project name. I also set the c and c++ standards and introduced two subdirectories.

cmake_minimum_required(VERSION 3.12)
set(APP freertos_single_core)
# initialize the SDK based on PICO_SDK_PATH# note:
# this must happen before project()

include(pico_sdk_import.cmake)

project(${APP})
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# initialize the Pico SDK
pico_sdk_init()

# rest of your project
add_subdirectory(freertos)
add_subdirectory(${APP})

CMakeLists.txt in folder freertos is a copy of Nakamura’s file:

set(PICO_SDK_FREERTOS_SOURCE FreeRTOS-Kernel)

add_library(freertos
${PICO_SDK_FREERTOS_SOURCE}/event_groups.c
${PICO_SDK_FREERTOS_SOURCE}/list.c
${PICO_SDK_FREERTOS_SOURCE}/queue.c
${PICO_SDK_FREERTOS_SOURCE}/stream_buffer.c
${PICO_SDK_FREERTOS_SOURCE}/tasks.c
${PICO_SDK_FREERTOS_SOURCE}/timers.c
${PICO_SDK_FREERTOS_SOURCE}/portable/MemMang/heap_3.c
)

target_include_directories(freertos PUBLIC
.
${PICO_SDK_FREERTOS_SOURCE}/include
${PICO_SDK_FREERTOS_SOURCE}/portable/GCC/ARM_CM0
)

 CMakeLists.txt of folder freertos_single_core introduces the main source file and sets USB as Pico’s standard output.

add_executable(${APP}
main.cpp
)

target_link_libraries(${APP} pico_stdlib freertos)

# enable USB output and disable UART output
pico_enable_stdio_usb(${APP} 1)
pico_enable_stdio_uart(${APP} 0)

pico_add_extra_outputs(${APP})

xTaskCreate and xQueueCreate

In the main function the queue and two tasks are cerated. eventGenerator will use the queue to send data to task eventManager. Once the tasks are set up, vTaskStartScheduler is called. This function must be called to start the tasks. It will not return unless there is insufficient RAM.

Task eventGenerator blinks output pin 15 and sends an according event, i.e. a uint8_t variable, to task eventManager. I set the last parameter of function xQueueSend to ZERO. I.e. the function wil not block. eventManager reads from the Queue. It will block the task infinitly (portMAX_DELAY) if no new message is available. It prints the received event using a simple switch statement.

#include <FreeRTOS.h>
#include <task.h>
#include <queue.h>
#include <iostream>
#include "pico/stdlib.h"

TaskHandle_t tskHdnl_evGen = nullptr;
TaskHandle_t tskHdnl_stateManager = nullptr;
QueueHandle_t eventQueue = nullptr;

const uint8_t EVENT_PIN_HIGH = 1;
const uint8_t EVENT_PIN_LOW = 0;

void eventGenerator(void *p)
{
const int pin = 15;
gpio_init(pin);
gpio_set_dir(pin, GPIO_OUT);

while (true)
{
gpio_put(pin, 1);
xQueueSend(eventQueue, &EVENT_PIN_HIGH, 0);
vTaskDelay( 200/portTICK_PERIOD_MS );
gpio_put(pin, 0);
xQueueSend(eventQueue, &EVENT_PIN_LOW, 0);
vTaskDelay( 100/portTICK_PERIOD_MS );
}
}

void stateManager(void *p)
{
uint8_t i = 255;
while(true)
{
if( xQueueReceive(eventQueue, &i, portMAX_DELAY) == pdPASS )
{
std::cout << "received " << i << std::endl;
switch(i)
{
case EVENT_PIN_HIGH: std::cout << "Pin is high\n";
break;
case EVENT_PIN_LOW : std::cout << "Pin is low\n";
break;
default: std::cout << "Unkown state\n";
break;
}
}
}
}

int main()
{
stdio_init_all();
eventQueue = xQueueCreate(5, sizeof(uint8_t) );
xTaskCreate(eventGenerator,
"eventGenerator",
256,
nullptr,
1,
&tskHdnl_evGen);

xTaskCreate(stateManager,
"stateManager",
256,
nullptr,
1,
&tskHdnl_stateManager);

vTaskStartScheduler();


while (true)
{
// should only get here if there is insufficient RAM
}
}