So sánh I2C vs SPI vs UART: Khi nào dùng giao thức nào?
Trung cấp1/8/2026- Tác giả: IoTlabs Maker

So sánh I2C vs SPI vs UART: Khi nào dùng giao thức nào?

Bảng so sánh trực tiếp I2C, SPI, UART (số dây, tốc độ, cách chọn thiết bị) và ví dụ chạy đồng thời cả 3 giao thức trên cùng 1 board ESP32 DevKit.

I2CSPIUARTgiao-thucESP32kien-thuc-nen-tang

ESP32 hỗ trợ đồng thời 3 giao thức giao tiếp có dây phổ biến nhất: I2C (2 dây dùng chung, địa chỉ hoá 7-bit), SPI (4+ dây, chọn thiết bị bằng chân CS riêng, tốc độ cao) và UART (2 dây TX/RX, chỉ giao tiếp điểm-điểm giữa đúng 2 thiết bị, không địa chỉ hoá).

Bài này tổng hợp lại 3 bài trước (I2C, SPI, UART) thành bảng so sánh trực tiếp, đồng thời chạy thực tế cả 3 giao thức cùng lúc trên 1 board ESP32 DevKit: I2C với BMP280, SPI với màn hình TFT ST7735, và UART qua chính cổng USB-Serial dùng để debug.

Hướng dẫn chi tiết

Bảng so sánh trực tiếp I2C, SPI, UART và ví dụ chạy đồng thời cả 3 giao thức trên cùng 1 board ESP32.

1. Giới thiệu

Ba loạt bài trước đã đi sâu từng giao thức: I2C (2 dây dùng chung, địa chỉ 7-bit), SPI (4+ dây, chọn thiết bị bằng chân CS, tốc độ cao) và UART (2 dây TX/RX, không địa chỉ, chỉ điểm-điểm).

Bài này gộp lại thành bảng so sánh trực tiếp để chọn nhanh giao thức phù hợp, đồng thời chứng minh bằng thực nghiệm: ESP32 chạy được cả 3 giao thức song song trên cùng 1 board, mỗi giao thức phục vụ một thiết bị khác nhau.

2. Linh kiện cần có

Linh kiện

Số lượng

Vai trò

ESP32 DevKit V4

1

Board chính, chạy cả 3 giao thức

BMP280 (I2C)

1

Đại diện cho I2C

TFT 1.44" 128x128 SPI (ST7735)

1

Đại diện cho SPI

(không cần thêm phần cứng cho UART)

Dùng ngay cổng USB-Serial có sẵn trên board

3. Sơ đồ kết nối

BMP280 (I2C)

ESP32

VCC / GND

3V3 / GND

SDA

GPIO21

SCL

GPIO22

TFT ST7735 (SPI)

ESP32

VCC / GND

3V3 / GND

SCL/SCK

GPIO18

SDA/MOSI

GPIO23

CS

GPIO14

DC

GPIO27

RST

GPIO26

UART không cần dây thêm — chính cổng USB nối ESP32 với máy tính đã là 1 kênh UART (UART0), dùng ngay để xem log qua Serial Monitor.

4. Code ví dụ #1 — Chạy đồng thời cả 3 giao thức

Sketch khởi tạo cả BMP280 (I2C) và TFT ST7735 (SPI) trong setup(), đồng thời in log qua Serial (UART) mỗi 2 giây, vừa đọc nhiệt độ I2C vừa cập nhật hiển thị SPI — chứng minh 3 giao thức hoạt động độc lập, không xung đột dù dùng chung 1 vi điều khiển.

/*
  So sánh I2C vs SPI vs UART — Ví dụ #1: 3 giao thức trên cùng 1 board
  Board: ESP32 DevKit V4
    - I2C : BMP280 tại 0x76        SDA=GPIO21 SCL=GPIO22
    - SPI : TFT ST7735 128x128     SCK=GPIO18 MOSI=GPIO23 CS=GPIO14 DC=GPIO27 RST=GPIO26
    - UART: cổng USB-Serial có sẵn trên ESP32 DevKit (UART0, GPIO1/GPIO3) -
            không cần thêm phần cứng, dùng ngay Serial.begin() bên dưới.

  Mục tiêu bài học: cùng 1 board có thể chạy đồng thời cả 3 kiểu bus,
  minh hoạ khác biệt số dây cần dùng và cách địa chỉ hoá thiết bị.
*/

