UART SPI I2C
The three most common serial communication protocols in embedded systems. Each trades off speed, wire count, and topology differently.
Why It Matters
Nearly every peripheral — sensors, displays, SD cards, wireless modules, EEPROMs — communicates over one of these three protocols. Choosing the right one and understanding its timing, wiring, and failure modes is fundamental to any embedded design.
Protocol Comparison
| UART | SPI | I2C | |
|---|---|---|---|
| Wires | 2 (TX, RX) | 4 + 1/slave (MOSI, MISO, SCK, CS) | 2 (SDA, SCL) |
| Speed | 9600 - 1 Mbps typical | 1 - 50 MHz | 100 kHz (standard), 400 kHz (fast), 1 MHz (fast+) |
| Duplex | Full duplex | Full duplex | Half duplex |
| Topology | Point-to-point | 1 master, N slaves (1 CS per slave) | Multi-master bus, 7-bit addressing |
| Clock | No (asynchronous) | Yes (master provides SCK) | Yes (master provides SCL) |
| Flow control | Optional (RTS/CTS) | CS acts as enable | ACK/NACK per byte |
| Typical use | Debug console, GPS, BT modules | Displays, SD cards, fast sensors, Flash | Sensors, EEPROM, RTC, port expanders |
UART (Universal Asynchronous Receiver/Transmitter)
No clock wire — both sides must agree on the baud rate beforehand. A typical frame:
Idle Start D0 D1 D2 D3 D4 D5 D6 D7 (Parity) Stop Idle
HIGH LOW -------- 8 data bits ---------- (opt) HIGH HIGH
___ _ _ _ ___ _ ___ _ _ _ ____
| | | | | | | | | | | | | | | | | | | | |
| |__| |___| |_| |_| |___| |_| |___|_|_|_| |_|___|
Baud rate: both sides must match exactly. Common values: 9600, 115200. A 2% mismatch causes bit errors at the end of a frame. The bit period = 1 / baud_rate.
// USART2 at 115200 baud on STM32F4 (APB1 = 42 MHz)
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
// BRR = PCLK / baud = 42000000 / 115200 = 364.58 -> 364 (0x16C)
// Mantissa = 364/16 = 22, Fraction = 364%16 = 12
USART2->BRR = (22 << 4) | 12;
USART2->CR1 = USART_CR1_TE // transmitter enable
| USART_CR1_RE // receiver enable
| USART_CR1_UE; // USART enable
// Send a byte
while (!(USART2->SR & USART_SR_TXE)); // wait for TX empty
USART2->DR = 'A';
// Receive a byte
while (!(USART2->SR & USART_SR_RXNE)); // wait for RX not empty
uint8_t ch = USART2->DR;SPI (Serial Peripheral Interface)
Master drives the clock (SCK). Full duplex — data shifts out on MOSI while simultaneously shifting in on MISO. Each slave has its own chip-select (CS, active LOW).
Master Slave
MOSI -------------> MOSI
MISO <------------- MISO
SCK -------------> SCK
CS -------------> CS (active LOW)
Multiple slaves: each gets its own CS line
CS0 --> Slave 0
CS1 --> Slave 1
CS2 --> Slave 2
Clock Polarity and Phase (CPOL/CPHA)
SPI has 4 modes depending on when data is sampled relative to the clock:
| Mode | CPOL | CPHA | Clock idle | Data sampled on |
|---|---|---|---|---|
| 0 | 0 | 0 | LOW | Rising edge |
| 1 | 0 | 1 | LOW | Falling edge |
| 2 | 1 | 0 | HIGH | Falling edge |
| 3 | 1 | 1 | HIGH | Rising edge |
Mode 0 is the most common. Check the slave datasheet for the required mode — a mismatch means corrupted data.
// SPI1 master, mode 0, 8-bit, prescaler /16 (APB2=84MHz -> 5.25MHz)
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
SPI1->CR1 = SPI_CR1_MSTR // master mode
| (3 << SPI_CR1_BR_Pos) // baud = PCLK/16
| 0; // CPOL=0, CPHA=0 (mode 0)
SPI1->CR1 |= SPI_CR1_SPE; // enable SPI
// Transfer one byte (full duplex: send + receive simultaneously)
uint8_t spi_transfer(uint8_t tx) {
while (!(SPI1->SR & SPI_SR_TXE));
SPI1->DR = tx;
while (!(SPI1->SR & SPI_SR_RXNE));
return SPI1->DR;
}
// Usage: read register 0x0F from a sensor
GPIOA->BSRR = (1 << (4 + 16)); // CS low (PA4)
spi_transfer(0x0F | 0x80); // register addr + read bit
uint8_t val = spi_transfer(0x00); // clock out dummy to receive data
GPIOA->BSRR = (1 << 4); // CS highI2C (Inter-Integrated Circuit)
Two-wire bus with addressing. Multiple devices share SDA (data) and SCL (clock). Both lines are open-drain with pull-up resistors (typically 4.7k to VDD).
Transaction Sequence
SDA: ──\___/─[A6]─[A5]─[A4]─[A3]─[A2]─[A1]─[A0]─[RW]─\_/─[D7]...
SCL: ──────\__/──\__/──\__/──\__/──\__/──\__/──\__/──\__/──\__/──
START 7-bit slave address R/W ACK data byte
(SDA falls 0=W (slave
while SCL 1=R pulls
is HIGH) SDA low)
- START: SDA goes LOW while SCL is HIGH
- Address + R/W: 7-bit address + 1-bit direction (0=write, 1=read)
- ACK: receiver pulls SDA LOW for one clock (NACK = SDA stays HIGH)
- STOP: SDA goes HIGH while SCL is HIGH
// I2C read: get temperature from sensor at address 0x48, register 0x00
i2c_start();
i2c_write(0x48 << 1 | 0); // write mode: send register address
i2c_write(0x00); // temperature register
i2c_start(); // repeated start (no STOP)
i2c_write(0x48 << 1 | 1); // read mode
uint8_t msb = i2c_read_ack();
uint8_t lsb = i2c_read_nack(); // NACK on last byte signals end
i2c_stop();
int16_t temp = (msb << 8) | lsb;When to Use Which
- UART: debug output, GPS NMEA, Bluetooth HC-05, any point-to-point link where simplicity matters
- SPI: anything that needs speed — displays (ILI9341), SD cards, external Flash (W25Q), ADCs, IMUs. Costs extra pins.
- I2C: multi-sensor setups on shared bus — temperature, humidity, accelerometer, EEPROM. Slower but uses only 2 wires for many devices.
DMA Integration
All three protocols can use DMA to transfer data without CPU intervention. The CPU sets up source, destination, and length, then the DMA controller handles byte-by-byte transfer. This frees the CPU for computation while a 512-byte SPI Flash page write or a UART log message streams out in the background. See Microcontroller Architecture for DMA’s place on the bus matrix.
Related
- Microcontroller Architecture — bus structure and DMA
- GPIO and Digital IO — alternate function pin config for peripherals
- Interrupts and Timers — ISR-driven receive for UART/SPI/I2C
- ADC and DAC — sensors often communicate ADC data over SPI/I2C
- Signal Integrity — pull-up values, bus capacitance, and termination
- Memory-Mapped IO — register access for peripheral configuration