LABORATORY 06

Serial Communication

Duration: 2 hours Material: Chapters 8-9 Registers: UBRR0 · UCSR0A/B/C · UDR0 PDF handout RO versiunea română

Serial communication lets you exchange data between the microcontroller and a computer, smart sensors, or other boards. It is the most used interface in practice - and the only one that lets you see what happens inside a screenless system.

1Lab objectives

  • Understanding the asynchronous frame structure: start, data, parity, stop
  • Computing the UBRR register and the baud rate error
  • Configuring the USART module through registers
  • Comparing the TTL, RS-232, and RS-485 standards
  • Receiving and interpreting commands sent from the computer
  • Implementing interrupt-driven reception

2Materials needed

  • 2 Arduino Uno boards
  • 1 Breadboard
  • 1 LED + 220 Ω resistor
  • 4 Jumper wires
  • 1 MAX485 module (optional)

3Synchronous vs. asynchronous

In synchronous communication (SPI, I2C), a separate wire carries the clock signal that marks when each bit should be read. In asynchronous communication (UART) there is no clock wire: both devices must be configured at the same speed, and synchronization is re-established at every frame, starting from the edge of the start bit.

Why it works without a shared clockThe receiver samples each bit at the middle of its interval, using an internal clock 16 times faster than the baud rate. Synchronization drifts gradually, but a frame is only 10 bits long - it does not have time to become a problem, as long as the speed error stays under 2%.

4Frame structure

ElementLengthValueRole
Idle line-1 (HIGH)indicates no transmission
Start bit1 bitalways 0the falling edge synchronizes the receiver
Data bits5-9 (usually 8)useful datasent LSB first
Parity bit0 or 1 biteven / oddsimple error detection
Stop bit(s)1 or 2 bitsalways 1marks the end and separates frames
Transmission efficiencyFor an 8-bit character with no parity, the complete frame is 10 bits. Only 80% of the traffic is useful data - the rest is synchronization overhead. At 9600 baud, this gives a maximum of 960 characters per second.

5UART frame builder

Type a character and watch the frame being built on the wire: the start bit, the eight data bits sent LSB first, the optional parity, and the stop bit. Change the speed and observe how the duration of each bit and the baud error change.

UART frame - waveform and timing
Try thisSelect 115200 baud and observe the -3.55% error. This is why, at high speeds, communication on an Arduino with a 16 MHz crystal becomes unreliable - especially if the other end has an error of the opposite sign.

6Computing the transmission speed

The UBRR formula
UBRR = F_CPU / (16 · baud) − 1
The result is rounded to an integer, which introduces an error. Communication works reliably if the total error, from both ends, stays under 2%.
Desired baudUBRR (16 MHz)Actual baudErrorVerdict
2,4004162,398-0.08%excellent
9,6001039,615+0.16%excellent
19,2005119,231+0.16%excellent
38,4002538,462+0.16%good
57,6001658,824+2.12%⚠ borderline
115,2008111,111-3.55%⚠ unreliable
Why 16 MHz is not ideal for high speedsBoards with a 14.7456 MHz crystal produce zero error for all standard speeds, being an exact multiple of the baud frequencies. Alternatively, the U2X0 bit in UCSR0A halves the divisor and reduces the error at 115200 baud from -3.55% to +2.12%.

7The USART registers

The default configuration below is the standard "8N1": 8 data bits, no parity, one stop bit, reception and transmission enabled.

UCSR0B and UCSR0C - configuring the serial module
The UCSR0A register
Fig. 1 - The UCSR0A status register: RXC0 signals a byte has arrived, UDRE0 that a transmission can occur, and FE0/DOR0/UPE0 report frame, overrun, and parity errors. Fig. 6.2 in the handout
The UCSR0B register
Fig. 2 - The UCSR0B control register: enables the interrupts (RXCIE0, TXCIE0), the receiver, and the transmitter (RXEN0, TXEN0). Note that RXB80 is marked read-only. Fig. 6.3 in the handout
The UCSR0C register
Fig. 3 - The UCSR0C register - the frame format: UMSEL selects the mode (asynchronous/synchronous), UPM the parity, USBS0 the number of stop bits, and UCSZ the character size. Fig. 6.4 in the handout
BitRegisterRole
RXC0UCSR0A1 = a complete byte has arrived, ready to read
UDRE0UCSR0A1 = the transmit register is free
U2X0UCSR0Adoubles the speed; reduces the error at high baud rates
RXCIE0UCSR0Benables the interrupt on reception
RXEN0 / TXEN0UCSR0Benable the receiver and the transmitter, respectively
UPM01:UPM00UCSR0C00 = no parity · 10 = even · 11 = odd
USBS0UCSR0C0 = one stop bit · 1 = two bits
UCSZ01:UCSZ00UCSR0C11 = 8 data bits

8Electrical standards

