Skip to content
Home » Embedded Systems » Communication Interfaces: UART, SPI, and I2C Explained

Communication Interfaces: UART, SPI, and I2C Explained

UART SPI and I2C featured image with dark green background, HARDWARE badge, TX RX icon in green circle, and Communication Interfaces Explained subtitle by nerdyelectronics.com
Embedded Systems Learning Path
Part 65 of 129 — View Full Path →

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
Each has its own strengths, trade-offs, and ideal use cases. By the end of this article, you will know how each one works and when to use which.

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
The TX pin of one device connects to the RX pin of the other, and vice versa. This is called a point-to-point connection – UART connects exactly two devices.
UART wiring between two devicesTwo devices, A and B, connected by three wires. Device A’s TX pin goes to device B’s RX pin, so A transmits to B. Device B’s TX pin goes to device A’s RX pin, so B transmits to A. The GND pins of the two devices are connected together to give both a common reference. TX always goes to RX: the data wires cross over.Device ADevice BTXRXGNDRXTXGNDA transmits → B receivesA receives ← B transmitscommon groundTX of one device always goes to RX of the other. Point-to-point: two devices only.
Figure: UART wiring: each device’s TX goes to the other’s RX, plus a shared ground.
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:
UART data frameThe UART line idles high. A frame starts with a start bit, which is always 0 and pulls the line low. Then come the data bits D0 to D7 in that order; each can be 0 or 1. Five to nine data bits are possible, usually eight. An optional parity bit follows for even or odd error checking. The frame ends with a stop bit, which is always high and can be one or two bits long, and the line returns to idle high.IdleStartD0D1D2D3D4D5D6D7ParityStopIdle100/10/10/10/10/10/10/10/10/1always 0data bits: 5 to 9 (usually 8)optionalalways 1(1 or 2 bits)Most common setting: 8N1 = 8 data bits, no parity, 1 stop bit.
Figure: A UART frame: the start bit (0), data bits D0–D7, an optional parity bit and the stop bit (1). The line idles high.
  • 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

FeatureDetail
Wires2 (TX, RX) + GND
SpeedTypically up to 115200 bps, some up to several Mbps
DirectionFull-duplex (send and receive simultaneously)
DevicesPoint-to-point (2 devices only)
ClockNo clock signal (asynchronous)
DistanceShort (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)
SPI master with two slavesOne SPI master connected to two slaves. Three lines are shared by all devices: MOSI (master out, slave in) runs from the master to the MOSI pin of both slaves; MISO (master in, slave out) runs from the MISO pin of both slaves back to the master; SCLK, the clock generated by the master, goes to the SCLK pin of both slaves. Each slave also has its own select line: the master’s SS1 pin goes only to Slave 1’s SS pin, and the master’s SS2 pin goes only to Slave 2’s SS pin. To talk to one slave, the master pulls that slave’s SS line low; only the selected slave responds.MasterMOSIMISOSCLKSS1SS2Slave 1Slave 2MOSIMISOSCLKSSMOSIMISOSCLKSSMOSIMISOSCLKSS1SS2MOSI, MISO, SCLK: shared by all slaves.SS: one line per slave. Pull it LOWto select that slave.
Figure: SPI with two slaves: MOSI, MISO and SCLK are shared, and each slave gets its own SS line.
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.
SPI byte transfer timingTiming of one SPI byte, drawn with the clock idling low. SS, the slave select line, is high when idle; the master pulls it low to start the transfer and keeps it low for the whole byte, then releases it high. The master then produces eight clock pulses on SCLK. On every clock cycle one bit moves in each direction at the same time: the master sends bits D7 down to D0 on MOSI, and the slave sends its bits D7 down to D0 on MISO. This is full duplex: for every byte the master sends, it receives a byte back.SCLKMOSIMISOSSmaster sendsslave sendsactive lowD7D6D5D4D3D2D1D0D7D6D5D4D3D2D1D0selectreleaseDrawn with the clock idling low; dotted lines mark the rising edges. CPOL/CPHA set the real edges.
Figure: One SPI byte: SS goes low, eight clocks shift D7…D0 out on MOSI and in on MISO at the same time, then SS goes high.

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)
ModeCPOLCPHADescription
Mode 000Clock idle low, sample on rising edge (most common)
Mode 101Clock idle low, sample on falling edge
Mode 210Clock idle high, sample on falling edge
Mode 311Clock idle high, sample on rising edge
Always check the slave device’s datasheet to use the correct mode.

SPI Characteristics

FeatureDetail
Wires4 (MOSI, MISO, SCLK, SS) + GND
SpeedUp to tens of MHz (very fast)
DirectionFull-duplex
Devices1 master, multiple slaves (1 SS pin per slave)
ClockSynchronous (master provides clock)
OverheadNo 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
Both lines need pull-up resistors (typically 4.7K ohm) because I2C uses open-drain outputs.
I2C bus with one master and three slavesAn I2C bus. Two shared lines, SDA and SCL, run across the board. Each line has its own 4.7 kilohm pull-up resistor to VCC. Four devices hang off the bus, each connected to both SDA and SCL: a master, and three slaves at addresses 0x48, 0x68 and 0x3C. No extra wires are needed per device; the master selects a slave by sending its address.VCC4.7 kΩ4.7 kΩSDASCLSDASCLSDASCLSDASCLSDASCLMasterSlave0x48Slave0x68Slave0x3C
Figure: I2C bus: a 4.7 kΩ pull-up on SDA and on SCL, and a master and three slaves (0x48, 0x68, 0x3C) on the same two wires.
Each 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:
I2C transaction sequenceThe fields of an I2C transaction in the order they appear on the bus. First the master sends a START condition: SDA goes low while SCL is high. Next the 7-bit slave address, then one R/W bit that sets the direction. The addressed slave answers with an ACK bit. Then a data byte of 8 bits follows, with its own ACK bit; this data-plus-ACK pair repeats for each byte. Finally the master sends a STOP condition: SDA goes high while SCL is high.STARTAddress7 bitsR/W1 bitACK1 bitData8 bitsACK1 bitSTOPfrom masterfrom masterfrom slavefrom masterrepeats for each data byteSTART: SDA goes low while SCL is high.STOP: SDA goes high while SCL is high.
Figure: Fields of an I2C transaction: START, 7-bit address, R/W, ACK, data byte, ACK, STOP.
  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)

I2C Speeds

ModeSpeed
Standard Mode100 kHz
Fast Mode400 kHz
Fast Mode Plus1 MHz
High Speed Mode3.4 MHz

I2C Characteristics

FeatureDetail
Wires2 (SDA, SCL) + GND
Speed100 kHz to 3.4 MHz
DirectionHalf-duplex (one direction at a time)
DevicesUp to 127 on one bus (7-bit addressing)
ClockSynchronous (master provides clock)
AcknowledgmentBuilt-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

FeatureUARTSPII2C
Wires24 + 1 per slave2
SpeedUp to ~1 MbpsUp to ~50 MHzUp to 3.4 MHz
DuplexFullFullHalf
Max Devices2Limited by SS pins127
ClockNone (async)Yes (master)Yes (master)
ComplexitySimpleMediumMedium
Error DetectionOptional parityNone built-inACK/NACK
Best ForDebug, GPS, BTFast data, displaysMany 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)
Choose SPI when:
  • 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
Choose I2C when:
  • 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:

Leave a Reply

Your email address will not be published. Required fields are marked *