2024-04-25

In section 19 for the ATMega328p datasheet, you can find detailed information about the USART functionality available in the microcontroller. It's recommended to at least scan through that section to see what is available.

The following will break down and distil the information necessary to set up a USART communication with baud rate of 9,600 and 8bit-long messages.

Initialising

In section 19.5, there are instructions for initialising USART, along with C and Assembly code snippets. The C code is pasted below as is:

#define FOSC 1843200 // Clock Speed
#define BAUD 9600
// See table 19.1 on the datasheet for this equation.
#define MYUBRR FOSC/16/BAUD-1
void main(void)
{
    ...
    USART_Init(MYUBRR)
    ...
}
void USART_Init(unsigned int ubrr)
{
    /*Set baud rate */
    UBRR0H = (unsigned char)(ubrr>>8);
    UBRR0L = (unsigned char)ubrr;
    /* Enable receiver and transmitter */
    UCSR0B = (1<<RXEN0)|(1<<TXEN0);
    /* Set frame format: 8data, 2stop bit */
    UCSR0C = (1<<USBS0)|(3<<UCSZ00);
}

For more information on where that equation came from, visit the table 19.1 on the ATMega328p datasheet.

Calculating the UBRR value for ATMega328p

Thus we have:

#define BAUD 9600
// F_CPU is passed in during compilation -DF_CPU=16000000UL
#define UBRR_VALUE = (F_CPU/(16UL * BAUD)) - 1

The UBRR_VALUE is split between registers UBRR0H and UBRR0L, and they can be set with like this:

// Our ubrr in decimal is 103, which in binary is 00000000 01100111.
// In this case the high bits will be 00000000.
// The low bits will be 01100111.
UBRR0H = (unsigned char)(ubrr >> 8);
UBRR0L = (unsigned char)(ubrr);

In our case, the ATMega328p will only be transmitting, and not receiving, so we'll set up the UCSR0B with TXEN0:

UCSR0B = 1 << TXEN0;

And we will be transmitting 8bit messages, so we'll set up UCSR0C with UCSZ00:

UCSR0C = 3 << UCSZ00;

The whole code for this bit is:

void usart_init(uint16_t ubrr) {
  /* Initialises USART communication.
   *
   * - Use Asynchronous normal mode.
   * - Enable transmitter only.
   * - Use 8-bit characters for communication.
   *
   * More details are in section 19 of the ATMega328p datasheet.
   *
   */
  UBRR0H = (unsigned char)(ubrr >> 8);
  UBRR0L = (unsigned char)(ubrr);
  UCSR0B = 1 << TXEN0;
  UCSR0C = 3 << UCSZ00;
}

Sending data to the terminal

From section 19.6.1 in the datasheet:

A data transmission is initiated by loading the transmit buffer with the data to be transmitted. The CPU can load the transmit buffer by writing to the UDRn I/O location. The buffered data in the transmit buffer will be moved to the shift register when the shift register is ready to send a new frame. The shift register is loaded with new data if it is in idle state (no ongoing transmission) or immediately after the last stop bit of the previous frame is transmitted. When the shift register is loaded with new data, it will transfer one complete frame at the rate given by the baud register, U2Xn bit or by XCKn depending on mode of operation.

The following C code example is provided:

void USART_Transmit(unsigned char data)
{
    /* Wait for empty transmit buffer */
    while (!(UCSRnA & (1<<UDREn)));
    /* Put data into buffer, sends the data */
    UDRn = data;
}

We will use that same function, just changing n to 0:

void usart_transmit(unsigned char data) {
    /* Transmit 8bit data via USART
     *
     * Waits until UCSR0A is ready to receive data,
     * and then pushes to register UDR0.
     *
     * More details are in section 19.6.1 of the ATmega328p datasheet.
     */
    while (!(UCSR0A & (1<<UDRE0)));
    UDR0 = data;
}

Setting up the data stream

We want the USART message to be print to the linux terminal of the machine we are using the ATMega328p with.

We need to tell our program, that the output of stdio functions must be redirected to a stream of our choice. This stream will be the USART transmitter function we built in the previous step (usart_transmit)

This can be achieved as follows:

static FILE uartout = FDEV_SETUP_STREAM(usart_transmit, NULL, _FDEV_SETUP_WRITE);

However, we need to change the usart_transmit function signature to suit the protocol.

- void usart_transmit(unsigned char data) {
+ int usart_transmit(char data, struct __file* stream) {
   /* Transmit 8bit data via USART.
    *
    * Waits until UCSR0A is ready to receive data,
    * and then pushes to register UDR0.
    *
    * More details are in section 19.6.1 of the ATmega328p datasheet.
    */
   while (!(UCSR0A & (1<<UDRE0)));
   UDR0 = data;
+  return 0;
}

The code

The following script can be compiled using the script in my other note:

#include <avr/io.h>
#include <stdint.h>
#include <stdio.h>
#include <util/delay.h>

#define BAUD 9600
#define UBRR_VALUE ((F_CPU / (16UL * BAUD)) - 1)

int usart_transmit(char data, struct __file *stream) {
  /* Transmit 8bit data via USART.
   *
   * Waits until UCSR0A is ready to receive data,
   * and then pushes to register UDR0.
   *
   * More details are in section 19.6.1 of the ATmega328p datasheet.
   */
  while (!(UCSR0A & (1 << UDRE0)))
    ;
  UDR0 = data;
  return 0;
}
static FILE uartout =
    FDEV_SETUP_STREAM(usart_transmit, NULL, _FDEV_SETUP_WRITE);

void usart_init(uint16_t ubrr) {
  /* Initialises USART communication.
   *
   * - Use Asynchronous normal mode.
   * - Enable transmitter only.
   * - Use 8-bit characters for communication.
   *
   * More details are in section 19 of the ATMega328p datasheet.
   *
   */
  UBRR0H = (unsigned char)(ubrr >> 8);
  UBRR0L = (unsigned char)(ubrr);
  UCSR0B = 1 << TXEN0;
  UCSR0C = 3 << UCSZ00;
}

int main(void) {
  usart_init(UBRR_VALUE);
  stdout = &uartout;
  int times = 0;
  while(1) {
    printf("Hello folks! %d\r\n", times);
    _delay_ms(2000);
    times += 1;
  }
  return 0;
}

Checking the output

You will have to use picocom to verify what has been pushed to your terminal via USART:

picocom -b9600 /dev/ttyUSB0

Where /dev/ttyUSB0 is the port your device is connected to, which can be found via:

ls /dev/tty*

Useful resources