Sunday, August 7, 2022

UART library for AVR microcontrollers using interrupts

This is a UART library that is made for AVR microcontrollers that can be used for serial communications.

UART is a type of serial interface, as opposed to a parallel interface. A parallel interface can work at higher speeds but the disadvantage is that it needs multiple input/output lines. Other examples of serial interfaces are SPI and I2C.

UART library for AVR microcontrollers using interrupts

Features

  • Custom Baud rate
  • Asynchronous or synchronous modes
  • Supports serial frames with 5, 6, 7, 8, or 9 data bits
  • Odd, even or no parity
  • Error detection
  • Multi-processor communication mode used to address multiple devices on the same serial bus
  • Double speed asynchronous communication mode

Contents

 

USART Characteristics

USART can be set to work in the following modes: Normal asynchronous, Double Speed asynchronous, Master synchronous, Slave synchronous mode and Multi-Processor Communication Mode.

Asynchronous and synchronous serial

Asynchronous means that data is transferred without support from an external clock signal since the clock is generated internally and is synchronized to the incoming serial frames.

Synchronous mode requires an extra clock line but has the advantage that it can work at higher speed rates that asynchronous mode.

In Master synchronous the main device generates and outputs the clock on XCKn pin while in Slave synchronous mode the device uses the clock generated by the Master.

Multi-Processor Communication Mode

The Multi-Processor Communication mode enables several slave MCUs to receive data from a master MCU. This is done by first decoding an address frame to find out which MCU has been addressed. If a particular slave MCU has been addressed, it will receive the following data frames as normal, while the other slave MCUs will ignore the received frames until another address frame is received.

Frame formats

A serial frame is composed of a character of data bits with synchronization bits (start and stop bits), and optionally a parity bit for error checking.

A frame starts with the start bit, followed by the data bits (from 5 up to 9 data bits in total): first the least significant data bit, then the next data bits ending with the most significant bit. If enabled, the parity bit is inserted after the data bits, before the one or two stop bits. When a complete frame is transmitted, it can be directly followed by a new frame, or the communication line can be set to an idle (high) state. The figure below illustrates the possible combinations of the frame formats. Bits inside brackets are optional.

AVR UART frame formats

IDLE: No transfers on the communication line (Rx or Tx). An IDLE line must be high.

St: Start bit, always low.

(n): Data bits (0 to 8).

P: Parity bit. Can be odd or even.

Sp: Stop bit, always high.

The Stop bit can be followed by a Start bit (low) for another frame, or Idle (high). All devices connected to the UART bus must use the same frame format and baud rate.

Baud Rate

The baud rate specifies how fast data is sent over a serial line. It's usually expressed in units of bits-per-second (bps). Most commonly used baud rates are 9600 and 115200 bps.

Wiring

Some AVRs have two UARTs with pins named RXDn (Receiver) and TXDn (Transmitter). n represents the UART number - 0 or 1.

Lets say you want to interface UART 0 with a device that has this pins: RX and TX. Then you connect RXD0 to TX and TXD0 to RX. Also they must share a common ground.

In Synchronous mode you also need the XCKn pin for clock.

For ATmega328PB the TX/RX pins are as follows:

TXD0 - PD1, RXD0 - PD0, XCK0 - PD4 for UART 0

TXD1 - PB3, RXD1 - PB4, XCK1 - PB5 for UART 1

For other types of microcontrollers check the Pin Configurations chapter of the datasheet.

Library Structure

When a microcontroller has two USART peripherals, both of them can be used with a single library, by using the appropriate structure object as a function argument. There are two structure objects defined, named: uart0 and uart1.

The file uart.h defines some user settings that can be modified accordingly.

TX and RX buffers

Transmitted and Received data are buffered in two circular arrays that by default have a size of 32 bytes each. The size can be from 1 to 255 bytes except when using 9-bit mode - in this case the buffer size must be 32 bytes. Having a larger array can be a bit faster. When using higher baud rates it is recommended a bigger buffer to avoid loosing incoming data and to give more time for the microcontroller to process the data.

#define UART_TX_BUFFER_SIZE	32
#define UART_RX_BUFFER_SIZE	32

CPU clock frequency - F_CPU

 
#define F_CPU    16000000UL

