Lập trình ESP32 với ESP-NOW — giao tiếp trực tiếp không cần router

Lập trình ESP32 với ESP-NOW

Ở bài giao tiếp 2 ESP32 qua HTTP trước đây, 2 board bắt buộc phải cùng kết nối chung 1 router WiFi mới nói chuyện được với nhau. Bài này chúng ta tìm hiểu ESP-NOW — giao thức đặc trưng của chip Espressif cho phép 2 (hoặc nhiều) ESP32 giao tiếp trực tiếp với nhau, hoàn toàn không cần router hay mạng WiFi nào cả.

Đây là một bài thực hành trong Serie Lập trình ESP32 từ A tới Z.

ESP-NOW là gì?

ESP-NOW là giao thức không dây riêng của Espressif (hãng sản xuất chip ESP32/ESP8266), hoạt động dựa trên tầng vật lý WiFi nhưng bỏ qua toàn bộ phần kết nối mạng thông thường — không cần SSID, không cần mật khẩu, không cần địa chỉ IP, không cần router. Mỗi board chỉ cần biết đúng địa chỉ MAC của board kia là gửi được dữ liệu.

So với việc giao tiếp qua router (như bài HTTP trước), ESP-NOW có 3 ưu điểm rõ rệt:

  • Độ trễ thấp hơn nhiều — không phải đi vòng qua router.
  • Không phụ thuộc router — 2 ESP32 mang đi bất kỳ đâu, không có WiFi cũng giao tiếp được.
  • Tiết kiệm năng lượng hơn — phù hợp cho các thiết bị chạy pin gửi dữ liệu ngắt quãng.

Đánh đổi: mỗi gói tin ESP-NOW chỉ gửi được tối đa 250 byte, và không có cơ chế định tuyến qua nhiều chặng phức tạp như mạng WiFi thật — phù hợp cho khoảng cách gần tới trung bình (vài chục tới cỡ 200m ngoài trời, tùy điều kiện).

Mô hình ESP-NOW: Sender gửi struct_message trực tiếp tới địa chỉ MAC của Receiver, không cần router, Receiver xử lý trong callback onDataRecv

Lập trình ESP-NOW cho 2 board

Bài này dùng 2 sketch riêng biệt: 1 board Sender đọc nút bấm cục bộ và gửi trạng thái đi, 1 board Receiver nhận dữ liệu rồi điều khiển LED tương ứng.

Sơ đồ đấu nối: board Sender nối nút nhấn vào GPIO4 (đầu kia xuống GND), board Receiver nối LED vào GPIO26 qua điện trở 220Ω.

Sơ đồ đấu nối ESP-NOW: board Sender có nút nhấn GPIO4 xuống GND, board Receiver có LED GPIO26 qua điện trở 220Ω

Định nghĩa cấu trúc dữ liệu dùng chung

typedef struct struct_message {
  bool buttonPressed;
} struct_message;

struct_message myData;

Lưu ý: Cấu trúc struct_message này phải khai báo giống hệt nhau ở cả 2 board — đúng tên trường, đúng kiểu dữ liệu, đúng thứ tự. ESP-NOW truyền dữ liệu ở dạng byte thô (memcpy), không tự chuyển đổi định dạng như JSON — chỉ cần lệch 1 kiểu dữ liệu (vd int thay vì bool) là dữ liệu đọc ra sẽ sai hoàn toàn mà không có cảnh báo lỗi nào.

Code Sender — đọc nút bấm, gửi qua ESP-NOW

uint8_t receiverMac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};

void setup() {
  ...
  WiFi.mode(WIFI_STA); // can bat WiFi STA nhung KHONG goi WiFi.begin()
  esp_now_init();

  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, receiverMac, 6);
  peerInfo.channel = 0;
  peerInfo.encrypt = false;
  esp_now_add_peer(&peerInfo);
}

void loop() {
  if (buttonState != lastButtonState) {
    myData.buttonPressed = (buttonState == LOW);
    esp_now_send(receiverMac, (uint8_t *)&myData, sizeof(myData));
  }
}
  • WiFi.mode(WIFI_STA) bật radio WiFi ở chế độ Station, nhưng chúng ta không gọi WiFi.begin() — ESP-NOW dùng chung phần cứng radio với WiFi thường nhưng không cần kết nối mạng thật sự.
  • esp_now_add_peer() đăng ký board nào (theo địa chỉ MAC) mà mình sẽ gửi dữ liệu tới — giống như lưu số điện thoại vào danh bạ trước khi gọi.
  • esp_now_send() gửi dữ liệu đi ngay lập tức khi trạng thái nút bấm thay đổi, không cần chờ polling định kỳ như bài HTTP trước.

Lưu ý: receiverMac phải đúng là địa chỉ MAC thật của board Receiver, lấy từ dòng WiFi.macAddress() mà board đó in ra Serial Monitor sau khi nạp code. Địa chỉ MAC là duy nhất cho từng board vật lý, không thể đoán hay dùng chung giữa các board khác nhau.

Code Receiver — nhận dữ liệu qua callback, điều khiển LED

void onDataRecv(const uint8_t *macAddr, const uint8_t *incomingData, int len) {
  memcpy(&receivedData, incomingData, sizeof(receivedData));
  digitalWrite(led, receivedData.buttonPressed ? HIGH : LOW);
}

void setup() {
  ...
  esp_now_init();
  esp_now_register_recv_cb(onDataRecv);
}

void loop() {
  // khong can lam gi - moi xu ly nam trong callback onDataRecv
}

