UART là gì: khung truyền, tốc độ baud và cách dùng Serial2 trên ESP32

Giải thích giao tiếp UART: khung truyền 8N1, tốc độ baud, nối chéo TX với RX. Hướng dẫn dùng Serial2 của ESP32 để tự gửi tự nhận và để hai mạch trao đổi từng dòng lệnh.

UART là kiểu giao tiếp nối tiếp đơn giản nhất của vi điều khiển: hai dây dữ liệu, không cần dây clock. Dòng chữ hiện trên Serial Monitor, lệnh AT gửi cho module GPS hay Bluetooth, và cả Modbus RTU trên RS-485 đều dựa trên UART. Bài viết giải thích khung truyền của một byte, ý nghĩa của tốc độ baud và 8N1, rồi dùng cổng Serial2 của ESP32 để tự kiểm tra và để hai mạch nói chuyện với nhau.

1. UART là gì?

  • UART (Universal Asynchronous Receiver-Transmitter) là bộ thu phát nối tiếp không đồng bộ: dữ liệu đi từng bit một trên một dây, không có dây clock đi kèm.
  • Mỗi thiết bị có một chân gửi TX và một chân nhận RX. Hai chiều dùng hai dây riêng nên hai bên gửi cùng lúc được.
  • UART chỉ nối hai thiết bị với nhau. Muốn nhiều thiết bị trên cùng một đường dây thì cần thêm lớp vật lý khác như RS-485.
  • Vì không có clock, hai bên phải đặt trước cùng một tốc độ và cùng một định dạng khung.

2. Khung truyền của một byte

Khung truyền UART 8N1 của ký tự W có mã 0x57: đường truyền nghỉ ở mức cao, một bit bắt đầu mức thấp, tám bit dữ liệu gửi bit thấp trước và một bit kết thúc mức cao
Khung truyền 8N1 của ký tự 'W' (0x57): bit bắt đầu, 8 bit dữ liệu, bit kết thúc.
  • Trạng thái nghỉ: khi không gửi gì, chân TX ở mức cao.
  • Bit bắt đầu: một bit mức thấp. Sườn xuống này là mốc để bên nhận bắt đầu canh thời gian.
  • Bit dữ liệu: thường là 8 bit, gửi bit thấp nhất trước.
  • Bit chẵn lẻ (parity): tùy chọn, dùng để phát hiện lỗi một bit. Phần lớn ứng dụng không dùng.
  • Bit kết thúc: một hoặc hai bit mức cao, đưa đường truyền về trạng thái nghỉ.

Ký hiệu 8N1 nghĩa là 8 bit dữ liệu, không có parity (None), 1 bit kết thúc. Đây là định dạng mặc định của Arduino. Với 8N1, mỗi byte chiếm 10 bit trên dây.

3. Tốc độ baud

Tốc độ baud là số bit truyền trong một giây. Bên nhận đọc mỗi bit ở giữa khoảng thời gian của bit đó, nên đồng hồ của hai bên chỉ được lệch nhau vài phần trăm.

Tốc độ baudThời gian một bitSố byte mỗi giây (8N1)Thường gặp ở
9600104,2 µs960Module GPS, cảm biến, Modbus RTU
1920052,1 µs1920Modbus RTU
1152008,68 µs11.520Serial Monitor, nạp chương trình
9216001,09 µs92.160Nạp chương trình nhanh, truyền dữ liệu lớn
  • Đặt sai tốc độ baud thì bên nhận vẫn thấy có dữ liệu nhưng toàn ký tự vô nghĩa. Đây là lỗi thường gặp nhất với UART.
  • Tốc độ càng cao, dây càng phải ngắn. Ở mức 3,3 V, dây dài quá vài mét dễ gây lỗi; đi xa hơn nên chuyển sang RS-485.

4. UART trên ESP32

CổngĐối tượng trong ArduinoChân mặc định trên DevKit V1Ghi chú
UART0SerialTX0 (GPIO1), RX0 (GPIO3)Nối với chip USB: dùng để nạp chương trình và Serial Monitor
UART1Serial1Phải tự chọn chânChân mặc định trùng với bộ nhớ flash của module, không dùng được
UART2Serial2TX2 (GPIO17), RX2 (GPIO16)Dùng tự do; bài này dùng cổng này
  • ESP32 cho gán UART vào hầu hết các chân: Serial2.begin(tốc độ, SERIAL_8N1, chân RX, chân TX).
  • Các chân của ESP32 làm việc ở 3,3 V và không chịu được 5 V. Chân TX của thiết bị 5 V phải qua mạch chia áp hoặc mạch chuyển mức trước khi vào RX của ESP32.
  • Không nối thiết bị khác vào TX0, RX0 khi đang nạp chương trình.

