ESP-NOW 快速上手
ESP-NOW是什麼?兩片 ESP32、一顆溫濕度感測器、一個小螢幕,不用連WiFi網路、不用架伺服器,就能讓 A 板偵測到的溫濕度,隔空出現在 B 板的 OLED 上。
兩片 ESP32、一顆溫濕度感測器、一個小螢幕,不用連WiFi網路、不用架伺服器,就能讓 A 板偵測到的溫濕度,隔空出現在 B 板的 OLED 上。這就是 ESP-NOW 好玩的地方,簡單、直接、幾行程式就搞定。
ESP-NOW 簡介
ESP-NOW 是 Espressif(樂鑫)開發的低功耗無線通訊協議,基於 Wi-Fi 但不需要路由器。
特點
- 不需要 Wi-Fi 路由器:裝置之間點對點直接通訊
- 低延遲:通常 < 5ms
- 傳輸距離:空曠環境約 200 公尺
- 單次最大傳輸量:250 bytes
- 配對數量:最多 20 個裝置(加密模式下 10 個)
- 省電:比 Wi-Fi 連線省電非常多
適用場景
- 遙控器 / 遠端控制
- 感測器資料回傳
- 多裝置同步(如 LED 燈光秀)
- 簡易聊天 / 訊息傳遞
- 遊戲手把 / 搖桿
🛠️ 準備材料 (硬體需求)
在開始動手前,請確保你準備好了以下零件:
- ESP32 開發板 x2 (建議 NodeMCU-32S,或其他相容板,型號不需要相同!)
- 0.96 吋 OLED 顯示模組 x1 (解析度 128x64,I2C 介面,驅動晶片 SSD1306)
- DHT11 溫濕度感測器模組 x1 (建議買已焊接在 PCB 板上,有 3 支腳的那種模組,比較方便接線)
- 杜邦線 若干 (公對母)
- 麵包板 x1 (或是擴展板,方便接線用)





Arduino IDE 設定
安裝開發板:如果之前已使用過ESP32開發板,就不用重複進行了。若是第一次,請參考我們另一篇專文教學哦!

如果你其中一片開發板也是C3 SuperMini,那就建議也看一下另篇入門教學哦!

發射端接線(ESP32-C3 + DHT11)
DHT11 模組 ESP32-C3
─────────── ─────────
VCC ───────── 5V
DATA ───────── D10 (GPIO10)
GND ───────── GND接收端接線(NodeMCU-32S + SSD1306 OLED)
SSD1306 OLED NodeMCU-32S
──────────── ───────────
VCC ───────── 5V
GND ───────── GND
SDA ───────── GPIO21(預設 I2C SDA)
SCL ───────── GPIO22(預設 I2C SCL)安裝所需函式庫
在 Arduino IDE 中:工具 → 管理程式庫,搜尋並安裝:
| 函式庫名稱 | 用途 | 安裝在 |
|---|---|---|
DHT sensor library(by Adafruit) | 讀取 DHT11 | 發射端 |
Adafruit Unified Sensor | 必備相關 | 發射端 |
Adafruit SSD1306 | 驅動 OLED | 接收端 |
Adafruit GFX Library | OLED 繪圖基礎 | 接收端 |
esp_now.h和WiFi.h是 ESP32 Arduino Core 內建的,不需要額外安裝。
ESP32 NOW運作流程示意圖

第一件事:取得兩片板子的 MAC 位址
將以下程式分別燒錄到兩片板子,從 Serial Monitor 記下各自的 MAC。
ESP NOW傳送端必須先註冊接收端的MAC 位址。以我們要進行的範例來說,因為是單向傳送,所以其實只需要知道一塊板子的MAC 位址,也就是有OLED那塊。
// ============================================================
// 功能:讀取並顯示 MAC 位址
// 兩片板子都燒這份,分別記下 MAC
// ============================================================
#include <WiFi.h>
#include <esp_wifi.h>
void setup() {
Serial.begin(115200);
delay(1000);
WiFi.mode(WIFI_STA);
delay(100);
// 用底層 API 讀取 MAC(避免某些板子回傳全零)
uint8_t mac[6];
esp_wifi_get_mac(WIFI_IF_STA, mac);
// 標準格式
Serial.printf("MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
// 可直接貼到程式中的格式
Serial.printf("uint8_t mac[] = {0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X};\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
void loop() {}

發射端程式(ESP32-C3 + DHT11)
這支程式的工作很單純:讀感測器 → 打包 → 發出去,不斷重複。
程式一開始會引入三個函式庫:esp_now.h 負責 ESP-NOW 通訊、WiFi.h 提供底層無線功能、DHT.h 用來讀取 DHT11 感測器。
最關鍵的部分是 sensor_data 這個 struct。它就像一個信封的格式,裡面規定了要裝哪些東西:溫度、濕度、資料是否有效、第幾次讀取。發射端和接收端必須用一模一樣的信封格式,對方才能正確拆開來看。
setup() 裡面做了五件事,順序很重要:啟動 DHT11 感測器、把 Wi-Fi 設成 STA 模式、初始化 ESP-NOW、註冊發送完成的 callback、最後把接收端的 MAC 位址加入 peer 清單。少了任何一步都無法正常發送。
loop() 每 2 秒跑一次。先用 dht.readTemperature() 和 dht.readHumidity() 讀取溫濕度,然後檢查讀到的值是不是 NaN(DHT 偶爾會讀取失敗回傳 NaN)。不管成功或失敗,都會把資料打包進 struct 並發送出去,差別只在 isValid 這個欄位會標記 true 或 false,讓接收端知道這筆資料能不能用。
OnDataSent() 是發送完成後系統自動呼叫的 callback,它只負責在 Serial 印出成功或失敗。要注意的是,這裡的「成功」只代表封包已經從無線電送出去了,並不保證對方真的有收到,因為 ESP-NOW 沒有回傳確認機制。

// ============================================================
// ESP-NOW 發射端(ESP32-C3 + DHT11)
// 適用:ESP32 Arduino Core 3.x
//
// 功能:每 2 秒讀取 DHT11 溫濕度,透過 ESP-NOW 發送到接收端
// 接線:DHT11 DATA → GPIO10 (D10)
// ============================================================
#include <esp_now.h> // ESP-NOW 協議
#include <WiFi.h> // Wi-Fi(ESP-NOW 基於 Wi-Fi)
#include <DHT.h> // DHT 感測器函式庫
// ======================== 硬體設定 ========================
#define DHT_PIN 10 // DHT11 的 DATA 腳接在 GPIO10(D10)
#define DHT_TYPE DHT11 // 感測器型號(如果用 DHT22 就改這裡)
// ======================== ESP-NOW 設定 ========================
// 【必改】填入「接收端(NodeMCU-32S)」的 MAC Address
uint8_t receiverMAC[] = {0x8C, 0xFD, 0x49, 0x49, 0x64, 0x54};
// ======================== 資料結構 ========================
// 發射端和接收端的 struct 必須完全一致
// 欄位名稱、型別、順序都要一樣
typedef struct sensor_data {
float temperature; // 溫度(°C)
float humidity; // 濕度(%)
bool isValid; // 資料是否有效(DHT 讀取有時會失敗)
int readCount; // 第幾次讀取(方便 debug)
} sensor_data;
// ======================== 全域變數 ========================
DHT dht(DHT_PIN, DHT_TYPE); // 建立 DHT 物件
sensor_data myData; // 要發送的資料
int count = 0; // 讀取計數器
// ============================================================
// 發送回呼函式(Core 3.x 簽名)
//
// 每次 esp_now_send() 完成後自動呼叫
// 注意:「成功」只代表封包已送出,不保證對方收到
// ============================================================
void OnDataSent(const wifi_tx_info_t *info, esp_now_send_status_t status) {
if (status == ESP_NOW_SEND_SUCCESS) {
Serial.println(" → 發送成功 ✅");
} else {
Serial.println(" → 發送失敗 ❌");
}
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("================================");
Serial.println(" ESP-NOW 發射端(C3 + DHT11)");
Serial.println("================================");
// --- 初始化 DHT11 ---
dht.begin();
Serial.println("DHT11 初始化完成");
// --- 設定 Wi-Fi ---
WiFi.mode(WIFI_STA); // Station 模式(ESP-NOW 必須)
delay(100);
Serial.print("本機 MAC: ");
Serial.println(WiFi.macAddress());
// --- 初始化 ESP-NOW ---
if (esp_now_init() != ESP_OK) {
Serial.println("❌ ESP-NOW 初始化失敗!");
while (true) { delay(1000); }
}
Serial.println("✅ ESP-NOW 初始化成功");
// --- 註冊發送回呼 ---
esp_now_register_send_cb(OnDataSent);
// --- 新增接收端為 Peer ---
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo)); // 清空(避免殘留值)
memcpy(peerInfo.peer_addr, receiverMAC, 6); // 填入接收端 MAC
peerInfo.channel = 0; // 0 = 自動頻道
peerInfo.encrypt = false; // 不加密
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("❌ 新增 Peer 失敗!請檢查 MAC");
while (true) { delay(1000); }
}
Serial.println("✅ Peer 新增成功");
Serial.println("開始讀取 DHT11 並發送...\n");
}
void loop() {
// === 讀取 DHT11 ===
// DHT11 每次讀取最少間隔 1 秒,DHT22 最少 2 秒
float temp = dht.readTemperature(); // 讀取溫度(°C)
float humi = dht.readHumidity(); // 讀取濕度(%)
count++;
// 檢查讀取是否成功
// isnan() = is Not A Number,DHT 讀取失敗時會回傳 NaN
if (isnan(temp) || isnan(humi)) {
Serial.printf("[#%03d] ⚠️ DHT11 讀取失敗!\n", count);
// 即使失敗也發送,讓接收端知道狀態
myData.temperature = 0;
myData.humidity = 0;
myData.isValid = false; // 標記為無效資料
myData.readCount = count;
} else {
Serial.printf("[#%03d] 溫度: %.1f°C | 濕度: %.1f%%\n", count, temp, humi);
myData.temperature = temp;
myData.humidity = humi;
myData.isValid = true; // 標記為有效資料
myData.readCount = count;
}
// === 透過 ESP-NOW 發送 ===
esp_err_t result = esp_now_send(
receiverMAC, // 目標 MAC
(uint8_t *)&myData, // 資料指標(轉為 byte 陣列)
sizeof(myData) // 資料大小
);
if (result != ESP_OK) {
Serial.println(" → esp_now_send() 呼叫失敗");
}
delay(2000); // 每 2 秒讀取+發送一次
}
接收端程式(NodeMCU-32S + OLED)
這支程式的角色是被動等待:什麼都不用做,資料來了就顯示。
引入的函式庫比發射端多:除了 ESP-NOW 和 Wi-Fi 之外,還需要 Wire.h 處理 I2C 通訊、Adafruit_GFX.h 提供繪圖基礎函式、Adafruit_SSD1306.h 驅動 OLED 螢幕。
setup() 的重點是先初始化 OLED,確認螢幕能正常運作後顯示「Waiting for data...」等待畫面,接著初始化 ESP-NOW 並註冊接收 callback。注意,接收端不需要呼叫 esp_now_add_peer(),因為它不需要主動發送,只要被動接收就好。
整支程式最核心的是 OnDataRecv() 這個接收 callback。每當有 ESP-NOW 封包進來,系統會自動呼叫它。這個函式做三件事:檢查資料大小是否正確、用 memcpy 把收到的原始 bytes 複製到 struct 裡、設定 newDataReceived 旗標通知 loop() 去更新畫面。這裡刻意不直接在 callback 裡更新 OLED,因為 callback 是在 Wi-Fi 任務的 context 中執行的,做太多事可能會影響系統穩定性。
loop() 不斷檢查旗標,有新資料就呼叫 updateOLED() 重繪畫面。另外每秒也會刷新一次,讓底部狀態列的「幾秒前收到」能持續跳動,這樣即使沒有新資料進來,使用者也能從秒數判斷連線是否正常。
updateOLED() 負責整個畫面的繪製。畫面分成三區:頂部標題列、中間的溫濕度大字、底部的狀態列。如果收到的資料 isValid 是 false,中間會改顯示「Sensor Error!」提示 DHT 讀取出了問題。

// ============================================================
// ESP-NOW 接收端(NodeMCU-32S + SSD1306 OLED)
// 適用:ESP32 Arduino Core 3.x
//
// 功能:接收 ESP-NOW 溫濕度資料,顯示在 OLED 上
// 接線:OLED SDA → GPIO21, SCL → GPIO22
// ============================================================
#include <esp_now.h> // ESP-NOW 協議
#include <WiFi.h> // Wi-Fi
#include <Wire.h> // I2C 通訊(OLED 用)
#include <Adafruit_GFX.h> // OLED 繪圖基礎函式庫
#include <Adafruit_SSD1306.h> // SSD1306 OLED 驅動
// ======================== OLED 設定 ========================
#define SCREEN_WIDTH 128 // OLED 寬度(像素)
#define SCREEN_HEIGHT 64 // OLED 高度(像素)
#define OLED_RESET -1 // Reset 腳,-1 表示與 ESP32 共用
#define OLED_I2C_ADDR 0x3C // OLED I2C 位址(常見為 0x3C 或 0x3D)
// 建立 OLED 物件
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// ======================== 資料結構 ========================
// 【必須與發射端完全一致】
typedef struct sensor_data {
float temperature;
float humidity;
bool isValid;
int readCount;
} sensor_data;
// ======================== 全域變數 ========================
sensor_data incomingData; // 存放收到的資料
bool newDataReceived = false; // 旗標:是否有新資料
unsigned long lastRecvTime = 0; // 上次收到資料的時間
int totalReceived = 0; // 累計收到幾筆
// ============================================================
// 接收回呼函式(Core 3.x 簽名)
//
// 收到 ESP-NOW 封包時由系統自動呼叫
// 注意:callback 中盡量不要做耗時操作(如 OLED 更新)
// 只複製資料、設旗標,在 loop() 中再處理顯示
// ============================================================
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
// 安全檢查:確認資料大小
if (len != sizeof(incomingData)) {
Serial.printf("⚠️ 資料大小不符!預期 %d,收到 %d\n", sizeof(incomingData), len);
return;
}
// 複製資料到 struct
memcpy(&incomingData, data, sizeof(incomingData));
newDataReceived = true; // 設定旗標,通知 loop() 更新顯示
lastRecvTime = millis(); // 記錄收到時間
totalReceived++;
// Serial 也印一份方便 debug
Serial.printf("[收到 #%03d] 溫度: %.1f°C | 濕度: %.1f%% | %s\n",
incomingData.readCount,
incomingData.temperature,
incomingData.humidity,
incomingData.isValid ? "有效" : "無效");
}
// ============================================================
// 更新 OLED 畫面
// ============================================================
void updateOLED() {
display.clearDisplay(); // 清除畫面
// --- 標題列 ---
display.setTextSize(1); // 字體大小 1(6x8 像素)
display.setTextColor(SSD1306_WHITE); // 白色文字
display.setCursor(0, 0); // 游標位置(左上角)
display.println("== ESP-NOW Monitor ==");
// --- 分隔線 ---
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
if (!incomingData.isValid) {
// DHT 讀取失敗的情況
display.setTextSize(1);
display.setCursor(0, 20);
display.println("Sensor Error!");
display.setCursor(0, 35);
display.printf("Pkt: #%d", incomingData.readCount);
} else {
// --- 溫度(大字顯示)---
display.setTextSize(2); // 字體大小 2(12x16 像素)
display.setCursor(0, 16);
display.printf("%.1fC", incomingData.temperature); // 溫度值
// 溫度標籤
display.setTextSize(1);
display.setCursor(100, 16);
display.println("TEMP");
// --- 濕度(大字顯示)---
display.setTextSize(2);
display.setCursor(0, 38);
display.printf("%.1f%%", incomingData.humidity); // 濕度值
// 濕度標籤
display.setTextSize(1);
display.setCursor(100, 38);
display.println("HUMI");
}
// --- 底部狀態列 ---
display.drawLine(0, 54, 127, 54, SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 56);
// 計算距離上次收到資料的秒數
unsigned long elapsed = (millis() - lastRecvTime) / 1000;
display.printf("Rx:%d %lus ago", totalReceived, elapsed);
display.display(); // 將緩衝區內容推送到 OLED
}
// ============================================================
// 顯示等待畫面(尚未收到任何資料時)
// ============================================================
void showWaitingScreen() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("== ESP-NOW Monitor ==");
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
display.setCursor(15, 28);
display.println("Waiting for data...");
display.display();
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("================================");
Serial.println(" ESP-NOW 接收端(OLED 顯示)");
Serial.println("================================");
// --- 初始化 OLED ---
// 使用 NodeMCU-32S 預設 I2C 腳位:SDA=21, SCL=22
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR)) {
Serial.println("❌ OLED 初始化失敗!請檢查接線和 I2C 位址");
while (true) { delay(1000); }
}
Serial.println("✅ OLED 初始化成功");
showWaitingScreen(); // 顯示等待畫面
// --- 設定 Wi-Fi ---
WiFi.mode(WIFI_STA);
delay(100);
Serial.print("本機 MAC: ");
Serial.println(WiFi.macAddress());
// --- 初始化 ESP-NOW ---
if (esp_now_init() != ESP_OK) {
Serial.println("❌ ESP-NOW 初始化失敗!");
while (true) { delay(1000); }
}
Serial.println("✅ ESP-NOW 初始化成功");
// --- 註冊接收回呼 ---
// 接收端只要註冊 callback 就好,不需要 add_peer
esp_now_register_recv_cb(OnDataRecv);
Serial.println("✅ 等待接收資料...\n");
}
void loop() {
// 檢查是否有新資料需要更新到 OLED
if (newDataReceived) {
newDataReceived = false; // 清除旗標
updateOLED(); // 更新 OLED 畫面
}
// 每秒更新一次底部的「幾秒前收到」
// 即使沒有新資料,也會持續刷新時間
static unsigned long lastRefresh = 0;
if (totalReceived > 0 && millis() - lastRefresh > 1000) {
lastRefresh = millis();
updateOLED();
}
delay(10); // 小延遲,避免看門狗觸發
}
OLED 畫面示意

