
UART/Serial: Giao tiếp giữa 2 vi điều khiển
Nguyên lý UART/Serial (TX/RX, baud rate, khung 8N1) qua ví dụ thực tế: 2 board ESP32 DevKit trao đổi dữ liệu qua UART2 phần cứng, dây chéo TX-RX.
UART (Universal Asynchronous Receiver-Transmitter) là kiểu giao tiếp nối tiếp không đồng bộ — không có dây xung nhịp (clock) riêng như I2C/SPI, hai bên chỉ đồng bộ được với nhau nhờ thống nhất trước tốc độ baud (ví dụ 115200 bit/giây) và khung dữ liệu (8 bit dữ liệu, không parity, 1 stop bit — viết tắt 8N1).
Giao tiếp chỉ cần 2 dây tín hiệu: TX (Transmit) và RX (Receive), luôn phải nối chéo - TX bên này vào RX bên kia.
Bài này dùng UART2 phần cứng của ESP32 (không đụng UART0 đang bận phục vụ Serial Monitor qua USB) để 2 board ESP32 DevKit trao đổi dữ liệu trực tiếp với nhau, không qua WiFi/Bluetooth.
Hướng dẫn chi tiết
Nguyên lý UART (TX/RX, baud rate, khung 8N1), thực hành 2 board ESP32 trao đổi dữ liệu qua UART2 phần cứng với dây chéo TX-RX.
1. Giới thiệu
UART (Universal Asynchronous Receiver-Transmitter) là kiểu giao tiếp nối tiếp không đồng bộ: không có dây clock riêng, hai thiết bị tự đếm thời gian theo baud rate đã thống nhất trước (ví dụ 115200 bit/giây) để biết khi nào lấy mẫu từng bit.
Khung dữ liệu UART phổ biến là 8N1: 1 start bit, 8 bit dữ liệu, không có parity bit, 1 stop bit. Chỉ cần 2 dây tín hiệu TX (Transmit) và RX (Receive) cho giao tiếp 2 chiều, nhưng bắt buộc phải nối chéo: chân TX của thiết bị này nối vào chân RX của thiết bị kia (không bao giờ nối TX-TX hoặc RX-RX).
ESP32 có 3 bộ UART phần cứng độc lập (UART0, UART1, UART2). UART0 mặc định gắn với cổng USB để phục vụ nạp code và Serial Monitor — nếu dùng UART0 để giao tiếp với board khác sẽ xung đột với việc debug qua máy tính.
Bài này dùng UART2 (chân mặc định RX2=GPIO16, TX2=GPIO17 trên ESP32 DevKit) làm kênh giao tiếp riêng giữa 2 board, giữ UART0/Serial Monitor rảnh để xem log debug qua USB.
2. Linh kiện cần có
Linh kiện | Số lượng | Ghi chú |
|---|---|---|
ESP32 DevKit V4 (Board A) | 1 | Nạp code "sample1" — vai gửi PING |
ESP32 DevKit V4 (Board B) | 1 | Nạp code "sample2" — vai nhận và echo ACK |
Dây jumper | 3 | TX↔RX chéo, GND chung |
3. Sơ đồ kết nối
Board A (ESP32) | Board B (ESP32) |
|---|---|
TX2 / GPIO17 | RX2 / GPIO16 |
RX2 / GPIO16 | TX2 / GPIO17 |
GND | GND |
Lưu ý quan trọng: đây là dây chéo — TX của Board A phải cắm vào RX của Board B, và ngược lại. Cả hai board vẫn giữ nguyên cáp USB riêng để cấp nguồn + xem Serial Monitor (UART0) độc lập; dây UART2 chỉ dùng để 2 board nói chuyện trực tiếp với nhau.
4. Code ví dụ #1 — Board A (gửi PING, nhận phản hồi)
Board A dùng HardwareSerial LinkSerial(2) khởi tạo UART2 với begin(115200, SERIAL_8N1, 16, 17), gửi 1 dòng PING:<số đếm>\n mỗi giây qua TX2, đồng thời lắng nghe phản hồi từ Board B trên RX2 và in ra Serial Monitor (UART0/USB).
/*
UART/Serial: Giao tiếp giữa 2 vi điều khiển — BOARD A (gửi lệnh, nhận phản hồi)
Board: ESP32 DevKit V4 #A <--> ESP32 DevKit V4 #B (sample "Board B")
Dùng UART2 phần cứng (không đụng UART0 đang bận cho USB/Serial Monitor):
Board A TX2 (GPIO17) --> Board B RX2 (GPIO16)
Board A RX2 (GPIO16) <-- Board B TX2 (GPIO17)
Board A GND --- Board B GND (bắt buộc: 2 board phải chung mass)
Giao thức khung dữ liệu đơn giản: mỗi dòng kết thúc bằng '\n'.
Board A đóng vai "master": gửi số đếm mỗi giây, chờ Board B echo lại.
*/
HardwareSerial LinkSerial(2); // UART2
#define LINK_RX_PIN 16
#define LINK_TX_PIN 17
uint32_t counter = 0;
unsigned long lastSend = 0;
void setup() {
Serial.begin(115200); // UART0 -> USB, chỉ dùng để debug/monitor
delay(300);
Serial.println(F("=== UART Board A boot ==="));
LinkSerial.begin(115200, SERIAL_8N1, LINK_RX_PIN, LINK_TX_PIN);
Serial.println(F("UART2 san sang: TX=GPIO17 RX=GPIO16 @115200 8N1"));
}
void loop() {
// Gui 1 dong moi giay
if (millis() - lastSend >= 1000) {
lastSend = millis();
counter++;
LinkSerial.printf("PING:%lu\n", counter);
Serial.printf("[A->B] PING:%lu\n", counter);
}
// Doc phan hoi tu Board B (neu co)
static String rxBuf;
while (LinkSerial.available()) {
char c = (char)LinkSerial.read();
if (c == '\n') {
Serial.printf("[B->A] %s\n", rxBuf.c_str());
rxBuf = "";
} else if (c != '\r') {
rxBuf += c;
}
}
}
5. Code ví dụ #2 — Board B (nhận PING, echo ACK)
Board B lắng nghe UART2, khi nhận đủ 1 dòng kết thúc bằng \n thì tách số đếm và gửi lại ACK:<số đếm>\n — minh hoạ giao tiếp 2 chiều thực sự (không chỉ 1 chiều broadcast).
/*
UART/Serial: Giao tiếp giữa 2 vi điều khiển — BOARD B (nhận lệnh, echo phản hồi)
Board: ESP32 DevKit V4 #B <--> ESP32 DevKit V4 #A (sample "Board A")
Dây nối chéo TX<->RX giữa 2 board (bắt buộc với UART: chân TX của bên này
phải vào chân RX của bên kia, không nối TX-TX):
Board B TX2 (GPIO17) --> Board A RX2 (GPIO16)
Board B RX2 (GPIO16) <-- Board A TX2 (GPIO17)
Board B GND --- Board A GND
*/
HardwareSerial LinkSerial(2); // UART2
#define LINK_RX_PIN 16
#define LINK_TX_PIN 17
void setup() {
Serial.begin(115200);
delay(300);
Serial.println(F("=== UART Board B boot ==="));
LinkSerial.begin(115200, SERIAL_8N1, LINK_RX_PIN, LINK_TX_PIN);
Serial.println(F("UART2 san sang: TX=GPIO17 RX=GPIO16 @115200 8N1"));
}
void loop() {
static String rxBuf;
while (LinkSerial.available()) {
char c = (char)LinkSerial.read();
if (c == '\n') {
Serial.printf("[A->B] nhan: %s\n", rxBuf.c_str());
// Tach so dem sau dau ':' va echo lai kem nhan "ACK"
int sep = rxBuf.indexOf(':');
String payload = (sep >= 0) ? rxBuf.substring(sep + 1) : rxBuf;
LinkSerial.printf("ACK:%s\n", payload.c_str());
Serial.printf("[B->A] gui: ACK:%s\n", payload.c_str());
rxBuf = "";
} else if (c != '\r') {
rxBuf += c;
}
}
}
6. Ứng dụng thực tế & mở rộng
Giao tiếp UART trực tiếp giữa 2 vi điều khiển thường dùng khi: (1) một board phụ trách phần cứng thời gian thực (đọc encoder, điều khiển động cơ tốc độ cao) và gửi kết quả tóm tắt cho board chính xử lý WiFi/logic; (2) module GPS, module SIM/4G, cảm biến công nghiệp RS232/RS485 đều dùng UART làm giao tiếp gốc; (3) debug bridge giữa 2 hệ thống khi không có bus chung nào khác. Vì UART không có địa chỉ hoá như I2C và không có CS như SPI, nó chỉ phù hợp cho kết nối điểm-điểm (point-to-point) giữa đúng 2 thiết bị.
7. Lỗi thường gặp
Lỗi | Nguyên nhân | Cách sửa |
|---|---|---|
Không nhận được gì / dữ liệu rác | Baud rate 2 bên không khớp nhau | Đảm bảo cả 2 board dùng đúng cùng 1 giá trị baud (ở đây 115200) |
Không giao tiếp được dù dây đã cắm | Nối TX-TX / RX-RX thay vì chéo | Đảo lại: TX board này phải vào RX board kia |
Dữ liệu lẫn lộn với log debug | Nhầm dùng UART0 (Serial) cho cả debug lẫn giao tiếp liên board | Tách riêng: Serial (UART0/USB) chỉ debug, LinkSerial (UART2) chỉ giao tiếp liên board |
Hai board chạy độc lập không lỗi nhưng không thấy phản hồi | Thiếu dây GND chung giữa 2 board | Nối GND-GND bắt buộc để 2 UART có cùng mức điện áp tham chiếu |
8. Tổng kết
UART là giao thức đơn giản nhất trong 3 giao thức (I2C/SPI/UART): chỉ 2 dây, không cần địa chỉ hay chân chọn thiết bị, nhưng đổi lại chỉ giao tiếp được giữa đúng 2 thiết bị và đòi hỏi 2 bên phải thống nhất trước tốc độ baud.
Đây là lựa chọn tự nhiên khi cần kết nối điểm-điểm giữa 2 vi điều khiển, hoặc giao tiếp với các module ngoại vi (GPS, SIM, RS232) vốn chỉ hỗ trợ UART.