4、点对点通信(一):发送端配置——初始化ESP-NOW、注册发送回调函数、发送数据

好,咱们正式开始动手写代码了。

这一章,我带你完成ESP-NOW发送端的配置。说白了,就是让一块ESP32学会“说话”——把数据发出去。接收端怎么收,下一章再讲。

我个人习惯,做通信项目时,先把发送端调通。为什么呢?因为发送端逻辑相对独立,你发出去的数据对不对,用串口打印出来就能验证。接收端那边,等发送端稳定了再搞,效率更高。

4.1 初始化ESP-NOW

ESP-NOW的初始化,其实就两步:先初始化WiFi,再初始化ESP-NOW本身。嗯,这里有个坑,我当年第一次搞的时候踩过。

注意:ESP-NOW依赖WiFi库,但不需要连接路由器。WiFi模式必须设置为WIFI_STA(站点模式),否则ESP-NOW无法正常工作。

为什么是STA模式?你可以这么理解:ESP-NOW本质上是在WiFi的2.4G频段上直接通信,它借用了WiFi的硬件,但跳过了TCP/IP协议栈。所以WiFi得先“开机”,但不用连网。

来看代码:

#include <esp_now.h>
#include <WiFi.h>

void setup() {
  Serial.begin(115200);
  
  // 第一步:设置WiFi为STA模式
  WiFi.mode(WIFI_STA);
  
  // 第二步:初始化ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("ESP-NOW 初始化失败!");
    return;
  }
  
  Serial.println("ESP-NOW 初始化成功");
}

这段代码里,esp_now_init()返回ESP_OK才算成功。我曾经在项目里遇到过WiFi没初始化就直接调这个函数的情况,结果返回错误码,查了半天才发现是顺序问题。

小技巧:初始化失败时,可以用esp_err_to_name(err)把错误码转成字符串打印出来,方便定位问题。

4.2 注册发送回调函数

初始化完了,下一步是注册回调函数。这个回调函数,会在每次数据发送完成后被自动调用。

你想想看,数据发出去了,你怎么知道对方收到了没有?回调函数就是干这个的——它会告诉你发送结果:成功还是失败,失败的原因是什么。

回调函数的格式是固定的:

// 发送回调函数
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  Serial.print("发送状态:");
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "成功" : "失败");
  
  // 打印目标MAC地址
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2],
           mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print("目标设备:");
  Serial.println(macStr);
}

然后在setup()里注册它:

esp_now_register_send_cb(OnDataSent);

就这么简单。注册之后,每次发数据,系统都会自动调用OnDataSent

重点:回调函数是在中断上下文中执行的。不要在回调里做耗时操作,比如打印大量日志、延时、或者操作外设。我见过有人在这里面加delay(1000),结果整个系统卡死。

4.3 发送数据

终于到发送环节了。ESP-NOW发送数据,核心函数就一个:esp_now_send()

它的原型是:

esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len);

三个参数:

  • peer_addr:目标设备的MAC地址。如果是广播,传NULL
  • data:要发送的数据缓冲区指针。
  • len:数据长度,单位字节。

来看一个完整的发送示例:

// 要发送的数据结构
typedef struct {
  int id;
  float temperature;
  float humidity;
} sensor_data_t;

sensor_data_t myData;

void sendData() {
  // 填充数据
  myData.id = 1;
  myData.temperature = 25.6;
  myData.humidity = 60.2;
  
  // 发送数据
  esp_err_t result = esp_now_send(NULL, (uint8_t *)&myData, sizeof(myData));
  
  if (result == ESP_OK) {
    Serial.println("数据发送请求已提交");
  } else {
    Serial.println("数据发送失败");
  }
}

这里我传了NULL作为目标地址,表示广播发送。如果你想点对点发送给某个特定设备,就把它的MAC地址传进去。

重要限制:ESP-NOW单次发送的数据最大长度为250字节。超过这个长度,发送会失败。我在做传感器数据采集项目时,一开始没注意这个限制,把一整条JSON字符串塞进去,结果怎么都发不出去。

4.4 完整的发送端代码

把上面几部分拼起来,就是一个完整的发送端程序:

#include <esp_now.h>
#include <WiFi.h>

// 数据格式
typedef struct {
  int id;
  float temperature;
  float humidity;
} sensor_data_t;

sensor_data_t 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);
  
  // 初始化WiFi
  WiFi.mode(WIFI_STA);
  
  // 初始化ESP-NOW
  if (esp_now_init() != ESP_OK) {
    Serial.println("初始化失败");
    return;
  }
  
  // 注册回调
  esp_now_register_send_cb(OnDataSent);
}

void loop() {
  // 填充数据
  myData.id = 1;
  myData.temperature = 25.0 + random(0, 100) / 10.0;
  myData.humidity = 60.0 + random(0, 100) / 10.0;
  
  // 发送
  esp_err_t result = esp_now_send(NULL, (uint8_t *)&myData, sizeof(myData));
  
  if (result == ESP_OK) {
    Serial.println("数据已发送");
  } else {
    Serial.println("发送失败");
  }
  
  delay(5000);  // 每5秒发一次
}

这段代码每5秒发送一次模拟的温湿度数据。你可以用串口监视器看到发送结果。

调试建议:第一次跑的时候,把delay(5000)改成delay(1000),这样能更快看到回调函数的输出。等确认通信正常了,再改回实际需要的间隔。

4.5 常见问题与避坑

问题 原因 解决方法
初始化返回错误 WiFi模式未设置或顺序不对 先调WiFi.mode(WIFI_STA),再调esp_now_init()
发送回调一直显示失败 接收端未初始化或MAC地址错误 先确认接收端已上电并初始化ESP-NOW
数据发送成功但接收端收不到 数据长度超过250字节 检查sizeof(myData)是否超过250
程序卡死或重启 回调函数中执行了耗时操作 回调里只做标记,数据处理放到loop()

嗯,这一章的内容就到这里。你先把发送端的代码烧进去,看看串口输出。下一章,我们搞接收端,让两块ESP32真正聊起来。

记住:先跑通发送,再搞接收。这是我在多个项目里总结出来的经验,能省不少调试时间。