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
- 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 độ baud | Thời gian một bit | Số byte mỗi giây (8N1) | Thường gặp ở |
|---|---|---|---|
| 9600 | 104,2 µs | 960 | Module GPS, cảm biến, Modbus RTU |
| 19200 | 52,1 µs | 1920 | Modbus RTU |
| 115200 | 8,68 µs | 11.520 | Serial Monitor, nạp chương trình |
| 921600 | 1,09 µs | 92.160 | Nạ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 Arduino | Chân mặc định trên DevKit V1 | Ghi chú |
|---|---|---|---|
| UART0 | Serial | TX0 (GPIO1), RX0 (GPIO3) | Nối với chip USB: dùng để nạp chương trình và Serial Monitor |
| UART1 | Serial1 | Phải tự chọn chân | Chân mặc định trùng với bộ nhớ flash của module, không dùng được |
| UART2 | Serial2 | TX2 (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ị
- Một hoặc hai mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340) và cáp USB có truyền dữ liệu.
- Vài dây cắm.
- Arduino IDE 2.x với gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10). Không cần cài thêm thư viện.
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
- 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
- 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.
- 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òngNhan: ok led onhiện lại.
10. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Nhận toàn ký tự vô nghĩa | Hai 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ân | Nố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ịp | Rú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ình | Có thiết bị nối vào TX0, RX0 | Thá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 ven | Bên gửi không gửi ký tự xuống dòng | Chọn New Line trong Serial Monitor; bên gửi dùng println() |
| Module 5 V làm ESP32 nóng hoặc treo | Chân TX mức 5 V nối thẳng vào RX của ESP32 | Dùng mạch chia áp hoặc mạch chuyển mức 5 V sang 3,3 V |
11. Xem thêm
- Điều khiển relay bằng điện thoại qua Bluetooth: cũng là truyền nối tiếp, nhưng không dây.
- Bắt đầu với Arduino Nano CH340: chip USB sang UART và driver.
- Các mạch ESP32 tại PYWORLD: danh mục ESP32.
Xem thêm