ESP-NOW实战:ESP32与ESP8266免路由器无线组网全解析 📅 发布时间:2026/8/26 4:40:53 👁 浏览次数: 做无线通信项目这几年我前前后后折腾过不少方案WiFi、BLE、LoRa、NRF24L01 都上手试过。但如果你只是在几个 ESP32 或 ESP8266 节点之间传点状态、传感器数据或者控制指令用传统 WiFi 那套往往有点小题大做要配路由、要搞 IP 分配、要处理 TCP 连接调试的时候光是把三个以上节点稳定组网就能耗掉半天。后来乐鑫官方推出的 ESP-NOW 协议确实让我省了不少心。它不需要路由器不需要配网设备之间可以直接通信感觉就像给这些小板子装了个对讲机。这篇是 ESP-NOW 上手系列的第一篇重点讲讲它是什么、怎么跑通第一个最小收发链路以及我踩过的一些坑。1. 用 ESP-NOW 做传输到底在解决什么问题1.1 从一次组网经历说起前阵子我帮朋友做一个室内环境监测的小项目三个节点采集温湿度一个中心节点汇总数据。按照惯性思维我第一版用了普通的 WiFi TCP 连接方案结果在实际调试的时候问题不断。中心节点要开一个 TCP Server三个节点轮流连上来上报数据听起来简单但节点只要一多连接管理、断线重连、数据粘包等问题全都冒出来了。更麻烦的是这套系统完全依赖路由器一旦路由器重启或者信号波动所有节点全部掉线又得手动重连调了两天差点就放弃了。后来我去翻了翻乐鑫的官方文档发现 ESP-NOW 这个协议其实早就内置在 SDK 里了只是一直没被我注意到。它本质上是一个基于 2.4GHz 的轻量级无线通信协议不需要路由器也不需要额外的网关两个设备之间可以直接互相发送数据。我把原来 TCP 那套逻辑全部换掉重写代码只花了一个晚上整个链路就通了。从那时起我算是真正意识到在某些特定场景下ESP-NOW 可能是比 WiFi 更合适的方案。1.2 ESP-NOW 的通信模型与核心概念ESP-NOW 的工作原理可以这样理解它基于数据链路层的 MAC 地址直接寻址通信双方不需要建立连接直接往对方的 MAC 地址发送数据帧即可。这和 TCP 那种“先三次握手建立连接再维持会话”的模型完全不同它更像是一个无连接的报文收发机制。要理解 ESP-NOW有几个核心概念必须搞清楚。第一个是 MAC 地址。每一块 ESP32/ESP8266 在出厂时都有一个唯一的 MAC 地址类似于设备的身份证号。在 ESP-NOW 通信中MAC 地址就是收件人地址发送方必须知道接收方的 MAC 地址才能把数据准确送达。第二个是对等连接。在发送数据之前发送方需要调用esp_now_add_peer()把接收方添加为对等设备但这个动作只是在内核中登记一下对方的 MAC 地址和通信参数并不需要对方回应。也就是说你可以往一个根本不在线的 MAC 地址发数据只是对方收不到而已。第三个是通道和速率。ESP-NOW 使用的 WiFi 通道默认情况下是通道 1数据速率可以根据信号质量自动调整支持 1Mbps 到 20Mbps 不等。但在同一区域内所有参与通信的设备最好使用同一个通道否则会出现“你发你的、我收我的”这种尴尬情况。通信模型示意图非正式架构图 节点 A (MAC: AA:AA:AA:AA:AA:AA) | ESP-NOW 数据帧不经过路由器 v 节点 B (MAC: BB:BB:BB:BB:BB:BB)发送方 A 只需要知道 B 的 MAC 地址构造一个数据包通过 ESP-NOW 协议栈发送B 注册的回调函数就会收到数据。整个过程不涉及网络层、传输层的协议栈开销所以延迟很低功耗也比常规 WiFi 低很多。1.3 和传统 WiFi / BLE 相比它的取舍在哪里很多人刚接触 ESP-NOW 时会问一个问题它和 WiFi、BLE 有什么区别我为什么不用现成的方案先看 WiFi。传统的 WiFi 连接需要设备接入同一个 AP接入点然后通过 IP 地址进行路由寻址。这种方式的好处是组网规模大、传输速率高、可以实现互联网访问但代价是需要额外的路由器、需要配网、功耗高、连接建立时间长。如果你的场景只是在一个局域网内几个节点之间互相传数据WiFi 的很多特性实际上是浪费的。BLE 的问题则是另一个方向。BLE 的功耗确实低但是它的连接机制是主从模式一个主机最多只能连接有限数量的从机ESP32 上通常最多 9 个而且通讯需要先建立连接、协商 MTU用于传输一些频繁更新的状态数据时开销也不小。ESP-NOW 的定位正好卡在中间。它不需要路由器功耗比 WiFi 低发送速度快、延迟低、支持一对多与多对一通信而且代码写起来非常简单。它的限制是单次数据载荷有限、传输距离和穿透力远不如 LoRa、无法通过互联网直接访问但对很多短距离、低速率、低功耗的物联网场景来说它已经足够了。我之前做过一个粗略的对比同一个 ESP32 开发板用 WiFi TCP 发送一个 100 字节的数据包从建立连接到发送完成大约需要几十毫秒用 ESP-NOW 只需要几毫秒。这中间的差距在大量数据交互时非常明显。对比维度ESP-NOW传统 WiFiTCPBLE是否需要路由器否是否连接建立时间无连接即发即收秒级毫秒级~秒级单包数据长度最多 250 字节默认 MSS 约 1460 字节通常 20~244 字节典型延迟毫秒级几十毫秒十毫秒级设备间直接通信支持不支持需经 AP 转发支持但受连接数限制功耗低高极低2. 环境准备与最小收发链路搭建2.1 硬件和软件环境怎么选先说说我用的硬件。ESP-NOW 支持 ESP32 和 ESP8266 系列我测试时用的是 ESP32 DevKitC V4 和一块 ESP8266 NodeMCU两者都能跑通。如果你手头有 ESP32-S2、ESP32-S3、ESP32-C3 这类芯片也能用只是不同芯片在初始化细节上可能略有差异但总体 API 是一致的。软件方面我有两个选择Arduino IDE 和 ESP-IDF。如果是快速验证我建议直接用 Arduino IDE因为它的库封装已经非常完善ESP-NOW 的 API 调用只要几行代码就能跑通。如果用 ESP-IDF你会获得更底层的控制权和更高的执行效率但学习曲线也陡峭一些。这篇笔记我基于 Arduino IDE 来写因为它受众最广也最容易复现。安装好之后要确认你已经安装了 Arduino-ESP32 或 Arduino-ESP8266 的开发板支持包。安装方法不细讲了网上有很多教程关键是在“开发板管理器”里搜索 esp32 或 esp8266 然后安装即可。开发板 - 两块 ESP32 DevKitC或任意两块 ESP32/ESP8266 组合 - USB 数据线两根 - 可选LED、按键、传感器等外设软件环境Arduino IDE 1.8.x 或 2.x 均可安装 ESP32 支持包版本建议 2.0.x或安装 ESP8266 支持包版本建议 3.0.x2.2 发送端代码从初始化到发送先写一个最简发送端代码。目标是把一个结构体数据发送到指定的 MAC 地址并在按键触发时重复发送。在代码开始之前需要先了解 ESP-NOW 在 Arduino 中的所谓“标准初始化流程”设置 WiFi 模式为WIFI_STA并让 WiFi 完成底层启动。调用esp_now_init()初始化协议栈。注册发送回调函数esp_now_register_send_cb()用于获取发送结果。调用esp_now_add_peer()添加对端设备。调用esp_now_send()发送数据。下面是最小发送端代码我加了一些注释方便理解#include WiFi.h #include esp_now.h // 对端设备的 MAC 地址接收端 // 这里填接收端开发板实际打印出来的 MAC uint8_t broadcastAddress[] {0x24, 0x6F, 0x28, 0x12, 0x34, 0x56}; // 要发送的数据结构 typedef struct struct_message { int id; float temperature; float humidity; } struct_message; struct_message myData; // 发送回调 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); // 设置 Wi-Fi 模式为 STA WiFi.mode(WIFI_STA); // 初始化 ESP-NOW if (esp_now_init() ! ESP_OK) { Serial.println(ESP-NOW 初始化失败); return; } // 注册发送回调 esp_now_register_send_cb(OnDataSent); // 添加对等设备 esp_now_peer_info_t peerInfo; memset(peerInfo, 0, sizeof(peerInfo)); memcpy(peerInfo.peer_addr, broadcastAddress, 6); peerInfo.channel 0; // 0 表示使用当前 STA 的通道 peerInfo.encrypt false; if (esp_now_add_peer(peerInfo) ! ESP_OK) { Serial.println(添加对等设备失败); return; } // 填充数据 myData.id 1; myData.temperature 26.5; myData.humidity 58.3; } void loop() { // 每 5 秒发送一次 esp_now_send(broadcastAddress, (uint8_t *)myData, sizeof(myData)); delay(5000); }这里有几个细节需要注意。WiFi.mode(WIFI_STA)这一步不能省即使你不打算用它连接任何路由器也要先让 WiFi 工作在 STA 模式因为 ESP-NOW 依赖 WiFi 驱动来完成射频收发。如果不设置这个模式esp_now_init()很可能会失败。还有一个点是peerInfo.channel 0。默认情况下ESP32 在没有连接 AP 时WiFi 工作在通道 1所以写 0 就代表“使用当前 WiFi 通道”这样可以避免手动指定通道带来的麻烦。如果你手动设置为其他值要注意接收端必须监听同一个通道否则收不到。2.3 接收端代码注册回调与数据处理接收端代码更简单核心就三步初始化 ESP-NOW、注册接收回调、等待数据。#include WiFi.h #include esp_now.h // 这里定义的数据结构必须和发送端一致 typedef struct struct_message { int id; float temperature; float humidity; } struct_message; struct_message myData; // 接收回调 void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len) { memcpy(myData, incomingData, sizeof(myData)); Serial.print(收到数据包字节数: ); Serial.println(len); Serial.print(设备 ID: ); Serial.println(myData.id); Serial.print(温度: ); Serial.println(myData.temperature); Serial.print(湿度: ); Serial.println(myData.humidity); Serial.println(); } 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); } void loop() { // 接收逻辑由回调函数完成这里可以处理其他任务 }这段代码里的OnDataRecv是一个中断驱动的回调每当有数据到达时协议栈会调用这个函数并把数据指针传入。这里有一个容易踩的坑不能在回调函数里做耗时操作比如打印大量日志、写 SD 卡、执行延时等因为这样会阻塞 WiFi 协议栈的处理容易导致丢包。正确做法是把数据拷贝到全局变量中标记一个标志位然后在主循环里做后续处理。实测下来如果只是做串口打印一般不会出问题但如果你在回调里写delay(100)很快你就会发现数据包开始丢失而且系统响应明显变慢。3. 核心细节解析参数、格式与可靠性设计3.1 通道、速率和功率参数怎么调ESP-NOW 虽然用起来简单但如果你想在真实项目里稳定运行有几个参数需要认真对待。第一个是通道。ESP32 在没有连接 WiFi AP 时默认的通道是 1。如果你在一个环境里同时存在多个 WiFi 网络建议把所有 ESP-NOW 设备固定在同一个非繁忙通道上例如通道 6 或通道 9。这样做的原因是ESP-NOW 包本质上是在 WiFi 的广播信道上发送的如果这个通道上有很多其他 WiFi 流量碰撞概率会明显上升。可以用下面的代码来设置通道#include esp_wifi.h // 设置 WiFi 通道为 6 esp_wifi_set_channel(6, WIFI_SECOND_CHAN_NONE);需要注意的是在esp_now_add_peer()时也要把peerInfo.channel设置为 6与上面保持一致。第二个是发射功率。ESP32 默认的发射功率通常是最大值 78对应约 19.5dBm但也可以通过esp_wifi_set_max_tx_power()来调节。如果你的设备布局比较密集可以适当降低功率减少对周围设备的干扰、降低功耗如果距离较远就保持最大值。不过从我测试的经验看ESP-NOW 的传输距离总体比较有限空旷环境下 50 米内比较可靠隔一堵墙就可能衰减严重想依靠调功率来大幅提升距离不太现实。第三个是速率。ESP-NOW 支持多种速率长距离传输时可以选择较低速率以增加可靠性但 API 没有直接暴露速率设置需要调用底层 WiFi 接口去调整。这部分我建议保持默认因为 Arduino 库已经做了比较合理的默认配置实际项目中优先保证通道一致性和天线位置合理比纠结速率更有效。3.2 数据包格式、长度与自定义协议ESP-NOW 的一个硬性限制是单次数据包最大长度为 250 字节。这是协议栈限制发送超过这个长度的数据会被截断或直接报错。250 字节说多不多说少不少。如果你只是传温湿度、开关状态、坐标等小数据完全够用。但如果要传字符串、图片、日志等就会显得非常局促。我的经验是尽量用结构体来定义通信协议因为结构体在内存中是连续存放的发送和接收时可以直接通过memcpy拷贝非常方便。但要注意字节对齐的问题。结构体中如果既有char又有int编译器可能会插入填充字节导致结构体大小与你的预期不符。比如typedef struct { int id; float value; bool status; } payload_t;你可以用sizeof(payload_t)来确认大小。实际发送时就用这个大小不要假设是 “4419”而要用sizeof(payload_t)这样才能保证数据一致。如果你的数据长度比较长我建议在结构体里加上一个目标地址字段或者消息类型字段这样接收端可以区分不同节点的数据。比如typedef struct { uint8_t msgType; // 消息类型 uint8_t nodeId; // 节点编号 float data[8]; // 数据载荷 } payload_t;这样接收端就可以根据msgType和nodeId判断如何处理数据。这种自定义协议的成本几乎为零但能让你后面的业务逻辑清晰很多。3.3 可靠传输ACK、重传和加密的取舍ESP-NOW 在数据链路层有一个 ACK 机制就是接收端收到数据后会回复一个 ACK 帧发送端通过发送回调函数获取发送状态。但要注意这个 ACK 仅代表链路层已经发送成功并不代表接收方的应用层已经处理完毕。而且 ESP-NOW 没有隐式重传机制一旦丢包数据就丢了它不会像 TCP 那样自动重传。因此在需要可靠传输的场景下我通常会在应用层做以下两件事第一是开启重传机制。在发送回调中检查status如果为失败则重新放入发送队列延迟一段时间后再次发送。简单实现如下bool sendSucceeded false; void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { sendSucceeded (status ESP_NOW_SEND_SUCCESS); } void sendWithRetry(uint8_t *data, size_t len, int maxRetries) { for (int i 0; i maxRetries; i) { sendSucceeded false; esp_now_send(broadcastAddress, data, len); // 等待发送完成回调 for (int wait 0; wait 100; wait) { delay(1); if (sendSucceeded) break; } if (sendSucceeded) break; delay(20); } }这里有个细节OnDataSent回调是在协议栈处理完发送结果后异步触发的所以你不能在发送后立刻读取结果需要先设置一个标志位等回调触发后再判断。第二是针对关键数据包在应用层加一个简单的序列号和确认机制。例如发送方每次发送时带一个自增序号接收方收到后回复一个 ACK 数据包发送方如果一定时间内没收到 ACK 就重传。这一套做起来也不复杂但对可靠性提升非常明显。我的一个经验法则是如果业务数据重要程度高就直接上应用层 ACK如果只是传感器采集偶尔丢一帧也无所谓就不需要额外开销。关于加密ESP-NOW 支持peerInfo.encrypt true配合预共享密钥密钥长度是 16 字节。这个机制可以防止简单的数据包被窃取或篡改但需要注意加密后的数据包处理速度会略慢而且配对双方必须使用相同的密钥否则无法通信。我的建议是在开放环境中且对数据安全有要求时启用加密如果是自己家里或实验室内的调试先关闭加密快速跑通再说。4. 常见问题与排查技巧实录4.1 典型故障与排查思路实际调试过程中我遇到过的坑不算少。下面整理几个出现频率最高的问题和排查思路。第一个是esp_now_init()返回错误。这个问题最常见的原因是 WiFi 模式设置不对或者底层驱动没有初始化完成。解决方法是确保在调用esp_now_init()前已经执行了WiFi.mode(WIFI_STA)并且没有在初始化前调用其他 WiFi 连接函数。第二个是发送回调一直返回失败。最常见的原因是目标 MAC 地址没有添加到对等设备列表里。一定要确认在发送前已经调用了esp_now_add_peer()并且 MAC 地址是按uint8_t数组顺序填写的不要搞反字节序。第三个是“发送成功但接收端收不到”。这个问题比较复杂我遇到过的触发原因包括接收端和发送端的 WiFi 通道不一致、天线位置不佳、接收端在回调里做了阻塞操作导致数据没来得及处理、或者是在同一区域存在较强干扰。排查时可以先打印两端的通道号确认一致然后逐步减少发送频率看是不是因为发送频率太高导致接收端处理不过来。第四个是数据恢复出来是乱码或者结构体不对。这种问题大多是结构体字节对齐不一致导致的。发送端和接收端的代码必须使用完全一致的结构体定义如果跨平台开发建议把所有字段定义为固定宽度的类型如uint8_t、uint16_t、int32_t避免不同架构下int长度不同。问题表现可能原因排查方法初始化失败WiFi 模式未设置 / 驱动异常检查是否执行 WiFi.mode(WIFI_STA)发送回调失败对等设备未添加 / MAC 地址错误打印对等设备列表核对地址发送成功但收不到通道不一致 / 天线位置差打印通道号调整天线位置与方向数据乱码结构体定义不一致 / 字节对齐差异统一字段类型使用 sizeof 字节数拷贝设备互相干扰多个设备同时发送增加随机延迟错开发送时间4.2 调试技巧和避坑建议除了上面的问题我还想分享几个自己总结的调试技巧。第一个是利用广播地址做无头调试。在开始设计协议之前我建议先让发送端向广播地址FF:FF:FF:FF:FF:FF发送数据接收端不指定 MAC 地址只要在同一个通道就能收到。这种方式不需要预先知道对方的 MAC 地址能快速验证硬件和链路是否正常。等你确认数据链路通畅了再改成单播模式添加具体的对等设备。第二个是善用串口时间戳。ESP-NOW 的调试信息很多但你不能靠人眼去判断数据是几毫秒前收到的。我通常会在接收端打印millis()的时间戳用来判断数据是否实时、是否有堆积。比如Serial.printf(时间戳: %lu ms, 数据: %f\n, millis(), myData.temperature);这样很容易发现“接收有规律延迟”还是“随机延迟”从而推断是通道拥塞还是回调阻塞。第三个是发送频率不要太高。虽然 ESP-NOW 的延迟很低但它本质上是“尽力而为”的协议如果每秒发送几百次WiFi 射频很快就拥堵了。我实际测试过在 20Hz 的发送频率下丢包率基本可以接受一旦超过 50Hz丢包率会明显上升尤其是多个节点同时发送的时候。控制发送频率的做法很简单串口命令控制、定时器限速、或者根据数据变化程度决定是否发送。第四个是考虑设备掉电重启后的状态。ESP-NOW 的对等设备信息保存在内存中掉电后需要重新添加。如果你的项目有动态添加设备的需求要考虑在设备重启后自动重新配对否则会出现“刚才还正常重启之后怎么都不通”的诡异现象。我在做工程样机时会在setup()中打印本机 MAC再把对端 MAC 写在代码里这样重启后也能保持稳定。第五个是多设备组网时的随机退避。当多个节点同时发送数据时碰撞概率会提高。简单解决方案是让每个节点在不同的时刻发送比如节点 1 每隔 1000ms 发送节点 2 每隔 1010ms 发送节点 3 每隔 1030ms 发送。错开几毫秒也可以显著降低碰撞概率。更复杂的方案是使用 CSMA/CA 机制但 ESP-NOW 协议栈本身会做一部分冲突避免所以简单的错峰发送已经能解决大部分问题。5. 场景适配与后续扩展5.1 ESP-NOW 适合哪些项目聊了这么多最后说说什么场景适合用 ESP-NOW什么场景不适合。适合的场景有短距离、分组明确、数据量小、实时性要求较高的局域网设备。比如智能家居里的传感器节点汇总、遥控小车、无人机编队中的指令同步、农业大棚里的多点温湿度采集、工厂里的设备状态监测。这类场景通常设备数量在几台到几十台之间数据包大小在几十到二百字节以内而且不需要接入互联网ESP-NOW 的简洁优势能发挥得淋漓尽致。不适合的场景也很明确需要覆盖几百米甚至几公里的场景需要接入互联网或云平台的场景需要传输视频或高分辨率图像的场景或者设备数量特别多上百个的场景。这些情况要么选 LoRa要么选 NB-IoT要么还是老老实实走 WiFi 加路由的方案更合适。我在做的一个小型环境监测项目用了 15 个 ESP32 节点分布在办公区各处中心节点汇总后通过串口上报到一台主机。整套系统跑了一周只在极端情况下偶尔丢一两帧数据稳定性比我预期的好很多。这个方案如果换成 BLE光是把 15 个节点的数据按固定周期汇总到中心节点就要花不少心思管理连接。5.2 后续可以怎么扩展ESP-NOW 的潜力比我一开始想的要大得多。它不只是用来实现“A 发数据给 B”这么简单。通过合理的协议设计可以实现多跳中继、自动组网、远程控制等复杂功能。我后续打算在 Part 2 里详细讲讲这些内容包括怎么在 ESP-NOW 基础上实现一对多数据分发、怎么处理设备动态加入和离开、怎么实现一个简单可靠的应用层 ACK 机制以及如何把 ESP-NOW 和 WiFi 共存。如果你已经跑通了这篇的最小收发链路那恭喜你ESP-NOW 的大门已经打开了一半。就拿多跳中继来说ESP-NOW 本身不直接支持路由但你可以在应用层做转发每个节点登记自己的邻居表收到一个不是发给自己的数据包时根据路由规则转发给下一个节点。虽然网络拓扑复杂时实现起来会有点烧脑但用来做线性距离的延伸完全可行。5.2 最后再分享一个实用小技巧在收尾之前我再分享一个我实际用下来非常顺手的小技巧写一个get_mac()工具函数在调试阶段把每块板的 MAC 地址和角色信息打印出来这样每次上电时串口输出一目了然。void printMacAddress() { uint8_t mac[6]; esp_read_mac(mac, ESP_MAC_WIFI_STA); Serial.printf(MAC: %02X:%02X:%02X:%02X:%02X:%02X\n, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); }把这个函数放在setup()的前几行调用能省掉很多“哎我这块板子到底是谁”的困惑。尤其在同时调试多块开发板时这个输出能帮你快速确认自己在操作哪一块板子减少低级失误。我的个人经验是ESP-NOW 这种协议特别适合固定拓扑、小数据量、低功耗的局域网通信场景。它不像 WiFi 那样显得笨重也不像 BLE 那样受连接限制更像是一个专注于“把数据从一个点送到另一个点”的轻量工具。如果你也有几个 ESP 设备之间需要直接传数据的需求非常值得在动手写 TCP 之前先花一小时试试 ESP-NOW。