Using multiple cores with FreeRTOS and the Pico
FreeRTOS is a real time operating system kernel which is designed for embedded devices, such as the Raspberry Pico. This article will give a brief overview of how this can be used on the Pico with an example repository.
TLDR
Git repo can be found here
Scheduling on multiple cores
Lets say you’re looking to use your Pico to monitor data being received on one GPIO pin, whilst at the same time trying to provide output on that data, say via an OLED display. To perform both of these functions without interrupting the high priority task which is reading the data, it’s useful to be able to make use of both of the Raspberry Pico cores.
It’s worth noting that this is possible to do without using FreeRTOS, take a look at the code here. A function can be declared and then run on the second core(core 1) with this function call:
multicore_launch_core1(core1_entry);
FreeRTOS however gives us Preemptive multitasking. This allows us to share both cores of the Pico between many different tasks that we can create, and FreeRTOS will ensure that they all get their fair share of time on the processor depending on what priority we have assigned those tasks. We can also create tasks and pin them to either of the cores.
Setup and building
The example code which follows is a modified version from this repo. That example uses a git submodule to include FreeRTOS into the project, whereas this example will follow the same pattern that the official Pico SDK guide talks you though with a structure which looks like:
~/pico/pico-sdk/
~/pico/FreeRTOS-Kernel/
~/pico/PicoFreertosBlink/
In this way we can then have multiple projects which all make use of the FreeRTOS-Kernel
at the top level of the ~/pico
directory.
To download the FreeRTOS-Kernel
do the following:
cd ~/pico/
git clone https://github.com/FreeRTOS/FreeRTOS-Kernel.git
cd FreeRTOS-Kernel/
git checkout smp
We need to checkout the smp
branch so that we can take advantage of multicore support which was released in June of 2021.
Finally to download the example code build the project we need to do:
cd ~/pico/
git clone https://github.com/ghubcoder/PicoFreertosBlink.git
cd PicoFreertosBlink/
mkdir build && cd build/
export PICO_SDK_PATH=../../pico-sdk
export FREERTOS_PATH=../../FreeRTOS-Kernel
cmake ..
make
That should compile everything and produce a blink.uf2
file to load onto your Pico.
Example code
This example is very straightforward. We have 3 tasks which all flash LED’s connected to the Pico. The 3rd task is pinned to the second core. As tasks 1 and 2 are not pinned, FreeRTOS may schedule them to be ran on either core0 or core1. You can see how this looks from the output:
LED_Task 2: core0 <--- first
LED_Task 3: core1
LED_Task 1: core0
LED_Task 1: core0
LED_Task 2: core1 <--- second
LED_Task 1: core0
LED_Task 3: core1
etc
The first time Task 2 ran it was executed on core0, but the subsequent run was executed on core1. Whereas Task 3 will always report it is running on core1.
Tasks 1 and 2 could be pinned to core0 to ensure task 3 is free to consume as much processor time as it needs on its dedicated core.
Another option available to us is task priority which can be set by modifying the fifth argument in the following xTaskCreate
calls:
struct led_task_arg arg1 = {25, 100};
xTaskCreate(led_task, "LED_Task 1", 256, &arg1, 1, NULL);
struct led_task_arg arg2 = {28, 200};
xTaskCreate(led_task, "LED_Task 2", 256, &arg2, 1, NULL);
Here they both have the priority of 1, but one could be configured with a higher number, giving it higher priority.
For the third task we want to force it to run on core1:
// Create a 3rd task
TaskHandle_t task3_handle;
UBaseType_t uxCoreAffinityMask;
struct led_task_arg arg3 = {8, 300};
xTaskCreate(led_task, "LED_Task 3", 256, &arg3, 1, &( task3_handle ));
// To force to run on core0:
// uxCoreAffinityMask = ( ( 1 << 0 ));
// force to run on core1
uxCoreAffinityMask = ( ( 1 << 1 ));
vTaskCoreAffinitySet( task3_handle, uxCoreAffinityMask )
Once are tasks are defined we can then start them with:
vTaskStartScheduler();
This is a very simple example but a great way of fully utilizing your Pico by allowing you to schedule a multitude of processes across both cores and have them all share processor time based on the priorities you define.
Code location
Code can be found here.