StandardLevelsMax. distanceTopologyUse
TTL / UART0 V / 5 V~1 mpoint to pointbetween boards, on the same board
RS-232±3...±15 V, inverted logic~15 mpoint to pointmeasurement instruments, older industrial equipment
RS-485differential ±1.5...5 V~1200 mmultipoint, up to 32 nodesindustrial, Modbus, automation
RS-485: two wires with opposing signals A B a disturbance affects BOTH wires IDENTICALLY... ...and the receiver reads the DIFFERENCE A − B, so the common noise cancels out completely.
Fig. 4 - The principle of differential transmission. This is why RS-485 reaches 1200 m, while TTL UART is limited to about a meter.

9UART, SPI, and I2C - comparison

CriterionUARTSPII2C
Wires needed2 (+ ground)42
Clockno (asynchronous)yesyes
Typical speed9.6-115 kbit/sup to 8 Mbit/s100-400 kbit/s
Number of devices2many (one CS pin each)up to 127 (addressing)
Duplexfull duplexfull duplexhalf duplex
Pins on the UnoD0, D1D10-D13A4 (SDA), A5 (SCL)
Typical usePC, GPS/GSM modulesSD cards, displays, nRF24sensors, EEPROM, RTC

10Wiring diagram

Arduino A Arduino B TX (D1)RX (D0)GND TX (D1) RX (D0) GND TX always connects to RX - the wires cross over. A common ground is mandatory.
Fig. 5 - Connecting two boards through UART. Without a common ground, the logic levels have no reference and communication fails, even if the data wires are correct.
Conflict with programming the boardPins D0 and D1 are also used by the bootloader. If both boards are wired together, uploading a new sketch may fail. Disconnect the wires while programming, or use SoftwareSerial on other pins.

11Simulator: a command parser

Data arrives character by character, not as complete strings. Watch how characters accumulate in a buffer until the line terminator is received, at which point the command becomes complete and can be interpreted.

Receiving the "ON" command - character by character
The classic mistakeA simple if (Serial.available()) followed by a single read processes only one character per loop() iteration. If the loop contains a delay(), the 64-byte buffer fills up and data is lost. Use while, not if.

12Source code

A command interpreter

serial_commands.ino
const uint8_t LED_PIN = 13;
String command = "";

void setup() {
  Serial.begin(9600);
  pinMode(LED_PIN, OUTPUT);
  Serial.println("Commands: ON, OFF, STATE, PWM <0-255>");
}

void loop() {
  while (Serial.available() > 0) {         // WHILE, not IF!
    char c = Serial.read();

    if (c == '\n' || c == '\r') {
      command.trim();
      command.toUpperCase();

      if (command == "ON") {
        digitalWrite(LED_PIN, HIGH);
        Serial.println("LED on");
      } else if (command == "OFF") {
        digitalWrite(LED_PIN, LOW);
        Serial.println("LED off");
      } else if (command == "STATE") {
        Serial.print("LED is ");
        Serial.println(digitalRead(LED_PIN) ? "on" : "off");
      } else if (command.startsWith("PWM ")) {
        int val = command.substring(4).toInt();
        val = constrain(val, 0, 255);
        analogWrite(9, val);
        Serial.print("PWM set to "); Serial.println(val);
      } else if (command.length() > 0) {
        Serial.print("Unknown command: ");
        Serial.println(command);
      }
      command = "";
    } else {
      command += c;
    }
  }
}

Direct configuration of the USART module

usart_registers.ino
#define F_CPU 16000000UL
#define BAUD  9600
#define UBRR_VAL ((F_CPU / (16UL * BAUD)) - 1)

void usartInit(uint16_t ubrr) {
  UBRR0H = (uint8_t)(ubrr >> 8);              // high byte of the divisor
  UBRR0L = (uint8_t)ubrr;                     // low byte
  UCSR0B = (1 << RXEN0) | (1 << TXEN0);       // enable reception and transmission
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);     // 8N1 format
}

void usartTransmit(uint8_t data) {
  while (!(UCSR0A & (1 << UDRE0)));           // wait for the register to be free
  UDR0 = data;
}

uint8_t usartReceive(void) {
  while (!(UCSR0A & (1 << RXC0)));            // wait for a complete byte
  return UDR0;
}

void usartWriteText(const char *s) {
  while (*s) usartTransmit(*s++);
}

void setup() {
  usartInit(UBRR_VAL);
  usartWriteText("USART initialized\r\n");
}

void loop() {
  uint8_t c = usartReceive();
  usartTransmit(c);                          // echo back
}

Interrupt-driven reception

Busy-waiting blocks the program. Interrupt-driven reception fully frees up the main loop:

usart_interrupt.ino
volatile char    buffer[32];
volatile uint8_t index = 0;
volatile bool    messageComplete = false;

ISR(USART_RX_vect) {              // runs for every byte received
  char c = UDR0;                  // reading it automatically clears the RXC0 flag

  if (c == '\n' || index >= 31) {
    buffer[index] = '\0';
    index = 0;
    messageComplete = true;
  } else {
    buffer[index++] = c;
  }
}