5. Chuẩn bị

6. Chương trình 1: tự gửi tự nhận để xem khung truyền

Chỉ cần một mạch và một dây nối TX2 (GPIO17) sang RX2 (GPIO16). Mỗi giây chương trình gửi một chuỗi ba ký tự, đọc lại và in từng byte kèm dãy bit đã đi trên dây.

/*
  PYWORLD.VN - UART trên ESP32: tự gửi tự nhận qua Serial2 để xem khung truyền của từng byte
  Nối một dây từ TX2 (GPIO17) sang RX2 (GPIO16); mỗi giây gửi một chuỗi ngắn rồi in lại các byte nhận được
  Mỗi byte in kèm dãy bit đi trên dây: bit bắt đầu, 8 bit dữ liệu (bit thấp đi trước), bit kết thúc
*/

#define RX2_PIN 16
#define TX2_PIN 17

const uint32_t TOC_DO_BAUD = 9600;

// In một byte theo đúng thứ tự bit xuất hiện trên dây ở định dạng 8N1
void inKhungTruyen(uint8_t giaTri) {
  Serial.printf("  0x%02X '%c' : 0 ", giaTri, isPrintable(giaTri) ? giaTri : '.');
  for (int i = 0; i < 8; i++) {
    Serial.print((giaTri >> i) & 1);
  }
  Serial.println(" 1");
}

void setup() {
  Serial.begin(115200);
  Serial2.begin(TOC_DO_BAUD, SERIAL_8N1, RX2_PIN, TX2_PIN);

  Serial.printf("\nSerial2: %u baud, 8N1\n", (unsigned)TOC_DO_BAUD);
  Serial.printf("Moi bit dai %.1f us, moi byte (10 bit) mat %.2f ms\n",
                1000000.0 / TOC_DO_BAUD, 10000.0 / TOC_DO_BAUD);
}

void loop() {
  static int soLan = 0;
  String chuoi = "PW" + String(soLan % 10);
  soLan++;

  Serial2.print(chuoi);
  Serial2.flush();   // Chờ đến khi byte cuối rời khỏi chân TX2
  delay(20);         // Chờ các byte quay về bộ đệm nhận

  Serial.printf("Gui \"%s\", nhan lai %d byte\n", chuoi.c_str(), Serial2.available());
  if (Serial2.available() == 0) {
    Serial.println("  Khong nhan duoc gi: kiem tra day noi TX2 -> RX2");
  }
  while (Serial2.available()) {
    inKhungTruyen(Serial2.read());
  }
  delay(1000);
}

Rút dây nối ra, chương trình báo không nhận được gì. Đây cũng là cách nhanh để kiểm tra một cổng UART còn hoạt động hay không.

7. Nối hai mạch với nhau

Sơ đồ nối UART giữa hai mạch ESP32 qua Serial2: TX2 của mạch thứ nhất nối RX2 của mạch thứ hai, RX2 nối TX2 và hai chân GND nối chung
TX của mạch này nối RX của mạch kia và ngược lại; GND nối chung.
  • Nối chéo: TX của bên này vào RX của bên kia. Nối TX với TX là lỗi rất hay gặp.
  • Nối chung GND: thiếu dây GND thì hai mạch không có chung mức 0 V và dữ liệu sẽ sai hoặc mất.
  • Hai mạch cấp nguồn riêng qua hai cáp USB; không cần nối 3V3 hay VIN với nhau.

8. Chương trình 2: hai mạch trao đổi từng dòng lệnh

Nạp cùng một chương trình cho cả hai mạch. Dòng gõ ở Serial Monitor của mạch này được gửi sang mạch kia. Hai lệnh led on, led off bật tắt LED trên mạch nhận, và mạch nhận trả lời lại.

/*
  PYWORLD.VN - UART trên ESP32: hai mạch trao đổi từng dòng lệnh qua Serial2
  Nạp cùng chương trình cho hai mạch; nối chéo TX2 với RX2, RX2 với TX2 và nối chung GND
  Gõ một dòng ở Serial Monitor của mạch này thì dòng đó hiện ở mạch kia; "led on", "led off" điều khiển LED GPIO2
*/

#define RX2_PIN 16
#define TX2_PIN 17
#define LED_PIN 2

const uint32_t TOC_DO_BAUD = 115200;
const size_t DO_DAI_TOI_DA = 80;
const unsigned long THOI_GIAN_CHO_MS = 200;   // Quá thời gian này mà chưa thấy ký tự xuống dòng thì bỏ phần đang nhận

String dongNhan;
unsigned long lucNhanCuoi = 0;

void xuLyDong(const String &dong) {
  Serial.printf("Nhan: %s\n", dong.c_str());
  if (dong == "led on") {
    digitalWrite(LED_PIN, HIGH);
    Serial2.println("ok led on");
  } else if (dong == "led off") {
    digitalWrite(LED_PIN, LOW);
    Serial2.println("ok led off");
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  Serial2.begin(TOC_DO_BAUD, SERIAL_8N1, RX2_PIN, TX2_PIN);
  Serial.println("\nGo mot dong roi nhan Enter de gui sang mach kia");
}

void loop() {
  // Máy tính -> mạch kia
  if (Serial.available()) {
    String dong = Serial.readStringUntil('\n');
    dong.trim();
    if (dong.length() > 0) {
      Serial2.println(dong);
      Serial.printf("Gui: %s\n", dong.c_str());
    }
  }

  // Mạch kia -> mạch này: gom từng ký tự cho đến khi gặp ký tự xuống dòng
  while (Serial2.available()) {
    char kyTu = Serial2.read();
    lucNhanCuoi = millis();
    if (kyTu == '\n') {
      dongNhan.trim();
      if (dongNhan.length() > 0) {
        xuLyDong(dongNhan);
      }
      dongNhan = "";
    } else if (dongNhan.length() < DO_DAI_TOI_DA) {
      dongNhan += kyTu;
    }
  }

  // Dòng nhận dở quá lâu: coi như hỏng, bỏ đi để không dính vào dòng sau
  if (dongNhan.length() > 0 && millis() - lucNhanCuoi > THOI_GIAN_CHO_MS) {
    Serial.printf("Bo dong chua tron ven: %s\n", dongNhan.c_str());
    dongNhan = "";
  }
}

Ba điểm đáng chú ý trong chương trình:

  • Ký tự xuống dòng làm dấu kết thúc: UART chỉ truyền từng byte, không biết đâu là hết một thông điệp. Quy ước đơn giản nhất là kết thúc mỗi thông điệp bằng ký tự xuống dòng.
  • Giới hạn độ dài: dòng dài quá 80 ký tự bị cắt, để một bên gửi sai không làm đầy bộ nhớ của bên kia.
  • Thời gian chờ: nếu nhận dở một dòng rồi im lặng quá 200 ms, phần đang nhận bị bỏ để không dính vào dòng sau.

9. Kết quả mong đợi

Hình minh họa Serial Monitor của hai chương trình UART trên ESP32: từng byte kèm dãy bit của khung truyền, và các dòng gửi nhận giữa hai mạch
Kết quả mong đợi trên Serial Monitor của hai chương trình (hình minh họa).
  1. Chương trình 1: mở Tools > Serial Monitor ở 115200 baud. Mỗi giây hiện ba byte; so dãy bit của ký tự 'W' với hình ở mục 2.
  2. Chương trình 2: mở hai cửa sổ Arduino IDE, mỗi cửa sổ chọn một cổng COM. Chọn kiểu kết thúc dòng New Line, gõ led on ở một bên: LED xanh trên mạch kia sáng và dòng Nhan: ok led on hiện lại.

10. Xử lý lỗi thường gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
Nhận toàn ký tự vô nghĩaHai bên đặt khác tốc độ baud hoặc khác định dạng khungĐặt cùng tốc độ và cùng 8N1 ở cả hai bên
Không nhận được gìNối TX với TX, thiếu dây GND, hoặc sai chânNối chéo TX với RX, nối chung GND; thử lại bằng chương trình 1
Thỉnh thoảng mất hoặc sai ký tựDây quá dài, tốc độ quá cao, hoặc chương trình đọc không kịpRút ngắn dây, giảm tốc độ baud, tránh dùng delay() dài khi đang nhận
Không nạp được chương trìnhCó thiết bị nối vào TX0, RX0Tháo thiết bị khỏi TX0, RX0 khi nạp; dùng Serial2 cho thiết bị ngoài
In ra Bo dong chua tron venBên gửi không gửi ký tự xuống dòngChọn New Line trong Serial Monitor; bên gửi dùng println()
Module 5 V làm ESP32 nóng hoặc treoChân TX mức 5 V nối thẳng vào RX của ESP32Dùng mạch chia áp hoặc mạch chuyển mức 5 V sang 3,3 V

11. Xem thêm


Tin tức liên quan

Bluetooth Low Energy (BLE) là gì: quảng bá, GATT, dịch vụ, đặc tính và ESP32
Bluetooth Low Energy (BLE) là gì: quảng bá, GATT, dịch vụ, đặc tính và ESP32

Giải thích Bluetooth Low Energy: quảng bá và kết nối, cấu trúc GATT gồm dịch vụ, đặc tính, mô tả, UUID. Hướng dẫn cho ESP32 làm thiết bị BLE có đặc tính đọc, ghi, thông báo và thử bằng điện thoại.

Điều khiển hồng ngoại hoạt động thế nào: sóng mang 38 kHz, giao thức NEC và ESP32
Điều khiển hồng ngoại hoạt động thế nào: sóng mang 38 kHz, giao thức NEC và ESP32

Giải thích điều khiển hồng ngoại: sóng mang 38 kHz, mắt thu và giao thức NEC với khung 32 bit, mã lặp. Hướng dẫn tự giải mã phím điều khiển và tự phát lệnh bằng ESP32, không dùng thư viện ngoài.

RF 433 MHz là gì: điều chế ASK/OOK và tự viết giao thức phát, thu bằng ESP32
RF 433 MHz là gì: điều chế ASK/OOK và tự viết giao thức phát, thu bằng ESP32

Giải thích sóng RF 433 MHz và điều chế ASK/OOK, cách mã hóa bit bằng độ rộng xung và khung tin 24 bit. Hướng dẫn tự viết chương trình phát và thu trên hai mạch ESP32, không dùng thư viện ngoài.

Vòng dòng 4-20 mA là gì và cách đọc cảm biến 4-20 mA bằng ESP32
Vòng dòng 4-20 mA là gì và cách đọc cảm biến 4-20 mA bằng ESP32

Giải thích vòng dòng 4-20 mA: vì sao dùng dòng điện, vì sao bắt đầu từ 4 mA, cách đổi ra giá trị đo và ngưỡng báo lỗi. Hướng dẫn đọc bằng ESP32 qua điện trở shunt 120 Ω, với ADC có sẵn và với ADS1115.

CAN bus (phần 2): bộ lọc nhận, tốc độ bit và xử lý lỗi bus-off trên ESP32
CAN bus (phần 2): bộ lọc nhận, tốc độ bit và xử lý lỗi bus-off trên ESP32

Phần 2 về CAN bus với ESP32: đặt bộ lọc nhận theo mã lọc và mặt nạ, chọn tốc độ bit theo chiều dài dây, hiểu ba trạng thái lỗi của nút và viết chương trình theo dõi bộ đếm lỗi, tự hồi phục sau bus-off.

CAN bus (phần 1): tín hiệu, khung tin, phân xử ưu tiên và gửi nhận bằng ESP32
CAN bus (phần 1): tín hiệu, khung tin, phân xử ưu tiên và gửi nhận bằng ESP32

Giải thích CAN bus: hai dây CAN_H, CAN_L, mức trội và mức lặn, khung tin có ID 11 bit, phân xử ưu tiên, CRC và ACK. Kèm hai chương trình ESP32 dùng bộ điều khiển TWAI: tự kiểm tra và hai nút gửi nhận.

Modbus TCP/IP (phần 2): ESP32 làm máy khách và cổng chuyển đổi Modbus TCP sang RTU
Modbus TCP/IP (phần 2): ESP32 làm máy khách và cổng chuyển đổi Modbus TCP sang RTU

Hướng dẫn dùng ESP32 làm máy khách Modbus TCP đọc thanh ghi của một thiết bị trong mạng, và làm cổng chuyển đổi Modbus TCP sang Modbus RTU qua RS-485: cách đổi khung, mã thiết bị, thời gian chờ, mã lỗi 0x0B.

Modbus TCP/IP (phần 1): phần đầu MBAP và ESP32 làm máy chủ qua Wi-Fi
Modbus TCP/IP (phần 1): phần đầu MBAP và ESP32 làm máy chủ qua Wi-Fi

Giải thích Modbus TCP/IP: phần đầu MBAP, cổng 502, điểm khác với Modbus RTU. Kèm chương trình ESP32 làm máy chủ Modbus TCP qua Wi-Fi với 10 thanh ghi giữ và 8 cuộn, tự phân tích khung, không dùng thư viện.


Bình luận
Đã thêm vào giỏ hàng