I2C vs SPI vs UART Compared: Which Protocol, When?
Intermediate1/8/2026- Author: IoTSpark Maker

I2C vs SPI vs UART Compared: Which Protocol, When?

A direct comparison table of I2C, SPI, and UART (wire count, speed, device selection) plus an example running all 3 protocols at once on a single ESP32 DevKit board.

I2CSPIUARTprotocolESP32kien-thuc-nen-tang
0 steps3 components

The ESP32 supports all 3 of the most common wired communication protocols at once: I2C (2 shared wires, 7-bit addressing), SPI (4+ wires, devices selected by a dedicated CS pin, high speed), and UART (2 wires TX/RX, point-to-point communication between exactly 2 devices, no addressing).

This lesson pulls together the 3 previous lessons (I2C, SPI, UART) into a direct comparison table, while actually running all 3 protocols at once on a single ESP32 DevKit board: I2C with a BMP280, SPI with an ST7735 TFT display, and UART over the very USB-Serial port used for debugging.

Detailed guide

A direct comparison of I2C, SPI, and UART with an example running all 3 protocols simultaneously on a single ESP32.

1. Introduction

The three previous lessons covered each protocol in depth: I2C (2 shared wires, 7-bit addressing), SPI (4+ wires, devices selected by a CS pin, high speed), and UART (2 wires TX/RX, no addressing, point-to-point only).

This lesson pulls them together into a direct comparison table for quickly picking the right protocol, and demonstrates it experimentally: the ESP32 can run all 3 protocols in parallel on the same board, each serving a different device.

2. Components Needed

ComponentQtyRole
ESP32 DevKit V41Main board, runs all 3 protocols
BMP280 (I2C)1Represents I2C
TFT 1.44" 128x128 SPI (ST7735)1Represents SPI
(no extra hardware needed for UART)Uses the board's built-in USB-Serial port directly

3. Wiring Diagram

BMP280 (I2C)ESP32
VCC / GND3V3 / GND
SDAGPIO21
SCLGPIO22
TFT ST7735 (SPI)ESP32
VCC / GND3V3 / GND
SCL/SCKGPIO18
SDA/MOSIGPIO23
CSGPIO14
DCGPIO27
RSTGPIO26

UART needs no extra wiring — the very USB cable connecting the ESP32 to your computer is already a UART channel (UART0), used directly to view logs via the Serial Monitor.

4. Example #1 — Running All 3 Protocols at Once

A sketch that initializes both the BMP280 (I2C) and the ST7735 TFT (SPI) in setup(), while printing a log over Serial (UART) every 2 seconds, reading temperature over I2C and updating the SPI display at the same time — demonstrating that all 3 protocols run independently without conflict, even sharing a single microcontroller.

/*
  I2C vs SPI vs UART Compared — Example #1: 3 protocols on one board
  Board: ESP32 DevKit V4
    - I2C : BMP280 at 0x76        SDA=GPIO21 SCL=GPIO22
    - SPI : TFT ST7735 128x128    SCK=GPIO18 MOSI=GPIO23 CS=GPIO14 DC=GPIO27 RST=GPIO26
    - UART: the ESP32 DevKit's built-in USB-Serial port (UART0, GPIO1/GPIO3) -
            no extra hardware needed, just use Serial.begin() below.

  Lesson goal: the same board can run all 3 bus types at once,
  illustrating the difference in wire count and how devices are addressed.
*/

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>

#define I2C_SDA 21
#define I2C_SCL 22
#define BMP280_ADDR 0x76

#define TFT_CS 14
#define TFT_DC 27
#define TFT_RST 26

Adafruit_BMP280 bmp;
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

void setup() {
  // UART: the built-in Serial USB, no dedicated wire needed - the simplest UART example
  Serial.begin(115200);
  delay(300);
  Serial.println(F("=== So sanh I2C / SPI / UART tren 1 board ==="));
  Serial.println(F("[UART] Serial USB dang chay 115200 baud (day nay chinh la UART0)"));

  // I2C: 2 day dung chung, dia chi hoa bang so 7-bit
  Wire.begin(I2C_SDA, I2C_SCL);
  bool bmpOk = bmp.begin(BMP280_ADDR);
  Serial.printf("[I2C] BMP280 tai 0x76: %s (SDA=%d SCL=%d)\n", bmpOk ? "OK" : "KHONG THAY", I2C_SDA, I2C_SCL);

  // SPI: nhieu day, moi thiet bi co CS rieng thay vi dia chi
  tft.initR(INITR_144GREENTAB);
  tft.fillScreen(ST77XX_BLACK);
  tft.setRotation(1);
  tft.setTextColor(ST77XX_WHITE);
  tft.setTextSize(1);
  tft.setCursor(4, 4);
  tft.println("I2C+SPI+UART");
  Serial.printf("[SPI] TFT ST7735: da khoi tao (SCK=18 MOSI=23 CS=%d DC=%d RST=%d)\n", TFT_CS, TFT_DC, TFT_RST);
}

