Hướng dẫn ESP-NOW trên ESP32: truyền dữ liệu giữa hai mạch không cần router Wi-Fi
Hướng dẫn dùng ESP-NOW để hai mạch ESP32 truyền dữ liệu trực tiếp không cần router: xem địa chỉ MAC, chương trình gửi, chương trình nhận, đọc RSSI và xử lý lỗi thường gặp.
ESP-NOW là giao thức không dây của Espressif cho phép các mạch ESP32 gửi dữ liệu thẳng cho nhau, không cần router Wi-Fi và không cần Internet. Gói tin nhỏ, gửi gần như ngay sau khi bật nguồn nên ESP-NOW hợp với remote điều khiển, cảm biến chạy pin và các mạch đặt ở nơi không có Wi-Fi. Bài viết hướng dẫn từ đầu: xem địa chỉ MAC, viết chương trình gửi, chương trình nhận trên Arduino IDE và đọc kết quả trên Serial Monitor.
1. ESP-NOW là gì?
ESP-NOW dùng chung phần vô tuyến 2,4 GHz với Wi-Fi nhưng bỏ qua bước đăng nhập mạng. Mạch gửi chỉ cần biết địa chỉ MAC của mạch nhận. Các đặc điểm chính:
| Đặc điểm | Giá trị |
|---|---|
| Băng tần | 2,4 GHz, dùng các kênh Wi-Fi 1 đến 13 |
| Kích thước một gói | Tối đa 250 byte. Bản ESP-NOW 2.0 trong các gói esp32 mới cho phép gói lớn hơn khi cả hai mạch cùng hỗ trợ |
| Xác nhận | Gửi tới một địa chỉ MAC cụ thể thì mạch nhận tự trả lời xác nhận ở tầng vô tuyến |
| Số mạch đối tác (peer) | Tối đa 20 peer trên một mạch |
| Mã hóa | Có thể bật mã hóa cho từng peer; bài này để mặc định là không mã hóa |
| Tầm xa | Phụ thuộc anten và vật cản; ngoài trời thoáng xa hơn nhiều so với trong nhà có tường chắn |
So với cách cho hai mạch cùng kết nối Wi-Fi rồi trao đổi qua router, ESP-NOW khởi động nhanh hơn và không phụ thuộc router. Đổi lại, dữ liệu không tự lên Internet; muốn đưa lên mạng thì cần một mạch làm trung gian vừa nhận ESP-NOW vừa kết nối Wi-Fi.
2. Chuẩn bị
- Hai mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340). Cần khoảng cách xa hơn thì dùng bản ESP32 có cổng anten rời và gắn anten 2,4 GHz.
- Hai cáp USB có truyền dữ liệu.
- Máy tính đã cài Arduino IDE 2.x và gói esp32 by Espressif Systems. Bài dùng gói esp32 phiên bản 3.3.10; ESP-NOW có sẵn trong gói, không phải cài thêm thư viện.
Bài không cần nối thêm linh kiện: nút BOOT (GPIO0) trên mạch gửi làm nút nhấn, LED trên mạch nhận (GPIO2) làm đèn báo.
3. Bước 1: xem địa chỉ MAC của mạch nhận
Mỗi mạch ESP32 có một địa chỉ MAC riêng gồm 6 byte. Chọn một mạch làm mạch nhận, nạp chương trình sau rồi mở Tools > Serial Monitor, chọn 115200 baud:
/*
PYWORLD.VN - Xem địa chỉ MAC của mạch ESP32
ESP-NOW dùng địa chỉ MAC (giao diện Wi-Fi station) để biết gửi gói tin cho mạch nào
*/
#include <esp_mac.h>
void setup() {
Serial.begin(115200);
}
void loop() {
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
Serial.printf("Dia chi MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
// Dòng dưới in sẵn dạng mảng để chép vào chương trình gửi
Serial.printf("Dang mang: {0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X}\n\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
delay(3000);
}
Serial Monitor in hai dòng, ví dụ:
Dia chi MAC: 24:6F:28:12:34:56
Dang mang: {0x24, 0x6F, 0x28, 0x12, 0x34, 0x56}
Chép dòng Dang mang để dùng ở bước 2. Nên dán nhãn lên từng mạch để khỏi nhầm mạch gửi với mạch nhận.
4. Bước 2: chương trình cho mạch gửi
Sửa mảng MAC_NHAN theo địa chỉ vừa chép, rồi nạp chương trình vào mạch thứ hai. Cứ 2 giây mạch gửi một gói gồm số thứ tự, thời gian đã chạy và trạng thái nút BOOT:
/*
PYWORLD.VN - ESP-NOW: mạch GỬI
Cứ 2 giây gửi một gói tin tới mạch nhận, không cần router Wi-Fi
*/
#include <WiFi.h>
#include <esp_now.h>
// Địa chỉ MAC của mạch NHẬN: sửa theo kết quả của chương trình xem địa chỉ MAC
uint8_t MAC_NHAN[6] = {0x24, 0x6F, 0x28, 0x12, 0x34, 0x56};
#define NUT_BOOT 0 // Nút BOOT trên mạch nối với GPIO0, nhấn thì đọc được mức LOW
const unsigned long CHU_KY_GUI_MS = 2000;
// Mạch gửi và mạch nhận phải khai báo cấu trúc này giống hệt nhau
typedef struct {
uint32_t soThuTu; // Số thứ tự gói, tăng dần
uint32_t thoiGianMs; // Thời gian mạch gửi đã chạy
bool nutBoot; // true khi nút BOOT đang được nhấn
} GoiTin;
GoiTin goi;
bool sanSang = false;
unsigned long lanGuiTruoc = 0;
// Được gọi sau mỗi lần gửi: cho biết mạch nhận có xác nhận đã nhận hay không
void khiGuiXong(const esp_now_send_info_t *thongTin, esp_now_send_status_t trangThai) {
if (trangThai == ESP_NOW_SEND_SUCCESS) {
Serial.println(" -> mach nhan da xac nhan");
} else {
Serial.println(" -> khong co xac nhan");
}
}
void setup() {
Serial.begin(115200);
pinMode(NUT_BOOT, INPUT_PULLUP);
// ESP-NOW chạy trên sóng Wi-Fi nhưng không cần kết nối vào mạng nào
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Khoi dong ESP-NOW that bai");
return;
}
esp_now_register_send_cb(khiGuiXong);
// Khai báo mạch nhận (peer) trước khi gửi
esp_now_peer_info_t machNhan = {};
memcpy(machNhan.peer_addr, MAC_NHAN, 6);
machNhan.channel = 0; // 0: dùng kênh Wi-Fi hiện tại của mạch
machNhan.encrypt = false; // Không mã hóa
if (esp_now_add_peer(&machNhan) != ESP_OK) {
Serial.println("Khong them duoc mach nhan");
return;
}
sanSang = true;
Serial.println("ESP-NOW san sang, bat dau gui");
}
void loop() {
if (!sanSang || millis() - lanGuiTruoc < CHU_KY_GUI_MS) {
return;
}
lanGuiTruoc = millis();
goi.soThuTu++;
goi.thoiGianMs = millis();
goi.nutBoot = (digitalRead(NUT_BOOT) == LOW);
esp_err_t ketQua = esp_now_send(MAC_NHAN, (const uint8_t *)&goi, sizeof(goi));
Serial.printf("Gui goi %lu (%u byte)%s\n", (unsigned long)goi.soThuTu, (unsigned)sizeof(goi),
ketQua == ESP_OK ? "" : " - LOI khi dua vao hang doi gui");
}
5. Bước 3: chương trình cho mạch nhận
Mạch nhận không cần biết trước địa chỉ của mạch gửi. Chương trình in nội dung gói, địa chỉ MAC của mạch gửi và cường độ tín hiệu (RSSI):
/*
PYWORLD.VN - ESP-NOW: mạch NHẬN
Nhận gói tin từ mạch gửi, in nội dung, địa chỉ MAC của mạch gửi và cường độ tín hiệu
LED trên mạch (GPIO2) sáng khi nút BOOT của mạch gửi đang được nhấn
*/
#include <WiFi.h>
#include <esp_now.h>
#define LED_PIN 2
// Phải giống hệt cấu trúc trong chương trình gửi
typedef struct {
uint32_t soThuTu;
uint32_t thoiGianMs;
bool nutBoot;
} GoiTin;
GoiTin goiMoi;
uint8_t macGui[6];
int cuongDo = 0;
volatile bool coGoiMoi = false;
// Được gọi mỗi khi có gói ESP-NOW đến. Hàm chạy trong tác vụ Wi-Fi nên chỉ chép dữ liệu rồi thoát
void khiNhan(const esp_now_recv_info_t *thongTin, const uint8_t *duLieu, int doDai) {
if (doDai != sizeof(GoiTin)) {
return; // Bỏ qua gói không đúng kích thước
}
memcpy(&goiMoi, duLieu, sizeof(goiMoi));
memcpy(macGui, thongTin->src_addr, 6);
cuongDo = thongTin->rx_ctrl->rssi;
coGoiMoi = true;
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Khoi dong ESP-NOW that bai");
return;
}
esp_now_register_recv_cb(khiNhan);
Serial.println("ESP-NOW san sang, dang cho goi tin");
}
void loop() {
if (!coGoiMoi) {
return;
}
coGoiMoi = false;
digitalWrite(LED_PIN, goiMoi.nutBoot ? HIGH : LOW);
Serial.printf("Nhan tu %02X:%02X:%02X:%02X:%02X:%02X | goi %lu | %lu ms | nut BOOT: %s | RSSI %d dBm\n",
macGui[0], macGui[1], macGui[2], macGui[3], macGui[4], macGui[5],
(unsigned long)goiMoi.soThuTu, (unsigned long)goiMoi.thoiGianMs,
goiMoi.nutBoot ? "NHAN" : "nha", cuongDo);
}
6. Kết quả mong đợi
- Mạch gửi in
mach nhan da xac nhansau mỗi gói khi mạch nhận đang bật và ở trong tầm sóng. - Giữ nút BOOT của mạch gửi: LED trên mạch nhận sáng ở gói kế tiếp, dòng in đổi thành
nut BOOT: NHAN. - Tắt nguồn mạch nhận: mạch gửi in
khong co xac nhan. - RSSI càng gần 0 thì tín hiệu càng mạnh. Đặt hai mạch cạnh nhau thường thấy khoảng -30 đến -50 dBm; khi RSSI xuống dưới khoảng -85 dBm thì gói bắt đầu rớt.
7. Giải thích các lệnh chính
WiFi.mode(WIFI_STA): bật phần vô tuyến ở chế độ station. Không gọiWiFi.begin()vì ESP-NOW không cần kết nối vào mạng nào.esp_now_init(): khởi động ESP-NOW, trả vềESP_OKkhi thành công.esp_now_add_peer(): khai báo mạch đối tác bằng địa chỉ MAC. Mạch gửi phải khai báo peer trước khi gọiesp_now_send().esp_now_send(mac, duLieu, doDai): đưa gói vào hàng đợi gửi. Giá trị trả về chỉ cho biết gói đã vào hàng đợi; kết quả gửi thật báo qua hàm gọi lại.esp_now_register_send_cb(): đăng ký hàm được gọi sau mỗi lần gửi.ESP_NOW_SEND_SUCCESSnghĩa là mạch nhận đã xác nhận ở tầng vô tuyến.esp_now_register_recv_cb(): đăng ký hàm được gọi khi có gói đến. Tham sốthongTinchứa địa chỉ MAC của mạch gửi (src_addr) và thông tin thu sóng (rx_ctrl->rssi).
Hai hàm gọi lại chạy trong tác vụ Wi-Fi của hệ thống, vì vậy chỉ nên chép dữ liệu ra biến rồi thoát, phần xử lý lâu (in, điều khiển, ghi bộ nhớ) đặt trong loop() như chương trình nhận ở trên.
8. Lưu ý khi dùng trong dự án thật
- Cùng kênh Wi-Fi: hai mạch phải ở cùng một kênh. Khi không kết nối Wi-Fi, cả hai mặc định ở kênh 1 nên chạy được ngay. Nếu một mạch đồng thời kết nối router, mạch đó sẽ theo kênh của router và mạch còn lại phải đặt cùng kênh.
- Cấu trúc gói giống nhau: đổi một biến trong
GoiTinthì phải nạp lại cả hai mạch. Chương trình nhận kiểm tra kích thước gói để bỏ qua gói lạ. - Gửi cho mọi mạch: dùng địa chỉ
FF:FF:FF:FF:FF:FFlàm peer thì mọi mạch ESP-NOW cùng kênh đều nhận được, nhưng kiểu gửi này không có xác nhận. - Bảo mật: gói không mã hóa có thể bị mạch khác cùng kênh đọc được. Dữ liệu điều khiển thiết bị nên bật mã hóa cho peer và kiểm tra địa chỉ MAC của mạch gửi.
- Tiết kiệm pin: mạch gửi có thể ngủ sâu giữa hai lần gửi, xem bài Deep Sleep trên ESP32.
9. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
Mạch gửi luôn in khong co xac nhan | Sai địa chỉ MAC, mạch nhận chưa cấp nguồn hoặc hai mạch khác kênh | Chạy lại chương trình xem MAC trên đúng mạch nhận; kiểm tra từng byte trong MAC_NHAN |
| Mạch nhận không in gì dù mạch gửi báo đã xác nhận | Cấu trúc GoiTin ở hai chương trình khác nhau nên gói bị bỏ qua | Chép lại cấu trúc cho giống hệt rồi nạp lại cả hai mạch |
| Lỗi biên dịch ở dòng đăng ký hàm gọi lại | Gói esp32 cũ dùng kiểu tham số khác cho hàm gọi lại | Cập nhật gói esp32 trong Tools > Board > Boards Manager lên bản 3.3 trở lên |
| Địa chỉ MAC in ra toàn số 0 | Chương trình khác đọc MAC khi Wi-Fi chưa khởi động | Dùng đúng chương trình ở mục 3, đọc MAC bằng esp_read_mac() |
| Thỉnh thoảng mất gói khi để xa | Tín hiệu yếu, có vật cản hoặc nhiễu từ mạng Wi-Fi khác | Xem RSSI ở mạch nhận, đưa hai mạch lại gần, tránh che anten bằng kim loại, hoặc dùng mạch có anten rời |
| Không thấy cổng COM khi cắm mạch | Cáp chỉ có chức năng sạc hoặc thiếu driver | Đổi cáp có truyền dữ liệu, cài driver CP210x hoặc CH340 theo chip trên mạch |
10. Xem thêm
- Hướng dẫn kết nối Wi-Fi trên ESP32: dùng khi cần đưa dữ liệu lên mạng nội bộ hoặc Internet.
- Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu cho nhiều ứng dụng qua broker.
- Hướng dẫn dùng Deep Sleep trên ESP32: kéo dài thời gian dùng pin cho mạch gửi.
- Các mạch ESP32 tại PYWORLD: danh mục ESP32.
Xem thêm