One of the great features of the Arduino Mega is the multiple UART interfaces it has. In previous articles we learnt how to setup UART0 to communicate between your PC and your Arduino, in this one, we will create a small ‘library’ that controls all four UARTs with the same functions, making it easer for you, the developer, to use those interfaces to their full potential.
Make sure that you have two Arduino Mega (get them here) and connect them like in the following image (TX1 to RX1 and RX1 to TX1).
Instead of using two Arduinos, you can also use an UART to USB converter, but you won’t be able to test the second example at the bottom of the article, only the first one.

Before we mess with the UART we need to create a data type that will help us have async communication, this data type will be the Ring Buffer, this data type is basically just an array with two indices, one where you will write to and one where you will read from. With this buffer, as long as you read from the buffer faster than you write to it, it will never be full.
Let’s start with the header file. We will need to include ‘inttype.h’ to be able to use the types ‘uintX_t’, then we can make our type, a pointer to an array, the size of the array, the head and the tail, that’s it.
For functions, we will have one to create the buffer, one to push (write), one to pop (read), get the size and clear the buffer:
#ifndef _ringbuf_h_
#define _ringbuf_h_
#include <inttypes.h>
/* Buffer with 'no end' as long as you
* read it faster than you write to it */
typedef volatile struct
{
uint8_t* data; /* internal buffer */
uint16_t maxlen; /* internal buf len */
uint16_t head; /* write pointer */
uint16_t tail; /* read pointer */
} ringbuf_t;
/* @brief Creates a ring buffer with the given array and size
* @param internal_buf internal buffer
* @param maxlen internal buffer size */
ringbuf_t ringbuf_init(uint8_t* internal_buf, uint16_t maxlen);
/* @brief Saves a value to the buffer
* @param buf ring buffer pointer
* @param val value to store in the buffer */
uint8_t ringbuf_push(ringbuf_t* buf, uint8_t val);
/* @brief Reads a value from the buffer
* @param buf ring buffer pointer
* @param out pointer to store the value read */
uint8_t ringbuf_pop(ringbuf_t* buf, uint8_t* out);
/* @brief Calculates the ammount of data in the buffer
* @param buf ring buffer pointer */
uint16_t ringbuf_len(const ringbuf_t* buf);
/* @brief Empties the buffer. Sets head and tail to 0
* @param buf ring buffer pointer */
void ringbuf_clear(ringbuf_t* buf);
#endif
On the C file, we include the header we just created and implement our functions. You can find examples of Ring Buffers everywhere on the internet with explanations, but i think the one below has sufficient comments for you to get an idea on how it works:
#include "ringbuf.h"
ringbuf_t ringbuf_init(uint8_t* internal_buf, uint16_t maxlen)
{
return (ringbuf_t)
{
.data = internal_buf,
.maxlen = maxlen,
.head = 0,
.tail = 0
};
}
uint8_t ringbuf_push(ringbuf_t* buf, uint8_t val)
{
if (buf == 0) // null buffer
return 0;
uint16_t n = buf->head + 1; // next pos
if (n >= buf->maxlen) // reached end of buffer
n = 0;
if (n == buf->tail) // buffer full
return 0;
buf->data[buf->head] = val; // save data
buf->head = n; // update head
return 1;
}
uint8_t ringbuf_pop(ringbuf_t* buf, uint8_t* out)
{
if (buf == 0) // null buffer
return 0;
if (buf->head == buf->tail) // empty
return 0;
uint16_t n = buf->tail + 1; // next pos
if (n >= buf->maxlen) // reached end of buffer
n = 0;
*out = buf->data[buf->tail]; // read data
buf->tail = n; // update tail pos
return 1;
}
uint16_t ringbuf_len(const ringbuf_t* buf)
{
if (buf == 0) // null buffer
return 0;
if (buf->head == buf->tail) // empty buf
return 0;
else if (buf->tail < buf->head) // tail behind head
return (buf->maxlen - buf->head + buf->tail);
else return buf->tail - buf->head; // tail after head
}
void ringbuf_clear(ringbuf_t* buf)
{
buf->head = 0;
buf->tail = 0;
}
If the comments aren’t enough, here is an example animation from hackaday.com:
With our new fancy data type we can now look into the UART ‘library’, let’s start with the header file, where we will include the same int types header as before, declare how many UARTs the Arduino Mega has (four) and then setup our functions, we will need one to initialize the interface, one to read and then a couple to write:
#ifndef _uart_h_
#define _uart_h_
#include <inttypes.h>
/* Ammount of UARTs in the Arduino Mega */
#define UART_COUNT 4
/* @brief Intializes the given UART
* @param uartN uart to intialize
* @param baud baud rate to use
* @param doubleSpeed non-zero = use double speed mode
* @param txbuffer transmit buffer
* @param txbufsize transmit buffer size
* @param rxbuffer receive buffer
* @param rxbufsize receive buffer size
*/
void uart_init(uint8_t uartN, uint32_t baud, uint8_t doubleSpeed, uint8_t* txbuffer, uint16_t txbufsize, uint8_t* rxbuffer, uint16_t rxbufsize);
/* @brief Read a value from the uart
* @param uartN uart to read from
* @param out pointer to store the value read */
uint8_t uart_read(uint8_t uartN, uint8_t* out);
/* @brief Send a string via uart
* @param uartN uart to use
* @param str string to send */
void uart_send(uint8_t uartN, const char* str);
/* @brief Send a character via uart
* @param uartN uart to use
* @param c Character to send */
void uart_sendc(uint8_t uartN, char c);
/* @brief Send bytes via uart
* @param uartN uart to use
* @param c Character to use */
void uart_sendn(uint8_t uartN, const uint8_t* data, uint16_t len);
#endif
On the C file we need to include our header, ‘avr/io’, ‘avr/interrupt’ and the ringbuffer. After the includes we will need to declare the buffer for every uart. Create for each one, a ‘txstat’ flag, which will tell us if the uart is currently sending anything and then the send and receive buffers.
Besides the flags and buffer, we will also create an array with a pointer to every one of the variables, that way, we can for example, use ‘uartN_txstat[0]’ if we want to change uart0_txstat.
#include "uart.h"
#include <avr/io.h>
#include <avr/interrupt.h>
#include "ringbuf.h"
// Buffers
// ========== ========== ========== ========== ========== ==========
volatile static uint8_t uart0_txstat = 0;
volatile static uint8_t uart1_txstat = 0;
volatile static uint8_t uart2_txstat = 0;
volatile static uint8_t uart3_txstat = 0;
volatile static uint8_t* uartN_txstat[] = { &uart0_txstat, &uart1_txstat, &uart2_txstat, &uart3_txstat };
volatile static ringbuf_t uart0_txbuf = {.data = 0, .head = 0, .tail = 0, .maxlen = 0};
volatile static ringbuf_t uart1_txbuf = {.data = 0, .head = 0, .tail = 0, .maxlen = 0};
volatile static ringbuf_t uart2_txbuf = {.data = 0, .head = 0, .tail = 0, .maxlen = 0};
volatile static ringbuf_t uart3_txbuf = {.data = 0, .head = 0, .tail = 0, .maxlen = 0};
volatile static ringbuf_t* uartN_txbuf[] = { &uart0_txbuf, &uart1_txbuf, &uart2_txbuf, &uart3_txbuf };
volatile static ringbuf_t uart0_rxbuf = {.data = 0, .head = 0, .tail = 0, .maxlen = 0};
volatile static ringbuf_t uart1_rxbuf = {.data = 0, .head = 0, .tail = 0, .maxlen = 0};
volatile static ringbuf_t uart2_rxbuf = {.data = 0, .head = 0, .tail = 0, .maxlen = 0};
volatile static ringbuf_t uart3_rxbuf = {.data = 0, .head = 0, .tail = 0, .maxlen = 0};
volatile static ringbuf_t* uartN_rxbuf[] = { &uart0_rxbuf, &uart1_rxbuf, &uart2_rxbuf, &uart3_rxbuf };
This first function returns the value to assign to UNRRN. You can get the values to use on the datasheet on page 226.
// Functions
// ========== ========== ========== ========== ========== ==========
uint16_t uart_ubrrn_from_baud(uint32_t baud, uint8_t u2xn)
{
if (u2xn)
{
switch (baud)
{
case 2400: return 832;
case 4800: return 416;
case 9600: return 207;
case 14400: return 138;
case 19200: return 103;
case 28800: return 68;
case 38400: return 51;
case 57600: return 34;
case 76800: return 25;
case 115200: return 16;
case 230400: return 8;
case 250000: return 7;
case 500000: return 3;
case 1000000: return 1;
default: return 16; // 115200
}
}
switch (baud)
{
case 2400: return 416;
case 4800: return 207;
case 9600: return 103;
case 14400: return 68;
case 19200: return 51;
case 28800: return 34;
case 38400: return 25;
case 57600: return 16;
case 76800: return 12;
case 115200: return 8;
case 230400: return 3;
case 250000: return 3;
case 500000: return 1;
case 1000000: return 0;
default: return 8; // 115200
}
}
The following functions do the same, but for different UARTs. They start by trying to read from the send buffer, if there is anything to send, set the send flag and write to ‘UDRn’, if the transmission is complete, reset the send flag.
At the end you will also find an array with the pointers to the functions, they will server the same purpose as the ones before, to help us make our code ‘cleaner’.
void uart0_i_sendc()
{
uint8_t c;
if (ringbuf_pop(&uart0_txbuf, &c))
{
uart0_txstat = 1;
UDR0 = c;
}
else uart0_txstat = 0;
}
void uart1_i_sendc()
{
uint8_t c;
if (ringbuf_pop(&uart1_txbuf, &c))
{
uart1_txstat = 1;
UDR1 = c;
}
else uart1_txstat = 0;
}
void uart2_i_sendc()
{
uint8_t c;
if (ringbuf_pop(&uart2_txbuf, &c))
{
uart2_txstat = 1;
UDR2 = c;
}
else uart2_txstat = 0;
}
void uart3_i_sendc()
{
uint8_t c;
if (ringbuf_pop(&uart3_txbuf, &c))
{
uart3_txstat = 1;
UDR3 = c;
}
else uart3_txstat = 0;
}
typedef void (*uartN_i_sendc_t)();
static const uartN_i_sendc_t uartN_i_sendc[] = { &uart0_i_sendc, &uart1_i_sendc, &uart2_i_sendc, &uart3_i_sendc };
Our initialization function is pretty long but it is the same thing repeated four times (one per uart), so let’s focus on case 0. We start by initializing the send and receive ring buffers by using the internal array passed to this function. Then we se the function we made before to set the UBRRn value, we activate the receiver and transmitter, their interrupt, set double speed mode and finally we set the data to 8 bit mode, 2 stop bits.
void uart_init(uint8_t uartN, uint32_t baud, uint8_t doubleSpeed, uint8_t* txbuffer, uint16_t txbufsize, uint8_t* rxbuffer, uint16_t rxbufsize)
{
if (uartN > UART_COUNT)
return;
switch (uartN)
{
case 0:
uart0_txbuf = ringbuf_init(txbuffer, txbufsize);
uart0_rxbuf = ringbuf_init(rxbuffer, rxbufsize);
// set baud rate
UBRR0 = uart_ubrrn_from_baud(baud, doubleSpeed);
// enable receiver and transmitter
UCSR0B |= _BV(RXEN0) | _BV(TXEN0);
// enable interrupts
UCSR0B |= _BV(RXCIE0) | _BV(TXCIE0);
// double speed
if (doubleSpeed)
UCSR0A |= _BV(U2X0);
// 8bit, 2bit stop
UCSR0C |= _BV(USBS0) | _BV(UCSZ02);
break;
case 1:
uart1_txbuf = ringbuf_init(txbuffer, txbufsize);
uart1_rxbuf = ringbuf_init(rxbuffer, rxbufsize);
// set baud rate
UBRR1 = uart_ubrrn_from_baud(baud, doubleSpeed);
// enable receiver and transmitter
UCSR1B |= _BV(RXEN1) | _BV(TXEN1);
// enable interrupts
UCSR1B |= _BV(RXCIE1) | _BV(TXCIE1);
// double speed
if (doubleSpeed)
UCSR1A |= _BV(U2X1);
// 8bit, 2bit stop
UCSR1C |= _BV(USBS1) | _BV(UCSZ12);
break;
case 2:
uart2_txbuf = ringbuf_init(txbuffer, txbufsize);
uart2_rxbuf = ringbuf_init(rxbuffer, rxbufsize);
// set baud rate
UBRR2 = uart_ubrrn_from_baud(baud, doubleSpeed);
// enable receiver and transmitter
UCSR2B |= _BV(RXEN2) | _BV(TXEN2);
// enable interrupts
UCSR2B |= _BV(RXCIE2) | _BV(TXCIE2);
// double speed
if (doubleSpeed)
UCSR2A |= _BV(U2X2);
// 8bit, 2bit stop
UCSR2C |= _BV(USBS2) | _BV(UCSZ22);
break;
case 3:
uart3_txbuf = ringbuf_init(txbuffer, txbufsize);
uart3_rxbuf = ringbuf_init(rxbuffer, rxbufsize);
// set baud rate
UBRR3 = uart_ubrrn_from_baud(baud, doubleSpeed);
// enable receiver and transmitter
UCSR3B |= _BV(RXEN3) | _BV(TXEN3);
// enable interrupts
UCSR3B |= _BV(RXCIE3) | _BV(TXCIE3);
// double speed
if (doubleSpeed)
UCSR3A |= _BV(U2X3);
// 8bit, 2bit stop
UCSR3C |= _BV(USBS3) | _BV(UCSZ32);
break;
default:
break;
}
}
Our UART read function is basically just a wrapper to ‘ringbuf_pop’, but we also check if uartN is a valid uart interface before passing the correct receive buffer to the pop function.
uint8_t uart_read(uint8_t uartN, uint8_t* out)
{
if (uartN > UART_COUNT)
return 0;
return ringbuf_pop(uartN_rxbuf[uartN], out);
}
Our send functions will all be basically the same, push into the send buffer, and then if the send flag is not sent, call the internal send function:
void uart_send(uint8_t uartN, const char* str)
{
if (uartN > UART_COUNT)
return;
while (*str != '\0')
ringbuf_push(uartN_txbuf[uartN], *str++);
if (!*uartN_txstat[uartN])
uartN_i_sendc[uartN]();
}
void uart_sendc(uint8_t uartN, char c)
{
if (uartN > UART_COUNT)
return;
ringbuf_push(uartN_txbuf[uartN], c);
if (!*uartN_txstat[uartN])
uartN_i_sendc[uartN]();
}
void uart_sendn(uint8_t uartN, const uint8_t* data, uint16_t len)
{
for (uint8_t i = 0; i < len; i++)
ringbuf_push(uartN_txbuf[uartN], data[i]);
if (!*uartN_txstat[uartN])
uartN_i_sendc[uartN]();
}
Finally, the most important step in the uart files is to create the ISRs, these are functions that will be called when their specific interrupt is triggered:
// ISR
// ========== ========== ========== ========== ========== ==========
ISR(USART0_TX_vect) { uart0_i_sendc(); }
ISR(USART1_TX_vect) { uart1_i_sendc(); }
ISR(USART2_TX_vect) { uart2_i_sendc(); }
ISR(USART3_TX_vect) { uart3_i_sendc(); }
ISR(USART0_RX_vect) { ringbuf_push(&uart0_rxbuf, UDR0); }
ISR(USART1_RX_vect) { ringbuf_push(&uart1_rxbuf, UDR1); }
ISR(USART2_RX_vect) { ringbuf_push(&uart2_rxbuf, UDR2); }
ISR(USART3_RX_vect) { ringbuf_push(&uart3_rxbuf, UDR3); }
Now that we have the uart files complete, we can go to main, create both send and receive buffer for all uarts, initalize them, enable global interrupts and then send a message on each one of them.
Also, don’t forget the infinte loop at the end, as the code will not work if you reach the end of main.
#include <avr/io.h>
#include <avr/interrupt.h>
#include "uart.h"
int main()
{
const uint8_t bufsize = 64;
uint8_t uart0_tx[bufsize];
uint8_t uart0_rx[bufsize];
uint8_t uart1_tx[bufsize];
uint8_t uart1_rx[bufsize];
uint8_t uart2_tx[bufsize];
uint8_t uart2_rx[bufsize];
uint8_t uart3_tx[bufsize];
uint8_t uart3_rx[bufsize];
uart_init(0, 115200, 1, uart0_tx, bufsize, uart0_rx, bufsize);
uart_init(1, 115200, 1, uart1_tx, bufsize, uart1_rx, bufsize);
uart_init(2, 115200, 1, uart2_tx, bufsize, uart2_rx, bufsize);
uart_init(3, 115200, 1, uart3_tx, bufsize, uart3_rx, bufsize);
sei(); // enable global interrupts
uart_send(0, "Hello from uart0\n");
uart_send(1, "Hello from uart1\n");
uart_send(2, "Hello from uart2\n");
uart_send(3, "Hello from uart3\n");
while (1);
}
Here is also an example of sending a message on one and reading it in the other uart interface, keep in mind that this example does not work if you are using the uart to usb converter with only one Arduino, you need two:
#include <avr/io.h>
#include <avr/interrupt.h>
#include "uart.h"
int main()
{
const uint8_t bufsize = 64;
uint8_t uart0_tx[bufsize];
uint8_t uart0_rx[bufsize];
uint8_t uart1_tx[bufsize];
uint8_t uart1_rx[bufsize];
uart_init(0, 115200, 1, uart0_tx, bufsize, uart0_rx, bufsize);
uart_init(1, 115200, 1, uart1_tx, bufsize, uart1_rx, bufsize);
sei(); // enable global interrupts
uart_send(0, "Hello from uart0\n");
uart_send(1, "Hello from uart1\n");
while (1)
{
uint8_t val;
if (uart_read(0, &val))
uart_sendc(1, val);
if (uart_read(1, &val))
uart_sendc(0, val);
}
}
You can then upload your code using this Makefile, just be sure to switch the port name in it to the one of your Arduino:
SPORT=/dev/ttyACM0
BAUD=115200
F_CPU=16000000UL
MMCU=atmega2560
PROGRAMMER_ID=wiring
# ====================================================================================================
# directories
DIR_SRC=src
DIR_OUT=out
OUT_NAME=program
# ====================================================================================================
# files
ELF=$(DIR_OUT)/$(OUT_NAME).elf
HEX=$(DIR_OUT)/$(OUT_NAME).hex
EXE=$(DIR_OUT)/$(OUT_NAME)
# find all c files in src dir (ignore test.c file)
SOURCES := $(shell find $(DIR_SRC) -name '*.c')
OBJECTS := $(patsubst $(DIR_SRC)/%.c, $(DIR_OUT)/%.o, $(SOURCES))
# comment line above and uncomment the one below to specify which c files to use
#SOURCES = $(DIR_SRC)/writeCfilesHere.c
# ====================================================================================================
# compilers
CC=avr-gcc
OCP=avr-objcopy
# upload to arduino cmd
UPLOADCMD = sudo avrdude -V -c $(PROGRAMMER_ID) -p $(MMCU) -P $(SPORT) -b $(BAUD) -D -U flash:w:$(HEX):i
# ====================================================================================================
# compiler flags
CC_FLAGS= -Os -Wall -DF_CPU=$(F_CPU)
# linker flags
LD_FLAGS=
# ====================================================================================================
# compile files
$(DIR_OUT)/%.o: $(DIR_SRC)/%.c
mkdir -p $(dir $@)
$(CC) $(LD_FLAGS) $(CC_FLAGS) -mmcu=$(MMCU) -c -o $@ $<
$(ELF): $(OBJECTS)
$(CC) $(LD_FLAGS) $(CC_FLAGS) -mmcu=$(MMCU) -o $@ $(SOURCES)
$(HEX): $(ELF)
$(OCP) $(ELF) -O ihex $(HEX)
# ====================================================================================================
# 'functions'
.PHONY: all, upload, flash, clean
dir:
mkdir -p $(DIR_OUT)
# compile
all: $(HEX) # compile hex file
# uploads the hex file to the arduino
flash:
$(UPLOADCMD)
# builds and uploads the hex file to the arduino
upload: all
$(UPLOADCMD)
# rm build files
clean:
rm $(HEX) $(ELF)
And that’s it, if you now open two Serial Monitors (one for each Arduino) you can send messages between them!

Thanks for reading and stay tuned for more tech insights and tutorials. Until next time, and keep exploring the world of tech!