
先講結論:不用 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 省掉的是「連線建立」那一整段。這也是為什麼它的延遲可以壓到 ~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)

很多人一看到「Payload 只有 250 bytes」就緊張,其實不用。感測器資料(溫度、濕度、氣壓這些 float 值通常 4~8 bytes)離 250 bytes 的上限還很遠,一筆讀值送一次剛剛好。
配對與資料傳輸:其實只有四個步驟
ESP-NOW 的配對流程比想像中簡單,我一開始還以為要像藍牙那樣配對半天:
- 廣播:發送端廣播自己的存在(或直接指定目標 MAC)
- 配對:接收端回應配對請求,建立 Peer 關係
- 傳輸:配對完成後,雙方可直接雙向傳送資料
- 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 模式
所以你的節點可以同時擁有「低延遲的區域通訊」和「偶爾上網回報」這兩件事。
實作一:一對一通訊(發送端)
先看發送端。整段程式的骨架就是「初始化 → 註冊回呼 → 設定 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);
}

這裡有個小細節:兩端的 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));

一句話記住差別:單播指定 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 不能同時收發 | 分時切換模式 |

其中我最想提醒的是重傳不是保證:預設重傳 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 加進去,剩下的就是把資料丟出去而已。
文章評論