In short: spi vs i2c vs uart comes down to three trade-offs — speed, pin count, and how many devices share the bus. SPI is the fastest and simplest to clock but needs a chip-select line per peripheral; I2C uses just two shared wires and device addresses but tops out slower; UART is a cheap, clockless point-to-point link ideal for connecting two chips or a console.
SPI (Serial Peripheral Interface), I2C (Inter-Integrated Circuit) and UART (Universal Asynchronous Receiver/Transmitter) are the three serial communication protocols you will meet on almost every microcontroller and embedded board. They all move data one bit at a time over a small number of wires, but they differ in how they clock that data, how they address peripherals, and how fast and how far they can push bits.
The quick mental model: UART is a two-wire conversation between exactly two parties with no shared clock. I2C is a two-wire shared bus where many devices listen and a controller calls each one by address. SPI is a four-wire bus built for raw speed, where the controller selects one peripheral at a time with a dedicated line.
Wiring is the single clearest way to tell these protocols apart, because the pin count drives most of the other trade-offs.
UART needs just TX (transmit) and RX (receive), plus a common ground. The TX of one device connects to the RX of the other and vice versa. There is no clock line, so both ends must agree in advance on a baud rate. It is inherently point-to-point: one device talks to exactly one other.
I2C uses SDA (serial data) and SCL (serial clock), both open-drain and pulled high by resistors. Every device hangs off the same two lines, so adding a tenth sensor costs no extra pins on the controller — only a unique address.
SPI uses SCLK (clock), MOSI (controller out, peripheral in), MISO (peripheral out, controller in) and one CS/SS (chip-select) line per peripheral. Five peripherals means five chip-selects, which is SPI's main scaling cost.
Choosing the wrong bus shows up later as missed deadlines, bus contention, or a redesign when you run out of pins. Three properties decide the fit.
SPI is synchronous — the controller drives SCLK, so the peripheral never has to guess timing, and clock rates of 10–50+ MHz are routine, making SPI the go-to for displays, SD cards and high-rate ADCs. I2C is also synchronous but open-drain pull-ups limit standard modes to 100 kHz, 400 kHz, 1 MHz (Fast-mode Plus) and 3.4 MHz (High-speed). UART is asynchronous with no clock wire; common baud rates run 9,600 to 115,200, with higher rates possible but sensitive to clock-accuracy mismatch.
SPI is full duplex: MOSI and MISO carry data in both directions simultaneously, and in fact a bit is shifted out and in on every clock edge. I2C is half duplex: one shared SDA line means a device either sends or receives at a given moment, never both. UART is full duplex because TX and RX are separate wires, though many simple links only use one direction.
I2C wins on scaling — a 7-bit address field allows up to 112 usable addresses on two wires. SPI has no addressing; the controller asserts a chip-select to pick a device, so device count is bounded by available GPIO. UART has no addressing at all and connects exactly two endpoints (barring extra transceiver hardware like RS-485).
There is no universally "best" protocol — the right choice follows the peripheral and the board constraints.
Common failure modes are protocol-specific too. On I2C, a forgotten or wrong-value pull-up resistor is the number-one cause of a dead bus, and two devices sharing one address force you to use an address-select pin or an I2C mux. On UART, a baud-rate mismatch or wrong framing (data bits, parity, stop bits) produces garbage characters. On SPI, the usual culprit is a wrong mode (CPOL/CPHA) so the controller samples on the wrong clock edge.
Professional bring-up rarely relies on guesswork. A logic analyzer (Saleae, or an open-source sigrok/PulseView setup) captures the raw lines and decodes SPI, I2C and UART frames in software, so you can confirm an I2C address acknowledgment (ACK/NACK) or read the actual bytes on MOSI. An oscilloscope with protocol decode verifies signal integrity — rise times on I2C pull-ups, clean SPI clock edges. On the firmware side, vendor HALs (STM32 HAL/LL, ESP-IDF, Zephyr, Linux's `spidev` and `i2c-dev`) expose each bus through driver APIs, and device trees or board files declare which pins and clock rates a peripheral uses. Mastering these decode-and-verify tools is what separates a datasheet reader from an engineer who can debug a bus at 2 a.m.
Real projects and industry tool flows, with certification.
Explore CourseTron Courses →SPI is the fastest. Its push-pull clock line has no open-drain pull-up limiting rise time, so it clocks comfortably at tens of MHz. I2C is capped by its open-drain bus (typically up to 400 kHz or 1 MHz Fast-mode Plus, 3.4 MHz High-speed). UART, being asynchronous, is limited by clock-accuracy tolerance between the two ends and usually runs well below 1 Mbps.
I2C lines are open-drain: devices can only pull SDA/SCL low, so an external pull-up resistor restores the high level and lets any device signal without bus contention. SPI uses push-pull drivers that actively drive both high and low, so it needs no pull-ups.
UART is asynchronous — there is no shared clock wire. Both ends must independently agree on the baud rate plus the frame format: number of data bits, parity, and stop bits. A start bit lets the receiver synchronize to each frame.
On SPI, the controller asserts that device's dedicated chip-select (CS/SS) line. On I2C, the controller sends the device's 7-bit (or 10-bit) address at the start of a transaction and the matching device ACKs. On UART there is no addressing — it is inherently point-to-point between two devices.
Related: a related topic · the VLSI Glossary · all courses.