Using FreeRTOS with the Raspberry Pi Pico: Part 4

By Daniel Gross

Senior Developer Advocate

Amazon Web Services

December 19, 2022

Blog

Using FreeRTOS with the Raspberry Pi Pico: Part 4

This is the fourth blog in the series where we explore writing embedded applications for the Raspberry Pi Pico using FreeRTOS.

In this blog, we will cover how to develop code with FreeRTOS that utilizes the dual-core processor onboard the Raspberry Pi Pico. We will also explain the differences between Asymmetric Multiprocessing (AMP) and Symmetric Multiprocessing (SMP). Furthermore, we will walk through the various configuration options available with SMP, and address how the use of SMP can lead to non-deterministic behavior on microcontrollers.

As mentioned from the first blog in this series, the Raspberry Pi Pico is a development board built around the RP2040 microcontroller. The RP2040 has a dual-core Arm Cortex-M0+ processor, with both cores available for application development. Multiple cores can provide added processing capability for an embedded application. It is worth mentioning that FreeRTOS has been supporting multicore processing with AMP for many years. In 2021, FreeRTOS introduced support for multicore processing using SMP. The RP2040 is one of the hardware platforms now supported with the FreeRTOS SMP kernel. You may have noticed from the first blog how we used the ‘-b smp’ flag when cloning the FreeRTOS kernel repository for building the example applications. This means we have been using the SMP branch of the FreeRTOS kernel all along.

Before we proceed, let us first cover what AMP and SMP are, and how they are different from each other in the context of FreeRTOS. As the name implies, Asymmetric Multiprocessing (AMP) treats each processor core independently in an embedded system. For AMP, each core in the system runs its own instance of FreeRTOS. These instances are entirely separate with just a single processor core available to each application. You can establish inter-core communication with AMP by enabling shared memory in the system, then use a stream buffer or message buffer to send and receive data between instances. This article from Richard Barry, Senior Principal Engineer at AWS and founder of the FreeRTOS project, describes how to implement inter-core communication with AMP.

Symmetric Multiprocessing (SMP), on the other hand, allows a multi-core embedded system to run a single instance of FreeRTOS with access to multiple processor cores within the same application. The term ‘symmetric’ refers to the fact that all cores in the system must have an identical hardware architecture. The RP2040 has two M0+ cores, which satisfies the requirement for SMP with this microcontroller. To understand the scheduler behavior for multitasking in FreeRTOS, refer to our second blog in this series. With SMP, the scheduler in FreeRTOS can run tasks across multiple cores, which introduces important considerations in terms of scheduler behavior and how you design your applications.

Using SMP on the RP2040 with FreeRTOS, there are notable configuration options available that affect how the scheduler behaves and the functions available to the application. In the next section, we will dive into these specific options and what they mean. The ‘FreeRTOSConfig.h’ file we used in the first blog to build our application contains the configuration definitions for FreeRTOS, and in that file are the specific options for SMP you can alter.

First, the ‘configNUM_CORES’ definition sets the number of cores available for FreeRTOS. To use both cores on the RP2040, set the value to ‘2’.

The next important definition is ‘configRUN_MULTIPLE_PRIORITIES’. Normally, FreeRTOS allows tasks to be given different priorities, which affects the order of execution and preemption by the scheduler. However, running tasks with multiple cores available can lead to simultaneous task execution, even if one of the tasks is a lower priority task. This could lead to unexpected and unwanted behavior within applications that assume only single core execution, so the developer should consider this setting carefully. By setting ‘configRUN_MULTIPLE_PRIORITIES’ to ‘0’, tasks will run simultaneously only if they have the same priority. Setting this value to ‘1’ means that tasks with different priorities could run simultaneously.

Another key definition for SMP is ‘configUSE_CORE_AFFINITY’. With multiple cores available, the application developer may want to bind or pin certain tasks to run on specific cores. Pinning a task to one or more specific cores is known as core affinity. By setting this option to ‘1’, a developer can use the vTaskCoreAffinitySet() and vTaskCoreAffinityGet() functions in the application. These functions allow the developer to set and get the core affinity mask for each task, which represents the cores a task can run on. These functions may also be helpful for transitioning an application from AMP or single-core to SMP. We will demonstrate the use of these functions in the code sample provided later in this blog.

Finally, the ‘configUSE_PREEMPTION’ definition allows the use of the vTaskPreemptionDisable() and the vTaskPreemptionEnable() functions. These functions allow the application developer to disable and enable preemption of a task over a specific section of code. This prevents the scheduler from preempting the task while the section of code is executing, allowing for even greater control of the behavior within a multicore application using SMP.

