M0AGX / LB9MG

Amateur radio and embedded systems

XMEGA USART driver with TX DMA

This is a quite universal, non-blocking UART driver for XMEGA. It supports both transmission (with optional DMA) and reception. Receive side can deliver callbacks whenever a complete line (terminated with \n) is received or received bytes can be retrieved one-by-one from a ringbuffer (more useful for GPS units). This driver can support multiple hardware USARTs in a single application.

Usage example

This is a real world usage example of this driver with a GPS unit. Due to bursty GPS transmission (a couple of NMEA sentences at once) I had to use a ringbuffer instead of a callback every line, because the time spent analyzing a sentence was too long and some sentences were randomly lost.

Data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include "uart.h"
#include <string.h>

/* --------- private data --------------- */
static volatile char _uart_buffer[128];
static uart_state_t _gps_uart_state;
static char _gps_line_buffer[100];
static uint8_t _gps_line_buffer_index = 0;

In order to use the ringbuffer the application has to keep:

  • uart_state_t structure
  • a generic buffer (_gps_line_buffer) for the exclusive use by the driver - in this case this is used as a ringbuffer for reception or as a linear buffer for transmission
  • optionally - another buffer to logically process the received bytes one by one

Initilization and interrupts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void gps_uart_init(void){
    //TODO: configure TX pin as an output
    uart_init(&USARTE0, &DMA.CH0, &_gps_uart_state, NULL /*line callback*/, _uart_buffer, sizeof(_uart_buffer));
    char* buffer = uart_get_buffer(&_gps_uart_state);
    strcpy_P(buffer, PSTR("$PMTK300,5000,0,0,0,0*18\r\n")); //update rate = 5 seconds
    uart_send(&_gps_uart_state);
}

//uncomment this interrupt when not using DMA
//ISR(USARTE0_DRE_vect){
//    uart_dre_isr(&_gps_uart_state);
//}

ISR(USARTE0_RXC_vect){
    uart_rx_isr_ringbuffer(&_gps_uart_state);
}

ISR(DMA_CH0_vect){
    uart_dma_isr(&_gps_uart_state);
}

The application passes pointers to USART, optionally DMA channel. Line callback is not used in this case. Last arguments are the buffer and its size.

