0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

ESP-NOW 通訊協定完整教學:ESP32 無需 Router 的點對點通訊

2026 年 6 月 7 日 458點熱度 0人點贊 0條評論

ESP-NOW 通訊協定完整教學封面

先講結論:不用 Router、延遲只要 2 ms 的無線

如果你做過 WiFi 專案,大概很熟那種「一定得先有一個 AP」的束縛感。ESP-NOW 把這件事直接拿掉 —— 兩塊 ESP32 不必連上任何路由器,就能互相傳資料。

延遲大約 2 ms,比走 MQTT 快兩個數量級。這篇的順序是:先搞懂它是什麼、封包長什麼樣,再看一對一與一對多的實作,最後補上 Deep Sleep 低功耗節點與幾個一定要知道的限制。

ESP-NOW 是什麼:把 WiFi 的底層拿來直接講話

它是 Espressif 自家開發的專有無線通訊協定,跑在 IEEE 802.11 的 PHY 層(也就是 WiFi 的底層),但不需要傳統的 WiFi 路由器或 AP。裝置之間可以直接點對點(P2P)通訊,延遲極低(~2 ms),非常適合 IoT 感測器網路和遙控器應用。

我看過最好的比喻是這個:ESP-NOW 運作起來更像一條「無線 UART」。發送端把資料直接「丟」給接收端,不必先建立連線,也不用等握手。

跟傳統 WiFi 差在哪:連線流程比一比

傳統 WiFi 要先連上 Router/AP 才能交換資料。有基礎設施的場合這沒問題,但沒有 AP 的時候,整條路就直接斷了。

ESP-NOW 與傳統 WiFi 的連線流程比較

看圖就很清楚:ESP-NOW 省掉的是「連線建立」那一整段。這也是為什麼它的延遲可以壓到 ~2 ms。

封包格式:250 bytes 到底花在哪裡

ESP-NOW 基於 WiFi 802.11 的底層封包,但做了簡化。整個封包由這幾塊組成:

  • Preamble:802.11 標準前導碼(8 bytes)
  • MAC DA:目標裝置 MAC 位址(6 bytes)
  • MAC SA:來源裝置 MAC 位址(6 bytes)
  • Type:封包類型(2 bytes,標記為 ESP-NOW)
  • Payload:自訂資料內容,最大 250 bytes
  • FCS:Frame Check Sequence,CRC 校驗(4 bytes)

ESP-NOW 250 bytes 上限的實務影響

很多人一看到「Payload 只有 250 bytes」就緊張,其實不用。感測器資料(溫度、濕度、氣壓這些 float 值通常 4~8 bytes)離 250 bytes 的上限還很遠,一筆讀值送一次剛剛好。

配對與資料傳輸:其實只有四個步驟

ESP-NOW 的配對流程比想像中簡單,我一開始還以為要像藍牙那樣配對半天:

  1. 廣播:發送端廣播自己的存在(或直接指定目標 MAC)
  2. 配對:接收端回應配對請求,建立 Peer 關係
  3. 傳輸:配對完成後,雙方可直接雙向傳送資料
  4. ACK:每個封包都會回傳 ACK 確認

注意第二步的「Peer」是關鍵字:ESP-NOW 不是廣播大放送,它是先建立 peer 關係、之後才送資料。所以程式裡一定會看到 esp_now_add_peer() 這個動作。

重傳機制:失敗了自己再送一次

ESP-NOW 內建重傳機制(最多 7 次),發送失敗會自動重傳。如果 WiFi 通道干擾嚴重,還可以切換通道(Channel)重試。

這裡提醒一句:重傳不代表保證送達。真的要確認資料沒漏,還是得在應用層自己補序號與檢查。

WiFi 和 ESP-NOW 能同時用嗎

不能同時,但可以「輪流」。ESP-NOW 底層用的是 WiFi 的 PHY,因此理論上不能同時運作;不過 ESP32 可以在 WiFi Station/AP 模式和 ESP-NOW 之間快速切換,達到「分時共存」的效果。

ESP-NOW 通道設定與 WiFi 分時共存

典型做法是這樣:

  • 大部分時間 ESP-NOW 用於低延遲資料傳輸
  • 每隔一段時間切換到 WiFi 模式,把感測器資料上傳到雲端
  • 切換完成後回到 ESP-NOW 模式

所以你的節點可以同時擁有「低延遲的區域通訊」和「偶爾上網回報」這兩件事。

實作一:一對一通訊(發送端)

先看發送端。整段程式的骨架就是「初始化 → 註冊回呼 → 設定 Peer → 送資料」,順序不能顛倒:

ESP-NOW 的 Peer 設定與註冊流程

// ESP-NOW 一對一通訊 - 發送端 (Sender)
#include 
#include 