F_CPU defines the processor clock frequency in Hertz and is used to calculate the baud rate. 

F_CPU is best to be defined inside project properties in Microchip Studio (or your particular IDE) or a Makefile if custom Makefiles are used. See https://www.programming-electronics-diy.xyz/2024/01/defining-fcpu.html.

9-bit USART mode

 
#define UART_USE_9BITS    0

When using UART in 9 bit mode, the UART_USE_9BITS must be set to 1. For all other character sizes set this to 0.

Exclude UART1 


#define UART_USE_UART1    1

Set this to 0 if the device has UART1 but is not used, to reduce code size.


Using UART library

TX pin

The transmission pin must be set as an output by the user, for the UART to work.

Initialization function

Sets the baud rate, frame format, enables UART interrupts and global interrupts. The function can be used at any point to change baud rate or frame parameters. It can also be used in case of any communications issues.

void UART_begin(UARTstruct_t* uart, float baudrate, uint8_t mode, uint8_t parity, uint8_t bits)

Parameters


UARTstruct_t* uart

Pointer to an uart structure object. E.g.: &uart0, &uart1.

uint32_t baudrate

Bits per second. E.g: 9600, 115200.

uint8_t mode

UART mode: Asynchronous, Synchronous Master/Slave mode. When using synchronous mode, the Data Direction Register for the XCKn pin controls whether the clock source is internal (Master mode) or external (Slave mode). The XCKn pin is only active when using synchronous mode.

Available constants:

UART_ASYNC -  Asynchronous Normal mode. Asynchronous Double Speed mode will be selected automatically when baud rate is above 57600.

UART_SYNC - Synchronous Master/Slave mode.

uint8_t parity

Even or odd parity can be selected for error checking. Available constants:

UART_NO_PARITY, UART_EVEN_PARITY, UART_ODD_PARITY

uint8_t bits

Number of bits in the frame, 5, 6, 7, 8 or 9. Most common format is having 8 bits but if you have small numbers and speed is important, using 5, 6 or 7 bits can provide a bit more speed. The maximum number that can be transmitted is 2^n where n is the number of bits. 9 bits is usually only used in Multi-processor Communication mode where the 9'th bit indicates an address or data frame.

UART_5_BIT, UART_6_BIT, UART_7_BIT, UART_8_BIT, UART_9_BIT.

Usage:

UART_begin(&uart0, 115200, UART_ASYNC, UART_NO_PARITY, UART_8_BIT);
 

Send a byte

Puts a byte of data in the transmit buffer and enables the Data Register Empty Interrupt.

uint8_t UART_send(UARTstruct_t* uart, char_size_t data)

Parameters

 
data

The data byte. By default data type is 8-bit. When UART_USE_9BITS is 1, the data type is 16-bit to accommodate for the 9th bit.

Return: 0 on success, 1 on failure.

Usage:

// Send 'A' ASCII character
UART_send(&uart0, 'A');

// Send '3' ASCII symbol
UART_send(&uart0, '3');

// Send number of the '3' ASCII symbol
UART_send(&uart0, 51);

Send a string

Send a null terminated string.

uint8_t UART_sendString(UARTstruct_t* uart, char* s)

Parameters

 
char* s

A string of characters.

Return: 0 on success, 1 on failure.

Usage:

UART_sendString(&uart0, "Sent from UART0");
// Or using the '\n' character to start a new line in a serial terminal.
UART_sendString(&uart0, "Sent from UART0\n");

Send bytes

Send a series of bytes in a buffer.

uint8_t UART_sendBytes(UARTstruct_t* uart, char_size_t* buff, uint8_t length)

Parameters


uint8_t* buff

An array of bytes.

uint8_t length

Length of the array.

Return: 0 on success, 1 on failure.

Usage:

const uint8_t bufferSize = 20;
uint8_t buff[bufferSize];

// bufferSize argument can be smaller to
// send only first few bytes
UART_sendBytes(&uart0, buff, bufferSize);

Send integer number

Convert an integer number into a string array and send it over UART.

void UART_sendInt(UARTstruct_t* uart, INT_SIZE number)

Parameters


INT_SIZE number

