1

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..

user12537
  • 11
  • 1
  • 1
    Serial comms are byte streams. You really should have a sane protocol to delineate the message blocks. Relying on timers is unlikely to be reliable. – Martin James Jul 19 '23 at 09:51
  • I think you're going down the wrong path here, making your system much more complex for no reason. You should not need DMA to process such slow serial data. If you don't want to block the CPU, you should just specify 0 as the second argument to `uart_is_readable_within_us`, which should make it return instantly if there is no data available. Or simply call `uart_is_readable` instead. – David Grayson Jul 19 '23 at 17:42
  • "*Shouldn't the UART have an expectation of a time frame ...*" -- Your expectations are unreasonable. This is asynchronous communication; data can arrive at any time. The UART frames each char or byte. The UART has no concept of "message" or "*chunk*". See https://stackoverflow.com/questions/27152926/parsing-time-delimited-uart-data/27156684#27156684 – sawdust Jul 19 '23 at 19:21
  • "*However, this blocked roughly 50% of CPU time*" -- Because your code *polls* the UART for each new byte rather than wait for an interrupt. A UART is a char device, not a block device. Each received char is an I/O event that is independent from the prior char as well as the next char. – sawdust Jul 19 '23 at 19:25
  • "this blocked roughly 50% of CPU time", the loop like `while(1){sleep_ms(1000);}` suggesting that the waste of time is not related to UART, even running at 9600bps, it should be at 960 bytes/s throughput. – hcheung Jul 21 '23 at 13:39
  • @MartinJames sawdust I think your response are the most relevant here. However, given the situation that the transmitting system is strictly real-time , I assume my timer approach would work. Thank you very much! – user12537 Jul 23 '23 at 10:10
  • The only U[S]ART that meets your needs (that I know of) is the type in Microchip/Atmel SoCs with the Receiver Timeout feature. Its Linux driver uses this Receiver Timeout to abort the current, active DMA read transfer because of an idle line condition, and then forwards the received bytes for termios processing. Solves your problem. – sawdust Jul 24 '23 at 04:25
  • @sawdust yes, that answers my question! Thank you! So still, I assume that my "timer"-approach is sort of practical, however, I could theoretically implement a U[S]ART via PIO. – user12537 Jul 25 '23 at 05:56

0 Answers0