// 接收端 MAC 位址 (請換成你的接收端 MAC)
uint8_t receiverMac[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// 要傳送的資料結構
typedef struct sensor_data {
    float temperature;
    float humidity;
    float pressure;
    uint32_t reading_id;
} sensor_data_t;

sensor_data_t myData;

// 發送完成回呼
void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
    Serial.printf("發送狀態: %s\n",
                  status == ESP_NOW_SEND_SUCCESS ? "✅ 成功" : "❌ 失敗");
}

void setup() {
    Serial.begin(115200);

    // 設定 WiFi 為 Station 模式(必要)
    WiFi.mode(WIFI_STA);

    // 初始化 ESP-NOW
    if (esp_now_init() != ESP_OK) {
        Serial.println("ESP-NOW 初始化失敗");
        return;
    }

    // 註冊發送回呼
    esp_now_register_send_cb(onDataSent);

    // 註冊 Peer
    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("新增 Peer 失敗");
        return;
    }

    myData.reading_id = 0;
}

void loop() {
    // 模擬感測器資料
    myData.temperature = 25.0 + random(-50, 50) / 10.0;
    myData.humidity    = 60.0 + random(-100, 100) / 10.0;
    myData.pressure    = 1013.25 + random(-50, 50) / 10.0;
    myData.reading_id++;

    // 發送資料
    esp_err_t result = esp_now_send(receiverMac, (uint8_t*) &myData, sizeof(myData));

    Serial.printf("發送 #%lu: %.1f°C %.1f%% %.1fhPa\n",
                  myData.reading_id,
                  myData.temperature, myData.humidity, myData.pressure);

    delay(5000);  // 每 5 秒發送一次
}

幾個容易漏掉的地方:WiFi.mode(WIFI_STA) 一定要先做(就算你不連 AP);esp_now_init() 與 esp_now_add_peer() 的回傳值都要檢查;這裡的 receiverMac 要換成你接收端真正的 MAC。

實作一:一對一通訊(接收端)

接收端反而更短 —— 它幾乎什麼都不用做,資料進來時回呼會自己叫你:

// ESP-NOW 一對一通訊 - 接收端 (Receiver)
#include 
#include 

typedef struct sensor_data {
    float temperature;
    float humidity;
    float pressure;
    uint32_t reading_id;
} sensor_data_t;

sensor_data_t incomingData;

// 接收回呼
void onDataRecv(const uint8_t *mac, const uint8_t *incomingData_bytes, int len) {
    memcpy(&incomingData, incomingData_bytes, sizeof(incomingData));
    Serial.printf("收到 #%lu: %.1f°C %.1f%% %.1fhPa (len=%d)\n",
                  incomingData.reading_id,
                  incomingData.temperature,
                  incomingData.humidity,
                  incomingData.pressure, len);
}

void setup() {
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);

    if (esp_now_init() != ESP_OK) {
        Serial.println("ESP-NOW 初始化失敗");
        return;
    }

    esp_now_register_recv_cb(onDataRecv);
}

void loop() {
    // 接收端不需要做任何事,靠回呼驅動
    delay(1000);
}

ESP-NOW 發送端與接收端角色對照

這裡有個小細節:兩端的 sensor_data 結構必須完全一致。因為接收端用 memcpy 把位元組直接倒進結構裡,欄位順序只要有一點不同,讀出來的溫度就會是亂碼。

實作二:一對多廣播

如果要同時跟多個 ESP32 通訊,就是註冊多個 Peer:

// ESP-NOW 一對多廣播
// 註冊多個接收端
esp_now_peer_info_t peerInfo;

