I2C, SPI, and UART
Almost every sensor, display, memory chip, GPS module, and radio talks to a microcontroller over one of three serial interfaces. UART sends bytes asynchronously over two wires with no shared clock. I²C shares two wires among many devices, each with an address. SPI uses a clock, two data lines, and a chip-select line per device to move data quickly. Each has its own failure modes, and most "the sensor doesn't work" problems are really interface problems: wrong address, missing pull-ups, mismatched baud rate, wrong SPI mode, or incompatible voltage levels.
Understanding how each bus frames data, who drives the clock, and what the electrical requirements are makes these problems quick to diagnose — especially with a logic analyzer to see what is actually on the wires.
TL;DR
- UART: two data lines (TX, RX), no clock; both sides must agree on baud rate and framing (commonly 8N1). Point-to-point.
- I²C: two shared lines (SDA, SCL), open-drain with pull-up resistors, 7-bit addresses, ACK/NACK per byte, 100 kHz to 1 MHz typical.
- SPI: clock, two data lines, and a chip select per device; four modes (CPOL/CPHA); fast and full duplex.
- Choose I²C for many slow devices on few pins, SPI for speed (displays, flash, ADCs), UART for modules and consoles.
- Check logic levels (3.3 V versus 5 V), wiring length, and grounds before suspecting code.
- Debug with scans, known registers (chip IDs), and a logic analyzer with protocol decoding.
Quick Example
Talking to devices from Linux (for example a Raspberry Pi) with standard userspace interfaces:
Core Concepts
UART
Common failures: wrong baud rate (garbage characters), TX and RX not crossed, inverted levels between TTL and RS-232, missing ground, and clock inaccuracy at high baud rates.
I²C
Pull-up values trade speed against power: too weak (large resistance) gives slow rising edges that fail at higher speeds or with long wires; too strong wastes current. Address confusion is common: datasheets list either the 7-bit address (0x76) or the 8-bit form including the read/write bit (0xEC/0xED). A device that resets mid-transaction can hold SDA low and lock the bus; recover by toggling SCL up to nine times and issuing a stop condition.
SPI
Common failures: wrong mode (data shifted by one bit or garbage), chip select not asserted or released too early, clock too fast for the wiring, and forgetting that reads require sending dummy bytes to clock data in.
Choosing an Interface
Electrical and Wiring Considerations
- Logic levels: connecting a 5 V device to a 3.3 V microcontroller pin can damage it; use level shifters (bidirectional open-drain shifters for I²C).
- Grounds: every link needs a common ground.
- Distance and capacitance: I²C is designed for short, on-board connections; long wires add capacitance and slow edges. SPI at high speed is sensitive to wire length and ringing.
- Pull-ups: some breakout boards include them; too many in parallel lower the effective resistance.
Software Considerations
Use DMA for large SPI and UART transfers to free the CPU, and interrupts rather than busy-waiting for UART reception, with ring buffers sized for bursts. Add timeouts to every bus operation so a stuck device cannot hang the firmware, and retry or reset the bus on errors. See Interrupts, Timers, and Peripheral Drivers. On Linux, use the kernel's i2c-dev, spidev, and serial (tty) interfaces or write kernel drivers for production devices.
Troubleshooting Method
- Power and ground: is the device powered at the right voltage, and is ground shared?
- Wiring: TX/RX crossed? Correct pins for SDA/SCL and SPI lines? Chip select connected?
- Configuration: baud rate and framing, I²C address (7-bit), SPI mode and clock speed.
- Known-good read: read a chip ID or version register before anything else.
- Look at the wires: a logic analyzer with I²C, SPI, and UART decoders shows addresses, ACKs, and data; an oscilloscope shows edges and levels. See Hardware Debugging with JTAG and Logic Analyzers.
Best Practices
Read a Known Register First
Verify communication with a chip ID or version register before implementing full drivers.
Put Timeouts on Every Transaction
Never let firmware wait forever for a bus; detect failures and recover.
Match Electrical Requirements
Check voltage levels, pull-ups, and wiring length against datasheets and bus specifications.
Keep a Logic Analyzer Nearby
Seeing actual traffic answers most interface questions in minutes.
Common Mistakes
Using the 8-Bit I2C Address
Missing Pull-Ups
Wrong SPI Mode
FAQ
What is the difference between I2C and SPI?
I²C uses two shared lines with addresses and acknowledgements, suited to many slower devices; SPI uses separate chip-select lines and a faster, full-duplex clocked transfer, suited to high-throughput devices.
Why does I2C need pull-up resistors?
Because I²C lines are open-drain: devices can only pull them low, so pull-up resistors are needed to return the lines to the high state.
What does 8N1 mean for UART?
Eight data bits, no parity bit, and one stop bit per character — the most common UART framing.
Can multiple devices share a UART?
Not with plain UART, which is point-to-point. RS-485 allows multiple devices on one differential bus with a protocol (such as Modbus RTU) to manage who talks.
Why do I get garbage characters on a serial console?
Usually a baud rate or framing mismatch, inverted logic levels, a missing ground connection, or noise on long cables.
Related Topics
- Embedded Systems & IoT — Software for constrained devices
- Microcontrollers — Peripherals that implement these buses
- Raspberry Pi and Embedded Linux — Accessing buses from Linux
- Arduino — Wire, SPI, and Serial libraries
- Hardware Debugging with JTAG and Logic Analyzers — Seeing bus traffic
- ESP32 — Configurable I²C, SPI, and UART peripherals