**PICO PI** UART: How to receive (and process) chunks of serial data using DMA

1.1k Views Asked by At

Using a Raspberry Pi Pico (i.e RP2040 chip), I want to receive and process serial data (no TX). The data comes in 500 byte chunks, @9600baud,8N1. A new data chunk is sent roughly every second. Using the UART, it was possible for me to obtain the data using the RX interrupt handler and subsequently calling uart_is_readable_within_us(UART_ID,1400). However, this blocked roughly 50% of CPU time. The code is:

...
#include "hardware/uart.h"
#include "hardware/irq.h"


#define bufsize_UART 0x400
#define UART_ID uart0

void on_uart_rx() {
    // disable IRQ for the moment we are here...
    uart_set_irq_enables(UART_ID, false, false);
    
    __uint32_t chars_rxed=0;
    uint16_t    offset=0;
    uint8_t  buf_UART[bufsize_UART];
    uint16_t buf_UART_len=0;
    buf_UART_len=0;


    while (uart_is_readable_within_us(UART_ID,1400)) {
        uint8_t ch = uart_getc(UART_ID);
        buf_UART[buf_UART_len++] = ch;
        if(buf_UART_len>=bufsize_UART){
            break;
        }
        chars_rxed++;
    }
    // CHUNK identified, processing my data
    process_data(buf_UART,buf_UART_len);

    // re-enable IRQ
    uart_set_irq_enables(UART_ID, true, false);

}
static void configure_uart() {
    uart_init(uart0, 9600);
    uart_getc(uart0);

    // Set (TX and RX) pin function modes
    gpio_set_function(0, GPIO_FUNC_UART);
    gpio_set_function(1, GPIO_FUNC_UART);
    int UART_IRQ = UART_ID == uart0 ? UART0_IRQ : UART1_IRQ;

    irq_set_exclusive_handler(UART_IRQ, on_uart_rx);
    irq_set_enabled(UART_IRQ, true);
    uart_set_irq_enables(UART_ID, true, false);

}

int main() {
    stdio_init_all();
    
    // configure UART
    configure_uart();

   while(1){
     sleep_ms(1000);
}
return 0;
}

Using up 50%of CPU time for receiving 500 Bytes/sec seemed a bit idiotic to me. I therefore tried to find out how DMA could help me to reduce CPU load. Basically, streaming UART RX data to a buffer is simple. You can have your DMA channel invoke an IRQ when the buffer is full. However, if I had set the buffer to strictly 500 Bytes, there is a realistic chance to get chunks composed of one chunk's end and another's chunk begin, i. e. I would not synchronize with the actual chunks, being separated by idle time only. As I couldn't find another alternative to have the UART(?) signal me the end of a byte stream, I now use a repeating timer to check whether the number of bytes written through the DMA channel has changed within a period longer than 1Byte cycle (@9600,8N1). If that is the case and the length of the data is nonzero, i process the data obtained so far. To reset the channel, I have learned (by trial and error) that calling dma_channel_set_write_addr(g_channel,buffer,false); dma_channel_set_trans_count(g_channel,1000,true); within the timer callback doesn't 'reset' the channel (i.e. restarting with a write position at the beginning of *buf). What eventually gave me the behavior I needed, was to call dma_channel_abort(). This fires the DMA IRQ, and within this handler, resetting (see above) works. My code is now as follows :

...
#include "hardware/uart.h"
#include "hardware/irq.h"
#include "hardware/dma.h"

    const uint32_t g_channel = 0;

void on_dma() {
    dma_channel_set_write_addr(g_channel,buffer,false);
    dma_channel_set_trans_count(g_channel,1000,true);
    // Clear interrupt
    dma_hw->ints0 = (1u << g_channel);
}
static void configure_dma(int channel) {
  // assuming uart0..

    dma_channel_config config = dma_channel_get_default_config(channel);
    channel_config_set_transfer_data_size(&config, DMA_SIZE_8);
    channel_config_set_read_increment(&config, false);
    channel_config_set_write_increment(&config, true);
    channel_config_set_dreq(&config, DREQ_UART0_RX);
    channel_config_set_irq_quiet(&config,false);

    dma_channel_configure(
        channel,
        &config,
        buffer,
        &uart0_hw->dr,
        1400,
        true);

      dma_channel_set_irq0_enabled(channel, true);
 irq_set_exclusive_handler(DMA_IRQ_0, on_dma);
 irq_set_enabled(DMA_IRQ_0, true);

}
bool repeating_timer_cb(struct repeating_timer *t){
static uint32_t last_pos = 0;
static uint8_t dt = 0;
uint32_t new_pos = ((uint32_t)(dma_channel_hw_addr(0)->write_addr));
uint32_t length = new_pos-((uint32_t)buffer);
if(last_pos == new_pos && length>0){
     // CHUNK identified!
     // Trigger channel reset!
     dma_channel_abort(0);

    //  here is where I process my data
     process_data((uint8_t*)buffer,(uint16_t)length);
  
    }
last_pos = new_pos;
return true;
}
static void configure_uart() {
    uart_init(uart0, 9600);
    uart_getc(uart0);

    // Set (TX and RX) pin function modes
    gpio_set_function(0, GPIO_FUNC_UART);
    gpio_set_function(1, GPIO_FUNC_UART);
}

int main() {
    stdio_init_all();
    
    // configure UART
    configure_uart();
    // Add timer to check for 
    struct repeating_timer timer;
    add_repeating_timer_ms(80, repeating_timer_cb, NULL, &timer);
    // Activate DMA channel
    configure_dma(g_channel);
   while(1){
     sleep_ms(1000);
}
return 0;
}

This works fine. However, My Question is: Is there a more elegant way to detect the end of a chunk when using DMA? Shouldn't the UART have an expectation of a time frame for a subsequent byte to be received? Isn't there a way to detect this timeout? UART interrupts don't fire if a DMA channel is active with the UART..

1

There are 1 best solutions below

0
Jeremy On

See RP2040 manual section Section 4.2.6.4. 'UARTRTINT', which describes the receive timeout interrupt. I believe the timeout is fixed at 32 bit times.

As @sawdust's comment indicates, the ISR for this would abort the current DMA transfer (or perhaps not, depending on your design).

Regarding "UART interrupts don't fire if a DMA channel is active with the UART": the timeout interrupt should be unaffected by DMA operation.

Of course you need to disable the specific rx and tx interrupts within the UART mask register, but enable the UARTRTINT interrupt, and enable the UART interrupt in the NVIC.