ESP-IDF 中 ESP-NOW 协议实战指南:帧格式、安全机制与 Wi-Fi 无连接通信开发
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
ESP-NOW 是乐鑫(Espressif)定义的一种无连接 Wi-Fi 通信协议,它无需建立 TCP/IP 连接,将应用程序数据封装在供应商动作帧(Vendor-Specific Action Frame)中直接从一个 Wi-Fi 设备传输到另一个 Wi-Fi 设备,广泛应用于智能照明、远程控制、传感器等物联网场景。本文基于 ESP-IDF 官方文档与 esp_now.h 源码,系统讲解 ESP-NOW 的帧格式、CCMP 安全机制、生命周期管理、配对与收发流程,并结合 espnow 示例工程 给出可直接落地的开发方案,帮助你快速掌握在 ESP32 系列芯片上使用 ESP-NOW 进行点对点通信的完整技能。
概述:ESP-NOW 是什么
ESP-NOW 是乐鑫公司定义的无连接 Wi-Fi 通信协议。在 ESP-NOW 中,应用程序数据被封装在各个供应商动作帧中,然后在无连接的情况下,从一个 Wi-Fi 设备传输到另一个 Wi-Fi 设备。这意味着通信双方不需要先完成 Wi-Fi 连接、DHCP 或 TCP/IP 握手,只要在同一个信道上即可直接收发数据,因此具有极低的建立延迟和极小的协议开销。
CTR 与 CBC-MAC 协议(CCMP,Counter Mode with CBC-MAC)可用来保护动作帧的安全,其具体实现参考 IEEE Std. 802.11-2012。目前 ESP-NOW 广泛应用于智能照明、远程控制、传感器等领域。
帧格式:供应商动作帧与特定供应商元素
ESP-NOW 使用供应商动作帧传输数据,默认比特率为 1 Mbps。理解其帧结构是进行协议分析、抓包调试和兼容性判断的基础。
协议版本与数据长度上限
目前 ESP-NOW 支持两个版本:v1.0和v2.0,对应的最大数据包长度定义在 esp_now.h:
| 版本 | 最大数据长度 | 宏定义 | 说明 |
|---|---|---|---|
| v1.0 | 250 字节 | ESP_NOW_MAX_DATA_LEN(即ESP_NOW_MAX_IE_DATA_LEN) | 单次传输的数据长度上限 |
| v2.0 | 1470 字节 | ESP_NOW_MAX_DATA_LEN_V2 | 单次传输的数据长度上限 |
版本间的兼容规则如下:
- v2.0 设备可以接收来自 v2.0 和 v1.0 设备的数据包;
- v1.0 设备只能接收来自 v1.0 设备的数据包;
- 不过,v1.0 设备也可以接收长度不超过 250(
ESP_NOW_MAX_IE_DATA_LEN)字节的 v2.0 数据包;如果长度超过此值,v1.0 设备要么只接收前 250 字节,要么直接丢弃该数据包。
具体行为请参考对应 IDF 版本中的文档。应用程序可以通过esp_now_get_version()查询当前固件支持的 ESP-NOW 版本。
供应商动作帧格式
供应商动作帧的整体格式如下:
----------------------------------------------------------------------------- | MAC 报头 | 分类代码 | 组织标识符 | 随机值 | 供应商特定内容 | FCS | ----------------------------------------------------------------------------- 24 字节 1 字节 3 字节 4 字节 7-x 字节 4 字节各字段含义:
- 分类代码(Category Code):用于指示各个供应商的类别,例如 127;
- 组织标识符(Organization Identifier,OUI):包含一个唯一标识符(比如
0x18fe34),为乐鑫指定的 MAC 地址的前三个字节。可通过esp_now_set_user_oui()/esp_now_get_user_oui()自定义或查询 OUI,传NULL时使用默认值0x18fe34; - 随机值(Random Value):用于防止重放攻击;
- 供应商特定内容(Vendor Specific Content):包含若干个(大于等于 1)特定供应商元素字段。对于 v2.0 版本,
x = 1512 (1470 + 6*7);对于 v1.0 版本,x = 257 (250 + 7); - FCS(Frame Check Sequence):帧校验序列。
特定供应商元素格式
特定供应商元素(Vendor-Specific Element)的帧格式因版本而异:
ESP-NOW v1.0: --------------------------------------------------------------------------- | 元素 ID | 长度 | 组织标识符 | 类型 | 保留 | 版本 | 正文 | --------------------------------------------------------------------------- 7~4 比特| 3~0 比特 1 字节 1 字节 3 字节 1 字节 1 字节 0-250 字节 ESP-NOW v2.0: ------------------------------------------------------------------------------------- | 元素 ID | 长度 | 组织标识符 | 类型 | 保留 |更多数据 | 版本 | 正文 | ------------------------------------------------------------------------------------- 7~5 比特 | 1 比特 | 3~0 比特 1 字节 1 字节 3 字节 1 字节 1 字节 0-250 字节字段说明:
- 元素 ID(Element ID):用于指示特定于供应商的元素;
- 长度(Length):组织标识符、类型、版本和正文的总长度,最大值为 255;
- 组织标识符:唯一标识符(比如
0x18fe34),为乐鑫指定的 MAC 地址的前三个字节; - 类型(Type):设置为 4,代表 ESP-NOW;
- 版本(Version):设置为 ESP-NOW 的版本(v1.0 或 v2.0);
- 正文(Body):包含实际要发送的 ESP-NOW 数据,最长 250 字节(元素级长度上限)。
v2.0 在保留字段中新增了“更多数据”(More Data)位(1 比特),用于支持超过单元素长度上限的更大数据包传输(1470 字节)。
MAC 报头的无连接特性
由于 ESP-NOW 是无连接的,其 MAC 报头与标准帧略有不同:
- FrameControl 字段的 FromDS 和 ToDS 位均为 0(即不经过 AP 转发,属于 IBSS 之外的直接传输);
- 第一个地址字段用于配置目标地址(目的 MAC);
- 第二个地址字段用于配置源地址(源 MAC);
- 第三个地址字段用于配置广播地址
0xff:0xff:0xff:0xff:0xff:0xff。
这种地址排列使得 ESP-NOW 既能支持单播(目标地址为对端 MAC),也能支持广播(目标地址为全0xFF)。
安全:PMK 与 LMK 的两级密钥体系
ESP-NOW 采用 CCMP 方法保护供应商特定动作帧的安全(参考 IEEE Std. 802.11-2012)。Wi-Fi 设备维护一个初始主密钥(PMK,Primary Master Key)和若干本地主密钥(LMK,Local Master Key),每个配对设备拥有一个 LMK,两者长度均为16 字节(对应源码中的ESP_NOW_KEY_LEN)。
两级密钥的分工如下:
- PMK用于使用 AES-128 算法加密 LMK。应用通过
esp_now_set_pmk()设置 PMK;如果未设置,将使用默认 PMK; - LMK用于通过 CCMP 方法对供应商特定动作帧进行加密。如果未设置配对设备的 LMK,则动作帧不进行加密。
需要注意:目前不支持加密组播(multicast)供应商特定动作帧。此外,从源码结构看,SoftAP 与 ESP-NOW 共享同一套加密硬件密钥资源,因此 esp_wifi.h 中提示max_connection参数会受CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM的影响。
初始化和反初始化
调用esp_now_init()初始化 ESP-NOW,调用esp_now_deinit()反初始化 ESP-NOW。API 的返回码定义在 esp_now.h,其中ESP_ERR_ESPNOW_NOT_INIT、ESP_ERR_ESPNOW_ARG、ESP_ERR_ESPNOW_NO_MEM等错误码可用于排查调用失败原因。
关键时序要求:
- ESP-NOW 数据必须在 Wi-Fi 启动后传输,因此建议在初始化 ESP-NOW之前启动 Wi-Fi,并在反初始化 ESP-NOW之后停止 Wi-Fi;
- 当调用
esp_now_deinit()时,配对设备的所有信息都将被删除。
添加配对设备
在将数据发送到其他设备之前,请先调用esp_now_add_peer()将目标设备添加到配对设备列表中。如果启用了加密,则必须设置 LMK。
esp_now_peer_info_t结构体(定义于 esp_now.h)是配对的核心参数:
| 字段 | 说明 |
|---|---|
peer_addr[6] | 对端 MAC 地址(Station 或 SoftAP 的 MAC 地址) |
lmk[16] | 本地主密钥,用于加密数据 |
channel | 对端收发 ESP-NOW 数据的 Wi-Fi 信道;若为 0,则使用当前信道 |
ifidx | 对端使用的 Wi-Fi 接口(STA 或 AP) |
encrypt | 该对端收发数据是否加密 |
priv | 对端私有数据指针 |
ESP-NOW 数据可以从Station 或 SoftAP 接口发送。确保在发送 ESP-NOW 数据之前已启用该接口。
信道规则(按芯片区分)
- ESP32 / ESP32-C2 / ESP32-S2 / ESP32-S3 / ESP32-C3 / ESP32-C6:配对设备的信道范围是0 ~ 14。如果信道设置为 0,数据将在当前信道上发送;否则,必须使用本地设备所在的信道;
- ESP32-C5:2.4G 频段配对设备的信道范围是1 ~ 14;5G 频段配对设备的信道范围是
[36, 40, 44, 48, 52, 56, 60, 64, 100, 112, 116, 120, 124, 128, 132, 136, 140, 144, 149, 153, 157, 161, 165, 169, 173, 177]。如果信道设置为 0,数据将在当前信道上发送;否则,必须使用本地设备所在的信道。
接收端是否必须添加配对设备
对于接收设备,调用esp_now_add_peer()不是必需的:
- 如果没有添加配对设备,只能接收广播包和不加密的单播包;
- 如果需要接收加密的单播包,则必须添加配对设备并设置相同的 LMK。
配对设备数量限制
- ESP32-C2:配对设备的最大数量是20,其中加密设备的数量不超过4,默认值是2。可通过 Wi-Fi menuconfig 中的
CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM修改加密设备的数量(该配置项定义于 esp_wifi/Kconfig); - ESP32 / ESP32-S2 / ESP32-S3 / ESP32-C3 / ESP32-C6 / ESP32-C5:配对设备的最大数量是20,其中加密设备的数量不超过17,默认值是7。同样可通过
CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM修改。
源码中的ESP_NOW_MAX_TOTAL_PEER_NUM(20)与ESP_NOW_MAX_ENCRYPT_PEER_NUM(6)提供了编译期默认约束,esp_now_get_peer_num()可查询当前已有的配对总数与加密对端数。
发送 ESP-NOW 数据
调用esp_now_send()发送 ESP-NOW 数据,调用esp_now_register_send_cb()注册发送回调函数。发送回调的原型为:
typedef void (*esp_now_send_cb_t)(const esp_now_send_info_t *tx_info, esp_now_send_status_t status);发送结果的判定与重传策略:
- 如果 MAC 层成功接收到数据,回调将返回
ESP_NOW_SEND_SUCCESS事件;否则返回ESP_NOW_SEND_FAIL; - ESP-NOW 数据发送失败可能有几种原因,比如目标设备不存在、设备的信道不相同、动作帧在传输过程中丢失等;
- 应用层并不一定总能接收到数据。如果需要,应用层可在接收 ESP-NOW 数据时发回一个应答(ACK)数据;如果接收 ACK 数据超时,则重新传输 ESP-NOW 数据;
- 可以为 ESP-NOW 数据设置序列号,从而在接收端删除重复的数据。
esp_now_send()的三种发送目标(见 esp_now.h 的注意事项):
peer_addr非 NULL:向 MAC 地址匹配的配对设备发送;peer_addr为 NULL:向配对列表中的所有设备发送;- 广播地址
0xFF:0xFF:0xFF:0xFF:0xFF:0xFF:广播发送。
大批量发送的注意事项
如果有大量 ESP-NOW 数据要发送,调用esp_now_send()时需注意:
- 单次发送的数据不能超过最大数据包长(v1.0 为 250 字节,v2.0 为 1470 字节);
- 两个 ESP-NOW 数据包的发送间隔太短可能导致回调函数返回混乱,因此建议在等到上一次回调函数返回 ACK 后再发送下一个 ESP-NOW 数据;
- 发送回调函数从高优先级的 Wi-Fi 任务中运行,不要在回调函数中执行冗长的操作。相反,应将必要的数据发布到队列,交给优先级较低的任务处理。
esp_now_send()可能的返回错误包括ESP_ERR_ESPNOW_NO_MEM(此时可延迟片刻再发送下一条)、ESP_ERR_ESPNOW_NOT_FOUND(对端不在列表中)、ESP_ERR_ESPNOW_IF(当前 Wi-Fi 接口与对端不匹配)、ESP_ERR_ESPNOW_CHAN(当前 Wi-Fi 信道与对端不匹配)等。
接收 ESP-NOW 数据
调用esp_now_register_recv_cb()注册接收回调函数。接收回调原型为:
typedef void (*esp_now_recv_cb_t)(const esp_now_recv_info_t *esp_now_info, const uint8_t *data, int data_len);esp_now_recv_info_t中包含源地址src_addr、目的地址des_addr以及接收控制信息rx_ctrl。由于广播与加密单播在接收路径上可能并存,可以通过检查des_addr是否为广播地址来区分(示例工程中即通过IS_BROADCAST_ADDR(des_addr)判断)。
接收回调函数同样在Wi-Fi 任务中运行,因此:
- 不要在回调函数中执行冗长的操作;
- 将必要的数据发布到队列,交给优先级较低的任务处理;
- 注意
esp_now_info是局部变量,只能在回调函数内部使用。
配置 ESP-NOW 速率
调用esp_now_set_peer_rate_config()配置每个 peer 的 ESP-NOW 速率(对应源码中的esp_now_rate_config_t,即wifi_tx_rate_config_t)。使用约束:
- 请确保在配置速率之前添加 peer;
- 此 API 应在
esp_wifi_start()和esp_now_add_peer()之后调用。
在示例工程的example_wifi_init()中,若启用 Long Range 选项(CONFIG_ESPNOW_ENABLE_LONG_RANGE),会通过esp_wifi_set_protocol()追加WIFI_PROTOCOL_LR,此时 ESP32 设备将以512Kbps 或 256Kbps 的 PHY 速率发送数据,从而在两端之间实现更远距离的传输。
配置 ESP-NOW 功耗参数
当且仅当目标芯片配置为STA 模式时,允许其进行休眠。进行休眠时,调用esp_now_set_wake_window()为 ESP-NOW 收包配置 Window:
- 参数
window表示每个间隔内芯片保持唤醒的毫秒数,范围 0 ~ 65535; - 默认情况下 Window 为最大值,将允许一直收包;
- 该配置在连接状态下可工作;当启用了
ESP_WIFI_STA_DISCONNECTED_PM_ENABLE时,在断开状态下也可工作(见 esp_now.h 的注释)。
如果对 ESP-NOW 进行功耗管理,还需要调用esp_wifi_connectionless_module_set_wake_interval()(声明于 esp_wifi.h),用于设置无连接模块的唤醒间隔。更完整的信息可参考 ESP-IDF 文档中的“非连接模块功耗管理”(connectionless-module-power-save)章节。
应用示例:基于 espnow 示例工程的完整开发流程
examples/wifi/espnow 演示了如何在 ESP32 系列芯片上使用 Wi-Fi 的 ESPNOW 功能,完整覆盖了本文前述的启动 Wi-Fi、初始化 ESP-NOW、注册发送/接收回调、添加对端信息以及在两台设备之间收发数据的全部步骤。工程至少需要两台 ESP 设备。
示例的工作流程
依据 README.md 与 espnow_example_main.c,示例的交互流程如下:
- 设备 1先发送
state=0的广播 ESP-NOW 数据,以便获知对方 MAC 地址; - 设备 2收到
state=0的广播数据后,将设备 1 加入配对列表,并开始发送state=1的广播数据; - 设备 1收到
state=1的广播数据后,比较本地 magic 数与数据中的 magic 数:若本地 magic 更大,则停止广播,开始向设备 2 发送单播 ESP-NOW 数据; - 设备 2收到单播数据后,也停止广播。
实际工程中,如果已知对方 MAC 地址,则不必先通过广播交互,直接将对端加入配对列表并收发单播数据即可。
示例中的数据可靠性设计
示例在 ESP-NOW 原始数据之上额外封装了type、state、seq_num(序列号)、crc(CRC16 校验)和magic等“附加字段”(见 espnow_example.h)。这些字段并非 ESP-NOW 协议所必需,仅用于让示例可靠运行;但官方明确建议实际产品在 ESP-NOW 数据中加入类似扩展,以提升数据的安全性与可靠性:
seq_num用于识别并去重(对应文档中“设置序列号删除重复数据”的建议);crc使用esp_crc16_le()计算校验,用于检测数据损坏;magic用于在广播握手阶段裁决哪台设备负责发起单播。
回调与低优先级任务解耦
示例中,发送/接收回调只负责把事件投递到 FreeRTOS 队列s_example_espnow_queue,真正的数据处理(解析、应答、发送下一条)在低优先级的example_espnow_task中完成。发送端严格遵循“等上一次发送回调返回后再发下一条”的策略,并通过send_param->delay控制两条数据之间的间隔,这正是文档中“不要连续快速发送、避免回调混乱”建议的工程化落地。
配置项说明(menuconfig)
进入工程目录后执行idf.py menuconfig,在Example Configuration菜单下可配置(定义见 Kconfig.projbuild):
| 配置项 | 默认值 | 取值范围 | 说明 |
|---|---|---|---|
ESPNOW_WIFI_MODE | Station | Station / SoftAP | Wi-Fi 工作模式 |
ESPNOW_PMK | pmk1234567890123 | 16 字节字符串 | 主密钥,收发双方必须一致 |
ESPNOW_LMK | lmk1234567890123 | 16 字节字符串 | 本地主密钥,收发双方必须一致 |
ESPNOW_CHANNEL | 1 | 0 ~ 14 | 收发 ESP-NOW 数据的信道,双方必须相同 |
ESPNOW_SEND_COUNT | 100 | 1 ~ 65535 | 单播数据发送总次数 |
ESPNOW_SEND_DELAY | 1000 | 0 ~ 65535 | 两条数据间的发送间隔(ms) |
ESPNOW_SEND_LEN | 10 | 10 ~ 1470 | 发送数据长度(字节) |
ESPNOW_ENABLE_LONG_RANGE | 关闭 | 开/关 | 启用后 PHY 速率为 512Kbps 或 256Kbps,可远距离传输 |
ESPNOW_ENABLE_POWER_SAVE | 关闭 | 开/关 | 启用 ESP-NOW 功耗管理(仅 Station 模式) |
ESPNOW_WAKE_WINDOW | 50 | 0 ~ 65535 | 唤醒窗口(ms) |
ESPNOW_WAKE_INTERVAL | 100 | 1 ~ 65535 | 唤醒间隔(ms) |
要点:PMK 与 LMK 在收发双方必须设置为相同值;发送与接收设备必须处于同一信道。
构建、烧录与运行
idf.py set-target esp32 # 按实际芯片选择目标 idf.py menuconfig # 按上文配置项进行设置 idf.py -p PORT flash monitor # 构建、烧录并打开串口监视器(退出串口监视器:按Ctrl-]。)
接收设备控制台输出示例:
I (898) espnow_example: WiFi started I (898) ESPNOW: espnow [version: 1.0] init I (5908) espnow_example: Start sending broadcast data I (52138) espnow_example: Receive 0th broadcast data from: 30:ae:a4:0c:34:ec, len: 200 I (54168) espnow_example: Receive 0th unicast data from: 30:ae:a4:0c:34:ec, len: 200常见问题排查
- 无法收到对端数据:可能原因是对端不在同一信道,或 PMK / LMK 配置不一致;
- Station 模式接收问题:在真实应用中,如果接收设备仅为 Station 模式且已连接 AP,应禁用 modem sleep,否则可能无法收到来自其他设备的 ESP-NOW 数据;
- 加密单播收不到:接收端必须添加对端并设置相同的 LMK;
- 发送失败:检查
esp_now_send()返回码,特别是ESP_ERR_ESPNOW_IF(接口不匹配)与ESP_ERR_ESPNOW_CHAN(信道不匹配)。
其他常用 API 速查
除本文重点讲解的 API 外,esp_now.h 还提供以下常用接口:
| 函数 | 作用 |
|---|---|
esp_now_del_peer() | 从配对列表删除对端 |
esp_now_mod_peer() | 修改对端信息(如切换加密、改信道) |
esp_now_get_peer()/esp_now_fetch_peer() | 查询 / 遍历配对列表 |
esp_now_is_peer_exist() | 判断对端是否已在列表中 |
esp_now_get_peer_num() | 获取当前配对总数与加密对端数 |
esp_now_unregister_recv_cb()/esp_now_unregister_send_cb() | 注销收发回调 |
esp_now_switch_channel_tx()/esp_now_remain_on_channel() | 切换到指定信道并发送数据 / 在目标信道停留指定时长 |
esp_now_get_version() | 查询当前 ESP-NOW 协议版本 |
小结
ESP-NOW 以供应商动作帧为载体,通过 PMK/LMK 两级密钥与 CCMP 加密提供安全能力,并以“无连接、免握手”的特性成为智能照明、远程控制、传感器等低延迟、低开销物联网场景的理想选择。开发时需重点把握:先启动 Wi-Fi 再初始化 ESP-NOW、发送前添加对端、收发回调中不做耗时操作、严格控制发送节奏与数据长度。结合 espnow 示例工程 的广播握手、CRC 校验、序列号去重与队列解耦设计,即可快速搭建出稳定可靠的 ESP-NOW 点对点通信应用。
【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考