摘要:本文主要介绍ESP-NOW的相关功能,以及在Arduino IDE开发的环境下,如何快速上手使用EPS-NOW;
一、ESP-NOW
ESP-NOW是乐鑫定义的一种无线通信协议,能够在无路由器的情况下直接、快速、低功耗地控制智能设备;
能够与 Wi-Fi 和 Bluetooth LE 共存,广泛应用于智能家电、远程控制和传感器等领域;

1、关于 ESP-NOW
ESP-NOW是乐鑫定义的一种无连接Wi-Fi通信协议,与传统的Wi-Fi协议不同;
ESP-NOW将OSI模型中的前5层简化为了一层,因此数据无需经过网络层、传输层、会话层、表示层和应用层进行传输,减少了网络拥塞下因数据丢包引起的延迟,实现了快速响应。
复用了WiFi的射频天线、2.4G无线物理层,但完全抛弃了标准WiFi协议、TCP/IP网络栈,基于802.11物理层的轻量点对点通信协议
目前支持:ESP8266、ESP32、ESP32-S 和 ESP32-C等多系列SoC;

2、ESP-NOW 特性 / 应用
快速响应:开机后,设备无需任何无线连接即可传输数据并控制其他配对设备,响应速度以毫秒为单位。
良好的兼容性:当设备连接到路由器或在热点模式下工作,也可以通过ESP-NOW实现快速稳定的通信。即使路由器出现故障或网络不稳定,设备也可以通过ESP-NOW保持稳定连接。
远距离通信:ESP-NOW支持远距离通信。适用于户外场景,即使有墙壁、地板阻隔,也能使设备保持稳定的连接。
多跳控制:ESP-NOW可以实现设备的多跳控制。通过单播、广播和群控等方式,可以控制数百台设备。
配网
除了Wi-Fi配网和蓝牙配网外,ESP-NOW还提供了一种新的配网方法。
通过蓝牙为第一台设备配网,其他设备不需要配置SSID/密码信息,因为连接到网络的第一台设备可以直接将这些信息发送给其他设备。
用户可以在APP端选择是否允许其他设备访问网络。
升级
ESP-NOW可用于海量数据传输,如固件升级。
从断点处恢复升级:使用ESP-NOW升级固件时,固件会以固定大小分包,逐个写入flash,设备会记录升级后的包。如果升级过程发生中断,设备将仅请求剩余的固件包,并从断点处继续升级操作。
多设备升级:ESP-NOW可支持多台设备同时升级。3分钟内可升级50台设备。
回滚:如果发生升级错误,固件可以回滚到之前的版本。
调试
ESP-NOW可以用来接收设备运行时的日志,便于设备调试;
通过多对多连接,发起者可以接收来自多个响应者的日志,快速诊断设备故障。
数据加密
ESP-NOW可以通过ECDH和AES128-CCM来保护数据安全。
安全性:
使用ECDH和所有权证明 (PoP) 字符串授权会话、生成共享密钥;
使用AES256-CTR模式加密配置数据;
使用AES128-CCM模式加密ESP-NOW数据;
ESP32-S3支持的共存场景

二、开发环境搭建
1、下载Arduino IDE

2、安装芯片库
安装esp32库

三、硬件设备(功能)
主要使用两款ESP32硬件设备,通过ESP-NOW进行点对点无线数据传输,实现数据回传的功能;
1、A设备(ATOM-S3R_CAM) 将板载IMU传感器的数据,通过ESP-NOW发送至B设备(XIAO_ESP32S3),并在B设备的串口上显示接收到的内容;
2、B设备接收到数据后,再通过ESP-NOW,将接收到的内容发送至A设备的串口上进行显示;
A设备(ATOM-S3R_CAM)

B设备(XIAO_ESP32S3)

四、使用方法
通过ESP-NOW协议实现设备间的数据传输:
相关配置:


