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. |
Differences with UART approach
The design in the previous posts used low level UART0 HAL API. With interrupts to detect incoming traffic. The stdio (easy) flavour of the TinyUML library doesn't support this approach though. That library is also interrupt driven, but they are different types of interrupts, and not interceptable. Shabaz and I have been reviewing options, and he suggested inbound traffic polling, at a 50 ms rate. That's what I'm going for here.
The stdio usb library allows you to talk via printf - and the other typical stdio functions. You don't need to go into the lower level USB API and create your own CDC. It's a convenience layer that will fit many scenarios.
Enable UART0 via USB instead of via the UART TX/RX pins
It's simple. Add these two lines to the CMalke script, and the build will use the stdio_usb library. You'll still talk using a handle called uart0, but the layer will transfer it to the RP2040 USB peripheral.
pico_enable_stdio_usb(scpi_switch 1)
pico_enable_stdio_uart(scpi_switch 0)
In your code, add these 2 includes (will work for both the UART and USB version of the lib: #include <stdio.h>
#include "pico/stdlib.h"
The main effect is that the stdio is routed to USB with the two lines in the CMake file. They 'll go to the UART pins if they aren't there (or commented out).
In your code, you can use these two defines if you need different implementations for the two choices. In my project, It's not a must - I could use polling for both UART and USB. But that UART with interrupts was half the story I wanted to bring in this mini series (learn FreeRTOS with interrupts, task messaging, ...). So I leave it in. Here's how I distinguish between the two options in C:
/* the PURE UART version of the code is compiled if the STDIO_UART lib is used. this is default for the SDK, except when you put these commands in the CMake script: # enable usb output, disable uart output pico_enable_stdio_usb(scpi_switch 1) pico_enable_stdio_uart(scpi_switch 0) */ #if LIB_PICO_STDIO_UART // ... #endif /* the TINYUSB UART version of the code is compiled if the STDIO_USB lib is used. this is the case when you put these commands in the CMake script: # enable usb output, disable uart output pico_enable_stdio_usb(scpi_switch 1) pico_enable_stdio_uart(scpi_switch 0) */ #if LIB_PICO_STDIO_USB // ... #endif
The Original UART code, with interrupt mechanism, now wrapped so that it only compiles if the usb library is not selected.:
/* the PURE UART version of the code is compiled if the STDIO_UART lib is used. this is default for the SDK, except when you put these commands in the CMake script: # enable usb output, disable uart output pico_enable_stdio_usb(scpi_switch 1) pico_enable_stdio_uart(scpi_switch 0) */ #if LIB_PICO_STDIO_UART #define UART_ID uart0 /* Stores the handle of the task that will be notified when the receive is complete. */ volatile TaskHandle_t xTaskToNotify_UART = NULL; void UART_receive(); void uart_task(void *pvParameters) { /* To avoid compiler warnings */ (void) pvParameters; uint32_t ulNotificationValue; xTaskToNotify_UART = NULL; scpi_instrument_init(); // TODO semaphore 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); if (rxChar == 255) break; scpi_instrument_input((const char *)&rxChar, 1); /* TODO this will be the character handler if (telemetry_queue) { // queue needs to be generated} if (xQueueSend(telemetry_queue, (void *)(&rxChar), (TickType_t)10) != pdPASS) { // Failed to post the message, even after 10 ticks. // Force an assert configASSERT((volatile void *)NULL); } } */ } } } } void initUART() { // 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; // TODO set USB handler if needed // irq_has_shared_handler(USBCTRL_IRQ) // And set up and enable the interrupt handlers irq_set_exclusive_handler(UART_IRQ, UART_Isr); irq_set_enabled(UART_IRQ, true); } // UART interrupt handler 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); } } // 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); } void UART_write(const char * data, uint32_t len) { uart_puts(UART_ID, data); } #endif
And the new USB code, with polling. Wrapped so that it only compiles if the usb library is selected.
* the TINYUSB UART version of the code is compiled if the STDIO_USB lib is used. this is the case when you put these commands in the CMake script: # enable usb output, disable uart output pico_enable_stdio_usb(scpi_switch 1) pico_enable_stdio_uart(scpi_switch 0) */ #if LIB_PICO_STDIO_USB void uart_task(void *pvParameters) { /* To avoid compiler warnings */ (void) pvParameters; scpi_instrument_init(); // TODO semaphore bool bHasChar; while (true) { vTaskDelay((uint32_t)(50 / portTICK_PERIOD_MS)); // sleep 50 ms bHasChar = true; while (bHasChar) { rxChar = getchar_timeout_us(0); // don't wait for characters if (rxChar == 255) { bHasChar = false; } else { scpi_instrument_input((const char *)&rxChar, 1); } } } } void initUART() { // USB mode does not need additional settings. It's polling vs interrupt return; } void UART_write(const char * data, uint32_t len) { printf(data); } #endif
The polling loop: The task will sleep 50 ms, and yields that time to the RTOS and other tasks. Then it'll check (poll) if there's traffic. A NOWAIT BLOCKING read call. The effect is that it 'll read if there's traffic. If no traffic, it'll fail immediately and return.
That's it. If you apply the CMake settings with the 2 lines in the CMake script, the code will compile, using the second part. You now need to connect a USB cable to the device being programmed / debugged. A COM port will pop up, and you can talk to it, just like to the initial UART scenario.
Source and binaries attached. Includes your homework from post 4.
scpi_switch_binary_20221022.zip