
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.
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.