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