void setup() {
  Serial.begin(9600);
  UCSR0B |= (1 << RXCIE0);        // enable the receive interrupt
  sei();
  Serial.println("Ready for commands");
}

void loop() {
  if (messageComplete) {
    messageComplete = false;
    Serial.print("Received: ");
    Serial.println((char*)buffer);
  }
  // the loop stays completely free for other tasks
}

Sending data between two boards

transmitter.ino
const uint8_t BUTTON_PIN = 2;

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
}

void loop() {
  static bool lastState = HIGH;
  bool state = digitalRead(BUTTON_PIN);

  if (state != lastState) {
    lastState = state;
    Serial.print("B:");             // prefix to identify the message
    Serial.println(state == LOW ? 1 : 0);
    delay(50);                      // simple debounce
  }
}
receiver.ino
const uint8_t LED_PIN = 13;
String line = "";

void setup() {
  Serial.begin(9600);
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();

    if (c == '\n') {
      line.trim();
      if (line.startsWith("B:")) {
        int val = line.substring(2).toInt();
        digitalWrite(LED_PIN, val);
      }
      line = "";
    } else {
      line += c;
    }
  }
}

13Interactive circuit

The serial monitor in the circuits below is bidirectional: the microcontroller writes to it, and you can write back, in the field below the monitor. This is exactly the flow you will have in the lab, over the USB cable.

USART on registers - transmission and reception with echo
#include <avr/io.h>

#define BAUD      9600
#define UBRR_VAL  ((F_CPU / 16 / BAUD) - 1)

void usartInit(void)
{
    UBRR0H = (unsigned char)(UBRR_VAL >> 8);
    UBRR0L = (unsigned char)UBRR_VAL;
    UCSR0B = (1 << TXEN0) | (1 << RXEN0);
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);   // 8 data bits
}

void usartTransmit(unsigned char d)
{
    while (!(UCSR0A & (1 << UDRE0))) ;        // wait for the buffer to be free
    UDR0 = d;
}

void usartWrite(const char *s)
{
    while (*s) usartTransmit(*s++);
}

unsigned char usartRead(void)
{
    while (!(UCSR0A & (1 << RXC0))) ;         // wait for a character
    return UDR0;
}

int main(void)
{
    DDRB |= (1 << PB5);
    usartInit();
    usartWrite("Ready. Send 1 or 0.\r\n");

    while (1) {
        unsigned char c = usartRead();

        usartWrite("received: ");
        usartTransmit(c);
        usartWrite("\r\n");

        if (c == '1') PORTB |=  (1 << PB5);
        if (c == '0') PORTB &= ~(1 << PB5);
    }
}
Try thisType 1 in the field below the monitor and press Enter: the LED lights up. Watch the RXC0 bit in UCSR0A - it becomes 1 the instant a character arrives and clears itself when the program reads UDR0. All serial communication boils down to these two status signals.
Exercise - a command interpreter
String command = "";

void setup() {
  pinMode(13, OUTPUT);
  Serial.begin(9600);
  Serial.println("Commands: LED ON, LED OFF, READ");
}

void loop() {
  if (Serial.available()) {
    command = Serial.readStringUntil('\n');
    command.trim();
    command.toUpperCase();

    if (command == "LED ON") {
      digitalWrite(13, HIGH);
      Serial.println("-> LED on");
    }

    /* Complete this:
       - the "LED OFF" command turns off the LED and confirms it
       - the "READ" command shows the value on A0
       - anything else replies: "unknown command: ..." */
  }
}
Try thisType the commands in the field below the monitor. readStringUntil('\n') reads up to Enter, and trim() strips the spaces and the line-ending character - without it, the comparison with "LED ON" fails for no apparent reason.

14Work tasks

  • Write a program that responds to the ON, OFF, and STATE commands received over serial.
  • Add the PWM <value> command that sets an LED's brightness.
  • Compute UBRR for 4800, 9600, and 115200 baud and check the resulting errors.
  • Set 9600 baud in the program and 19200 in the serial monitor - observe what characters appear and explain why.
  • Connect two boards through TX-RX and send a button's state from one to the other.
  • Measure how long it takes to send 100 characters and compare it with the theoretical value.
  • Implement interrupt-driven reception and check that the main loop stays responsive even with delay(500).
  • Replace while with if in the reading loop and observe the lost characters.

15Extended application

Extension - a custom protocol with a checksumDefine frames of the form <ADDRESS:COMMAND:VALUE:CRC>, for example <01:PWM:180:A3>. Implement: a parser that extracts the four fields, address checking (the board only responds to its own address), computing a simple checksum (XOR over all characters), and rejecting corrupted frames.

Test the robustness by intentionally introducing errors into the frame and check that they are detected. Further extension: connect three boards on the same bus through MAX485 modules and verify selective addressing.

16Review questions

17Resources