Khác hẳn mô hình polling của bài HTTP trước (Board B phải chủ động hỏi thăm mỗi giây), ESP-NOW dùng cơ chế callback — hàm onDataRecv tự động được gọi ngay khi có dữ liệu mới tới, không cần vòng lặp kiểm tra liên tục. Đây là lý do độ trễ của ESP-NOW thấp hơn hẳn so với polling qua HTTP.

Code đầy đủ

Code Sender:

#include <Arduino.h>

#include <WiFi.h>
#include <esp_now.h>

const int button = 4;

uint8_t receiverMac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};

typedef struct struct_message {
  bool buttonPressed;
} struct_message;

struct_message myData;

int lastButtonState = HIGH;

void onDataSent(const uint8_t *macAddr, esp_now_send_status_t status) {
  Serial.print("Gui du lieu: ");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Thanh cong" : "That bai");
}

void setup() {
  Serial.begin(115200);
  pinMode(button, INPUT_PULLUP);

  WiFi.mode(WIFI_STA);

  if (esp_now_init() != ESP_OK) {
    Serial.println("Loi khoi tao ESP-NOW");
    return;
  }

  esp_now_register_send_cb(onDataSent);

  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, receiverMac, 6);
  peerInfo.channel = 0;
  peerInfo.encrypt = false;

  if (esp_now_add_peer(&peerInfo) != ESP_OK) {
    Serial.println("Loi them peer");
    return;
  }

  Serial.print("Dia chi MAC cua board nay (Sender): ");
  Serial.println(WiFi.macAddress());
}

void loop() {
  int buttonState = digitalRead(button);

  if (buttonState != lastButtonState) {
    myData.buttonPressed = (buttonState == LOW);

    esp_err_t result = esp_now_send(receiverMac, (uint8_t *)&myData, sizeof(myData));

    if (result == ESP_OK) {
      Serial.println("Da gui trang thai nut bam qua ESP-NOW");
    } else {
      Serial.println("Loi khi gui du lieu");
    }

    lastButtonState = buttonState;
    delay(200);
  }
}

Code Receiver:

#include <Arduino.h>

#include <WiFi.h>
#include <esp_now.h>

const int led = 26;

typedef struct struct_message {
  bool buttonPressed;
} struct_message;

struct_message receivedData;

void onDataRecv(const uint8_t *macAddr, const uint8_t *incomingData, int len) {
  memcpy(&receivedData, incomingData, sizeof(receivedData));

  Serial.print("Nhan du lieu tu MAC: ");
  for (int i = 0; i < 6; i++) {
    Serial.printf("%02X", macAddr[i]);
    if (i < 5) Serial.print(":");
  }
  Serial.print(" - buttonPressed = ");
  Serial.println(receivedData.buttonPressed ? "true" : "false");

  digitalWrite(led, receivedData.buttonPressed ? HIGH : LOW);
}

void setup() {
  Serial.begin(115200);
  pinMode(led, OUTPUT);
  digitalWrite(led, LOW);

  WiFi.mode(WIFI_STA);

  Serial.print("Dia chi MAC cua board nay (Receiver) - copy vao code Sender: ");
  Serial.println(WiFi.macAddress());

  if (esp_now_init() != ESP_OK) {
    Serial.println("Loi khoi tao ESP-NOW");
    return;
  }

  esp_now_register_recv_cb(onDataRecv);
}

void loop() {
  // khong can lam gi - moi xu ly nam trong callback onDataRecv
}

Code đầy đủ mình để trên GitHub, các bạn xem tại 04-Protocol/07_ESPNOW/Sender và 04-Protocol/07_ESPNOW/Receiver nhé.

Kết quả

Nạp code Receiver trước, mở Serial Monitor, ghi lại địa chỉ MAC in ra. Dán đúng địa chỉ đó vào biến receiverMac trong code Sender rồi nạp cho board thứ 2.

Log Serial Monitor của board 1 (Sender), mỗi lần nhấn hoặc nhả nút là 1 gói tin gửi đi:

Log Serial Monitor board ESP32 Sender gửi trạng thái nút bấm qua ESP-NOW thành công

Log Serial Monitor của board 2 (Receiver):

Log Serial Monitor board ESP32 Receiver nhận dữ liệu ESP-NOW từ địa chỉ MAC của Sender

Nhấn nút trên board Sender, LED trên board Receiver phải sáng/tắt gần như ngay lập tức — nhanh hơn rõ rệt so với bài polling qua HTTP trước, đúng không nào. Theo dõi Serial Monitor của cả 2 board để thấy log gửi/nhận song song.

Kết

ESP-NOW là lựa chọn rất đáng cân nhắc cho các dự án cần nhiều ESP32 giao tiếp nhanh, gần nhau, không có sẵn hạ tầng WiFi (như cảm biến ngoài trời, mô hình robot nhiều module, điều khiển từ xa) — nơi việc phải cấu hình router hay giữ 1 mạng WiFi ổn định là bất tiện không cần thiết. So với bài HTTP trước, đây là lựa chọn nhẹ hơn, nhanh hơn cho đúng bài toán “vài thiết bị gần nhau cần nói chuyện với nhau”. Ở bài tiếp theo, chúng ta sẽ mở rộng ý tưởng “không cần router” này lên cả 1 mạng lưới nhiều node với WiFi Mesh.

Nếu thấy bài viết có ích, hãy chia sẻ cho bạn bè cùng học. Có thắc mắc gì cứ để lại bình luận, mình sẽ giải đáp nhé!

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *