UART/Serial: Giao tiếp giữa 2 vi điều khiển
Trung cấp1/8/2026- Tác giả: IoTlabs Maker

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.

UARTSerialgiao-thucESP32kien-thuc-nen-tang

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.