在物聯網(IoT)專案中,若需要兩台或多台 ESP32 進行感測器數據同步,通常會透過 MQTT 或 HTTP 協定,但這類做法必須依賴 Wi-Fi 路由器與網際網路。
若專案部署在戶外或沒有 Wi-Fi 的環境,ESP-NOW 就是最佳解答。本文將帶你了解 ESP-NOW 的優勢,並提供完整的程式碼範例。
什麼是 ESP-NOW?
ESP-NOW 是樂鑫科技(Espressif)開發的一種無伺服器(Serverless)無線通訊協定,採用 2.4GHz 頻段,運作方式類似 2.4GHz 無線滑鼠。
ESP-NOW 的核心優勢
-
免 Wi-Fi 路由器:裝置間可直接點對點(P2P)連線。
-
極低延遲與低功耗:無需經歷繁瑣的 Wi-Fi 握手連線過程,封包傳輸僅需數毫秒,非常適合電池供電專案。
-
支援多種拓撲:支援一對一、一對多(Master to Slaves)、多對一(Slaves to Master)等架構。
-
傳輸距離與穩定度:在空曠無遮蔽環境下,採用板載天線可達 50~100 公尺;若加上障礙物或牆壁遮蔽,有效距離約為 10~30 公尺。若需更遠距離,可改用外接天線版本。

