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
UBRRregister 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.
4Frame structure
| Element | Length | Value | Role |
|---|---|---|---|
| Idle line | - | 1 (HIGH) | indicates no transmission |
| Start bit | 1 bit | always 0 | the falling edge synchronizes the receiver |
| Data bits | 5-9 (usually 8) | useful data | sent LSB first |
| Parity bit | 0 or 1 bit | even / odd | simple error detection |
| Stop bit(s) | 1 or 2 bits | always 1 | marks the end and separates frames |
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.
6Computing the transmission speed
| Desired baud | UBRR (16 MHz) | Actual baud | Error | Verdict |
|---|---|---|---|---|
| 2,400 | 416 | 2,398 | -0.08% | excellent |
| 9,600 | 103 | 9,615 | +0.16% | excellent |
| 19,200 | 51 | 19,231 | +0.16% | excellent |
| 38,400 | 25 | 38,462 | +0.16% | good |
| 57,600 | 16 | 58,824 | +2.12% | ⚠ borderline |
| 115,200 | 8 | 111,111 | -3.55% | ⚠ unreliable |
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.
| Bit | Register | Role |
|---|---|---|
RXC0 | UCSR0A | 1 = a complete byte has arrived, ready to read |
UDRE0 | UCSR0A | 1 = the transmit register is free |
U2X0 | UCSR0A | doubles the speed; reduces the error at high baud rates |
RXCIE0 | UCSR0B | enables the interrupt on reception |
RXEN0 / TXEN0 | UCSR0B | enable the receiver and the transmitter, respectively |
UPM01:UPM00 | UCSR0C | 00 = no parity · 10 = even · 11 = odd |
USBS0 | UCSR0C | 0 = one stop bit · 1 = two bits |
UCSZ01:UCSZ00 | UCSR0C | 11 = 8 data bits |
8Electrical standards
| Standard | Levels | Max. distance | Topology | Use |
|---|---|---|---|---|
| TTL / UART | 0 V / 5 V | ~1 m | point to point | between boards, on the same board |
| RS-232 | ±3...±15 V, inverted logic | ~15 m | point to point | measurement instruments, older industrial equipment |
| RS-485 | differential ±1.5...5 V | ~1200 m | multipoint, up to 32 nodes | industrial, Modbus, automation |
9UART, SPI, and I2C - comparison
| Criterion | UART | SPI | I2C |
|---|---|---|---|
| Wires needed | 2 (+ ground) | 4 | 2 |
| Clock | no (asynchronous) | yes | yes |
| Typical speed | 9.6-115 kbit/s | up to 8 Mbit/s | 100-400 kbit/s |
| Number of devices | 2 | many (one CS pin each) | up to 127 (addressing) |
| Duplex | full duplex | full duplex | half duplex |
| Pins on the Uno | D0, D1 | D10-D13 | A4 (SDA), A5 (SCL) |
| Typical use | PC, GPS/GSM modules | SD cards, displays, nRF24 | sensors, EEPROM, RTC |
10Wiring diagram
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.
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
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
#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:
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
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
}
}
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.
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.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
whilewithifin the reading loop and observe the lost characters.
15Extended application
<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.