In order to reduce GPS refresh rate (I don't need an update every second) a special command is sent to the GPS (buffer is taken using get_uart_buffer and filled with sprintf).

The application also has to declare interrupts as shown - in this case the USART RX interrupt (with ringbuffer isr functions) and DMA channel interrupt.

Using DMA to transmit single command is of course an overkill, but it gives the general usage idea of the driver.

Receiving data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void gps_uart_task(void){
    char c = 0;
    while (uart_getc(&_gps_uart_state, &c)){
        if (c == '\n'){ //whole line has arrived - fire callback
            _gps_line_buffer[_gps_line_buffer_index++] = c; //add the newline, otherwise parser will fail
            _gps_line_buffer[_gps_line_buffer_index] = '\0';
            //TODO: process the line
            _gps_line_buffer_index = 0;
            continue;
        }
        _gps_line_buffer[_gps_line_buffer_index] = c;
        _gps_line_buffer_index++;
        if (unlikely(_gps_line_buffer_index > sizeof(_gps_line_buffer)-1)){
            _gps_line_buffer_index = 0;
        }
    }
}

The gps_uart_task is called from the main loop. It tries to receive as many bytes from the UART RX buffer as possible until a newline is encountered.

If line callback mode is used, then a function taking a string as an argument has to be implemented and passed to uart_init. Main loop has to keep calling uart_task and the function called from RXC ISR has to be uart_rx_isr.

Driver

Most parts are similar to my SPI driver. Have a look for a detailed overview.

uart.h

All the functions have doxygen comments that should make their intended use understood.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#ifndef UART_H_
#define UART_H_
#include <stdbool.h>
#include <stdint.h>
#include <avr/io.h>

#define LINE_SEPARATOR '\n'

typedef void (*uart_rx_callback_t)(const char *s);

typedef struct {
    volatile char *buffer; //must be null-terminated, shared by RX and TX directions
    uint8_t buffer_size;
    uint8_t buffer_index; //used wit uart_task
    uint8_t buffer_head;  //used with ringbuffer
    uint8_t buffer_tail;  //used with ringbuffer
    bool line_received;
    uart_rx_callback_t rx_callback;
    USART_t* usart;
    DMA_CH_t* dma_ch;
    uint8_t dma_trigger_source;
} uart_state_t;

/** Initializes the driver with a particular buffer and USART.
 * @param t Pointer to USART struct, eg. &USARTD0
 * @param dma_ch Pointer to DMA channel struct, eg. &DMA.CH0, NULL if DMA is not needed.
 * @param s Pointer to uart_state_t struct, allocated statically.
 * @param rx_callback Pointer to function that should be called whenever a complete line is received
 * @param buffer Pointer to array that will be used as the TX and RX buffer
 * @param buffer_size Size of buffer array.
 */
void uart_init(USART_t* t, DMA_CH_t* dma_ch, volatile uart_state_t* s, uart_rx_callback_t rx_callback, volatile char *buffer, uint8_t buffer_size);

/** Line received callbacks are delivered from this function. It should be called
 * from main loop. Not used with RX ringbuffer!
 * @param t Pointer to uart_state_t struct, allocated statically.
 */
void uart_task(volatile uart_state_t* t);

/** This function has to be called to obtain the buffer to fill with
 * data to be transmitted. It also stops RX operation of the driver.
 * DATA IN BUFFER MUST BE NULL TERMINATED.
 * After filling the buffer uart_send has to be called.
 * @param s Pointer to uart_state_t struct, allocated statically.
 * @return Pointer to buffer to be filled with data. The same as buffer argument of uart_init.
 */
char* uart_get_buffer(volatile uart_state_t *s);

/** Starts sending data in buffer (from uart_get_buffer)
 * @param s Pointer to uart_state_t struct, allocated statically.
 */
void uart_send(volatile uart_state_t* s);

/** Gets a single byte from receive ringbuffer.
 * @param t Pointer to uart_state_t struct, allocated statically.
 * @partam target Pointer where the byte from ringbuffer is to be put.
 * @return true if a byte was received, false otherwise
 */
bool uart_getc(volatile uart_state_t* t, char *target);

/** Receive interrupt handler in line mode. To be called within ISR(USARTxn_RXC_vect) function.
 * @param t Pointer to uart_state_t struct, allocated statically.
 */
void uart_rx_isr(volatile uart_state_t* t);

/** Receive interrupt handler in ringbuffer mode. To be called within ISR(USARTxn_RXC_vect) function.
 * @param t Pointer to uart_state_t struct, allocated statically.
 */
void uart_rx_isr_ringbuffer(volatile uart_state_t* t);

/** Transmit interrupt handler. Used when no DMA channel
 * is specified. To be called within ISR(USARTxn_DRE_vect) function.
 * @param t Pointer to uart_state_t struct, allocated statically.
 */
void uart_dre_isr(volatile uart_state_t* t);

/** Transmit interrupt handler. Used with a DMA channel
 * interrupt. To be called within ISR(DMA_CHx_vect) function.
 * @param t Pointer to uart_state_t struct, allocated statically.
 */
void uart_dma_isr(volatile uart_state_t* t);

#endif

uart.c

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#include <string.h>
#include "uart.h"

#define likely(x)       __builtin_expect(!!(x),1)
#define unlikely(x)     __builtin_expect(!!(x),0)

/* --------- private prototypes --------- */
static void uart_end_transmission(volatile uart_state_t *t);

/* ----------- implementation ----------- */

void uart_init(USART_t* u,
               DMA_CH_t *dma_ch,
               volatile uart_state_t* s,
               uart_rx_callback_t rx_callback,
               volatile char *buffer,
               uint8_t buffer_size){

    u->CTRLB = USART_TXEN_bm | USART_RXEN_bm; //enable receiver and transmitter

    //baud setting
    u->BAUDCTRLA = 12;                  //BSEL   = 12  9600bps
    u->BAUDCTRLB = 4 <<USART_BSCALE_gp; //BSCALE =  4  @32MHz

    s->buffer = buffer;
    s->buffer_size = buffer_size;
    s->rx_callback = rx_callback;
    s->usart = u;
    s->line_received = false;
    s->buffer_index = 0;
    s->buffer_head = 0;
    s->buffer_tail = 0;

    s->dma_ch = dma_ch;
    if (dma_ch){ //look for trigger source
        switch ((uint16_t)u){ //pointer has to be cast the ugly way 
        #ifdef USARTC0
            case (uint16_t)&USARTC0:
                s->dma_trigger_source = DMA_CH_TRIGSRC_USARTC0_DRE_gc; break;
        #endif
        #ifdef USARTC1
            case (uint16_t)&USARTC1:
                s->dma_trigger_source = DMA_CH_TRIGSRC_USARTC1_DRE_gc; break;
        #endif
        #ifdef USARTD0
            case (uint16_t)&USARTD0:
                s->dma_trigger_source = DMA_CH_TRIGSRC_USARTD0_DRE_gc; break;
        #endif
        #ifdef USARTD1
            case (uint16_t)&USARTD1:
                s->dma_trigger_source = DMA_CH_TRIGSRC_USARTD1_DRE_gc; break;
        #endif
        #ifdef USARTE0
            case (uint16_t)&USARTE0:
                s->dma_trigger_source = DMA_CH_TRIGSRC_USARTE0_DRE_gc; break;
        #endif
        }
    }

    //enable RXC interrupt - low priority
    //DRE interrupt must not be enabled, otherwise it will fire immediately
    u->CTRLA = USART_RXCINTLVL_LO_gc;
}

void uart_task(volatile uart_state_t *t){
    if (t->line_received){
        t->line_received = false;
        if (t->rx_callback){
            t->buffer_index = 0; //MUST BE BEFORE CALLBACK!
            t->rx_callback((const char*)t->buffer);
        }
    }
}

bool uart_getc(volatile uart_state_t* t, char *target){
    if (t->buffer_tail != t->buffer_head){
        uint8_t current_head = t->buffer_head;
        t->buffer_head = (t->buffer_head + 1) % t->buffer_size; //UART_BUFFER_SIZE;
        *target = t->buffer[current_head];
        return true;
    }
    return false;
}

//this function transmits data that is already in the buffer
void uart_send(volatile uart_state_t* s){
    s->buffer[s->buffer_size-1] = '\0'; //enforce that the string is always null-terminated
    if (s->dma_ch){
        //globally enable DMA, no double buffering, round-robin
        DMA.CTRL  = DMA_ENABLE_bm;

        /* ------ TX DMA channel setup ------ */
        s->dma_ch->CTRLA = DMA_CH_RESET_bm;
        s->dma_ch->ADDRCTRL = DMA_CH_SRCDIR_INC_gc | DMA_CH_DESTDIR_FIXED_gc | DMA_CH_DESTRELOAD_TRANSACTION_gc; //source - increment, destination (USARTxx.DATA) fixed
        s->dma_ch->TRIGSRC = s->dma_trigger_source;
        s->dma_ch->DESTADDR0 = (((uint16_t)&(s->usart->DATA)) >> 0) & 0xFF;
        s->dma_ch->DESTADDR1 = (((uint16_t)&(s->usart->DATA)) >> 8) & 0xFF;
        s->dma_ch->DESTADDR2 = 0x00;
        s->dma_ch->SRCADDR0 = (((uint16_t)s->buffer) >> 0) & 0xFF;
        s->dma_ch->SRCADDR1 = (((uint16_t)s->buffer) >> 8) & 0xFF;
        s->dma_ch->SRCADDR2 = 0x00; //internal SRAM 
        s->dma_ch->TRFCNT = strlen((char*)s->buffer); //transfer length

        //enable channel, USART DRE will trigger the transmission, single shot mode - one byte per trigger
        s->dma_ch->CTRLA = DMA_ENABLE_bm | DMA_CH_SINGLE_bm | DMA_CH_BURSTLEN_1BYTE_gc; 
        s->dma_ch->CTRLB = DMA_CH_TRNINTLVL_LO_gc; //transfer complete interrupt enable
        /* ---------------------------------- */

    } else { //DMA channel is not set, use ISR
        s->usart->DATA = s->buffer[0]; //transmit first character
        s->buffer_index = 1;           //ISRs will take care of the rest
        s->usart->CTRLA = USART_DREINTLVL_LO_gc; //enable DRE interrupt, disable RX interrupt
    }
}

char* uart_get_buffer(volatile uart_state_t *s){
    s->usart->CTRLA = 0; //disable all interrupts (stop receiving)
    s->buffer_tail = 0;
    s->buffer_head = 0;
    return (char*)s->buffer;
}

inline void uart_rx_isr(volatile uart_state_t *t){
    t->buffer[t->buffer_index] = t->usart->DATA;
    if (unlikely(t->buffer_index >= t->buffer_size-1)){ //rx buffer overflow protection
        t->buffer_index = 0;
    }
    if (unlikely(t->buffer[t->buffer_index] == LINE_SEPARATOR)){ //we received newline, let's terminate the command
        t->buffer_index++;
        t->buffer[t->buffer_index] = '\0';
        t->line_received = true; //now the line will be processed by uart_task
        t->buffer_index = 0; //hack
    } else {
        t->buffer_index++;
    }
}

inline void uart_rx_isr_ringbuffer(volatile uart_state_t *t){
    uint8_t next = (t->buffer_tail + 1) % t->buffer_size;
    t->buffer[next] = t->usart->DATA;
    t->buffer_tail = next;
}

inline void uart_dre_isr(volatile uart_state_t *t){ //data register ready for more data interrupt
    if (t->buffer[t->buffer_index]){ //if we have more data to transmit (string must be null-terminated)
        t->usart->DATA = t->buffer[t->buffer_index];
        t->buffer_index++;
    } else { //no more data to transmit
        uart_end_transmission(t);
    }
}

inline void uart_dma_isr(volatile uart_state_t* t){
    t->dma_ch->CTRLA = 0; //disable channel
    t->dma_ch->CTRLB = 0; //disable channel interrupts
    uart_end_transmission(t);
}

inline static void uart_end_transmission(volatile uart_state_t *t){
    //prepare buffer for reception
    t->buffer_index = 0;
    t->buffer_tail = 0;
    t->buffer_head = 0;
    t->usart->CTRLA = USART_RXCINTLVL_LO_gc; //disable DRE interrupt, enable RX interrupt
}

I release the code into public domain.