// Peer 1: 感測器節點 A
uint8_t macA[] = {0x24, 0x62, 0xAB, 0xF4, 0x12, 0x34};
memcpy(peerInfo.peer_addr, macA, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
esp_now_add_peer(&peerInfo);

// Peer 2: 感測器節點 B
uint8_t macB[] = {0x24, 0x62, 0xAB, 0xF4, 0x56, 0x78};
memcpy(peerInfo.peer_addr, macB, 6);
esp_now_add_peer(&peerInfo);

// 發送給特定節點
esp_now_send(macA, (uint8_t*) &data, sizeof(data));

// 或廣播給所有 Peer(MAC 全 FF)
esp_now_send(broadcastMac, (uint8_t*) &data, sizeof(data));

ESP-NOW 廣播與單播比較

一句話記住差別:單播指定 MAC,廣播用全 FF。想只叫某一台,就用它的 MAC;想一次通知全部,就送給廣播位址。

實作三:ESP-NOW + Deep Sleep 低功耗感測器

ESP-NOW 最強大的應用之一,就是拿來做低功耗感測器節點。配合 Deep Sleep 模式,一顆 18650 鋰電池可以運行數月。

它的邏輯很單純:醒來 → 送一筆 → 立刻睡回去。醒著的時間越短,電池就越撐。

// ESP-NOW + Deep Sleep 低功耗感測器節點
#include 
#include 
#include 

// RTC 記憶體(Deep Sleep 後仍保留)
RTC_DATA_ATTR int bootCount = 0;

void setup() {
    Serial.begin(115200);
    bootCount++;
    Serial.printf("開機次數: %d\n", bootCount);

    // 初始化 WiFi(短暫喚醒)
    WiFi.mode(WIFI_STA);
    esp_now_init();

    // 註冊 Peer(閘道器 MAC)
    uint8_t gatewayMac[] = {0x24, 0x62, 0xAB, 0xF4, 0xAA, 0xBB};
    esp_now_peer_info_t peerInfo = {};
    memcpy(peerInfo.peer_addr, gatewayMac, 6);
    esp_now_add_peer(&peerInfo);

    // 讀取感測器 + 發送資料
    float temp = 25.3;  // 實際應讀取 DS18B20 等
    float hum  = 60.5;
    esp_now_send(gatewayMac, (uint8_t*) &temp, sizeof(temp));

    // 進入 Deep Sleep(10 分鐘後喚醒)
    Serial.println("進入 Deep Sleep...");
    esp_deep_sleep(600 * 1000000ULL);  // 10 分鐘 (µs)
}

void loop() {
    // 不會執行到這裡
}

怎麼拿到自己的 MAC 位址

要用 ESP-NOW,就一定要知道對方的 MAC 位址,而每塊 ESP32 的 MAC 都不一樣。所以先燒這段小程式,把兩塊板的 MAC 抄下來:

// 獲取本機 MAC 位址
#include 

void setup() {
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    Serial.print("本機 MAC: ");
    Serial.println(WiFi.macAddress());
}

void loop() {}

網路拓樸:四種接法

ESP-NOW 支援多種網路拓樸,先想清楚你要哪一種,再決定怎麼寫:

  • 一對一:遙控器 → 裝置
  • 一對多:閘道器收集多個感測器資料
  • 多對一:多個控制器控制單一裝置
  • 網狀:透過中繼節點擴展範圍(需自行實作路由)

ESP-NOW 限制與注意事項

它是好東西,但不是萬能。下面這張表是我實際踩過之後整理出來的,建議動手前先看一眼:

限制 說明 解決方案
最大 20 個 Peer 最多只能註冊 20 個裝置 分層網狀拓樸
Payload 250 bytes 單次傳送最多 250 bytes 分包傳送
無重傳保證 預設重傳 7 次,仍可能遺失 應用層 ACK + 序號檢查
無多跳路由 不支援自動中繼 自行實作 Store-and-Forward
WiFi 共存 ESP-NOW 和 WiFi 不能同時收發 分時切換模式

ESP-NOW 除錯排查表

其中我最想提醒的是重傳不是保證:預設重傳 7 次,還是有可能遺失。要確定資料不漏,就自己加序號與應用層 ACK。

結合其他協定:ESP-NOW 管區域、MQTT 管雲端

ESP-NOW 常與其他通訊協定搭配,形成完整的 IoT 架構:感測器節點用 ESP-NOW 把資料丟給閘道器,閘道器再透過 MQTT 轉發到雲端。區域用 ESP-NOW(低延遲、低功耗),廣域用 MQTT(能上 Internet)。

// 感測器節點 → ESP-NOW → 閘道器 → MQTT → 雲端
// 區域網路:ESP-NOW(低延遲、低功耗)
// 廣域網路:MQTT(透過閘道器轉發到 Internet)

// 閘道器程式碼片段
void onEspNowRecv(const uint8_t *mac, const uint8_t *data, int len) {
    // 收到感測器資料
    float temp;
    memcpy(&temp, data, sizeof(temp));

    // 透過 MQTT 轉發到雲端
    client.publish("sensor/temperature", String(temp).c_str());

    // 記錄到 LittleFS(可參考 SPIFFS/LittleFS 文章)
    logToFile(mac, temp);
}

更多關於 MQTT 的細節,請參考 ESP32 MQTT 實戰教學。

總結

ESP-NOW 是 ESP32 生態中最被低估的通訊協定之一。它提供:

  • 超低延遲:~2 ms,比 MQTT 快兩個數量級
  • 無需基礎設施:不依賴 WiFi Router
  • 低功耗:搭配 Deep Sleep 可運行數月
  • 簡單直觀:API 只有 init / add_peer / send / 回呼

如果你的 IoT 網路不需要連上 Internet(智慧農業、感測器陣列、遙控系統都算),ESP-NOW 就是最省事的選擇。先把 MAC 抄下來、Peer 加進去,剩下的就是把資料丟出去而已。

標籤: 教學
最後更新:2026 年 9 月 30 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang