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ì?

So sánh Wi-Fi thông thường phải qua router với ESP-NOW gửi trực tiếp giữa hai mạch ESP32, kèm ba khái niệm địa chỉ MAC, peer và hàm gọi lại
ESP-NOW gửi thẳng từ mạch này sang mạch kia theo địa chỉ MAC (hình vẽ minh họa).

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ểmGiá trị
Băng tần2,4 GHz, dùng các kênh Wi-Fi 1 đến 13
Kích thước một góiTố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ậnGử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óaCó 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 xaPhụ 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ị

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.

Ba bước thiết lập ESP-NOW trên ESP32: xem MAC của mạch nhận, điền MAC vào chương trình gửi, nạp và theo dõi; bên dưới là cấu trúc gói tin 12 byte
Ba bước thực hiện và cấu trúc gói tin dùng trong bài (hình vẽ minh họa).

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

Hình minh họa Serial Monitor của mạch gửi và mạch nhận ESP-NOW: mạch gửi báo đã được xác nhận, mạch nhận in số thứ tự gói, trạng thái nút BOOT và RSSI
Kết quả mong đợi trên Serial Monitor của hai mạch (hình minh họa, địa chỉ MAC và RSSI là số ví dụ).
  • Mạch gửi in mach nhan da xac nhan sau 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ọi WiFi.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_OK khi 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ọi esp_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_SUCCESS nghĩ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ố thongTin chứ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 GoiTin thì 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:FF là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ượngNguyên nhân thường gặpCách xử lý
Mạch gửi luôn in khong co xac nhanSai địa chỉ MAC, mạch nhận chưa cấp nguồn hoặc hai mạch khác kênhChạ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ậnCấu trúc GoiTin ở hai chương trình khác nhau nên gói bị bỏ quaChé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ạiGói esp32 cũ dùng kiểu tham số khác cho hàm gọi lạiCậ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ố 0Chương trình khác đọc MAC khi Wi-Fi chưa khởi độngDù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 để xaTín hiệu yếu, có vật cản hoặc nhiễu từ mạng Wi-Fi khácXem 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ạchCá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


Tin tức liên quan

Phased array siêu âm 40 kHz với ESP32 (phần 2): mạch đệm 12 V, đầu thu và quét búp sóng
Phased array siêu âm 40 kHz với ESP32 (phần 2): mạch đệm 12 V, đầu thu và quét búp sóng

Phần 2 của loạt phased array siêu âm: thêm mạch đệm 12 V bằng hai module L298N, lắp mạch thu dùng LM358, đo biên độ 40 kHz bằng ADC của ESP32 và chạy chương trình tự quét góc lái để xem búp sóng.

Phased array siêu âm 40 kHz với ESP32 (phần 1): nguyên lý lái búp sóng và tạo 8 kênh lệch pha
Phased array siêu âm 40 kHz với ESP32 (phần 1): nguyên lý lái búp sóng và tạo 8 kênh lệch pha

Nguyên lý phased array và cách dùng ESP32 tạo 8 kênh xung 40 kHz lệch pha để lái búp sóng siêu âm: công thức độ trễ, giới hạn do khoảng cách đầu phát, cấu hình LEDC với hpoint, sơ đồ nối và code.

Phát hiện chuyển động bằng sóng Wi-Fi (CSI) trên ESP32: không cần cảm biến
Phát hiện chuyển động bằng sóng Wi-Fi (CSI) trên ESP32: không cần cảm biến

Giải thích Wi-Fi CSI và hướng dẫn dùng ESP32 phát hiện người di chuyển trong phòng chỉ bằng sóng Wi-Fi từ router: lấy dữ liệu CSI, tính điểm chuyển động, đặt ngưỡng và xem trên Serial Plotter.

Phát hiện chuyển động bằng cảm biến PIR HC-SR501 với ESP32: tự bật đèn qua relay
Phát hiện chuyển động bằng cảm biến PIR HC-SR501 với ESP32: tự bật đèn qua relay

Hướng dẫn dùng cảm biến chuyển động PIR HC-SR501 với ESP32: nguyên lý, cách chỉnh module, đọc tín hiệu bằng ngắt và ghép relay làm đèn tự bật khi có người, tự tắt sau 60 giây.

Làm gateway LoRa sang MQTT bằng ESP32: node cảm biến chạy pin gửi dữ liệu về broker
Làm gateway LoRa sang MQTT bằng ESP32: node cảm biến chạy pin gửi dữ liệu về broker

Hướng dẫn làm hệ thống LoRa hoàn chỉnh với ESP32 và SX1278: node chạy pin đo điện áp, gửi gói rồi ngủ sâu; gateway nhận gói LoRa và publish dữ liệu JSON lên broker MQTT qua Wi-Fi.

Cấu hình LoRa trên ESP32: SF, băng thông, tỉ lệ mã, công suất và cách thử tầm xa
Cấu hình LoRa trên ESP32: SF, băng thông, tỉ lệ mã, công suất và cách thử tầm xa

Giải thích bốn tham số cấu hình LoRa (SF, băng thông, tỉ lệ mã, công suất), cách tính thời gian phát một gói và hướng dẫn làm bộ thử tầm xa bằng ESP32, SX1278 và màn hình OLED.

Hướng dẫn dùng LoRa SX1278 (Ra-02) với ESP32: nối dây SPI, gửi và nhận gói tin đầu tiên
Hướng dẫn dùng LoRa SX1278 (Ra-02) với ESP32: nối dây SPI, gửi và nhận gói tin đầu tiên

Hướng dẫn nối module LoRa SX1278 Ra-02 433 MHz với ESP32 qua SPI, cài thư viện LoRa trên Arduino IDE, viết chương trình gửi và nhận gói tin, đọc RSSI, SNR và xử lý lỗi thường gặp.

ESP-NOW nhiều node với ESP32: nhiều mạch gửi về một trạm thu, hiển thị trên OLED
ESP-NOW nhiều node với ESP32: nhiều mạch gửi về một trạm thu, hiển thị trên OLED

Hướng dẫn dựng mô hình ESP-NOW nhiều node: tối đa bốn ESP32 gửi số đo về một trạm thu, trạm thu hiện điện áp, RSSI và tình trạng từng node trên màn hình OLED 0.96 inch.


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