void loop() {
  static unsigned long lastLoop = 0;
  if (millis() - lastLoop >= 2000) {
    lastLoop = millis();

    float tempC = bmp.readTemperature();
    Serial.printf("[UART->PC] Bao cao: [I2C] temp=%.1fC | [SPI] man hinh dang hien thi\n", tempC);

    tft.fillRect(4, 20, 110, 20, ST77XX_BLACK);
    tft.setCursor(4, 20);
    tft.printf("I2C T=%.1fC", tempC);
  }
}

5. Example #2 — Measuring Real I2C vs SPI Speed

A second variant that uses micros() to measure the actual duration of a single I2C read transaction (BMP280) versus a single SPI write command (drawing a colored square on the TFT), printing the results over UART so you can compare with real numbers instead of just reading datasheet figures.

/*
  I2C vs SPI vs UART Compared — Example #2: measuring real transfer speed of I2C and SPI
  Board: ESP32 DevKit V4 (same wiring as example #1)
  Prints over UART (Serial Monitor) the time (in microseconds) for each I2C read
  and SPI write, so you can compare with real numbers instead of just datasheet figures.
*/

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>

#define I2C_SDA 21
#define I2C_SCL 22
#define BMP280_ADDR 0x76

#define TFT_CS 14
#define TFT_DC 27
#define TFT_RST 26

Adafruit_BMP280 bmp;
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

void setup() {
  Serial.begin(115200);
  delay(300);
  Serial.println(F("=== Do toc do I2C (100kHz) vs SPI ==="));

  Wire.begin(I2C_SDA, I2C_SCL);
  Wire.setClock(100000); // I2C Standard-mode 100kHz
  bmp.begin(BMP280_ADDR);

  tft.initR(INITR_144GREENTAB);
  tft.setSPISpeed(4000000); // SPI 4MHz - much faster than I2C
  tft.fillScreen(ST77XX_BLACK);
}

void loop() {
  unsigned long t0 = micros();
  float tempC = bmp.readTemperature(); // 1 giao dich I2C hoan chinh
  unsigned long i2cDurationUs = micros() - t0;

  unsigned long t1 = micros();
  tft.fillRect(0, 0, 20, 20, ST77XX_RED); // 1 lenh ghi SPI len man hinh
  unsigned long spiDurationUs = micros() - t1;

  Serial.printf("[I2C] doc BMP280: %lu us | [SPI] ve o mau: %lu us | T=%.1fC\n",
                i2cDurationUs, spiDurationUs, tempC);

  delay(1500);
}

6. Summary Comparison Table — Which Protocol, When?

CriterionI2CSPIUART
Signal wire count2 (SDA, SCL)4+ (SCK, MOSI, MISO, CS per device)2 (TX, RX)
Devices per busMany (limited by 7-bit address, ~112 values)Many (limited by available CS pins)Exactly 2 (point-to-point)
How a device is selectedAn address in the data frameA physical CS pinNot needed (only 1 connection)
Typical speed100k–400kHzA few MHz – tens of MHz9600 – 921600 bps (baud)
SynchronousYes (SCL wire)Yes (SCK wire)No (baud rate must be agreed beforehand)
Best used forMany small sensors, saving pinsDisplays, memory cards, when high bandwidth is neededPoint-to-point links, GPS/SIM/RS232 modules

7. Common Issues

IssueCauseFix
The board hangs when running all 3 protocols togetherOverlapping GPIO pins between buses (e.g. accidentally reusing GPIO18 for something else)Carefully check the pin-allocation table, avoiding overlap between I2C/SPI/UART/plain GPIO
Not sure which protocol to pick for a new designHaven't clarified: many devices needed, or high speed, or just 2 devicesUse the table in section 6: many small devices → I2C; high bandwidth → SPI; 2 simple devices → UART
A device only supports one protocol, but the board is out of suitable pinsPin allocation wasn't planned upfront for every protocol neededMap out the full pin diagram before soldering/wiring for real

8. Summary

No protocol is universally "best" — each is optimized for a different need: I2C saves pins with many small devices, SPI wins on speed when high bandwidth is needed, UART is simplest when connecting exactly 2 devices.

In practice, most IoT projects combine all three: I2C for sensors, SPI for displays/storage, UART for GPS/SIM modules or debugging — exactly as shown in this lesson's example.