INT_SIZE is defined in the "utils.h" file, and can be int32_t or int64_t


Send float number

Convert a float number into a string array and send it over UART.

void UART_sendFloat(UARTstruct_t* uart, float number, uint8_t decimals)

Parameters


float number

A float number.

uint8_t decimals

Number of digits after the dot.


Send hex numbers

Convert a 1- 2- or 4-byte integer number into hexadecimal value and send it over UART.

void UART_sendHex8(UARTstruct_t* uart, uint8_t value)
void UART_sendHex16(UARTstruct_t* uart, uint16_t value)
void UART_sendHex32(UARTstruct_t* uart, uint32_t value)


Check for new received data

Returns true if new data is available and false otherwise.

bool UART_available(UARTstruct_t* uart)


Wait for transmission complete

Waits in a while loop until all bytes in the buffer have been transmitted. Can be used before putting the microcontroller to sleep to ensure all data has been transmitted. Baud rate and F_CPU are used to calculate the time it takes for the last byte to be transmitted after the interrupt is disabled but the last byte is still transmitted.

void UART_isSending(UARTstruct_t* uart, float fcpu, float baudrate)
 

Parameters

fcpu

CPU clock frequency in Hertz.

baudrate

UART baudrate used in initialization function.


Read a byte

Returns the next received byte or 0 if no new data is available. Should be used only if UART_available() returns true.

char_size_t UART_read(UARTstruct_t* uart)


Read bytes

Read received bytes into the provided buffer. The function terminates if the specified length has been read, or it times out (around 0.5s).

uint8_t UART_readBytes(UARTstruct_t* uart, char_size_t* buff, uint8_t length)

Parameters


uint8_t* buff

An array buffer where to put incoming data.

uint8_t length

Length of the array.

Return: the number of characters read.

Usage:

const uint8_t bufferSizeRX = 20;
uint8_t buff[bufferSizeRX];

if(UART_available(&uart0)){
    // Read serial data
    UART_readBytes(&uart0, buff, bufferSizeRX);
    // Now 'buff' contains received data
}

Read bytes until

Read received bytes into the provided buffer. The function terminates if the specified length has been read, the termination character has been found or it times out (around 0.5s). The termination character is not included in the buffer.

uint8_t UART_readBytesUntil(UARTstruct_t* uart, char character, char_size_t* buff, uint8_t length)

Parameters


char character

The termination character. When this character is encountered, the function terminates.

uint8_t* buff

An array buffer where to put incoming data.

uint8_t length

Length of the array.

Return: the number of characters read.


Disable UART

Disable UART transmitter, receiver and interrupts. According to the datasheet:

"When the transmitter is disabled, it will no longer override the TxDn pin, and the pin direction is set as input automatically by hardware, even if it was configured as output by the user."

void UART_end(UARTstruct_t* uart)

Flush

If the UART buffer has to be flushed during normal operation, due to for instance an error condition, this reads the UDRn I/O location until the RXCn Flag is cleared.

void UART_flush(UARTstruct_t* uart)

Redirect received data

Redirects received data to a user defined function.

void UART_setRXhandler(UARTstruct_t* uart, void (*rx_func)(uint8_t c))

Usage:

void rxHandler(uint8_t c){
	// This function is called inside the RX interrupt
	// and it should not take long time.
	// 'c' is the received byte.
	
}

UART_setRXhandler(&uart0, &rxHandler);

Enable Start Frame Detector

The Start Frame detector is able to wake-up the system from Idle or Standby Sleep modes when a high (IDLE) to low (START) transition is detected on the RxD line. 

Not all microcontrollers have this feature. If the micro is programmable via UPDI then most likely supports this function.

void UART_enableStartFrameDetection(UARTstruct_t* uart)

Enable One Wire Mode

In this mode only the TX pin is needed since the TX pin is connected to the RX pin internally. Also open-drain mode is enabled. If the receiver is enabled when transmitting it will receive what the transmitter is sending. This can be used to check that no one else is trying to transmit since received data will not be the same as the transmitted data. For more information about USART in one wire mode, see the application note from Microchip linked in the download section.

Internal pull-up resistor for TX pin needs to be enabled by the user since the pin number will vary depending of the microcontroller.

