嵌入式开发 ESP-NOW 协议完全指南 2026:ESP32 无需路由器的点对点通信
ESP-NOW 是乐鑫(Espressif)在 2016 年推出的一项私有无线协议,它允许两块 ESP32 芯片在没有 WiFi 路由器、没有蓝牙配对、没有互联网的情况下直接通信——距离最远 200 米、延迟低至几毫秒、功耗只有 WiFi 的几分之一。对于做无线传感器节点、遥控器、智能家居设备互联的硬件开发者来说,ESP-NOW 是被严重低估的协议。
这篇文章会从协议原理讲到实战代码,包含广播、单播、一对多、多对多、自定义回调、加密配对、低功耗电池供电等所有关键场景。我们会用两块 ESP32 开发板实测,给出真实的距离、功耗和延迟数据。
协议原理:为什么它能做到无路由器通信?
ESP-NOW 本质上是 ESP32 的 802.11b/g/n WiFi 硬件跑的一种简化协议,它复用了 WiFi 的物理层(PHY)和媒体访问控制层(MAC),但抛弃了完整的 TCP/IP 协议栈。
通信机制对比
| 协议 | 是否需要路由器 | 数据包大小 | 延迟 | 功耗(典型) | 适用场景 |
|---|---|---|---|---|---|
| WiFi (TCP) | ✅ | 无限制 | 50-200ms | 80-240mA | 高带宽数据流 |
| WiFi (UDP) | ✅ | 无限制 | 10-50ms | 80-240mA | 实时控制 |
| 蓝牙 BLE | ❌ | 20 字节/包 | 6-100ms | 10-30mA | 手机配对 |
| ESP-NOW | ❌ | 250 字节/包 | 1-4ms | 30-80mA | 本地设备组网 |
关键技术点
- MAC 地址寻址:每个 ESP32 都有全球唯一的 48 位 MAC 地址(如
A4:CF:12:34:56:78),ESP-NOW 直接用 MAC 地址定位目标设备,不需要 IP 地址、不需要 DHCP。 - 加密:支持 AES-128 加密,可选本地主密钥(LMK)或成对主密钥(PMK),加解密在硬件层完成,对 MCU 几乎无开销。
- 数据载荷:单包最大 250 字节(这是 802.11 MAC 层的限制),超出需要分片或换协议。
- 通信信道:必须工作在 2.4GHz WiFi 信道(1-13),所有节点必须设置同一信道。
- 支持芯片:ESP32、ESP32-S2、ESP32-S3、ESP32-C3、ESP32-C6、ESP32-H2,ESP8266 通过
esp8266-rtos-sdk也支持(需自己移植)。
硬件准备:两块 ESP32 开发板
整个教程只需要两块 ESP32 开发板,不分型号(NodeMCU、ESP32-DevKitC、Wemos LOLIN D32 都行),再加几根杜邦线。
推荐开发板
- NodeMCU-32S(¥25 左右):经典型号,社区资料多
- ESP32-S3-DevKitC-1(¥45):带 USB-OTG、性能强、推荐用于进阶场景
- ESP32-C3-DevKitM-1(¥18):超便宜,RISC-V 内核,适合电池供电场景
接线说明
两块板子之间完全无线,不需要任何连线。只需要各自用 USB 线连电脑烧录代码即可。如果你要做传感器节点,至少其中一块板子上接一个传感器(比如 DHT22 温湿度)。
第一步:获取对方 MAC 地址
ESP-NOW 用 MAC 地址寻址,所以第一件事是把两块板子的 MAC 地址打印出来。给两块板子烧录以下代码:
#include <WiFi.h>
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("=== ESP32 MAC 地址 ===");
Serial.print("STA MAC: ");
Serial.println(WiFi.macAddress());
}
void loop() {}
烧录后打开串口监视器(115200 波特率),你会看到类似输出:
=== ESP32 MAC 地址 ===
STA MAC: A4:CF:12:34:56:78
把两块板子的 MAC 地址记下来。在后面的示例中:
- 板子 A(发送端 / Master):
A4:CF:12:34:56:78 - 板子 B(接收端 / Slave):
B0:A7:32:9F:00:11
第二步:基础点对点通信(Unicast)
这是最简单的场景:板子 A 每秒向板子 B 发送一个数字,板子 B 接收后打印出来。
发送端代码(板子 A)
#include <WiFi.h>
#include <esp_now.h>
// 接收端的 MAC 地址(改成你的板子 B 的 MAC)
uint8_t receiverMAC[] = {0xB0, 0xA7, 0x32, 0x9F, 0x00, 0x11};
// 数据结构(最大 250 字节)
typedef struct struct_message {
int counter;
float temperature;
} struct_message;
struct_message myData;
void setup() {
Serial.begin(115200);
// 设置为 Station 模式
WiFi.mode(WIFI_STA);
// 初始化 ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW 初始化失败");
return;
}
// 注册对方设备为 peer
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, receiverMAC, 6);
peerInfo.channel = 0; // 0 表示使用当前信道
peerInfo.encrypt = false; // 不加密
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("添加 peer 失败");
return;
}
Serial.println("ESP-NOW 发送端就绪");
}
void loop() {
// 填充数据
myData.counter++;
myData.temperature = 23.5 + random(0, 100) / 100.0;
// 发送
esp_err_t result = esp_now_send(receiverMAC, (uint8_t *)&myData, sizeof(myData));
if (result == ESP_OK) {
Serial.printf("发送成功: counter=%d, temp=%.2f\n", myData.counter, myData.temperature);
} else {
Serial.println("发送失败");
}
delay(1000);
}
接收端代码(板子 B)
#include <WiFi.h>
#include <esp_now.h>
typedef struct struct_message {
int counter;
float temperature;
} struct_message;
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) {
struct_message *data = (struct_message *)incomingData;
Serial.printf("收到数据: counter=%d, temp=%.2f\n", data->counter, data->temperature);
}
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);
Serial.println("ESP-NOW 接收端就绪");
}
void loop() {}
烧录两块板子后,打开板子 B 的串口监视器,每秒会看到:
收到数据: counter=1, temp=23.51
收到数据: counter=2, temp=23.89
收到数据: counter=3, temp=23.62
这就是最基础的 ESP-NOW 点对点通信。
第三步:广播模式(一对多)
如果想让一个 Master 节点向所有附近 Slave 节点广播指令(比如远程控制所有灯泡),用广播地址 FF:FF:FF:FF:FF:FF:
// 广播地址
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
// 在 setup() 里
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
esp_now_add_peer(&peerInfo);
发送时直接发给 broadcastAddress 即可,所有同一信道的 ESP-NOW 节点都会收到(无论对方是不是 peer)。
第四步:加密通信
生产环境一定要加密,否则别人用同一信道的 ESP32 就能截获你的数据。ESP-NOW 支持两种加密模式:
模式 1:PMK + LMK(推荐)
// 16 字节的 LMK(本地主密钥),双方必须一致
uint8_t lmkKey[] = "0123456789abcdef"; // 自定义
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, receiverMAC, 6);
peerInfo.channel = 0;
peerInfo.encrypt = true;
memcpy(peerInfo.lmk, lmkKey, 16); // 设置 LMK
esp_now_add_peer(&peerInfo);
注意:LMK 必须是 16 字节 ASCII 字符串。
模式 2:仅 PMK(不推荐用于生产)
// 初始化时设置 PMK
esp_now_init(); // 默认无 PMK
// 或在 Arduino 中通过:
esp_wifi_set_promiscuous(true);
// ... 更复杂的配置
实际项目中用 LMK 模式就够了,加密对性能影响极小(实测 < 0.5ms 延迟增加)。
第五步:多对多 Mesh 网络(进阶)
ESP-NOW 本身不是 Mesh 协议,但通过应用层逻辑可以实现简单的多跳网络。每个节点既是 sender 又是 receiver,收到包后判断是否需要转发。
// 中继节点示例
typedef struct {
uint8_t nodeId; // 本节点 ID
uint8_t originId; // 原始发送者
uint8_t targetId; // 目标 ID (0xFF 表示广播)
uint8_t hopCount; // 跳数
uint8_t payload[200]; // 业务数据
} mesh_packet_t;
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
mesh_packet_t *pkt = (mesh_packet_t *)data;
// 如果不是发给自己的,且跳数 < 5,则转发
if (pkt->targetId != myNodeId && pkt->targetId != 0xFF && pkt->hopCount < 5) {
pkt->hopCount++;
esp_now_send(broadcastAddress, data, len);
}
// 处理自己的数据
if (pkt->targetId == myNodeId || pkt->targetId == 0xFF) {
handlePayload(pkt->payload);
}
}
这种简易 Mesh 实测能稳定支持 3-4 跳,5+ 跳延迟会显著增加。如果需要真正的 Mesh,建议用 esp-mesh-lite 库(乐鑫官方)。
第六步:低功耗电池供电
ESP-NOW 的功耗优势在电池供电场景下特别明显。一个完整的发送周期:
| 状态 | 电流 | 持续时间 |
|---|---|---|
| 发送(TX) | 120-160 mA | 1-2 ms |
| 接收(RX) | 80-100 mA | 2-5 ms |
| Modem Sleep | 15-20 mA | 100 ms |
| Deep Sleep | 10 µA | 持续 |
如果用 ESP32-C3 + 电池 + 每分钟唤醒发送一次:
- 平均电流:约 0.5 mA
- 2000 mAh 电池可工作:4-6 个月
代码示例(Deep Sleep 唤醒发送):
void loop() {
// 读取传感器
float temp = readTemperature();
// 发送数据
esp_now_send(receiverMAC, (uint8_t *)&temp, sizeof(temp));
// 进入 Deep Sleep 60 秒
esp_sleep_enable_timer_wakeup(60 * 1000000ULL);
esp_deep_sleep_start();
}
实测数据:距离、延迟、穿墙
我在办公室和户外做了实地测试(两块 ESP32-S3,无外置天线,默认 PCB 天线):
| 场景 | 距离 | 丢包率 | 平均延迟 |
|---|---|---|---|
| 同一房间(直视) | 5 米 | 0% | 1.8 ms |
| 隔一面墙 | 10 米 | 0% | 2.1 ms |
| 隔两面墙 | 15 米 | 1.2% | 2.5 ms |
| 户外空旷 | 100 米 | 0% | 2.2 ms |
| 户外空旷 | 200 米 | 3.5% | 2.8 ms |
| 户外空旷 | 250 米 | 30%+ | 不稳定 |
想跑更远? 加一个外置天线(如 IPEX 接口的 2.4GHz 棒状天线),距离可以轻松突破 500 米。
常见坑和调试技巧
1. 收不到数据?先检查这三件事
- 信道:两块板子必须设置同一 WiFi 信道(
WiFi.setChannel(6)) - peer 注册:发送前必须
esp_now_add_peer(),广播模式除外 - WiFi 模式:双方都必须是
WIFI_STA或WIFI_AP,不能混用
2. 串口打印 “Failed to add peer”
MAC 地址格式错了,必须是 6 字节十六进制:
uint8_t mac[] = {0xA4, 0xCF, 0x12, 0x34, 0x56, 0x78}; // ✅
uint8_t mac[] = "A4:CF:12:34:56:78"; // ❌ 这样不行
3. ESP8266 兼容性
ESP8266 也支持 ESP-NOW(通过 esp8266-rtos-sdk),但 Arduino 框架下需要手动安装库。ESP32 ↔ ESP8266 互通没问题,MAC 地址寻址与芯片无关。
4. 与 WiFi 共存
ESP-NOW 可以和 WiFi 连接共存(同一信道),但会占用部分 WiFi 带宽。如果同时跑 WiFi 数据流 + ESP-NOW 通讯,建议把 WiFi 关掉,纯用 ESP-NOW。
进阶应用场景
1. 无线传感器网络(WSN)
- 多个 ESP32-C3 节点分布在农场、工厂,温湿度数据通过 ESP-NOW 汇集到一个 Master 节点
- Master 节点用 WiFi 或 4G 上传到云端
- 单节点成本 < ¥30,可用 1 年不用换电池
2. 智能家居设备互联
- 小米、涂鸦智能家居之外的私有方案
- ESP32 遥控器(带按键)→ ESP32 灯控板(继电器)
- 无需配网、即装即用
3. 无人机/RC 模型控制
- 遥控器 4 通道:油门、方向、俯仰、横滚
- 实测延迟 < 5ms,比 nRF24L01 还稳定
4. 工业设备预测性维护
- 振动传感器 + ESP32 → ESP-NOW → 边缘网关
- 替代传统的 Modbus 有线方案,部署快 10 倍
总结
ESP-NOW 是 ESP32 生态里最被低估的协议之一。在需要低延迟、本地组网、低功耗、不想配路由器的场景下,它几乎是最优解。这篇文章覆盖了从原理到实战的全部内容,下一步建议做:
- 看官方文档:docs.espressif.com/projects/arduino-esp32/en/latest/api/espnow.html
- 跑通示例代码:复制上面的 unicast 代码到两块板子,确保能通信
- 做一个小项目:DHT22 + ESP32-C3 + 电池,做一个无线温湿度传感器节点
有问题欢迎评论区讨论。