
如果你用ESP32做过WiFi抓包多半遇见过一类很奇怪的短帧不是Beacon不是Probe Request显示成Data帧又解不出内容在Wireshark里只能看到一截不明所以的载荷。我第一次看到它们时还以为是邻居家的私有协议后来查了一圈才明白这是ESP32片内WiFi基带里一条被官方API半遮半掩的“无线电通路”——ESP-NOW。说它“没写进手册”是指《ESP32技术参考手册》通篇在讲MAC/BB/RF寄存器对这条免连接的点对点数据通路几乎不提而SDK里又确实留了完整接口连帧格式都不会给你系统讲清楚。这篇文章把我从偶然抓包到自建链路、再到跑通传感器数据上报的完整过程记录下来包括参数细节和踩坑记录适合想深入玩ESP32无线底层的开发者、嵌入式爱好者和物联网工程师。1. 先搞清楚这条“隐藏”通路到底是什么1.1 一次意外抓包让我注意到那些短帧事情发生得很偶然。当时我在一个房间里调试八个ESP32节点这些节点通过TCP Socket上报状态但总有几台设备延迟特别高。我怀疑是2.4GHz频段被附近AP挤爆了顺手开了一下WiFi混杂模式Promiscuous Mode想看看空气里到底在跑什么帧。结果除了常见的Beacon、Probe Request之外我注意到一组长度固定、源MAC地址前缀为乐鑫OUI的短帧它们不是普通的IP数据帧因为载荷里看不到TCP/UDP头也没有我熟悉的HTTP报文。更奇怪的是这些短帧出现得非常规律每秒一次和我的节点心跳周期完全对得上。那一刻我反应过来这就是我那些ESP32在互相通信而且走的不是传统网络栈。后来我确认这些帧就是ESP-NOW。ESP-NOW是乐鑫在WiFi协议栈里提供的一种无连接通信协议应用数据会被直接封装进802.11帧从射频口发出去。它在官方API文档里有专门章节但芯片技术参考手册确实没有系统描述其帧结构和实现机制。所以用“没写进手册的无线电通路”来形容它一点都不过分。1.2 这其实是一条“裸奔”的链路层通路理解ESP-NOW的关键在于“无连接”和“链路直通”这两个词。常规的WiFi通信节点要先扫描、认证、关联拿到IP地址之后还要跑TCP三次握手每次发数据协议栈层层封装接收端再层层解包中间任何一环有问题就重传。这套流程适合互联网但对局域网里“两个设备之间传一个布尔值”这种场景实在太重了。ESP-NOW把中间这些步骤全砍了。应用程序的数据经过极简封装直接送进WiFi驱动由基带把它按802.11帧的格式打到空中对端只要在同一个信道、注册过发送方的MAC地址就能直接从基带把帧提出来。整个过程没有关联、没有IP、没有TCP握手数据像从一条管道里直接滑过去。我用一个生活化类比普通WiFi通信像你打电话要先拨号、等待接通、说“喂”确认对方在听再开始说话。ESP-NOW则像用对讲机按下PTT就喊对方在同一个信道且开着机器就能听到。好处是极低的延迟和极小的开销代价是没有完善的连接管理链路质量、重传策略都要自己兜底。1.3 官方为什么没把它写进芯片手册很多人听到“隐藏通路”会觉得是什么后门其实真不是。ESP-NOW是官方SDK公开支持的功能只是它没有出现在技术参考手册里而已。这背后的原因并不复杂。芯片技术参考手册的定位是描述硬件模块的行为WiFi基带怎么配置、MAC控制器有哪些寄存器、射频前端如何校准。ESP-NOW的实现并不在这些硬件模块的固定寄存器里它更大程度上是ROM固件和WiFi驱动软件的功劳。对芯片厂商来说软件层面的协议栈迭代太快手册根本跟不上。今天写进去明天驱动改一版帧结构手册就过时了。另外还有一个很少被提到的原因ESP-NOW这种帧结构在普通802.11抓包里看起来就像“奇怪的私有数据帧”如果厂商把它当作正规无线协议来宣传用户可能会误以为它能和普通WiFi设备互联进而引发大量兼容性投诉。所以乐鑫的做法很务实SDK里API文档写得清清楚楚但在芯片手册这种偏硬件的资料里几乎不给它名分。加上很多人在网上分享时总带着“发现新大陆”的语气这条通路在社区里就越传越神秘了。2. 动手之前环境、板子和烧录基本功2.1 开发环境怎么选Arduino、ESP-IDF还是PlatformIO想玩转ESP-NOW第一步是选对开发环境。我自己三个环境都用过简单对比一下环境优点缺点适合人群Arduino IDE上手快ESP-NOW封装简单示例多类型安全弱底层控制不够细新手、快速验证想法的玩家PlatformIO项目管理规范跨平台构建库管理好用需要了解平台配置离线包需要单独准备中高级开发者、多人协作项目ESP-IDF官方维护最接近底层API最完整学习曲线陡峭Windows下编译速度慢想要深入理解WiFi协议栈的工程师Arduino IDE是目前最省事的。安装ESP32开发板支持包后直接在库管理器里搜索ESP-NOW相关示例即可。如果你在公司内网或者网络不稳定可以下载离线包手动安装Arduino IDE和PlatformIO都有对应的离线支持包这能省掉很多“下载开发板索引卡住”的烦恼。PlatformIO其实更适合做正经项目因为它能清楚地区分库依赖、环境配置和源码而且命令行编译方便后续脚本化。平台装好之后在platformio.ini里声明一下board esp32dev再引入Arduino框架就能用同一套代码跑起来。ESP-IDF是终极选项。如果你以后想改WiFi驱动甚至调试空口行为IDF是唯一能让你碰到底层的地方。缺点是Windows环境下全量编译一套工程经常要等好几分钟我建议把编译输出目录放到SSD上并把CCACHE_ENABLE打开实测下来能快不少。2.2 板子选型和烧录姿势实验用的板子其实只要是真ESP32芯片就行。原版ESP32 DevKit、D1 R32、ESP32-S3 Super Mini都可以跑通ESP-NOW。Super Mini这类小板子引脚少但胜在便宜、体积小适合做节点原版ESP32的双核和足够的RAM让它当网关更舒服。ESP32-S3的USB口可以直接烧录比老款省事。烧录这件事看起来简单其实坑不少。大多数开发板上电的时刻如果GPIO0保持低电平芯片就会进入下载模式。所以常见做法是先把IO0接地按着BOOT键再按一下EN复位最后松开BOOT。很多板子设计了自动下载电路CH340或CP2102这类USB转串口芯片会通过DTR/RTS信号控制EN和IO0的时序IDE点烧录的瞬间就能自动进入下载模式。我用过的板子里CP2102的自动下载电路通常比CH340更稳定。如果你发现点烧录后总是“连接超时”先别怀疑芯片坏了大概率是USB转串口芯片的驱动没装好或者自动下载电路的电容老化导致时序不对。这时候手动按BOOT再点烧录基本都能救回来。3. 打开这条通路的三个核心实验3.1 实验一两片ESP32隔空互发自定义数据第一个实验很直接让两片ESP32用ESP-NOW互发一条结构体数据。这里我会用Arduino环境因为它封装得最干净。发送端代码如下#include WiFi.h #include esp_now.h // 接收端的MAC地址 uint8_t peerMac[] {0x24, 0x6F, 0x28, 0x12, 0x34, 0x56}; typedef struct sensor_msg { float temperature; float humidity; uint32_t seq; } sensor_msg; sensor_msg msg; void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { Serial.print(发送状态); Serial.println(status ESP_NOW_SEND_SUCCESS ? 成功 : 失败); } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); esp_now_init(); esp_now_register_send_cb(OnDataSent); esp_now_peer_info_t peer {}; memcpy(peer.peer_addr, peerMac, 6); peer.channel 1; // 双方必须都在信道1 peer.encrypt false; // 先不加密方便抓包观察 esp_now_add_peer(peer); msg.temperature 26.5; msg.humidity 60.1; msg.seq 0; } void loop() { msg.temperature 0.1; msg.humidity 0.05; msg.seq; esp_now_send(peerMac, (uint8_t *)msg, sizeof(msg)); delay(1000); }接收端只需要初始化WiFi、注册接收回调#include WiFi.h #include esp_now.h typedef struct sensor_msg { float temperature; float humidity; uint32_t seq; } sensor_msg; sensor_msg msg; void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) { if (data_len sizeof(msg)) { memcpy(msg, data, sizeof(msg)); Serial.printf(温度: %.2f, 湿度: %.2f, seq: %u\n, msg.temperature, msg.humidity, msg.seq); } } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); esp_now_init(); esp_now_register_recv_cb(OnDataRecv); } void loop() {}我当时的实测结果两片板子相隔五米没有任何障碍物发送成功率接近100%回调里几乎每次都是ESP_NOW_SEND_SUCCESS。整套链路延迟非常低从发送端调用函数到接收端回调执行体感上就是毫秒级。需要注意的是发送间隔设置得再短也不要小于无线链路的实际传输时间ESP-NOW的单帧载荷上限是250字节这个限制后面还会提到。3.2 实验二用混杂模式“亲眼”看到通路上跑的帧实验一跑通之后我更想知道空中到底长什么样。于是把其中一块板子改成混杂模式让它把所有收到的802.11帧都打印出来#include WiFi.h #include esp_wifi.h void promiscuous_cb(void *buf, wifi_promiscuous_pkt_type_t type) { wifi_promiscuous_pkt_t *pkt (wifi_promiscuous_pkt_t *)buf; wifi_ieee80211_packet_t *frame (wifi_ieee80211_packet_t *)pkt-payload; // 只关心发往广播地址或对我们板卡MAC的帧 uint8_t *dst frame-addr1; if (dst[0] 0x01) { // 组播/广播帧 Serial.printf(类型%d len%u dstFF:FF:FF:FF:FF:FF\n, type, pkt-rx_ctrl.sig_len); } } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); esp_wifi_set_promiscuous(true); esp_wifi_set_promiscuous_rx_cb(promiscuous_cb); esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); } void loop() {}跑起来之后串口会刷出一堆广播帧其中就包含实验一里发送端发出的ESP-NOW帧。由于我们没有开加密把载荷以十六进制打印出来能看到里面就是sensor_msg结构体的原始字节温度、湿度、序号一览无余。这个现象也提醒了我一个很重要的安全点如果ESP-NOW不开加密任何在同一个信道里的WiFi设备都能看到你的载荷。所以传敏感数据时绝对不能裸奔。3.3 实验三把温湿度传感器数据塞进帧里实验二让我确信ESP-NOW是一条透明的数据管道。接下来的问题很自然能不能把真实传感器数据塞进去我把一个SHT30温湿度传感器接到ESP32上在循环里读取数据更新同一个结构体再通过ESP-NOW发走。代码和实验一几乎一样只是在loop里加了真实的传感器读取float temp sht30.readTemperature(); float rh sht30.readHumidity(); msg.temperature temp; msg.humidity rh; msg.seq; esp_now_send(peerMac, (uint8_t *)msg, sizeof(msg));有一个很重要的细节结构体在发送和接收两端的内存布局必须完全一致。如果发送端用float数组接收端以为收到的是int数组解析出来就是乱七八糟的数字。建议在结构体里使用固定大小的整数或浮点数并且不要依赖于编译器自动对齐。如果结构体里有字符串最好约定好最大长度在接收端做长度校验之后再使用。我在代码里判断了data_len sizeof(msg)这个判断虽然简单但能挡住大部分格式不匹配的意外帧。4. 参数、原理与取舍4.1 信道为什么必须一致2.4GHz频段的规矩ESP-NOW工作在2.4GHz频段这个频段被划分成多个信道。实验一的代码里我在配对时指定了peer.channel 1这行代码很容易被忽略但它特别重要。ESP-NOW本身没有独立的频点选择逻辑它复用WiFi射频的当前信道。如果发送端和接收端的WiFi信道不一致帧发出去之后接收端根本不会去解调那个信道上的信号看起来就像数据凭空消失在空气里。更麻烦的是如果接收端以STA模式连接着一个路由器一旦路由器发生信道跳变接收端也会跟着跳而发送端还傻傻地停在原来的信道通信就断了。所以做ESP-NOW实验时我通常会让设备以WIFI_STA模式运行但不要去连接任何AP然后显式调用esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE)来锁死信道。有些项目里用WIFI_AP_STA模式要特别注意AP本身占了哪个信道别让AP自动选的信道打乱了ESP-NOW的收发。4.2 能跑多快吞吐量实测与场景判断ESP-NOW速度怎么样这个问题的答案取决于你使用哪种发送模式。我实测过两种方式。第一种是普通模式也就是发送之后不管结果只管把结构体交给驱动吞吐量能到几百kbps到1Mbps左右。第二种是带ACK的模式发送回调会返回成功或失败底层会做重传但这种模式会明显降低有效吞吐更适用于小数据量的可靠传输。做个粗略估算单帧最多250字节加上帧头开销如果每毫秒发一帧理论速率就超过2Mbps。但空口环境不是实验室2.4GHz频段干扰很重实际稳定速率能到1Mbps已经算不错了。这个吞吐量决定了ESP-NOW不适合传视频这类大流量数据但非常适合传传感器状态、遥控指令、设备间握手信号。传固件升包也可以就是得耐心拆包后面我会讲到OTA思路。4.3 帧结构里有什么加密、MAC地址和载荷虽然官方没有公开一份完整的ESP-NOW帧格式文档但通过抓包和经验总结可以大致看出它的组成802.11头部帧控制、时长、目的MAC、源MAC、BSSID、序列号、帧体里的数据载荷以及帧尾的FCS校验。我抓包时最直接的感受是只要关闭加密载荷就是赤裸裸的明文。Wireshark里看到的是Data类型的802.11帧但发给谁、数据是什么一眼就能看出来。这也解释了为什么ESP-NOW可以用在局域网内的高效通信场景却不太适合直接在公网或者陌生环境中传播。如果你担心数据被旁边的人截获配对时可以开启加密模式。在esp_now_peer_info_t里把encrypt设为true并配置16字节的psk密钥双方密钥相同才能正常解密。开启加密后抓包工具看到的就是一串密文基本没办法直接还原结构体。需要提醒的是密钥管理是应用层的事如果你把密钥硬编码在固件里别人读出来一样能解密。5. 进阶玩法与避坑手册5.1 从点对点升级成Mesh和OTA实验做完后我把它用在了更大的项目里。最值得尝试的两个方向是Mesh和OTA。ESP-NOW本身是点对点的但借助MAC地址寻址的特点可以做多跳接力。每一个节点除了发送自己的传感器数据还转发别的节点发来的数据。配置一个简单的路由表让数据朝网关方向逐跳传递就能构建一个低功耗的无线Mesh网络。这里最大的设计难点是防止数据风暴因为广播转发很容易产生环路。我的做法是给每个数据包加一个源节点ID和序号收到重复序号就丢弃实测下来能有效控制广播风暴。OTA方面固件通常几百KB需要拆包发送。接收端收到分片后先写入临时分区等全部收齐再校验CRC并切换启动分区。用ESP-IDF里完善的OTA接口配合ESP-NOW可以做一个完全不需要TCP/IP的无线升级链路。这个方案的好处是网关和节点可以处于无AP的环境中直接用私有通路升级非常适合野外部署的节点。5.2 低功耗、共存和其他坑低功耗场景下最常遇到的坑是设备从Deep Sleep醒来后RAM内容全部丢失ESP-NOW的配对信息、回调函数全部没了。所以每次唤醒都必须重新执行esp_now_init()和esp_now_add_peer()。如果你保存了关键状态记得放在RTC内存里否则醒来后连重发序号都会丢失。和蓝牙共存是另一个问题。ESP32的WiFi和蓝牙共享同一个射频前端同时开BLE广播和ESP-NOW收发时因为射频切换调度偶尔会有丢包。这是我调试时最头疼的部分症状非常随机有时连续发100包全成功有时突然丢两包然后又完全恢复。排查下来不是代码逻辑问题而是两个协议在抢射频资源。对策是降低发送频率、错开BLE广播的调度或者干脆在关键控制链路里只用ESP-NOW、关闭BLE。还有一个容易忽略的坑在混杂模式的回调函数里不能做耗时操作比如调用Serial.print刷屏、写Flash、动态分配大内存否则会导致WiFi驱动栈溢出甚至系统崩溃。回调函数应该只做字节搬运把数据拷到缓冲区里交给主循环去处理。5.3 常见问题速查表现象可能原因排查方向两端配对成功但收不到数据信道不一致两边都显式设置同一个信道偶尔丢包看起来毫无规律射频共存或2.4GHz干扰降低发包频率错开BLE调度换信道开启混杂模式后系统卡死回调里做了耗时操作回调里只拷贝不打印不写FlashDeep Sleep唤醒后收不到数据RAM丢失配对信息没了RTC保存参数重新初始化ESP-NOW载荷解析出来是乱码结构体布局不一致接收端加长度校验避免依赖编译器对齐烧录总失败自动下载电路/驱动问题手动进入下载模式检查驱动换USB线发送回调一直返回失败目标不在信道或MAC错误核对MAC地址确认目标处于同一信道5.4 迁移到ESP32-C6/S3的注意事项新出的ESP32-S3、ESP32-C6同样支持ESP-NOW但有一些细节值得注意。ESP32-S3的USB烧录功能很香很多板子可以直接通过USB口拖拽或串口烧录不用额外买USB转串口模块。ESP32-C6则更激进它同时支持2.4GHz WiFi6、BLE、Zigbee和Thread射频硬件能力更强。C6上的Zigbee和Thread通信也会占用2.4GHz频段和ESP-NOW放在同一块射频前端上时共存调度变得更复杂。我建议在正式项目里把无线协议按优先级划分好让ESP-NOW负责关键控制Zigbee负责周期性的低速率采集二者通过应用层的调度错开时间片。无论用哪款芯片都别忽略无线电发射合规问题。ESP-NOW发射功率跟普通WiFi相当虽然实验环境里短距离低功率问题不大但如果你要做产品或者长期部署一定要查一下当地对2.4GHz设备发射功率的限制尽量把发射功率配置到满足应用需求的最小值既省电又少惹麻烦。最后再分享一个小技巧。排查ESP-NOW链路是否真的在工作时与其反复看串口日志不如准备第三块板子专门开混杂模式过滤源MAC地址这样能在一堆Beacon里精准看到目标设备的每一帧。我喜欢在拆包调试时顺便观察时间戳如果相邻两帧间隔稳定说明链路健康如果间隔突然拉长多半是射频被占或者系统在忙别的事。这个观察法帮我解决过好几次诡异丢包问题比闷头改代码效率高得多。