不依赖Wi-Fi路由器,无需TCP/IP协议栈,ESP32设备间直接"对话"------ESP-NOW是乐鑫基于Wi-Fi MAC层打造的无连接私有协议。它继承了Wi-Fi物理层约2Mbps的实际吞吐,却将协议栈开销降到最低:配对即用、一发即忘,功耗仅为标准Wi-Fi的十分之一。从传感器数据采集、遥控器到智能家居本地控制网络,ESP-NOW填补了"低功耗"和"高速率"之间的黄金地带。本文将从协议原理出发,带你完成点对点通信、一对多广播、加密传输,并和第11章的Deep Sleep结合,打造一个唤醒后秒发数据的超低功耗传感器节点。
一、引言
在前面的章节中,我们分别学习了 Wi-Fi 联网和 BLE 通信。它们都能满足设备之间的数据交换需求,但都有一个共同的前提:需要一个"中心节点"。Wi-Fi 需要路由器做中继,BLE 需要手机或网关做主机。
那如果两个 ESP32 之间想直接通信呢?------用串口有线连接当然可以,但不方便远距离部署;用 Wi-Fi 走 TCP/IP 又太重,还得事先配好路由器。ESP-NOW 就是为了解决这个"无中心直连"需求而生的。
ESP-NOW 的核心设计哲学是:简单到极致。它没有连接建立的过程,没有握手,没有 ACK 重传的复杂状态机(虽然底层的 Wi-Fi ACK 仍在工作),上层 API 只有三个核心动作------初始化、添加对端、发送数据。接收方注册一个回调,数据到了就被调用。仅此而已。
二、ESP-NOW 协议概览
2.1 什么是 ESP-NOW
ESP-NOW 是乐鑫在 Wi-Fi 的 MAC 层(数据链路层)之上实现的一种无连接通信协议。它直接利用 Wi-Fi 的物理层帧格式,在 MAC 帧的 Vendor Specific 字段中封装 ESP-NOW 数据,省去了 TCP/IP 协议栈的层层封装和解析开销。
关键特征:
| 特性 | 说明 |
|---|---|
| 通信模式 | 点对点、一对多广播、组播 |
| 最大数据负载 | 250 字节 |
| 加密支持 | AES-CCMP(对标 WPA2 加密强度) |
| 实际吞吐 | 约 1~2 Mbps(取决于环境) |
| 通信距离 | 同标准 Wi-Fi(开阔环境约 200~300m) |
| 对端数量 | 加密对端 ≤ 6 个,非加密对端 ≤ 20 个 |
| 是否需要 Wi-Fi 连接 | 否(但底层需要初始化 Wi-Fi 驱动) |
2.2 与 Wi-Fi / BLE 的对比
| 维度 | ESP-NOW | Wi-Fi (Station) | BLE |
|---|---|---|---|
| 是否需要路由器/主机 | 否 | 是 | 是 |
| 单次传输时间 | 1~3ms | 10~100ms(含TCP握手) | 3~15ms |
| 峰值电流 | ~80mA(仅发送) | ~180mA | ~30mA |
| 数据速率 | ~2Mbps | 最高 150Mbps | 1Mbps |
| 协议复杂度 | 极低 | 高 | 中 |
ESP-NOW 的优势在于极低的传输等待时间 ------从调用 esp_now_send() 到对端收到数据,通常只需要 1~3ms。而标准 Wi-Fi 从建立 TCP 连接到真正发送数据,至少需要几十毫秒。
2.3 工作模式与限制
ESP-NOW 需要初始化 Wi-Fi 驱动,但不要求连接路由器。它支持两种组合模式:
- Station 模式(最常用):使用 Station MAC 地址作为身份标识
- SoftAP + Station 共存模式:可以同时作为 AP 供其他设备连接,又通过 ESP-NOW 与其他 ESP32 通信
⚠️ 注意:ESP-NOW 和标准 Wi-Fi 共用同一个 Wi-Fi 射频硬件。如果设备既用 ESP-NOW 又要连接路由器,两者会分时复用射频通道。ESP-NOW 的通道必须与路由器所在通道一致,否则通信会中断。
三、ESP-NOW 核心 API
ESP-IDF 为 ESP-NOW 提供了简洁的 C 语言 API。我们先从最核心的初始化、发送和接收入手。
3.1 初始化流程
c
#include "esp_now.h"
#include "esp_wifi.h"
#define ESPNOW_CHANNEL 1 // 默认通道
static void espnow_init(void)
{
// 1. 初始化 Wi-Fi(必须,即使不连路由器)
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
// 2. 初始化 ESP-NOW
ESP_ERROR_CHECK(esp_now_init());
// 3. 注册发送和接收回调
ESP_ERROR_CHECK(esp_now_register_send_cb(espnow_send_cb));
ESP_ERROR_CHECK(esp_now_register_recv_cb(espnow_recv_cb));
}
3.2 发送回调与接收回调
c
// 发送完成回调
static void espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status)
{
if (status == ESP_NOW_SEND_SUCCESS) {
ESP_LOGI(TAG, "发送成功 -> " MACSTR, MAC2STR(mac_addr));
} else {
ESP_LOGE(TAG, "发送失败 -> " MACSTR, MAC2STR(mac_addr));
}
}
// 接收回调
static void espnow_recv_cb(const esp_now_recv_info_t *recv_info,
const uint8_t *data, int len)
{
ESP_LOGI(TAG, "收到来自 " MACSTR " 的 %d 字节数据",
MAC2STR(recv_info->src_addr), len);
ESP_LOG_BUFFER_HEX("data", data, len > 32 ? 32 : len);
}
3.3 添加对端与发送数据
c
#include "esp_now.h"
#include "esp_mac.h" // MACSTR 宏
// 示例:添加对端并发送数据
void espnow_send_to_peer(const uint8_t *peer_mac)
{
// 1. 注册对端
esp_now_peer_info_t peer = {
.channel = ESPNOW_CHANNEL,
.ifidx = WIFI_IF_STA,
.encrypt = false,
};
memcpy(peer.peer_addr, peer_mac, 6);
// 如果对端已存在,先移除再添加(确保参数更新)
esp_now_del_peer(peer_mac);
ESP_ERROR_CHECK(esp_now_add_peer(&peer));
// 2. 发送数据
uint8_t payload[] = "Hello ESP-NOW!";
ESP_ERROR_CHECK(esp_now_send(peer_mac, payload, sizeof(payload)));
}
// 广播:将 peer_addr 设为广播地址
void espnow_broadcast(void)
{
uint8_t broadcast_mac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
esp_now_peer_info_t peer = {
.channel = ESPNOW_CHANNEL,
.ifidx = WIFI_IF_STA,
.encrypt = false,
};
memcpy(peer.peer_addr, broadcast_mac, 6);
esp_now_del_peer(broadcast_mac);
ESP_ERROR_CHECK(esp_now_add_peer(&peer));
uint8_t data[] = "Broadcast!";
esp_now_send(broadcast_mac, data, sizeof(data));
}
四、实战一:点对点通信
现在我们实现一个完整的点对点示例:设备 A(发送端) 每 5 秒向设备 B(接收端) 发送一次温湿度数据。
4.1 发送端代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_now.h"
#include "esp_wifi.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#define TAG "SENDER"
#define ESPNOW_CHANNEL 1
// 接收端 MAC 地址(替换为你的实际 MAC)
#define PEER_MAC {0xC8, 0xF0, 0x9E, 0xXX, 0xXX, 0xXX}
static void espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status)
{
ESP_LOGI(TAG, "发送%s", status == ESP_NOW_SEND_SUCCESS ? "成功" : "失败");
}
void app_main(void)
{
// 初始化 NVS(Wi-Fi 需要)
nvs_flash_init();
esp_netif_init();
esp_event_loop_create_default();
// Wi-Fi Station 模式
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
// 设置 Wi-Fi 通道(必须与接收端一致)
esp_wifi_set_channel(ESPNOW_CHANNEL, WIFI_SECOND_CHAN_NONE);
// ESP-NOW 初始化
esp_now_init();
esp_now_register_send_cb(espnow_send_cb);
// 添加对端
uint8_t peer_mac[6] = PEER_MAC;
esp_now_peer_info_t peer = {
.channel = ESPNOW_CHANNEL,
.ifidx = WIFI_IF_STA,
.encrypt = false,
};
memcpy(peer.peer_addr, peer_mac, 6);
esp_now_add_peer(&peer);
// 模拟温湿度数据
float temp = 25.5, humid = 60.0;
while (1) {
esp_now_send(peer_mac, (uint8_t *)&temp, sizeof(temp));
vTaskDelay(pdMS_TO_TICKS(5000));
esp_now_send(peer_mac, (uint8_t *)&humid, sizeof(humid));
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
4.2 接收端代码
c
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_now.h"
#include "esp_wifi.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#define TAG "RECEIVER"
#define ESPNOW_CHANNEL 1
static void espnow_recv_cb(const esp_now_recv_info_t *info,
const uint8_t *data, int len)
{
if (len == sizeof(float)) {
float value;
memcpy(&value, data, sizeof(float));
ESP_LOGI(TAG, "收到浮点数据: %.2f (来自 " MACSTR ")",
value, MAC2STR(info->src_addr));
} else {
ESP_LOGI(TAG, "收到 %d 字节 (来自 " MACSTR ")", len, info->src_addr);
}
}
void app_main(void)
{
nvs_flash_init();
esp_netif_init();
esp_event_loop_create_default();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
esp_wifi_set_channel(ESPNOW_CHANNEL, WIFI_SECOND_CHAN_NONE);
esp_now_init();
esp_now_register_recv_cb(espnow_recv_cb);
ESP_LOGI(TAG, "接收端已启动,MAC: ");
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
ESP_LOGI(TAG, MACSTR, MAC2STR(mac));
// 接收端不需要添加对端,只需注册回调
while (1) {
vTaskDelay(pdMS_TO_TICKS(60000));
}
}
关键点:接收端无需调用
esp_now_add_peer()------ESP-NOW 的接收回调对任何来源的数据都触发,不论是否注册过对端。只有发送时才需要将对端添加到对端列表。
五、实战二:一对多广播
广播模式是 ESP-NOW 的一大亮点。将目标 MAC 地址设为 0xFF:FF:FF:FF:FF:FF,同频道内所有 ESP-NOW 设备都会收到数据。
c
#define ESPNOW_CHANNEL 1
// 发送端------广播
void app_main(void)
{
nvs_flash_init();
esp_netif_init();
esp_event_loop_create_default();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
esp_wifi_set_channel(ESPNOW_CHANNEL, WIFI_SECOND_CHAN_NONE);
esp_now_init();
esp_now_register_send_cb(espnow_send_cb);
// 添加广播对端
esp_now_peer_info_t peer = {
.channel = ESPNOW_CHANNEL,
.ifidx = WIFI_IF_STA,
.encrypt = false,
};
memset(peer.peer_addr, 0xFF, 6);
esp_now_add_peer(&peer);
uint32_t count = 0;
while (1) {
char msg[32];
snprintf(msg, sizeof(msg), "broadcast seq=%lu", (unsigned long)count++);
esp_err_t ret = esp_now_send(peer.peer_addr, (uint8_t *)msg, strlen(msg) + 1);
ESP_LOGI(TAG, "广播: %s (%s)", msg, ret == ESP_OK ? "OK" : "FAIL");
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
广播场景下,接收端可以用 recv_info->src_addr 区分消息来源。同一个频道内最多可以有几十个设备同时监听,无需任何配网流程,上电即通。
六、实战三:加密通信
ESP-NOW 支持 AES-CCMP 加密(128 位密钥),对标 WPA2 的加密强度。加密通信需要先通过不加密通道交换 Primary Master Key(PMK),然后设置每个对端的 Local Master Key(LMK)。
6.1 发送端(加密配对)
c
#include "esp_now.h"
static uint8_t s_pmk[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10}; // PMK(保密)
static uint8_t s_lmk[16] = {0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C,
0x1D, 0x1E, 0x1F, 0x20}; // LMK(保密)
void espnow_encrypted_send(const uint8_t *peer_mac)
{
// 1. 设置全局 PMK
esp_now_set_pmk(s_pmk);
// 2. 添加加密对端
esp_now_peer_info_t peer = {
.channel = ESPNOW_CHANNEL,
.ifidx = WIFI_IF_STA,
.encrypt = true, // 启用加密
};
memcpy(peer.peer_addr, peer_mac, 6);
memcpy(peer.lmk, s_lmk, 16); // 设置该对端的 LMK
esp_now_add_peer(&peer);
// 3. 发送数据(自动加密)
uint8_t secret[] = "This is secret data";
esp_now_send(peer_mac, secret, sizeof(secret));
}
6.2 注意事项
- PMK(Primary Master Key):全局密钥,所有加密对端共享。用于派生加密参数。
- LMK(Local Master Key):每个对端独立设置。两端设备的 LMK 必须相同,否则无法解密。
- 加密对端最大数量:6 个(非加密对端最多 20 个)。
- 加密数据长度不变,但底层 Wi-Fi 帧会增加约 16 字节的 MIC(消息完整性校验码)。
七、实战四:结合 Deep Sleep 的低功耗节点
我们将第11章的"BLE 传感器节点"改造为ESP-NOW 版本,实现"Deep Sleep 唤醒 → 采集 → ESP-NOW 发送 → 继续睡"的超低功耗循环。
c
#include "esp_now.h"
#include "esp_sleep.h"
#include "esp_wifi.h"
#include "esp_mac.h"
RTC_DATA_ATTR static int s_send_count = 0;
// 接收端 MAC(替换为你的接收网关 MAC)
#define GATEWAY_MAC {0xC8, 0xF0, 0x9E, 0xAA, 0xBB, 0xCC}
void espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status)
{
// 回调运行在 ISR 或高优先级任务中,仅设置标志
}
void app_main(void)
{
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
if (cause == ESP_SLEEP_WAKEUP_UNDEFINED) {
// 首次上电:初始化
nvs_flash_init();
s_send_count = 0;
}
// 每次唤醒都执行的采集和发送流程
s_send_count++;
ESP_LOGI(TAG, "#%d 唤醒", s_send_count);
// 1. 初始化 Wi-Fi + ESP-NOW
esp_netif_init();
esp_event_loop_create_default();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
esp_now_init();
esp_now_register_send_cb(espnow_send_cb);
// 2. 添加网关对端
uint8_t gw_mac[6] = GATEWAY_MAC;
esp_now_peer_info_t peer = {
.channel = 1, .ifidx = WIFI_IF_STA, .encrypt = false,
};
memcpy(peer.peer_addr, gw_mac, 6);
esp_now_add_peer(&peer);
// 3. 采集传感器数据(I2C 初始化)
init_i2c();
float temp, humid;
sht30_read_data(&temp, &humid);
// 4. 通过 ESP-NOW 发送到网关
// 自定义数据包格式:count + temp + humid
uint8_t packet[12];
memcpy(packet, &s_send_count, 4);
memcpy(packet + 4, &temp, 4);
memcpy(packet + 8, &humid, 4);
esp_now_send(gw_mac, packet, sizeof(packet));
vTaskDelay(pdMS_TO_TICKS(10)); // 等待发送完成
// 5. 清理并进入 Deep Sleep
esp_wifi_stop();
esp_now_deinit();
esp_sleep_enable_timer_wakeup(30 * 60 * 1000000); // 30 分钟
esp_deep_sleep_start();
}
这个方案的功耗特性和第11章的 BLE 版本几乎一致(都用了相同的 Deep Sleep 策略),但发送延迟从 BLE 的 200~500ms 连接建立时间降低到 ESP-NOW 的 1~3ms。网关端可以是一个常开电源的 ESP32,接收数据后通过 Wi-Fi 上传到 MQTT 服务器,实现低功耗传感器网络与云端的桥梁。
🧠 深度思考
ESP-NOW 是我非常欣赏的一个协议------不是因为它技术有多先进,而是因为它在"够用"和"简单"之间拿捏得恰到好处。
Wi-Fi 协议栈太重,BLE 的连接模型太繁琐,Zigbee 需要网关和特定硬件...... 嵌入式开发者其实经常面临一个困境:我就想传一个温度值,为什么需要搭一套完整的网络协议栈? ESP-NOW 的回答是:不需要。它直接把数据塞进 Wi-Fi MAC 帧的厂商自定义字段里,一发一收,干净利落。
当然,这种"简单"是有代价的。它没有 TCP 那样的拥塞控制,没有应用层的 ACK 重传(虽然你可以自己在数据包里加序列号实现),也没有路由能力------两个设备必须在一个 Wi-Fi 频道内,中间隔一堵厚墙就不可靠了。所以 ESP-NOW 并不适合做大面积组网,但在"单个房间内的传感器采集"、"遥控器控制"、"玩具之间互动"这些场景里,它几乎是完美的选择。
我个人的经验是:ESP-NOW 适合做系统的"最后三米"通信层,而不要试图用它做整个网络。网关收集 ESP-NOW 数据后再通过 Wi-Fi/Ethernet/4G 上云,这可能是 ESP-NOW 最高频、最实用的部署模式。
⚠️ 避坑指南 / 注意事项
-
Wi-Fi 初始化是必须的 :即使不连接路由器,也必须调用
esp_wifi_init()和esp_wifi_start()。Wi-Fi 驱动负责 MAC 层的收发操作,没有它 ESP-NOW 无法工作。 -
频道必须一致 :ESP-NOW 通信双方必须工作在同一个 Wi-Fi 频道上。如果一方调用了
esp_wifi_set_channel()改变了频道,双方需保持一致。特别是同时使用 Wi-Fi Station 连接路由器 + ESP-NOW 时,ESP-NOW 频道必须与路由器频道一致(连路由器时频道由 AP 决定,不可手动设置)。 -
250 字节数据限制 :ESP-NOW 单次发送的最大数据负载为 250 字节。超过此长度会返回
ESP_ERR_ESPNOW_ARG。如果数据量较大,需要分片发送并在应用层重组。 -
发送回调的上下文 :
esp_now_register_send_cb()注册的回调运行在 Wi-Fi 任务的上下文中 (不是你的应用任务),里面不能做阻塞操作(如vTaskDelay()、malloc()、互斥锁等待)。建议在回调中只设置标志位或信号量,在主循环中处理后续逻辑。 -
发送失败不等于对方没收到 :ESP-NOW 的发送回调返回
ESP_NOW_SEND_SUCCESS只表示底层 Wi-Fi ACK 已收到 ,即数据已被对端的 Wi-Fi 硬件确认接收,但并不保证对端的应用层一定处理了该数据。反之,ESP_NOW_SEND_FAIL通常是底层连续重试失败(超过最大重试次数),但如果只是偶尔丢包,发送回调仍然可能是SUCCESS。 -
Deep Sleep 后需重新初始化 :因为 Deep Sleep 会完全断电 Wi-Fi 外设,唤醒后必须重新调用
esp_wifi_init()→esp_now_init()→ 重新添加对端。esp_now_deinit()和esp_wifi_stop()在入睡前的清理也非常重要,否则底层可能会漏电。 -
ESP-NOW 与 BLE 不能同时使用:ESP32-S3 的 Wi-Fi 和 BLE 共享 2.4GHz 射频前端,且 ESP-NOW 依赖 Wi-Fi MAC 层。虽然理论上 Wi-Fi 和 BLE 可以分时复用,但 ESP-NOW 运行期间 BLE 功能不可用。如果项目同时需要 ESP-NOW 和 BLE,需要在软件中切换模式。
-
MAC 地址白名单:生产环境中建议在接收端维护一个 MAC 地址白名单,只处理已知设备的数据。否则只要同一频道上的任意 ESP32 发送数据,接收端的回调都会被触发,容易被恶意或无意干扰。
总结
本章我们全面覆盖了 ESP-NOW 无线通信的核心知识点:
- 无连接协议:ESP-NOW 在 Wi-Fi MAC 层上实现,无需 TCP/IP 协议栈,无需路由器
- 低延迟:单次传输仅 1~3ms,适合对实时性有要求的场景
- 三种通信模式:点对点、一对多广播、加密通信
- 四类实战:从基础的一对一通信到加密传输,再到结合 Deep Sleep 的低功耗传感器节点
- 250 字节数据限制:适合传感器数据、遥控指令等小数据量场景
至此,你已经掌握了 ESP32-S3 上所有的无线通信方式:Wi-Fi、BLE、ESP-NOW。下一章我们将整合这些通信能力,搭建一个完整的物联网云平台接入方案------让 ESP32-S3 通过 MQTT 协议连接到阿里云 IoT / AWS IoT / 自建 Broker,实现远程监控和数据上云。
下篇预告
下一章我们将打通"设备到云端"的最后一段路------MQTT 物联网通信。你将学习如何在 ESP32-S3 上实现 MQTT 客户端,连接到主流的物联网平台,实现设备属性上报、远程控制指令下发和 OTA 固件升级。这是每个物联网项目都绕不开的实战环节。