尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

ESP32无线通信实战:ESP-NOW点对点连接,告别路由器

ESP32无线通信实战:ESP-NOW点对点连接,告别路由器 前阵子接了个小项目三个温湿度采集节点一个网关屏数据量小到可怜节点离网关也就几十米。刚开始我习惯性想上MQTT路由器、Topic、QoS一套流程走下来发现为了传几十个字节我先把一堆网络基础设施背上了身。后来把方案改成ESP-NOW整个世界清净了。这篇文章就来聊聊ESP-NOW的入门玩法也就是Part 1怎么在两块ESP32之间不接路由器直接把数据点对点传起来。这篇内容适合的读者很明确手头有ESP32/ESP8266开发板想做小型无线传感网络、遥控器、数据采集上报但不想折腾路由器、不想接触复杂协议栈的人。读完你可以拿到一套完整的可运行代码知道怎么读MAC地址、怎么配对、怎么打包发送和接收以及我实测中真实踩过的坑。1. 为什么我放弃了先上MQTT改用ESP-NOW1.1 一次小型环境监测项目的选型纠结当时的需求是三个传感器节点把温湿度、电池电压发给一个带屏幕的网关节点。频率不高5秒一次每个包算下来也就20字节左右。按照我以往的习惯第一反应就是Espressif设备走WiFi连路由器然后MQTT订阅发布再搞个broker。这套方案成熟但笨重。细算一笔账在传统WiFiMQTT链路里即使你的应用数据只有20字节经过TCP/IP和MQTT协议封装后实际在空气里跑的数据包至少几十字节甚至上百字节。如果还要TLS加密握手阶段的流量和内存开销更是惊人。我那几个ESP32的SRAM本来就紧张再塞一个MQTT客户端库内存余量立刻缩水。相比之下ESP-NOW的传输帧本身就很小应用数据上限是250字节没有任何TCP握手、IP路由这些中间环节。传输层几乎零额外开销延迟在毫秒级。对于小数据、多节点、低延迟这种场景它天生就是更合适的选项。另外家用路由器做IoT网关还有一个隐患路由器出问题所有节点一起掉线。我吃过这个亏半夜设备离线排查到最后发现是路由器自动重启。ESP-NOW不依赖路由器设备之间自己直接通信稳定性反而更可控。每个节点只认它通讯录里的MAC地址链路清晰没有中间商赚差价。1.2 ESP-NOW在无线协议栈里到底处在哪一层很多人误以为ESP-NOW是乐鑫私有的物理层协议其实不是。它跑在标准的2.4GHz WiFi物理层之上只是把协议栈砍到了数据链路层。普通WiFi通信要经过应用层-传输层-网络层-链路层这么长一串流程而ESP-NOW直接从链路层发帧大量中间环节被跳过。这个设计有点像对讲机和打电话的区别。打电话要先拨号、经过总机路由器转接才能接通对讲机只要双方在同一频道按下就讲松开就听。ESP-NOW就是这样双方设备只要在同一个信道MAC地址配对好数据就能直接发出去。跳过TCP/IP还有一个效果没有会话状态没有重传握手也没有port的概念。发送方把数据交给WiFi硬件就完事能不能送出去、能送多快全看无线环境和硬件调度。这种无连接机制让ESP-NOW极其轻量但也决定了它不适合大数据量、不适合跨网段、不适合强安全场景。1.3 ESP-NOW擅长什么不擅长什么我整理了一张常用场景对照表帮你在选型时快速判断典型需求是否适合ESP-NOW说明温湿度/传感器数据周期上报很适合数据量小周期发送无需中心化网络遥控器/开关控制设备很适合毫秒级延迟按键触发即发无人机/小车实时控制很适合低延迟点对点直传多节点星型组网接网关很适合一个节点收多个子节点汇总后转云端视频/音频/文件传输不适合单包250字节限额吞吐量有限设备需要跨路由器远程通信不适合没有IP层无法跨网段高安全级别数据默认不适合有加密选项但密钥管理要自己设计超长距离(几百米)不适合无中继机制距离受发射功率限制所以我的判断标准很简单如果应用数据在几十到两百字节以内设备之间物理距离可控而且不需要经过公网中转ESP-NOW几乎是性价比最高的选择。如果你的需求真的需要公网推送、远程访问再把ESP-NOW作为传感器汇聚层由网关承担转发角色这样组合也很常见。2. 开工前准备板子、开发环境和一个小常识2.1 选ESP32还是ESP8266做ESP-NOW开发手头有ESP32建议直接用ESP32有ESP8266也不是不行但要注意两者的差异。ESP32的优势是资源充裕双核240MHz、320KB SRAM跑ESP-NOW的同时还能干别的活。它支持ESP-NOW的加密传输peerInfo里的encrypt字段也支持BLE后续如果想做低功耗蓝牙辅助配网同一个板子就能搞定。它的Arduino core更新也比较勤API对齐官方文档更快。ESP8266价格便宜也能跑ESP-NOW但它只有单核、内存小很多而且某些版本的Arduino core对ESP-NOW加密支持不够顺手API签名也不完全一样。如果只是做极简发送节点ESP8266够用如果要做网关、要跑加密、要同时维护多个peer我建议不要省这点钱直接上ESP32。我测试时用的是一块ESP32 DevKitC和一块NodeMCU-32S两块板子都能正常工作。ESP32-S2、S3、C3这些新芯片也都兼容ESP-NOW只是部分引脚和板载LED不同代码层面几乎不用改。2.2 Arduino IDE和PlatformIO怎么选两个环境我都用过各有各的适用阶段。如果你只是想快速跑通一个demoArduino IDE最省事。安装Arduino IDE后在开发板管理器里添加ESP32支持。需要把乐鑫提供的JSON包地址加入到附加开发板管理器网址里然后在工具-开发板-开发板管理器中搜索ESP32安装即可。装好后选择你的板子型号就可以编译烧录了。如果你打算做正式项目我推荐PlatformIO。它的工程结构清晰依赖库版本可控还能自动下载工具链避免不同环境互相污染。PlatformIO里创建一个esp32dev项目platform选择espressif32然后写代码就行了。我个人现在做项目基本都切到PlatformIOArduino IDE只用来快速验证小片段。不管用哪个环境记得把串口波特率设为115200我第一次测试时用9600看日志乱码了半天排查到最后才发现是波特率不匹配。2.3 我测试用的实物连接ESP-NOW测试其实不需要任何外设两块开发板通过USB线接电脑供电即可。发送端和接收端都靠串口输出日志。如果你想让结果更直观可以在接收端某个GPIO上接一个LED数据一收到就闪一下这样不用看串口也能知道是否收到。我实际测试时发送端接了USB供电接收端也接了USB供电两块板子之间没有任何物理连线。测试距离在室内大概10米左右隔一堵墙也能收到这已经是ESP-NOW很典型的水平。需要注意的是WiFi信号在2.4GHz频段穿透力一般隔了钢筋混凝土墙可能会衰减明显实测时如果距离远先把板子放在同一个房间验证逻辑再拉距离测极限。3. 先把每台设备的MAC地址搞到手3.1 ESP-NOW的配对机制相当于一份通讯录ESP-NOW没有建立连接的过程但有一个配对的步骤。发送端需要知道接收端的MAC地址把这个地址添加为自己的peer之后就可以直接发送。反过来也一样接收端如果想给发送端回数据也要把发送端的MAC加进来。这就像手机通讯录你不需要先呼叫对方再存号码只要把号码存进去随时可以直接发消息。ESP-NOW的MAC地址是设备的物理地址出厂就固化在芯片里每一块板子的MAC都不同。它不像手机号还能换号对于固定设备来说这个地址足够稳定。所以要做的第一件事把参与通信的每块板子的MAC地址打出来记录下来。这就是后续通讯录里的名字。3.2 烧录MAC地址查询程序MAC地址查询程序极简单下面这段代码烧到板子上打开串口监视器就能看到。#include WiFi.h void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); delay(500); Serial.print(本机MAC: ); Serial.println(WiFi.macAddress()); } void loop() { }这里有个关键点必须先调用WiFi.mode(WIFI_STA)。ESP-NOW是在WiFi接口之上工作的WiFi接口必须被激活。很多人第一次跑ESP-NOW时esp_now_init失败就是因为没设置WiFi模式。STA模式并不是说要去连路由器它只是让WiFi接口工作在站点模式——也就是普通的设备模式不开启热点不让自己变成接入点。打开串口监视器后你会看到类似这样的输出本机MAC: A4:CF:12:34:56:78把这个地址记下来。然后换另一块板子重复同样的操作拿到第二个MAC地址。我建议你准备一个小本子或者直接在代码注释里记下每块板子的MAC多设备测试时真的会记混。3.3 不填MAC也能发聊聊广播模式ESP-NOW支持广播把目标MAC填成全FF的地址就能一次性发到所有在线的ESP-NOW设备。uint8_t broadcastAddress[] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};广播模式对调试很有用比如不确定接收端MAC地址的时候可以先广播测试。但广播的缺点也很明显没有定向接收的概念所有监听该信道的ESP-NOW设备都能收到而且广播帧在数据链路层的确认机制更弱发送成功与否的反馈不如点对点可靠。Part 1里我建议还是做点对点把MAC地址配对这件事搞清楚。点对点配好之后广播理解起来就很简单了无非是把通讯录里的地址换成一个公共广播地址而已。4. 发送端初始化、配对、打包、发送4.1 五步初始化的完整代码发送端的初始化流程可以拆成五步激活WiFi、初始化ESP-NOW、注册发送回调、添加peer、发送数据。我把完整代码放在下面说明部分跟着代码走。#include WiFi.h #include esp_now.h // 接收端的MAC地址必须改成你实测拿到的那串 uint8_t receiverMac[] {0xA4, 0xCF, 0x12, 0x34, 0x56, 0x78}; // 发送完成后的回调 void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { if (status ESP_NOW_SEND_SUCCESS) { Serial.println(回调发送成功); } else { Serial.println(回调发送失败); } } void setup() { Serial.begin(115200); // 第一步激活WiFi接口STA模式即可不必连路由器 WiFi.mode(WIFI_STA); // 第二步初始化ESP-NOW if (esp_now_init() ! ESP_OK) { Serial.println(ESP-NOW 初始化失败); return; } // 第三步注册发送回调 esp_now_register_send_cb(OnDataSent); // 第四步把接收端加入peer列表 esp_now_peer_info_t peerInfo; memcpy(peerInfo.peer_addr, receiverMac, 6); peerInfo.channel 0; // 0表示使用当前WiFi信道 peerInfo.encrypt false; // Part 1先不开加密 peerInfo.ifidx WIFI_IF_STA; // 使用STA接口 if (esp_now_add_peer(peerInfo) ! ESP_OK) { Serial.println(添加peer失败); return; } Serial.println(发送端初始化完成); } void loop() { // 第五步发送数据这里循环发送 const char *msg Hello ESP-NOW; esp_now_send(receiverMac, (uint8_t *)msg, strlen(msg)); delay(2000); }这段代码每2秒发送一条Hello ESP-NOW。编译烧录之前记得把receiverMac改成你第3节抄下来的接收端MAC。第四步里有个容易忽略的字段ifidx。在部分新版ESP32 Arduino core里peerInfo结构体多了ifidx成员指定用哪个WiFi接口发送。如果你用的是较新的core建议显式设为WIFI_IF_STA否则某些环境下add_peer可能不生效。4.2 用结构体打包比字符串靠谱实际项目里几乎不会只发字符串更常见的是把多个传感器数据一次性打包发送。这时候用结构体比拼接字符串靠谱得多。// 定义一个数据包结构体 typedef struct __attribute__((packed)) { uint8_t nodeId; // 节点编号 float temperature; // 温度 float humidity; // 湿度 uint16_t batteryMv; // 电池电压单位mV } sensor_data_t;使用结构体的好处是接收端可以直接用memcpy还原出原始数据不需要像字符串那样去split、parse省掉大量解析代码。__attribute__((packed))是用来取消编译器对齐填充的。打个比方编译器在结构体里放字段时为了CPU读取效率可能会在字段之间塞入空闲字节导致结构体实际占用的内存比字段总和大。如果发送端和接收端的编译器对齐规则不一致两边算出来的sizeof不一样数据就会错位。加packed之后结构体占用内存完全等于所有字段的总和谁编译都一样跨芯片平台也能稳定收发。代价是读取未对齐字段时效率稍低但对于几十字节的小数据包这个影响可以忽略。4.3 别把esp_now_send返回值当成对方收到凭证esp_now_send的返回值只能告诉你数据是否成功提交给WiFi协议栈不代表对端真的收到了。真正的送达结果要看发送回调里的status。esp_err_t result esp_now_send(receiverMac, (uint8_t *)data, sizeof(data)); if (result ESP_OK) { Serial.println(数据已提交发送); } else { Serial.println(提交失败请检查peer和参数); }我在初学阶段踩过这个坑esp_now_send返回ESP_OK就以为对方收到了结果打开接收端串口什么数据都没有。后来才明白发送回调里的ESP_NOW_SEND_SUCCESS才是链路层确认过对方收到。所以调试时一定要同时观察发送端的回调串口输出和接收端的串口输出。如果你在发送回调里看到大量发送失败优先排查几件事接收端是否开机、接收端是否初始化了ESP-NOW、两个设备是否在同一信道、距离是否过远。5. 接收端数据在回调函数里等你5.1 接收初始化与注册回调接收端的初始化流程和发送端几乎一样只是不需要添加peer至少Part 1不需要核心是注册接收回调。#include WiFi.h #include esp_now.h typedef struct __attribute__((packed)) { uint8_t nodeId; float temperature; float humidity; uint16_t batteryMv; } sensor_data_t; 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() { // 数据在回调里处理loop可以干别的事 }重点在OnDataRecv这个回调函数它是整个接收端的核心。5.2 新版API的recv_info参数难住不少新手如果你在网上搜ESP-NOW接收回调会看到很多老代码长这样void OnDataRecv(const uint8_t *mac, const uint8_t *data, int len)但在较新版本的ESP32 Arduino core里回调签名变成了void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len)多出来的info结构体里包含了发送方的MAC地址等信息。很多新手抄老代码编译不过去或者提示too many arguments to function就是因为API签名变了。我的建议是用新版签名并且用info-src_addr来获取发送方MACvoid OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len) { sensor_data_t d; if (len ! sizeof(d)) { Serial.printf(数据长度不匹配收%d期望%d\n, len, (int)sizeof(d)); return; } memcpy(d, data, sizeof(d)); Serial.printf(来自 %02X:%02X:%02X:%02X:%02X:%02X\n, info-src_addr[0], info-src_addr[1], info-src_addr[2], info-src_addr[3], info-src_addr[4], info-src_addr[5]); Serial.printf(节点%d 温度%.1f℃ 湿度%.1f%% 电池%d mV\n, d.nodeId, d.temperature, d.humidity, d.batteryMv); }这里做了一个长度校验如果收到的len和结构体大小不一致直接丢弃。这个习惯很重要能防止接收端因为错误数据去解析垃圾字节导致程序崩溃。如果你用的是ESP8266API还是旧的const uint8_t* mac签名移植的时候注意区分。5.3 回调里尽量只拷贝数据别做耗时操作这是ESP-NOW开发的经典坑接收回调运行在WiFi任务上下文不是Arduino主循环。在回调里调用delay、长时间Serial.print、或者执行复杂计算都可能影响WiFi协议栈的收包处理轻则丢包重则触发看门狗重启。正确的做法是回调里只把数据拷贝到全局变量置一个收到新数据标志位然后在loop循环里处理。我在实际项目中是这样做的volatile bool hasNewData false; sensor_data_t latestData; void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len) { if (len ! sizeof(latestData)) return; memcpy(latestData, data, sizeof(latestData)); hasNewData true; } void loop() { if (hasNewData) { hasNewData false; // 在这里做显示、存储、转发等耗时操作 handleNewSensorData(latestData); } // 其他任务... }这种回调只置标志位的模式对所有嵌入式外设中断处理都适用。你甚至可以把它当作一个通用模板。数据到主循环处理不仅不会卡协议栈调试也更方便因为可以在主循环里打断点或打印日志。6. 实测两块ESP32点对点跑起来6.1 不用接线也能实测ESP-NOW最大的爽点之一就是不需要额外接线。两块板子各自通过USB口供电和烧录串口接电脑看日志即可。如果接收端接了LED数据一到LED就闪烁这种可见反馈比串口日志直观很多。我用的是这样一组搭配发送端ESP32 DevKitC烧录第4节发送端代码接收端NodeMCU-32S烧录第5节接收端代码两块板子都通过USB口连到电脑的两个USB口各开一个串口监视器。6.2 烧录顺序和启动顺序烧录顺序没有特殊要求先烧哪个都行。启动顺序也没有硬性要求因为ESP-NOW是无连接协议接收端不在线时发送端只会回调失败不会阻塞等接收端上线之后下一个数据包就能正常送达。不过有个细节建议先把接收端烧好并运行起来再烧发送端。这样第一次烧完发送端时接收端串口立刻能看到数据验证效率最高。6.3 串口日志里能看到什么正常运行时发送端串口大概是这样发送端初始化完成 数据已提交发送 回调发送成功 数据已提交发送 回调发送成功接收端串口大概是来自 A4:CF:12:34:56:78 节点1 温度25.6℃ 湿度60.1% 电池4023 mV 来自 A4:CF:12:34:56:78 节点1 温度25.6℃ 湿度60.1% 电池4023 mV看到发送回调是成功且接收端连续打印数据整个链路就算真正跑通了。我建议你第一轮测试时把发送间隔调大一点比如5秒一次给自己留足观察时间。等确认链路稳定了再把间隔缩短试试ESP-NOW的延迟极限。我实测过20ms间隔发送30字节左右的数据丢包率在室内短距离环境下非常低应对遥控类应用毫无压力。6.4 中途拔掉接收端会怎样保持发送端运行把接收端USB线拔掉。发送端会开始输出回调发送失败。把接收端重新插上下一个发送周期数据又恢复成功。这个现象能帮你理解ESP-NOW的无连接本质它没有持续的会话每一包数据都是独立的。发送失败只代表当前这包没送到不代表链路永久断开。如果你的应用需要检测对端离线不能靠一次失败判断而是要统计连续失败次数超过阈值再判定离线。7. 第一批测试里踩过的坑7.1 esp_now_init返回失败九成是WiFi模式没设对我最早在ESP8266上跑ESP-NOW时以为WiFi库不调用也能工作直接esp_now_init结果返回失败。后来翻文档才发现ESP-NOW必须依赖一个已激活的WiFi接口。必须先调WiFi.mode(WIFI_STA)或者至少调用WiFi.begin()让WiFi栈跑起来。如果你写的代码里WiFi.mode放在esp_now_init之后也会失败。这个顺序问题极其隐蔽因为编译不会报任何错误只有运行时才知道。我的建议是把WiFi初始化和ESP-NOW初始化紧紧挨在一起先WiFi后ESP-NOW中间不插入其他耗时操作。7.2 250字节上限结构体别超重ESP-NOW单次发送的应用数据上限是250字节。这个应用数据指的是你传给esp_now_send的payload长度不包括数据链路层帧头。如果你用结构体打包很容易在不知不觉中超过这个限制。比如结构体里塞了一个很大的char数组或者嵌套了多层结构。建议做两道检查static_assert(sizeof(sensor_data_t) 250, 数据包超过250字节上限);static_assert是编译期检查结构体一旦超限编译直接报错比运行时才发现问题高效得多。这行代码放在结构体定义后面即可。另一个建议是定期用Serial打印sizeof结果我在调试时经常加一行log方便确认实际发送长度。7.3 还想连路由器信道冲突先记下ESP-NOW设备之间要通信必须在同一个WiFi信道上。默认情况下如果你没有调用WiFi.beginESP32会处于某个默认信道两台设备的默认信道一致所以能直接通信。但如果你想同时让设备连路由器比如节点用ESP-NOW收数据网关再用WiFi转发到云端网关的WiFi信道必须和节点一致。多数路由器配置的是自动选择信道重启后信道可能变化导致ESP-NOW失联。这是很典型的隐藏坑白天一切正常晚上路由器自动优化信道第二天早上ESP-NOW全断。解决办法是手动固定路由器信道或者在代码里显式设置channel。Part 1先不做展开但你如果计划让设备同时跑WiFi和ESP-NOW必须把这个信道问题记住。7.4 调试技巧用LED代替串口盯数据串口日志虽然详细但调试链路问题时不直观。我更推荐在接收端接一个LED在回调里翻转它的电平digitalWrite(ledPin, !digitalRead(ledPin));数据每收到一次LED闪一下。这样你不用盯着串口看远远扫一眼LED就能知道数据链路通没通。排查大范围丢包时甚至可以数LED闪烁频率来估算收包率。我测试时用的板载LED引脚常常和SPI Flash共用不确定就直接外接一个LED加电阻到GPIO。这个办法帮我快速确认了代码能编译过和数据真的在空中飞是两码事。如果你也遇到过为了传20字节先配了半小时路由器的破事那ESP-NOW基本就是为你准备的。现在的几个节点已经稳定跑了半个多月没有路由器、没有云平台串口一开数据清清楚楚这种体验是真的省心。下一步我准备把加密和低功耗唤醒加进去让节点用电池也能撑很久到时候再整理Part 2。
返回列表