#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: Serial USB co san, khong can day rieng - day la vi du UART don gian nhat
  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. Code ví dụ #2 — Đo tốc độ thực tế I2C vs SPI

Biến thể thứ hai đo bằng micros() thời gian thực tế của 1 giao dịch đọc I2C (BMP280) so với 1 lệnh ghi SPI (vẽ ô màu lên TFT), in kết quả qua UART để người học tự so sánh bằng số liệu thật thay vì chỉ đọc datasheet.

/*
  So sánh I2C vs SPI vs UART — Ví dụ #2: đo tốc độ truyền thực tế của I2C và SPI
  Board: ESP32 DevKit V4 (cùng sơ đồ như ví dụ #1)
  In ra UART (Serial Monitor) thời gian (micro giây) mỗi lần đọc I2C và ghi SPI
  để người học tự so sánh, thay vì chỉ đọc số liệu datasheet.
*/

#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 - nhanh hon I2C rat nhieu
  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. Bảng so sánh tổng hợp — khi nào dùng giao thức nào

Tiêu chí

I2C

SPI

UART

Số dây tín hiệu

2 (SDA, SCL)

4+ (SCK, MOSI, MISO, CS/thiết bị)

2 (TX, RX)

Số thiết bị trên 1 bus

Nhiều (giới hạn bởi địa chỉ 7-bit, ~112 giá trị)

Nhiều (giới hạn bởi số chân CS còn trống)

Đúng 2 (điểm-điểm)

Cách chọn thiết bị

Địa chỉ trong khung dữ liệu

Chân CS vật lý

Không cần (chỉ có 1 kết nối)

Tốc độ điển hình

100k–400kHz

Vài MHz – vài chục MHz

9600 – 921600 bps (baud)

Đồng bộ

Có (dây SCL)

Có (dây SCK)

Không (thống nhất baud rate trước)

Dùng khi nào

Nhiều cảm biến nhỏ, tiết kiệm chân

Màn hình, thẻ nhớ, cần băng thông cao

Kết nối điểm-điểm, module GPS/SIM/RS232

7. Lỗi thường gặp

Lỗi

Nguyên nhân

Cách sửa

Chạy chung cả 3 giao thức bị treo máy

Trùng chân GPIO giữa các bus (ví dụ vô tình dùng lại GPIO18 cho GPIO khác)

Kiểm tra kỹ bảng phân bổ chân, không trùng giữa I2C/SPI/UART/GPIO thường

Không biết chọn giao thức nào cho thiết kế mới

Chưa xác định rõ: cần nhiều thiết bị hay tốc độ cao hay chỉ 2 thiết bị

Dùng bảng mục 6: nhiều thiết bị nhỏ → I2C; băng thông cao → SPI; 2 thiết bị đơn giản → UART

Thiết bị chỉ hỗ trợ 1 giao thức nhưng board đã hết chân phù hợp

Phân bổ chân từ đầu không tính trước cho tất cả giao thức cần dùng

Lên sơ đồ chân đầy đủ trước khi hàn/nối dây thật

8. Tổng kết

Không có giao thức nào "tốt nhất" tuyệt đối — mỗi giao thức tối ưu cho một nhu cầu khác nhau: I2C tiết kiệm chân khi có nhiều thiết bị nhỏ, SPI mạnh về tốc độ khi cần băng thông cao, UART đơn giản nhất khi chỉ cần nối đúng 2 thiết bị.

Trong thực tế, hầu hết dự án IoT dùng kết hợp cả 3: I2C cho cảm biến, SPI cho màn hình/lưu trữ, UART cho module GPS/SIM hoặc debug đúng như ví dụ ở bài này.