- TEMP / HUMI:溫度和濕度用大字顯示
- Rx:42:累計收到 42 筆資料
- 2s ago:距離上次收到資料 2 秒
總結
這個專案用兩片不同型號的 ESP32 完成了一組無線溫濕度監測系統。ESP32-C3 負責讀取 DHT11 感測器,透過 ESP-NOW 把資料丟出去;NodeMCU-32S 負責接收並顯示在 OLED 上。整個過程不需要 Wi-Fi 路由器、不需要雲端服務、不需要設定 IP,兩片板子靠 MAC 位址就能直接溝通。
ESP-NOW 的優勢在這個場景下很明顯:延遲低、省電、程式碼簡單。從初始化到成功傳輸,核心的 API 其實只有 esp_now_init()、esp_now_add_peer()、esp_now_send() 和兩個 callback,學會這幾個就能應付大部分應用。

幾個值得記住的經驗:
struct 是 ESP-NOW 傳資料的核心概念,兩端定義必須完全一致。加一個 isValid 之類的狀態欄位是好習慣,讓接收端能區分「感測器壞了」和「沒收到資料」這兩種情況。
接收端的 callback 裡盡量只做資料複製和設旗標,把 OLED 更新這類耗時操作留給 loop() 處理,系統會穩定很多。
如果之後要擴充,可以往幾個方向走:加入更多感測器節點變成一對多架構、讓接收端同時連 Wi-Fi 把資料上傳到雲端、或是改成雙向通訊讓接收端能回傳指令控制發射端。ESP-NOW 和 Wi-Fi 可以同時運作,所以這些都做得到。








