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 add a second task to my own little SMP project in VSCode: measure the on-package temperature. |
Create a Temperature task
On the Pico, ADC channel 4 is used to measure onboard temperature. In the pico-examples distribution, there is a ready-to-use temperature project. I've translated it into a FreeRTOS task.
#include "hardware/adc.h" #define mainTEMPERATURE_TASK_PRIORITY (tskIDLE_PRIORITY +1) #define mainTEMPERATURE_TASK_FREQUENCY_MS (5000 / portTICK_PERIOD_MS) #define TEMPERATURE_UNITS 'C' 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 and * select its channel (do this once for efficiency, but beware that this * is a global operation). */ adc_set_temp_sensor_enabled(true); for (;;) { // TODO set a semaphore // switch to the temperature mux if needed if (adc_get_selected_input() != 4) { adc_select_input(4); } float temperature = read_onboard_temperature(TEMPERATURE_UNITS); printf("Onboard temperature = %.02f %c\n", temperature, TEMPERATURE_UNITS); xTaskDelayUntil(&xNextWakeTime, mainTEMPERATURE_TASK_FREQUENCY_MS); } }
The read_onboard_temperature() function is exactly the same as in pico-examples:
/* References for this implementation: * raspberry-pi-pico-c-sdk.pdf, Section '4.1.1. hardware_adc' * pico-examples/adc/adc_console/adc_console.c */ float read_onboard_temperature(const char unit) { /* 12-bit conversion, assume max value == ADC_VREF == 3.3 V */ const float conversionFactor = 3.3f / (1 << 12); float adc = (float)adc_read() * conversionFactor; float tempC = 27.0f - (adc - 0.706f) / 0.001721f; if (unit == 'C') { return tempC; } else if (unit == 'F') { return tempC * 9 / 5 + 32; } return -1.0f; }
I had two places in the code where I could enable the adc peripherals. I could do it in the initialisation part of the temperature task, or in the existing prvSetupHardware() procedure. I chose the procedure, because I prefer a style where hardware initialisation is part of the board setup.
static void prvSetupHardware( void ) { stdio_init_all(); gpio_init(PICO_DEFAULT_LED_PIN); gpio_set_dir(PICO_DEFAULT_LED_PIN, 1); gpio_put(PICO_DEFAULT_LED_PIN, 0); // Initialize hardware AD converter, for the temperature sensor adc_init(); }
The last line is my addition. This code is called at the start of main(). That's also where the new task gets registered:
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); /* Start the tasks and timer running. */ vTaskStartScheduler(); // ...
Not Thread Safe
In the temperature task, you'll see a TODO: set a semaphore. In a multi-threaded system, you have to guard resources that you're using. In my case, I'm switching the ADC channel, then reading its value. If another task switches the channel in the meantime, I'm reading a wrong value. Or if another task is busy reading another channel, I interfere with its operation. This is not an issue in the current design, because there's only one adc task. But it's good to be forward looking. That's why in the next post, I'll add a guard around the adc. In FreeRTOS lingo: a semaphore.