PORTA.PIN6CTRL |= PORT_PULLUPEN_bm; // pin 6 on port A

Not all microcontrollers have this feature. If the micro is programmable via UPDI then most likely supports this function.

void UART_enableOneWireMode(UARTstruct_t* uart)

The tricky part is to discard data from own transmission. A proper implementation depends on the application, however I provide this code example as an idea.

char_size_t buff[10] = {0};
bool sent_bytes_discarded = true;
	
// Enable pin internal pull-up resistor
PORTA.PIN6CTRL |= PORT_PULLUPEN_bm;
	
// Enable 1-wire mode
UART_enableOneWireMode(&uart0);
	
// Send 10 characters of data
UART_sendString(&uart0, "Send data\n");
sent_bytes_discarded = false; // set flag
	
// Wait to receive data from own transmission
if(UART_available(&uart0)){
    // Read and discard 10 bytes transmitted previously
    UART_readBytes(&uart0, buff, 10);
    sent_bytes_discarded = true; // clear flag
 
// Read data from other devices only after the bytes 
// from own transmission have been discarded
if(UART_available(&uart0) && sent_bytes_discarded == true){
    UART_readBytes(&uart0, buff, 5); // number of bytes to read is just an example
}

Receiver error flags

After each byte is received, the error bits in the USART register are saved in a variable. The following functions can be used to check if an error occurred and the type of error. At the end, the error flag must be cleared.

if(UART_isError(&uart0)){
	if(UART_frameError(&uart0)){
		UART_sendString(&uart0, "Frame error");	
	}
			
	if(UART_bufferOverflowError(&uart0)){
		UART_sendString(&uart0, "Buffer overflow");
	}
			
	if(UART_parityError(&uart0)){
		UART_sendString(&uart0, "Parity error");
	}
			
	UART_clearErrorFlags(&uart0);
}

Frame Error

The Frame Error Flag indicates the state of the first stop bit of the next readable frame stored in the receive buffer. The FE Flag is zero when the stop bit was correctly read as '1', and the FE Flag will be one when the stop bit was incorrect (zero). This flag can be used for detecting out-of-sync conditions, detecting break conditions and protocol handling.

Parity Error

The Parity Error Flag indicates that the next frame in the receive buffer had a Parity Error when received.

Buffer Overflow

This flag is set if a Buffer Overflow condition is detected. A Buffer Overflow occurs when the receive buffer is full (two characters), it is a new character waiting in the Receive Shift register, and a new Start bit is detected.


Enable the Multi-processor Communication mode (MPCM)

After this function sets the MPCM bit, frames that do not contain an address will be ignored by the UART. Frames containing an address have the 9'th bit set to 1. The Transmitter is unaffected by the MPCM setting.

void UART_mpcmEnable(UARTstruct_t* uart)

Disable the Multi-processor Communication mode (MPCM)

Disables the Multi-processor Communication mode (MPCM). After this function sets the MPCM bit to 0, data frames can be received. This should be used after a valid address has been received after using UART_mpcmEnable().

void UART_mpcmDisable(UARTstruct_t* uart)


Send UART address

Select a device by sending an address frame over UART.

void UART_mpcmSelectDevice(UARTstruct_t* uart, uint8_t address)

Parameters


uint8_t address

Address of the device to select.

Usage:

// In MPCM mode the character size should be selected as 9-bits
UART_begin(&uart0, 115200, UART_ASYNC, UART_NO_PARITY, UART_9_BIT);
	
/* Subscriber (MCU) with address 0x01 */
// Enable 'waiting for address mode'.
// Incoming data that doesn't have the 9th bit set to 1 
// will be ignored by the USART peripheral.
// Data cannot be received, only address data.
uint8_t sub1_addr = 0x01; 
UART_mpcmEnable(&uart0);
	
/* Subscriber (another MCU) with address 0x02 */
uint8_t sub2_addr = 0x02; 
UART_mpcmEnable(&uart0);
	
/* Subscriber (yet another MCU) with address 0x03 */
// Subscribers can share the same address
// if they are interested in the same content.
uint8_t sub3_addr = 0x03; 
UART_mpcmEnable(&uart0);
	
/* Influencer (MCU) */
// Influencer decides to interact with subscriber
// with address 0x02 because they give support on Patron.
// The function sends 0x02 as usual, except this time
// the 9th bit is set to 1 to indicate that the data contains an address.
UART_mpcmSelectDevice(&uart0, 0x02);
	
// Subscribers receive the address in a while loop
// and check if they are subscribed to the topic.
// Example of subscriber 2
char_size_t data = 0;
	
if(UART_available(&uart0)){
    data = UART_read(&uart0);
		
    // Variable 'data' contains the address and also the 9th bit, like so 0x0102.
    // 0x02 is the address and 0x01 is the 9th bit.
    // This is because the 9-bit mode can be used without MPCM mode by this library.
    // The subscriber address is ORed with 0x0100 to set the 9th bit.
    // This is not necessary when the address is stored with the 9th bit set
    // but then the data type should be uint16_t instead of uint8_t.
    if(data == (sub2_addr | 0x0100)){
        // Received address matches device address.
	// Disable MPCM to be able to receive data.
	UART_mpcmDisable(&uart0);
    }
		
    // A variable could be used based on which the application
    // could decide when to enable or disable MPCM. For example
    // bool enable MPCM = true or false after n bytes have been received.
    // To check if data is address: (data & 0x0100) checks if 9th bit is 1.
}

Code example

// #define F_CPU	16000000 // defined inside uart.h
#include "uart.h"
#include <util/delay.h>
 
int main(void){
    const uint8_t bufferSize = 20;
    char_size_t buff[bufferSize];
    uint8_t bytes_read = 0;
	
    UART_begin(&uart0, 115200, UART_ASYNC, UART_NO_PARITY, UART_8_BIT);
    UART_begin(&uart1, 115200, UART_ASYNC, UART_NO_PARITY, UART_8_BIT);
    UART_sendString(&uart1, "I'm UART 1\r");
	
    UART_sendString(&uart0, "Hi there!\r");
    _delay_ms(2000);
    UART_sendString(&uart0, "Is this thing working?\r");

    while(1){
        if(UART_available(&uart0)){
	    // Read serial data in the 'buff' array
	    bytes_read = UART_readBytes(&uart0, buff, bufferSize);
            UART_sendString(&uart0, "Received "); 
            UART_sendInt(&uart0, bytes_read);
	    UART_sendString(&uart0, " bytes\r");
			
	    if(bytes_read == bufferSize){
	        // Print received data to a serial terminal
		UART_sendBytes(&uart0, buff, bytes_read);
		UART_send(&uart0, '\r');
	    }
			
	    // Check for reception errors
	    if(UART_isError(&uart0)){
	        if(UART_frameError(&uart0)){
		    UART_sendString(&uart0, "Frame error");
		}
				
		if(UART_bufferOverflowError(&uart0)){
		    UART_sendString(&uart0, "Buffer overflow");
		}
				
		if(UART_parityError(&uart0)){
		    UART_sendString(&uart0, "Parity error");
		}
				
		UART_clearErrorFlags(&uart0);
	    } // end if UART_isError()
	} // end if UART_available()
    }
}

Download

Changelog and license can be found at the beginning of the header files
uart v2.2

uart.h

uart.c
utils v1.0

utils.h Used by sendInt() and sendFloat()

utils.c
External Links

Termite terminal Useful serial terminal to use with UART

Microchip - AN2658 - USART in One-Wire Mode Microchip - USART in One-Wire Mode in PDF format

Microchip - AN2451 - Getting Started with Core Independent Peripherals on AVR On modern AVR (UPDI devices) USART can be used as an Event Generator for the Event System where the received data can be filtered without the use of the CPU. This PDF file from Microchip describes how to use the Event System, LUTs, CCL and Sequential Logic.
Changelog
v2.2 31-01-2024:
- included a function used to wait in a while loop until all bytes in the buffer have been transmitted.
v2.1 - UART1 can now be excluded by a preprocessor directive to save space.
v2.0 - added support for modern AVR (UPDI devices). Tested on tinyAVR402.
- all USART peripherals can now be controlled by a single library by the use of structures.
- removed functions dedicated to 9-bit UART. Now 9-bit is supported by interrupts.

No comments:

Post a Comment