Below, we have a code example that demonstrates the use of SMP. You can follow the same instructions from the previous blogs in this series to build and run the example on your own Raspberry Pi Pico. In the code, we create four tasks, named A, B, C, and D. Task A is pinned to core 0 on the RP2040 using the vTaskCoreAffinitySet() function, while task B is pinned to core 1. Tasks C and D are not pinned to any particular core, meaning they can run on either core. On execution, each task gets the core affinity mask for itself by using vTaskCoreAffinityGet(). Then, each task prints its name, the core that it is currently running on, the current tick count, and the core affinity mask to the serial console. The get_core_num() function is provided by the Raspberry Pi Pico C/C++ SDK. You might notice we borrowed the vSafePrint() function that uses the Semaphore API from the previous blog.

#include <stdio.h>

#include "pico/stdlib.h"

#include "FreeRTOS.h"

#include "task.h"

#include "semphr.h"

 

const int taskDelay = 100;

const int taskSize = 128;

 

SemaphoreHandle_t mutex;

 

void vSafePrint(char *out) {

    xSemaphoreTake(mutex, portMAX_DELAY);

    puts(out);

    xSemaphoreGive(mutex);

}

 

void vTaskSMP(void *pvParameters) {

    TaskHandle_t handle = xTaskGetCurrentTaskHandle();

    UBaseType_t mask = vTaskCoreAffinityGet(handle);

    char *name = pcTaskGetName(handle);

    char out[32];

    for (;;) {

        sprintf(out,"%s  %d  %d  %d", name,

            get_core_num(), xTaskGetTickCount(), mask);

        vSafePrint(out);

        vTaskDelay(taskDelay);

    }

}

 

void main() {

    stdio_init_all();   

    mutex = xSemaphoreCreateMutex();

    TaskHandle_t handleA;

    TaskHandle_t handleB;

    xTaskCreate(vTaskSMP, "A", taskSize, NULL, 1, &handleA);

    xTaskCreate(vTaskSMP, "B", taskSize, NULL, 1, &handleB);

    xTaskCreate(vTaskSMP, "C", taskSize, NULL, 1, NULL);

    xTaskCreate(vTaskSMP, "D", taskSize, NULL, 1, NULL);

    vTaskCoreAffinitySet(handleA, (1 << 0));

    vTaskCoreAffinitySet(handleB, (1 << 1));

    vTaskStartScheduler();

}

When you run this application on the Raspberry Pi Pico, you should see output in the serial console similar to this:

D  1  9101  -1

C  0  9101  -1

B  1  9102  2

A  0  9102  1

 

A  0  16000  1

C  1  16001  -1

D  0  16001  -1

B  1  16002  2

 

D  1  60701  -1

A  0  60701  1

C  1  60702  -1

B  1  60702  2

Above, we have extracted three sections of the output to illustrate potential behavior of tasks when using SMP. Each section shows that all four tasks are executing in close proximity to one another, just within a tick or two. However, you will notice that the order of execution for each task changes between the sections. You should also notice that tasks A and B run only on the cores they were configured to run on (0 and 1 respectively), but tasks C and D run on either core because of the affinity mask. Also, recognize that C may execute before D, D may execute before C, or they may execute simultaneously. This kind of behavior can be considered non-deterministic behavior, because we cannot predict the exact order the tasks will execute or where they will execute due to the affinity mask and preemption across cores. Therefore, it is important for a developer employing SMP to properly configure and use affinity masks, task priorities, and preemption to suit the needs of the application. This may mean consciously limiting some of the available capabilities to ensure the desired behavior.

Historically, embedded developers working on single-core microcontrollers did not have to consider certain kinds of non-deterministic behavior introduced by using multiple cores. As we covered in this blog, there are several different ways to use SMP, depending on the configuration and usage. SMP with FreeRTOS on the Raspberry Pi Pico provides new capabilities, and developers should design their applications thoughtfully when leveraging these capabilities. Additional resources for learning more about SMP can be found here and here.

We hope you have found this blog series instructive and informative. Using FreeRTOS with the Raspberry Pi Pico for embedded development is straightforward and offers many capabilities for embedded applications as we have shown. Use the example code provided as a starting point to experiment. Remember to visit freertos.org for more information as you explore and build.

Daniel Gross is a Senior Developer Advocate at Amazon Web Services (AWS). Focused on FreeRTOS and embedded devices interacting with cloud services, Daniel is passionate about delivering a great developer experience for IoT builders.

More from Daniel