FreeRTOS Symmetric Multiprocessing (SMP) is a recent version of the RTOS that can schedule tasks across multiple controller cores. It's currently in test phase, and they have a version for the RP2040. In this blog post, I protect the ADC peripheral in a multi-threaded environment.. |
Mutex to reserve a shared resource
Certain CPU resources should not be tampered with, while you are using them. And you shouldn't touch them when another task is using them. Some examples are serial ports, ADC blocks, GPIO pins. You do not want that a task changes the baud rate or CS status of your SPI, while you are exchanging data in another task. In RTOSses, this is managed via semaphores.
A semaphore is like a token you can grab. If you have the token, you can use the resource. Then you return the token. In our case, we only want to give one token, so that only one task at the time can use our ADC. This single token style is called a binary semaphore. There is one (if the resource is free), or there isn't one (if some other task grabbed the token and is using the ADC). In FreeRTOS, a binary semaphore is called a MUTEX (mutual exclusive). That's the one I'll be using here to reserve the ADC peripheral.
Define the mutex
Before you can use a mutex, you have to create it. I've done that in the main().
// ... #include "semphr.h" // handles static SemaphoreHandle_t xADCMutex; int main(void) { /* Configure the hardware ready to run the demo. */ prvSetupHardware(); xTaskCreate(prvBlinkTask, "blink", configMINIMAL_STACK_SIZE, NULL, mainBLINK_TASK_PRIORITY, NULL); xTaskCreate(prvTemperatureTask, "temperature", configMINIMAL_STACK_SIZE, NULL, mainTEMPERATURE_TASK_PRIORITY, NULL); /* Create a mutex type semaphore. */ xADCMutex = xSemaphoreCreateMutex(); /* Start the tasks and timer running. */ vTaskStartScheduler(); // ...
Use the mutex
Then, in any code that changes the state, or relies on the state of the ADC, I try to grab the mutex first. That will only succeed if no one else uses the ADC. I 've defined a maximum wait (forever). My code will wait - block without taking CPU ticks - until the ADC is free. Another option is to give up after a timeout. Or to increase task priority each time the mutex grab failed, ...
static void prvTemperatureTask(void *pvParameters) { (void)pvParameters; TickType_t xNextWakeTime; /* Initialise xNextWakeTime - this only needs to be done once. */ xNextWakeTime = xTaskGetTickCount(); /* Enable onboard temperature sensor */ adc_set_temp_sensor_enabled(true); for (;;) { xSemaphoreTake(xADCMutex, portMAX_DELAY); // switch to the temperature mux if needed if (adc_get_selected_input() != 4) { adc_select_input(4); } float temperature = read_onboard_temperature(TEMPERATURE_UNITS); xSemaphoreGive(xADCMutex); printf("Onboard temperature = %.02f %c\n", temperature, TEMPERATURE_UNITS); xTaskDelayUntil(&xNextWakeTime, mainTEMPERATURE_TASK_FREQUENCY_MS); } }
You can see that I try to make the surface between grabbing the mutex and giving it back, as small as possible. I use it just before setting the channel. And I return it after reading. That's the critical part of the execution where I want to be sure that the ADC settings aren't changed.
c++: In object-oriented RTOSes, mutexes and semaphores are often objects that you just have to instantiate in the context where you need to have access. You don't need to call methods or clean up. Their constructor will try to grab the token, and their destructor will give it back, once you exit the context. |
In the current project, there is no competition from another task, so I can't prove that it works. But in the next blog I'll create a competitor for the ADC: a task that reads a voltage. We can then run the example with and without mutex protection, and experience the mayhem of not protecting a resource.
critical area: FreeRTOS also has the concept of critical code areas. Those are parts of the code that should run uninterrupted (atomic). |