ESP-NOW 支援點對點(P2P)、星狀(一對多)、匯聚(多對一)及混和網狀等多種彈性通訊拓撲架構
步驟一:獲取接收端(Receiver)的 MAC 位址
ESP-NOW 是透過硬體 MAC 位址(MAC Address)來指定資料要傳給哪一台裝置。在開始傳輸前,必須先查詢接收端 ESP32 的 MAC 位址。
將以下程式碼燒錄至接收端 ESP32,並打開 Serial Monitor(波特率 115200):
#include "WiFi.h"
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_MODE_STA);
Serial.print("ESP32 Board MAC Address: ");
Serial.println(WiFi.macAddress());
}
void loop() {
// 僅需執行一次取得 MAC 位址
}
提示:記下 Serial Monitor 中輸出的 MAC 位址(格式如 08:3A:8D:12:34:56),後續發送端程式碼會用到。
步驟二:傳送端(Sender)程式碼範例
將取得的 MAC 位址替換至程式碼中的 broadcastAddress 陣列內。此程式碼將每 2 秒發送一組溫濕度結構體(Struct)資料給接收端。
#include <esp_now.h>
#include <WiFi.h>
// 填入接收端 ESP32 的 MAC 位址(請根據實際查詢結果修改)
// 格式:{0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}
uint8_t broadcastAddress[] = {0x08, 0x3A, 0x8D, 0x12, 0x34, 0x56};
// 定義要傳送的資料結構(傳送端與接收端結構必須一致)
typedef struct struct_message {
char a[32]; // 字串資料(例如訊息或狀態描述)
int b; // 整數資料(例如感測器數值或計數)
float c; // 浮點數資料(例如溫度或濕度)
bool d; // 布林值(例如開關狀態 true/false)
} struct_message;
// 建立資料結構變數,用來存放要發送的資料
struct_message myData;
// 建立 Peer 裝置資訊結構體變數,用來登記接收端資訊
esp_now_peer_info_t peerInfo;
/**
* @brief 發送狀態回調函數(Callback Function)
* 當發送動作完成時,ESP-NOW 會自動調用此函數回報發送結果
*
* @param mac_addr 接收端的 MAC 位址
* @param status 發送狀態(ESP_NOW_SEND_SUCCESS 或 ESP_NOW_SEND_FAIL)
*/
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
Serial.print("\r\n最後一次封包發送狀態:\t");
// 檢查是否成功送達
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "發送成功 (Delivery Success)" : "發送失敗 (Delivery Fail)");
}
void setup() {
// 初始化序列埠通訊(波特率 115200)
Serial.begin(115200);
// 將 ESP32 設為 Wi-Fi 站台模式(Station Mode)
// ESP-NOW 必須在 Wi-Fi 啟動的狀態下才能運行
WiFi.mode(WIFI_STA);
// 初始化 ESP-NOW 通訊協定
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW 初始化失敗!");
return;
}
// 註冊「發送狀態」回調函數,當數據發送完畢時自動執行 OnDataSent
esp_now_register_send_cb(OnDataSent);
// 設定 Peer 裝置資訊(把目標接收端的 MAC 位址複製進去)
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0; // 設定 Wi-Fi 信道(0 表示使用當前預設信道)
peerInfo.encrypt = false; // 是否加密傳輸(預設不加密)
// 新增 Peer 裝置(將目標接收端登記至 ESP-NOW 允許清單中)
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("新增 Peer 裝置失敗!");
return;
}
}
void loop() {
// 1. 準備要發送的測試數據
strcpy(myData.a, "Hello ESP-NOW"); // 給予字串數值
myData.b = random(1, 20); // 產生 1~20 的隨機整數
myData.c = 25.5; // 寫入浮點數(例如溫度 25.5 度)
myData.d = false; // 寫入布林狀態
// 2. 透過 ESP-NOW 發送封包
// 參數分別為:目標 MAC 位址、資料記憶體指標、資料長度
esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
// 檢查呼叫發送函式本身是否成功(非最終送達狀態,最終狀態由回調函數回報)
if (result == ESP_OK) {
Serial.println("封包已發送至緩衝區...");
} else {
Serial.println("發送指令執行錯誤!");
}
// 每隔 2 秒發送一次
delay(2000);
}
步驟三:接收端(Receiver)程式碼範例
接收端 ESP32 收到封包時,會自動觸發 OnDataRecv 回調函數,解析並印出傳來的結構體內容。
#include <esp_now.h>
#include <WiFi.h>
// 定義要接收的資料結構(必須與傳送端結構完全一致)
typedef struct struct_message {
char a[32]; // 字串資料(例如訊息或狀態描述)
int b; // 整數資料(例如感測器數值或計數)
float c; // 浮點數資料(例如溫度或濕度)
bool d; // 布林值(例如開關狀態 true/false)
} struct_message;
// 建立資料結構變數,用來存放接收到的數據
struct_message myData;
/**
* @brief 接收資料回調函數(ESP32 Core v2.x 舊版語法)
* 當 ESP32 收到 ESP-NOW 數據封包時,系統會自動呼叫此函數進行解析
*
* @param mac 傳送端 ESP32 的 MAC 位址指標 (uint8_t *)
* @param incomingData 接收到的原始記憶體資料指標
* @param len 接收到的資料位元組(Bytes)長度
*/
void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len) {
// 將接收到的原始記憶體資料,複製填入 myData 結構體中
memcpy(&myData, incomingData, sizeof(myData));
// 1. 印出發送端的 MAC 位址(格式:XX:XX:XX:XX:XX:XX)
Serial.print("收到來自 MAC 位址:");
for (int i = 0; i < 6; i++) {
Serial.printf("%02X", mac[i]);
if (i < 5) Serial.print(":");
}
Serial.println();
// 2. 於 Serial Monitor 印出解析後的資料欄位內容
Serial.print("接收到的位元組數:");
Serial.println(len);
Serial.print("字串內容 (a):");
Serial.println(myData.a);
Serial.print("整數內容 (b):");
Serial.println(myData.b);
Serial.print("浮點數內容 (c):");
Serial.println(myData.c);
Serial.print("布林狀態 (d):");
Serial.println(myData.d ? "true" : "false");
Serial.println("----------------------------------------");
}
void setup() {
// 初始化序列埠通訊(波特率 115200)
Serial.begin(115200);
// 將 ESP32 設為 Wi-Fi 站台模式(Station Mode)
// ESP-NOW 必須在 Wi-Fi 啟動的狀態下才能運行
WiFi.mode(WIFI_STA);
// 初始化 ESP-NOW 協定
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW 初始化失敗!");
return;
}
// 註冊「接收資料」回調函數,當收到封包時自動觸發 OnDataRecv
esp_now_register_recv_cb(OnDataRecv);
}
void loop() {
// 接收端完全由中斷回調函數(OnDataRecv)自動處理資料
// loop 內部可保持空閒,或放置其他控制邏輯(如驅動馬達、控制繼電器、顯示器繪製等)
}
ESP-NOW 常見問題與除錯指南(FAQ)
Q1:報錯 'esp_now_recv_info' does not name a type 怎麼辦?
-
原因:Arduino ESP32 核心庫版本不同(v3.0+ 與 v2.x 語法差異)。
-
解法:使用舊版 v2.x 時,請將接收回調函數參數改為
(const uint8_t *mac, const uint8_t *incomingData, int len)。
Q2:序列埠一直顯示 Delivery Fail(發送失敗)?
-
檢查 MAC 位址:核對
broadcastAddress填寫是否與接收端一致。 -
Wi-Fi 信道相符:確保兩台 ESP32 處於相同的 Wi-Fi Channel。
-
確認接收端狀態:接收端必須已通電並成功執行
esp_now_init(),否則無法回傳 ACK。
Q3:單次傳送的資料容量上限是多少?
-
大小限制:單一封包最多 250 Bytes。
-
建議作法:使用
struct結構體打包資料一次發送;超過容量需手動分包。
Q4:ESP32 與 ESP8266 能互相通訊嗎?
-
可以。但定義
struct時建議加上__attribute__((packed)),避免不同晶片間的記憶體對齊差異導致解析錯誤。
Q5:傳輸距離有多遠?如何延長?
-
預設距離:空曠無遮蔽環境約 50~100 公尺(室內穿牆約 10~30 公尺)。
-
改善方式:選用帶 IPEX 介面的 ESP32 改接外接天線,或於程式中調整 Wi-Fi 發射功率:
WiFi.setTxPower(WIFI_POWER_19_5dBm);