目前 ESP-NOW 支持两个版本:v1.0 和 v2.0;
v2.0 的设备支持的最大数据包长度为 1470 (ESP_NOW_MAX_DATA_LEN_V2) 字节;
v1.0 的设备支持的最大数据包长度为 250 (ESP_NOW_MAX_DATA_LEN) 字节;
v2.0 设备可以接收来自 v2.0 和 v1.0 设备的数据包;
v1.0 设备只能接收来自 v1.0 设备的数据包;
库默认使用 v2.0 版本;
配对设备的最大数量是20;
(若开启加密,加密设备的数量不超过17,默认值是7)
详见 #include "esp_now.h"
示例代码:
使用 ESP32_NOW_Serial.h 封装的API方法;
可通过 WiFi.softAPmacAddress() / WiFi.macAddress() 方法获取当前设备MAC地址;
设备A
#include "ESP32_NOW_Serial.h"
#include "MacAddress.h"
#include "WiFi.h"
#include "esp_wifi.h"
#include <M5Unified.h> // M5_IMU库
// 0 = AP热点模式,1 = STA站点模式
#define ESPNOW_WIFI_MODE_STATION 1
// ESP-NOW 协议使用的 WiFi 信道 主信道号(1~14)
#define ESPNOW_WIFI_CHANNEL 1
#if ESPNOW_WIFI_MODE_STATION // ESP-NOW 工作在 WiFi 站点模式
#define ESPNOW_WIFI_MODE WIFI_STA // WiFi 工作模式
#define ESPNOW_WIFI_IF WIFI_IF_STA
#else // ESP-NOW 工作在 WiFi 热点模式
#define ESPNOW_WIFI_MODE WIFI_AP // WiFi 工作模式
#define ESPNOW_WIFI_IF WIFI_IF_AP
#endif
// B设备地址:1C:DB:D4:45:3B:54
const MacAddress peer_mac({0x1C, 0xDB, 0xD4, 0x45, 0x3B, 0x54});
ESP_NOW_Serial_Class NowSerial(peer_mac, ESPNOW_WIFI_CHANNEL, ESPNOW_WIFI_IF);
unsigned long lastSend = 0;
// IMU数据采集
void getIMU(){
// 采集并计算 IMU 数据
if (M5.Imu.update()) {
float ax, ay, az;
float gx, gy, gz;
float mx, my, mz;
M5.Imu.getAccel(&ax, &ay, &az);
M5.Imu.getGyro(&gx, &gy, &gz);
M5.Imu.getMag(&mx, &my, &mz);
// 计算姿态角
float pitch = atan2(-ax, sqrt(ay * ay + az * az)) * 180 / PI;
float roll = atan2(ay, az) * 180 / PI;
// 计算方位角
float heading = atan2(my, mx) * 180 / PI;
if (heading < 0) heading += 360;
// 判断运动状态
bool isMoving = (abs(gx) > 5) || (abs(gy) > 5) || (abs(gz) > 5);
// 通过 ESP-NOW 发送IMU数据
NowSerial.printf("=== ESP_NOW ===\n");
NowSerial.printf("俯仰角: %.1f° (%s)\n", pitch, pitch > 45 ? "上仰" : pitch < -45 ? "下俯" : "水平");
NowSerial.printf("横滚角: %.1f° (%s)\n", roll, roll > 45 ? "右倾" : roll < -45 ? "左倾" : "水平");
NowSerial.printf("方位角: %.1f° (%s)\n", heading, heading < 45 ? "北" : heading < 135 ? "东" : heading < 225 ? "南" : heading < 315 ? "西" : "北");
NowSerial.printf("运动状态: %s\n\n", isMoving ? "运动中" : "静止");
}
}
void setup() {
Serial.begin(115200);
M5.begin();
M5.Imu.init();
if (!M5.Imu.isEnabled()) {
Serial.println("IMU初始化失败");
while (1) { }
}
Serial.println("IMU初始化成功");
Serial.print("WiFi 工作模式:");
Serial.println(ESPNOW_WIFI_MODE == WIFI_AP ? "AP热点" : "STA站点");
WiFi.mode(ESPNOW_WIFI_MODE);
Serial.print("通信信道:");
Serial.println(ESPNOW_WIFI_CHANNEL);
WiFi.setChannel(ESPNOW_WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);
// 等待 WiFi 接口启动完成
while (!(WiFi.STA.started() || WiFi.AP.started())) {
delay(100);
}
Serial.print("本机MAC地址:");
Serial.println(ESPNOW_WIFI_MODE == WIFI_AP ? WiFi.softAPmacAddress() : WiFi.macAddress());
// 启动 ESP-NOW 通信
Serial.println("启动 ESP-NOW 通信...");
NowSerial.begin(115200);
Serial.printf("ESP-NOW 版本:%d 单包最大数据长度:%d\n", ESP_NOW.getVersion(), ESP_NOW.getMaxDataLen());
Serial.println("----------------------------\n");
Serial.println("");
}
void loop() {
M5.update();
// 500ms 发送一次数据
if (millis() - lastSend >= 500) {
lastSend = millis();
getIMU();
}
// 接收B设备回传的数据转发到本地串口输出
while (NowSerial.available()) {
Serial.write(NowSerial.read());
}
delay(1);
}设备B
#include "ESP32_NOW_Serial.h"
#include "MacAddress.h"
#include "WiFi.h"
#include "esp_wifi.h"
// 0 = AP热点模式,1 = STA站点模式
#define ESPNOW_WIFI_MODE_STATION 1
// ESP-NOW 协议使用的 WiFi 信道
#define ESPNOW_WIFI_CHANNEL 1
#if ESPNOW_WIFI_MODE_STATION // ESP-NOW 工作在 WiFi 站点模式
#define ESPNOW_WIFI_MODE WIFI_STA // WiFi 工作模式
#define ESPNOW_WIFI_IF WIFI_IF_STA
#else // ESP-NOW 工作在 WiFi 热点模式
#define ESPNOW_WIFI_MODE WIFI_AP // WiFi 工作模式
#define ESPNOW_WIFI_IF WIFI_IF_AP
#endif
String recv_buf =""; // 接收缓冲区
// A设备地址:B4:3A:45:BE:23:14
const MacAddress peer_mac({0xB4, 0x3A, 0x45, 0xBE, 0x23, 0x14});
ESP_NOW_Serial_Class NowSerial(peer_mac, ESPNOW_WIFI_CHANNEL, ESPNOW_WIFI_IF);
void setup() {
Serial.begin(115200);
Serial.print("WiFi 工作模式:");
Serial.println(ESPNOW_WIFI_MODE == WIFI_AP ? "AP 热点" : "STA 站点");
WiFi.mode(ESPNOW_WIFI_MODE);
Serial.print("通信信道:");
Serial.println(ESPNOW_WIFI_CHANNEL);
WiFi.setChannel(ESPNOW_WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);
// 等待 WiFi 接口启动完成
while (!(WiFi.STA.started() || WiFi.AP.started())) {
delay(100);
}
Serial.print("本机 MAC 地址:");
Serial.println(ESPNOW_WIFI_MODE == WIFI_AP ? WiFi.softAPmacAddress() : WiFi.macAddress());
// 启动 ESP-NOW 通信
Serial.println("正在启动 ESP-NOW 通信...");
NowSerial.begin(115200);
Serial.printf("ESP-NOW 版本:%d 单包最大数据长度:%d\n", ESP_NOW.getVersion(), ESP_NOW.getMaxDataLen());
Serial.println("----------------------------\n");
Serial.println("");
}
void loop() {
// 本地显示 + 拼接帧 + 整组回传
while (NowSerial.available()) {
char c = NowSerial.read();
Serial.write(c);
recv_buf += c;
// 检测到连续两个换行:一整组IMU数据
if (recv_buf.endsWith("\n\n")) {
// 整组数据原样回传给发送端
NowSerial.print(recv_buf);
// 清空缓冲区,准备接收下一组
recv_buf = "";
}
}
}五、效果演示

我要赚赏金
