ESP32 Communication Buses Guide: UART, I2C and SPI

Why buses exist: GPIO/ADC only “read voltages” — but OLED screens, sensor chips and GPS modules have minds of their own. They converse through buses. Picking the right bus is the first decision of any peripheral project.


1. The three buses at a glance

UART (two-wire chat):     I2C (one-master bus):       SPI (high-speed dedicated):
ESP32 ──TX──> RX── device  ESP32 ──SDA── many devices  ESP32 ──MOSI──> device
      <──RX<── TX──           └──SCL── (shared 2 wires)      <──MISO<──
                           each slave has a 7-bit addr  ──SCK── + one CS per slave
AspectUARTI2CSPI
Wires2 (TX/RX)2 (SDA/SCL)4+ (MOSI/MISO/SCK/CS)
Speedcommonly ≤115.2kbps100k/400kHzup to 80MHz
Devices per bus1-to-1Many slaves (addressed)Many slaves (one CS each)
Typical peripheralsGPS, BT modules, serial screensOLED, temp/humidity, IMUTFT screens, SD cards, flash
Pull-ups neededNoYes (4.7k–10k)No

Selection mnemonic: slow small parts → I2C (saves pins); screens and storage → SPI (bandwidth); module conversations → UART (ready-made protocol).


2. UART: more than the debug port

Serial is just the default UART. The ESP32 has three hardware UARTs; Serial1/Serial2 can be remapped to any GPIO:

// GPS on Serial2: RX16 ← GPS TX, TX17 → GPS RX
#define GPS_RX 16
#define GPS_TX 17

HardwareSerial GPSSerial(2);   // using UART2

void setup() {
  Serial.begin(115200);              // debug (UART0)
  GPSSerial.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX);
}

void loop() {
  while (GPSSerial.available()) {
    Serial.write(GPSSerial.read());  // pipe NMEA sentences through
  }
}

Three classic mistakes:

  1. Cross wiring: ESP32 TX goes to the peer’s RX (mouth to ear, like a phone call)
  2. Baud rates must match on both ends (GPS commonly 9600; BT 9600/115200)
  3. SERIAL_8N1 = 8 data bits, no parity, 1 stop bit — the default of nearly everything

The interrupts “heavy work in loop” principle applies: UART data arrival is an event — buffer, then parse in loop.


3. I2C: a pile of sensors on two wires

1. Master-slave and 7-bit addresses

I2C is a one-master bus: the ESP32 initiates everything; slaves answer to factory-hardwired 7-bit addresses. Common ones: SSD1306 OLED = 0x3C, BMP280 = 0x76/0x77, MCP4725 DAC = 0x62.

2. Run the scanner first (step one for any new module)

#include <Wire.h>

void setup() {
  Wire.begin(21, 22);   // SDA=21, SCL=22 (remappable)
  Serial.begin(115200);
}

void loop() {
  Serial.println("Scanning I2C bus...");
  for (uint8_t addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    if (Wire.endTransmission() == 0) {     // 0 = a device ACKed
      Serial.printf("Device found: 0x%02X\n", addr);
    }
  }
  delay(5000);
}

Nothing found = wiring/pull-up trouble (see the cheat sheet). Fix that before touching the protocol.

3. Reading temperature (raw Wire conversation)

// AHT20 (address 0x38), master flow shown
Wire.beginTransmission(0x38);
Wire.write(0xAC); Wire.write(0x33); Wire.write(0x00);   // trigger measurement
Wire.endTransmission();
delay(80);                                              // conversion time

Wire.requestFrom(0x38, 7);                              // read 7 bytes
uint8_t data[7];
for (int i = 0; i < 7; i++) data[i] = Wire.read();
// convert data[1..6] per the datasheet formulas

In practice you’ll use a sensor library (Adafruit_AHTX0 etc.) — but understanding this raw conversation tells you what to check when the library misbehaves.

4. Pull-up resistors

SDA/SCL are open-drain — high levels only exist with pull-ups:


4. SPI: the speed king for screens and storage

A color TFT moves hundreds of KB per frame — I2C’s 400kHz chokes. SPI trades four wires for bandwidth:

SignalDirectionRole
MOSImaster→slavedata (Master Out Slave In)
MISOslave→masterdata
SCKmaster→slaveclock (sets the speed)
CSmaster→slavechip select (which slave is active)
#include <SPI.h>

// VSPI defaults: SCK=18, MISO=19, MOSI=23, CS=5
SPIClass SPI(VSPI);
#define TFT_CS 5

void setup() {
  SPI.begin(18, 19, 23, TFT_CS);
  SPI.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE0));  // 40MHz
  digitalWrite(TFT_CS, LOW);
  SPI.transfer16(0xABCD);   // send 16 bits
  digitalWrite(TFT_CS, HIGH);
}

CPOL/CPHA: the four SPI modes

Clock polarity (CPOL) and phase (CPHA) combine into 4 modes that must match the slave:

ModeCPOLCPHAIdle levelSampling edge
MODE000lowrising
MODE101lowfalling
MODE210highfalling
MODE311highrising

A white/garbled screen or an SD init failure: check the mode first (datasheet “SPI timing”), then the wiring. Most devices are MODE0 or MODE3.

Full TFT practice (TFT_eSPI configuration) is an L3 display topic — here you only need the underlying layer.


5. Pitfall cheat sheet (bookmark this)

SymptomCause and fix
I2C scan finds nothingSDA/SCL swapped; no pull-ups; datasheet gave an 8-bit address (shift right by 1)
OLED dark but scanner sees 0x3CRight address, wrong init (library: ssd1306_128x64 vs 72)
UART garbageBaud mismatch; TX/RX not crossed
SPI screen stays whiteWrong MODE; CS not asserted; speed beyond the device
Two I2C modules clashSame address — some modules are solder-configurable (0x76/0x77)
I2C occasionally hangsNoise/weak pull-ups; advanced: 9-clock-bus unstick trick
Two devices on SPIIndependent CS per slave; SCK/MOSI/MISO shared

6. Further reading


License: CC BY-SA 4.0. Repost & remix with attribution: ESPDIY Maker Lab - espdiy.com