Table of Contents
KEY TAKEAWAYS
- UART is asynchronous (no clock line), simple, and used for point-to-point communication between two devices
- SPI uses 4 wires (MOSI, MISO, SCK, CS) and supports high-speed full-duplex communication with multiple slaves
- I2C uses only 2 wires (SDA, SCL) with addressing to support multiple devices on one bus
- Choose UART for simplicity, SPI for speed, and I2C for multiple devices with fewer pins
Why Communication Interfaces Matter
Microcontrollers rarely work alone. They need to communicate with sensors, displays, memory chips, other microcontrollers, and computers. This communication happens through standardized communication interfaces or protocols.The three most common serial communication interfaces in embedded systems are:- UART – Universal Asynchronous Receiver/Transmitter
- SPI – Serial Peripheral Interface
- I2C – Inter-Integrated Circuit
UART (Universal Asynchronous Receiver/Transmitter)
How UART Works
UART is the simplest serial communication interface. It uses just two wires:- TX (Transmit) – sends data
- RX (Receive) – receives data
Device A Device B
+------+ +------+
| TX |--------------| RX |
| RX |--------------| TX |
| GND |--------------| GND |
+------+ +------+UART is asynchronous, meaning there is no shared clock signal. Both devices must agree on the same baud rate (speed) beforehand. Common baud rates are 9600, 19200, 38400, 57600, and 115200 bits per second.UART Data Frame
Each byte is sent as a frame: Idle |Start| D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 |Parity| Stop | Idle
______ _____ _____ _____ _____ _____ _____ _____ _____ ______
|_____| | | | | | | | |______|_____|_____|
- Start bit: Always 0 (pulls the line low)
- Data bits: 5 to 9 bits (usually 8)
- Parity bit: Optional error checking (even or odd)
- Stop bit: 1 or 2 bits, always high
The most common configuration is 8N1: 8 data bits, No parity, 1 stop bit.UART Characteristics
| Feature | Detail |
|---|---|
| Wires | 2 (TX, RX) + GND |
| Speed | Typically up to 115200 bps, some up to several Mbps |
| Direction | Full-duplex (send and receive simultaneously) |
| Devices | Point-to-point (2 devices only) |
| Clock | No clock signal (asynchronous) |
| Distance | Short (PCB level), longer with RS-232/RS-485 transceivers |
Common Uses
- Debugging output (serial monitor)
- GPS modules
- Bluetooth modules (HC-05, HM-10)
- GSM/LTE modems
- Communication between two microcontrollers
SPI (Serial Peripheral Interface)
How SPI Works
SPI is a synchronous protocol that uses a shared clock signal. It follows a master-slave architecture where one master device controls communication with one or more slave devices.SPI uses four wires:- MOSI (Master Out, Slave In) – data from master to slave
- MISO (Master In, Slave Out) – data from slave to master
- SCLK (Serial Clock) – clock signal generated by the master
- SS/CS (Slave Select / Chip Select) – selects which slave to talk to (one per slave)
Master Slave 1 Slave 2
+------+ +------+ +------+
| MOSI |---+----------|MOSI |---+--------|MOSI |
| MISO |---+----------|MISO |---+--------|MISO |
| SCLK |---+----------|SCLK |---+--------|SCLK |
| SS1 |--------------|SS | | | |
| SS2 |----------------------------+------|SS |
+------+ +------+ +------+To communicate with a specific slave, the master pulls that slave’s SS/CS pin LOW. Only the selected slave responds.SPI Data Transfer
SPI is full-duplex: the master and slave exchange data simultaneously. For every byte the master sends, it receives a byte back.SCLK: ___|‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾|
MOSI: ---| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |--- (Master sends)
MISO: ---| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |--- (Slave sends)
SS: ‾‾‾|_______________________________________________|‾‾‾SPI Clock Modes
SPI has 4 clock modes based on two settings:- CPOL (Clock Polarity) – idle state of the clock (0 = low, 1 = high)
- CPHA (Clock Phase) – when data is sampled (0 = leading edge, 1 = trailing edge)
| Mode | CPOL | CPHA | Description |
|---|---|---|---|
| Mode 0 | 0 | 0 | Clock idle low, sample on rising edge (most common) |
| Mode 1 | 0 | 1 | Clock idle low, sample on falling edge |
| Mode 2 | 1 | 0 | Clock idle high, sample on falling edge |
| Mode 3 | 1 | 1 | Clock idle high, sample on rising edge |
SPI Characteristics
| Feature | Detail |
|---|---|
| Wires | 4 (MOSI, MISO, SCLK, SS) + GND |
| Speed | Up to tens of MHz (very fast) |
| Direction | Full-duplex |
| Devices | 1 master, multiple slaves (1 SS pin per slave) |
| Clock | Synchronous (master provides clock) |
| Overhead | No addressing, no acknowledgment (minimal overhead) |
Common Uses
- SD cards
- TFT/OLED displays
- Flash memory chips
- ADC and DAC chips
- High-speed sensor modules
I2C (Inter-Integrated Circuit)
How I2C Works
I2C (pronounced “I-squared-C” or “I-two-C”) uses just two wires to connect multiple devices on a shared bus:- SDA (Serial Data) – bidirectional data line
- SCL (Serial Clock) – clock signal from the master
VCC VCC
| |
[4.7K] [4.7K]
| |
SDA ---+-----+------+-----+---
| | | |
SCL ---+-----+------+-----+---
| | | |
Master Slave Slave Slave
0x48 0x68 0x3CEach slave device has a unique 7-bit address (some use 10-bit). The master sends the address to select which slave to communicate with. No extra wires are needed per device.I2C Data Transfer
An I2C transaction follows this sequence:1. Master sends START condition (SDA goes low while SCL is high)
2. Master sends 7-bit slave address + 1-bit R/W direction
3. Slave sends ACK (acknowledgment)
4. Data bytes are transferred (8 bits + ACK each)
5. Master sends STOP condition (SDA goes high while SCL is high)
START Address (7 bits) R/W ACK Data (8 bits) ACK STOP
|_____|__________________|____|_____|_______________|_____|_____/‾‾‾I2C Speeds
| Mode | Speed |
|---|---|
| Standard Mode | 100 kHz |
| Fast Mode | 400 kHz |
| Fast Mode Plus | 1 MHz |
| High Speed Mode | 3.4 MHz |
I2C Characteristics
| Feature | Detail |
|---|---|
| Wires | 2 (SDA, SCL) + GND |
| Speed | 100 kHz to 3.4 MHz |
| Direction | Half-duplex (one direction at a time) |
| Devices | Up to 127 on one bus (7-bit addressing) |
| Clock | Synchronous (master provides clock) |
| Acknowledgment | Built-in ACK/NACK after each byte |
Common Uses
- Temperature sensors (LM75, BME280)
- EEPROM memory chips
- Real-time clocks (DS1307, DS3231)
- Small OLED displays (SSD1306)
- Accelerometers and gyroscopes (MPU6050)
UART vs SPI vs I2C: Comparison
| Feature | UART | SPI | I2C |
|---|---|---|---|
| Wires | 2 | 4 + 1 per slave | 2 |
| Speed | Up to ~1 Mbps | Up to ~50 MHz | Up to 3.4 MHz |
| Duplex | Full | Full | Half |
| Max Devices | 2 | Limited by SS pins | 127 |
| Clock | None (async) | Yes (master) | Yes (master) |
| Complexity | Simple | Medium | Medium |
| Error Detection | Optional parity | None built-in | ACK/NACK |
| Best For | Debug, GPS, BT | Fast data, displays | Many slow sensors |
How to Choose
Choose UART when:- You need to connect just two devices
- You need debug/serial output
- The peripheral uses UART (GPS, Bluetooth modules)
- You need high data transfer speed
- You are interfacing with displays, SD cards, or flash memory
- You have enough free GPIO pins for chip select lines
- You need to connect many devices with minimal wiring
- Speed is not critical (sensors, EEPROMs, RTCs)
- You are short on GPIO pins
Summary
UART, SPI, and I2C are the backbone of embedded communication. Most real-world projects use a combination of all three. A typical embedded system might use UART for debugging, SPI for a display and SD card, and I2C for temperature and motion sensors, all running simultaneously. Understanding when and how to use each one is a fundamental skill for every embedded developer.📘 Working with UART, SPI or I²C? Solid C fundamentals make every protocol easier to debug. My complete Master C & Embedded C course takes you from zero to hardware-ready code — free on YouTube (53 videos), or guided on Udemy with quizzes, certificate and my Q&A support.
Code: Initializing Each Protocol on ATmega328P
Theory is only half the picture. Here is how you actually configure each protocol in code on an ATmega328P. These are complete, compilable init routines you can drop into your project.
UART Initialization (9600 baud, 16 MHz clock)
#include <avr/io.h>
void uart_init(void)
{
/* Baud rate = 9600, F_CPU = 16 MHz
UBRR = F_CPU / (16 * baud) - 1 = 103 */
UBRR0H = 0;
UBRR0L = 103;
UCSR0B = (1 << TXEN0) | (1 << RXEN0); /* Enable TX and RX */
UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); /* 8-bit data, 1 stop, no parity */
}
void uart_send(uint8_t data)
{
while (!(UCSR0A & (1 << UDRE0))); /* Wait until buffer is empty */
UDR0 = data;
}
uint8_t uart_recv(void)
{
while (!(UCSR0A & (1 << RXC0))); /* Wait until data is received */
return UDR0;
}SPI Master Initialization
#include <avr/io.h>
void spi_master_init(void)
{
/* MOSI (PB3), SCK (PB5), SS (PB2) = output; MISO (PB4) = input */
DDRB |= (1 << PB3) | (1 << PB5) | (1 << PB2);
DDRB &= ~(1 << PB4);
/* Enable SPI, Master mode, clock = F_CPU/16 */
SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0);
}
uint8_t spi_transfer(uint8_t data)
{
SPDR = data; /* Start transmission */
while (!(SPSR & (1 << SPIF))); /* Wait for transfer complete */
return SPDR; /* Return received byte */
}
/* Usage: pull SS low, transfer, pull SS high */
void spi_read_sensor(uint8_t reg, uint8_t *value)
{
PORTB &= ~(1 << PB2); /* SS LOW — select slave */
spi_transfer(reg | 0x80); /* Send register address (read bit) */
*value = spi_transfer(0xFF); /* Clock out data */
PORTB |= (1 << PB2); /* SS HIGH — deselect */
}I2C (TWI) Master Initialization
#include <avr/io.h>
#define F_SCL 100000UL /* 100 kHz standard mode */
void i2c_init(void)
{
/* TWBR = (F_CPU / F_SCL - 16) / 2 (with prescaler = 1) */
TWSR = 0x00; /* Prescaler = 1 */
TWBR = ((F_CPU / F_SCL) - 16) / 2; /* Set bit rate */
}
void i2c_start(void)
{
TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);
while (!(TWCR & (1 << TWINT))); /* Wait for START to complete */
}
void i2c_stop(void)
{
TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN);
}
void i2c_write(uint8_t data)
{
TWDR = data;
TWCR = (1 << TWINT) | (1 << TWEN);
while (!(TWCR & (1 << TWINT)));
}
uint8_t i2c_read_ack(void)
{
TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWEA);
while (!(TWCR & (1 << TWINT)));
return TWDR;
}
/* Example: read 1 byte from device 0x68, register 0x75 */
uint8_t read_register(uint8_t dev_addr, uint8_t reg)
{
uint8_t data;
i2c_start();
i2c_write(dev_addr << 1); /* Address + Write bit */
i2c_write(reg); /* Register address */
i2c_start(); /* Repeated START */
i2c_write((dev_addr << 1) | 1); /* Address + Read bit */
data = i2c_read_ack();
i2c_stop();
return data;
}Quick Decision Guide: Which Protocol Should You Use?
Use UART when you need simple point-to-point communication between two devices — a microcontroller talking to a GPS module, a Bluetooth module, or a PC over a serial-to-USB adapter. UART is easy to set up (just TX, RX, and GND), needs no clock line, and works well for distances up to a few metres. The trade-off is speed (typically 115200 baud max) and the two-device limitation.
Use SPI when you need speed. SPI can run at several MHz — 10× faster than I2C — making it the right choice for SD cards, TFT displays, high-speed ADCs, and flash memory. The cost is pin count: you need MOSI, MISO, SCK, plus one chip-select (SS) line per slave device. For two or three devices on one bus, that is manageable. For a dozen, the wiring becomes impractical.
Use I2C when you have many low-speed devices on one bus. I2C uses only two wires (SDA, SCL) regardless of how many devices are connected — each device has a unique 7-bit address. It is ideal for sensors (temperature, humidity, accelerometer), EEPROMs, and RTCs. The trade-off is speed (100–400 kHz standard) and the need for pull-up resistors on both lines.
Articles in This Communication Protocols Series
Explore each protocol in depth with these dedicated guides:
- UART Protocol Deep Dive: Framing, Baud Rate, and Flow Control — detailed understanding on configuring UART
- Writing a UART Driver in Embedded C — from registers up — practical UART driver implementation
- SPI Protocol Deep Dive: Clock Polarity, Phase, and Multi-Slave Design — detailed understanding of SPI protocol
- Writing an SPI Driver in Embedded C — practical SPI driver implementation with chip-select handling
- I2C Protocol Deep Dive for Embedded Engineers — let's understand I2C protocol in details
- Writing an I2C Driver in Embedded C — practical I2C driver with start/stop, ACK, and arbitration
- Building an I2C Sensor Network in C: Complete Project Guide — I2C in a real multi-sensor project
- Serial Programming of AVR Microcontrollers — practical serial implementation on real AVR hardware
- Debugging Communication Protocols — logic analyzer techniques, common bugs, and how to fix them
- Writing a UART Bootloader in C for STM32 — advanced UART use case — building a working bootloader

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.







