UART, SPI & I²C
Embedded devices talk over serial buses. Learn UART framing and baud rate, SPI modes, I²C addressing and pull-ups, CAN arbitration, RS-485 and Modbus, and how to choose the right protocol for each job.
No MCU works alone: it reads sensors, drives displays, talks to radios and joins vehicle or factory networks. Serial protocols send data one bit at a time over a few wires, each trading speed, distance, wire count, and robustness differently.
UART, SPI and I²C connect chips on a board; CAN, RS-485 and Ethernet connect devices across machines, vehicles and plants.
An analogy
UART is a phone call between two people; SPI is a teacher calling on students one at a time with a dedicated line to each; I²C is a meeting where everyone shares one line and speaks only when their name is called; CAN is a meeting with strict priority rules for who may speak first.
UART
Asynchronous: no shared clock. Both ends agree on a baud rate (9600, 115200…). Each frame: start bit (0), 5–9 data bits LSB-first, optional parity, 1–2 stop bits (1). 8N1 means 8 data bits, no parity, 1 stop bit — 10 bits per byte. Point-to-point, TX of one to RX of the other, common ground.
SPI
Synchronous and full duplex with four signals: SCLK, MOSI, MISO and one chip-select per device. Very fast (tens of MHz), simple, but needs a CS line per peripheral. Four modes (CPOL, CPHA) define clock idle level and sampling edge — master and slave must match.
I²C
Two open-drain wires (SDA, SCL) with pull-ups shared by up to ~100 devices, each with a 7-bit address. Transactions: START, address + R/W, ACK, data bytes with ACKs, STOP. Speeds 100 kHz, 400 kHz, 1 MHz. Pull-up values balance rise time (bus capacitance) against sink current.
CAN and RS-485
CAN uses a differential pair and bitwise arbitration: dominant (0) bits win, so the lowest-ID message gets the bus without collisions. It adds CRC, ACK and automatic retransmission — the standard in vehicles and industrial machines (1 Mbit/s classic, up to 5–8 Mbit/s CAN-FD). RS-485 is a differential multi-drop physical layer (up to ~1200 m) carrying protocols like Modbus RTU.
Choosing and hardening a link
Consider distance, speed, node count, noise, power and cost. Long or noisy links need differential signalling, termination (120 Ω at each end of CAN/RS-485), isolation and ESD/TVS protection. Add framing, checksums/CRCs, timeouts and retries in software — a protocol is only as reliable as its error handling.
Formulas
- UART throughput:
bytes/s = baud / (1 + data bits + parity + stop bits) - I²C max pull-up (rise time):
R_p,max = t_r / (0.8473 · C_b) - I²C min pull-up (sink current):
R_p,min = (V_DD − V_OL,max) / I_OL - Baud error tolerance:
total clock mismatch ≲ ±2–3 % for 8N1
Worked example: UART throughput
Given: 115 200 baud, 8N1
Find: Bytes per second and time per byte.
- Bits per byte = 1 + 8 + 0 + 1 = 10
- 115 200/10 = 11 520 bytes/s
- Time per byte = 86.8 µs
Answer: 11.5 kB/s
Worked example: I²C pull-up range
Given: V_DD = 3.3 V, V_OL = 0.4 V, I_OL = 3 mA; Fast mode: t_r ≤ 300 ns; C_b = 200 pF
Find: Pull-up range.
- R_min = (3.3 − 0.4)/3 mA = 967 Ω
- R_max = 300 ns/(0.8473 × 200 pF) = 1770 Ω
- Choose 1.2–1.5 kΩ
Answer: ≈ 1.2–1.5 kΩ
SPI transfer and I²C register read (bare-metal style)
// SPI: full duplex — every byte sent clocks one byte in.
uint8_t spi_transfer(uint8_t out) {
while (!(SPI1->SR & SPI_SR_TXE)) { } // wait for TX empty
*(volatile uint8_t *)&SPI1->DR = out;
while (!(SPI1->SR & SPI_SR_RXNE)) { } // wait for RX byte
return *(volatile uint8_t *)&SPI1->DR;
}
// I2C register read: START, addr+W, reg, repeated START, addr+R, data, NACK, STOP
int i2c_read_reg(uint8_t addr7, uint8_t reg, uint8_t *val) {
if (i2c_start() || i2c_write((addr7 << 1) | 0)) return -1; // expect ACK
if (i2c_write(reg)) return -1;
if (i2c_start() || i2c_write((addr7 << 1) | 1)) return -1; // repeated start
*val = i2c_read(/*ack=*/0); // NACK last byte
i2c_stop();
return 0;
}SPI exchanges bytes simultaneously; I²C uses addresses, acknowledgements and a repeated start to switch from writing the register pointer to reading data.
Common mistakes
- Wrong: Connecting UART TX to TX. — Right: Cross over: TX → RX and RX → TX, with a common ground and matching voltage levels.
- Wrong: Omitting I²C pull-ups or CAN termination. — Right: I²C needs pull-ups; CAN/RS-485 need 120 Ω at both ends of the bus.
Applications
- On-board: I²C for sensors and EEPROMs, SPI for displays, flash and ADCs, UART for GPS, Bluetooth modules and debug consoles.
- Vehicles: Dozens of ECUs share CAN/CAN-FD buses; LIN handles low-cost body electronics.
- Industry: RS-485 Modbus RTU and industrial Ethernet link PLCs, drives and meters across plants.
Key takeaways
- UART: async, point-to-point, 10 bits per 8N1 byte.
- SPI: fast, full duplex, CS per device; match CPOL/CPHA.
- I²C: two wires, addresses, pull-ups sized for rise time.
- CAN/RS-485: differential, robust, multi-drop — terminate the bus.
Going further
- Clock stretching and bus recovery: I²C slaves can hold SCL low to pause the master; a stuck bus (slave holding SDA) is recovered by clocking SCL up to 9 times and issuing a STOP.
- CAN bit timing: Each bit is divided into time quanta (sync, propagation, phase segments); setting the sample point (~75–87.5 %) correctly is essential for long buses and CAN-FD's faster data phase.
- Deterministic networks: EtherCAT, PROFINET IRT and TSN Ethernet give microsecond-level deterministic timing for motion control across hundreds of nodes.