In this series, I design a programmable lab switch based on RP2040, that can turn a set of signals or supplies on or off. Over USB. SCPI compatible. |
Check the previous post for the analysis of the bare metal uart_advanced example. It enables receiving side (incoming data) interrupt. When data arrives, the interrupt fires and the service handler reads the input character(s).
I'm going to translate this to 3 FreeRTOS chunks:
- the UART initialisation will go into the main function, before the RTOS tasks are created and scheduled.
- the interrupt handler will just wake up the sleeping UART task by sending it an RTOS message, instead of performing logic. Then it re-arms itself.
This is slightly different than the example. Reading data is done in the service handler there. - the UART task will ask RTOS to sleep until it gets a message to wake up (it will get that from the interrupt handler above). After waking up, it reads the incoming data, and does "meaningful business logic" with it.
Then it asks RTOS to sleep again. - goto 2.
The meaningful business logic in the final design will be: send the incoming data to the SCPI parser, because it's a SCPI command.
In today's blog, where we just want to test it out, we'll echo it back to the UART where we got it from (the USB port). And if the character is a 'b', we also toggle the LED.
Simple, but good enough to prove that this inbound UART part of the design is functional.
UART initialisation
All the meaningful things from the uart_advanced examples happen here. I've put this in a dedicated procedure, and call it early in main().
void initUART() { // Set data format uart_set_format(UART_ID, DATA_BITS, STOP_BITS, PARITY); // Turn off FIFO's - we want to do this character by character uart_set_fifo_enabled(UART_ID, false); // Set up a RX interrupt // We need to set up the handler first // Select correct interrupt for the UART we are using int UART_IRQ = UART_ID == uart0 ? UART0_IRQ : UART1_IRQ; // And set up and enable the interrupt handlers irq_set_exclusive_handler(UART_IRQ, UART_Isr); irq_set_enabled(UART_IRQ, true); }
I don't enable the interrupt handlers yet. I'm doing it later, in the UART task.
UART task
In the init part of the task, we do little. The hardware is already initialised. We just take care that some variables are set.
// some of this code is in a header file. Check the attached zip to see what's where. /* Task parameters for UART Task. */ #define UART_TASK_PRIORITY (2) #define UART_TASK_STACK_SIZE (1024 * 3) /* application dependent UART settings */ #define UART_BUFFER_SIZE 26 // ... #define UART_ID uart0 #define BAUD_RATE 115200 #define DATA_BITS 8 #define STOP_BITS 1 #define PARITY UART_PARITY_NONE /* Stores the handle of the task that will be notified when the receive is complete. */ volatile TaskHandle_t xTaskToNotify_UART = NULL; uint8_t rxChar; void uart_task(void *pvParameters) { /* To avoid compiler warnings */ (void) pvParameters; uint32_t ulNotificationValue; xTaskToNotify_UART = NULL; // TODO semaphore while (true) {
Then, in the loop part, we wait for incoming messages (which means the trigger informed us there's input):
while (true) { /* Start the receiving from UART. */ UART_receive(); /* Wait to be notified that the receive is complete. Note the first parameter is pdTRUE, which has the effect of clearing the task's notification value back to 0, making the notification value act like a binary (rather than a counting) semaphore. */ ulNotificationValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY); if (ulNotificationValue == 1) { /* Handle received data */ while (uart_is_readable(UART_ID)) { rxChar = uart_getc(UART_ID); // TODO remove test code if (uart_is_writable(UART_ID)) { uart_putc(UART_ID, rxChar); // echo incoming char } if (rxChar == 'b') { gpio_xor_mask(1u << PICO_DEFAULT_LED_PIN); // toggle led } } } } }
We read characters one by one. Then we perform the dummy business logic: echo it back to the serial port, and toggle the on-board LED status if it happens to be the character 'b'.
In a future blog, I'll remove the dummy part, and send the characters to the SCPI parser, so that it can pick them up and examine what command(s) we received.
The read preparation function
This helper function (re)initialises the UART for the next activity. It does so little that I just could have put the 3 commands in the task above.
In the case of this RP2040, where the activities to re-arm are so small, it would make it easier to understand the code to just do this inside the the task. Maybe I do this in a next post ...
// UART activate a receive with interrupt. Wait for ever for UART_BUFFER_SIZE bytes void UART_receive() { /* At this point xTaskToNotify should be NULL as no receive is in progress. A mutex can be used to guard access to the peripheral if necessary. */ configASSERT(xTaskToNotify_UART == NULL); /* Store the handle of the calling task. */ xTaskToNotify_UART = xTaskGetCurrentTaskHandle(); // Now enable the UART to send interrupts - RX only uart_set_irq_enables(UART_ID, true, false); }
The UART interrupt handler
It's on purpose a very short function. Most things are common in all FreeRTOS-savvy service handlers. It disables the interrupt and notifies the UART task, so that it wakes up and does its thing.
void UART_Isr() { BaseType_t xHigherPriorityTaskWoken = pdFALSE; // Now disable the UART to send interrupts uart_set_irq_enables(UART_ID, false, false); if (xTaskToNotify_UART != NULL) { /* Notify the task that the receive is complete. */ vTaskNotifyGiveFromISR(xTaskToNotify_UART, &xHigherPriorityTaskWoken); /* There are no receive in progress, so no tasks to notify. */ xTaskToNotify_UART = NULL; /* If xHigherPriorityTaskWoken is now set to pdTRUE then a context switch should be performed to ensure the interrupt returns directly to the highest priority task. The macro used for this purpose is dependent on the port in use and may be called portEND_SWITCHING_ISR(). */ portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } }
The interrupt will be re-enabled inside that task.
main()
The UART initialisation is called, the task registered and the RTOS started.
int main() { /* Configure the hardware */ prvSetupHardware(); // our initUART is called inside this function /* Create the UART task. */ xTaskCreate(uart_task, "UART task", UART_TASK_STACK_SIZE, NULL, UART_TASK_PRIORITY, NULL); /* Start the tasks and timer running. */ vTaskStartScheduler(); for (;;) {} // RTOS code never gets to here return 0; }
The project in its current state is attached. You can send text to the Pico-PI USB COM port. It replies back with the same character. If you type a 'b', the LED will toggle.
It's worth checking it out, and see some decisions that I do not describe in the posts (build file setup, code organisation)