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

UARTSPII2C
Wires2 (TX, RX)4 + 1/slave (MOSI, MISO, SCK, CS)2 (SDA, SCL)
Speed9600 - 1 Mbps typical1 - 50 MHz100 kHz (standard), 400 kHz (fast), 1 MHz (fast+)
DuplexFull duplexFull duplexHalf duplex
TopologyPoint-to-point1 master, N slaves (1 CS per slave)Multi-master bus, 7-bit addressing
ClockNo (asynchronous)Yes (master provides SCK)Yes (master provides SCL)
Flow controlOptional (RTS/CTS)CS acts as enableACK/NACK per byte
Typical useDebug console, GPS, BT modulesDisplays, SD cards, fast sensors, FlashSensors, 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:

ModeCPOLCPHAClock idleData sampled on
000LOWRising edge
101LOWFalling edge
210HIGHFalling edge
311HIGHRising 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 high

I2C (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.