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
| Aspect | UART | I2C | SPI |
|---|---|---|---|
| Wires | 2 (TX/RX) | 2 (SDA/SCL) | 4+ (MOSI/MISO/SCK/CS) |
| Speed | commonly ≤115.2kbps | 100k/400kHz | up to 80MHz |
| Devices per bus | 1-to-1 | Many slaves (addressed) | Many slaves (one CS each) |
| Typical peripherals | GPS, BT modules, serial screens | OLED, temp/humidity, IMU | TFT screens, SD cards, flash |
| Pull-ups needed | No | Yes (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:
- Cross wiring: ESP32 TX goes to the peer’s RX (mouth to ear, like a phone call)
- Baud rates must match on both ends (GPS commonly 9600; BT 9600/115200)
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_AHTX0etc.) — 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:
- Module boards with onboard 10kΩ ✅ (practically all Taobao modules)
- Bare chips: add 4.7kΩ ×2 to 3.3V yourself
- Multiple modules on one bus: pulls parallel to smaller values (two 10k = 5k) — usually fine; only trim when stacking many
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:
| Signal | Direction | Role |
|---|---|---|
| MOSI | master→slave | data (Master Out Slave In) |
| MISO | slave→master | data |
| SCK | master→slave | clock (sets the speed) |
| CS | master→slave | chip 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:
| Mode | CPOL | CPHA | Idle level | Sampling edge |
|---|---|---|---|---|
| MODE0 | 0 | 0 | low | rising |
| MODE1 | 0 | 1 | low | falling |
| MODE2 | 1 | 0 | high | falling |
| MODE3 | 1 | 1 | high | rising |
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)
| Symptom | Cause and fix |
|---|---|
| I2C scan finds nothing | SDA/SCL swapped; no pull-ups; datasheet gave an 8-bit address (shift right by 1) |
| OLED dark but scanner sees 0x3C | Right address, wrong init (library: ssd1306_128x64 vs 72) |
| UART garbage | Baud mismatch; TX/RX not crossed |
| SPI screen stays white | Wrong MODE; CS not asserted; speed beyond the device |
| Two I2C modules clash | Same address — some modules are solder-configurable (0x76/0x77) |
| I2C occasionally hangs | Noise/weak pull-ups; advanced: 9-clock-bus unstick trick |
| Two devices on SPI | Independent CS per slave; SCK/MOSI/MISO shared |
6. Further reading
- 🧭 Previous: Interrupts — responding to UART arrivals
- ⏭️ Next: Wi-Fi First Steps — wireless and HTTP
- 🔍 Tool: Pinout Explorer — default bus pin assignments
License: CC BY-SA 4.0. Repost & remix with attribution: ESPDIY Maker Lab - espdiy.com