PAYATHON 2026

WIFI_AP_STA 模式下 ESP32 连接 AP 后为何无法接收 ESP-NOW?

排障赵师傅

结论

ESP32 可以在连接路由器的同时接收 ESP-NOW。题中这组 ESP8266 与 ESP32 应先调整两处配置:

  • 启用 ESP32 的 SoftAP 接口,将 ESP8266 的发送目标从 WiFi.macAddress() 返回的 STA MAC 地址改为 SoftAP MAC 地址。
  • 将 ESP8266 的 esp_now_add_peer() 第三个参数从写死的 3 改为实际使用的路由器信道。

MicroPython 的 ESP-NOW 文档记录了这样一种现象:ESP32 连接 Wi-Fi 后,无法通过 STA 地址接收 ESP8266 的数据,需要改用 AP 地址。这与题中的症状吻合,但不能据此认定所有 Arduino Core、SDK 版本都有相同行为。

为什么 WIFI_AP_STA 仍然收不到

开启 AP 和 STA 后,发送目标仍需选对

ESP32 的两个接口各有自己的 MAC 地址:

接口用途Arduino 中的地址读取方法
STAESP32 连接路由器WiFi.macAddress()
SoftAPESP32 自身提供接入点WiFi.softAPmacAddress()

原代码只打印了 STA MAC:

Serial.printf("MAC Address: %s\n", WiFi.macAddress().c_str());

如果把这个地址填入 ESP8266 的 peer[],ESP8266 就会一直向 ESP32 的 STA 接口发送。设置 WIFI_AP_STA 不会自动将发送目标改为 SoftAP 地址。

排查题中故障时,应启动 SoftAP,读取它的 MAC,再更新 ESP8266 的目标地址。ESP8266 无须通过 WiFi.begin() 连接这个 SoftAP,也无须输入它的密码。

ESP8266 的两处信道配置不一致

代码中同时存在:

#define WIFI_CHANNEL 8

wifi_set_channel(WIFI_CHANNEL);

以及:

esp_now_add_peer(peer, ESP_NOW_ROLE_COMBO, 3, NULL, 0);

esp_now_add_peer() 的第三个参数是 peer 信道,这里写死为 3。即使将 WIFI_CHANNEL 改成路由器信道,这两处配置仍可能冲突。参数含义可在 ESP8266 SDK 头文件中核对。

AP 和 STA 共用同一个无线信道

WIFI_AP_STA 提供两个逻辑接口,两者共用无线硬件。ESP32 连接路由器后,SoftAP 必须跟随 STA 使用路由器所在的信道,无法让 STA 使用信道 8、SoftAP 独立使用信道 3。ESP-IDF Wi-Fi 文档说明了这一规则。

ESP32 中的:

peer.channel = 0;

表示使用当前信道,可以保留。这个设置不会让远端 ESP8266 自动寻找或跟随路由器信道。

调整步骤

  1. 将路由器的 2.4 GHz Wi-Fi 设为固定信道。
  2. 让 ESP32 连接路由器,连接成功后读取实际信道。
  3. 在 ESP32 上启动 SoftAP,并打印 STA、SoftAP 两个 MAC 地址。
  4. 将 ESP8266 的目标地址改为 ESP32 的 SoftAP MAC。
  5. 将 ESP8266 的本地信道和 peer 信道统一为路由器信道。
  6. 如果需要双向通信,让 ESP32 显式通过 WIFI_IF_AP 向 ESP8266 的 STA MAC 发送。

验证这些配置后,再排查省电、扫描和版本差异。

ESP32 端代码调整

用以下代码替换原有 setup(),保留题中的全局变量和旧式 onRecv()、onSent() 签名。peer_addr 应填写 ESP8266 的 STA MAC。

#include <esp_err.h>

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(routerSsid, routerPwd);

  const uint32_t startedAt = millis();

  while (WiFi.status() != WL_CONNECTED) {
    if (millis() - startedAt >= 20000) {
      Serial.println("Wi-Fi connection timeout");
      while (true) {
        delay(1000);
      }
    }
    delay(200);
  }

  const uint8_t channel = WiFi.channel();

  if (!WiFi.softAP(apSsid, apPwd, channel)) {
    Serial.println("SoftAP start failed");
    while (true) {
      delay(1000);
    }
  }

  // 排查期间关闭 Wi-Fi 省电。
  ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));

  Serial.print("Station IP Address: ");
  Serial.println(WiFi.localIP());

  Serial.print("STA MAC: ");
  Serial.println(WiFi.macAddress());

  Serial.print("SoftAP MAC: ");
  Serial.println(WiFi.softAPmacAddress());

  Serial.print("Wi-Fi Channel: ");
  Serial.println(WiFi.channel());

  ESP_ERROR_CHECK(esp_now_init());
  ESP_ERROR_CHECK(esp_now_register_recv_cb(onRecv));
  ESP_ERROR_CHECK(esp_now_register_send_cb(onSent));

  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, peer_addr, 6);

  peer.channel = 0;
  peer.ifidx = WIFI_IF_AP;
  peer.encrypt = false;

  ESP_ERROR_CHECK(esp_now_add_peer(&peer));
}

这两个设置控制的方向不同:

  • ESP8266 的目标 MAC 决定数据发往 ESP32 的哪个接口。
  • ESP32 的 peer.ifidx = WIFI_IF_AP 决定 ESP32 向该 peer 发送时使用哪个本地接口。

仅修改 peer.ifidx,不会改变 ESP8266 发来的数据的目标地址。

如果 ESP32 只接收题中这种未加密单播,就无须为接收添加发送方 peer。上面的 peer 配置用于保留原程序的双向通信。ESP-IDF ESP-NOW 文档说明了未加密接收与 peer 注册的关系。

ESP8266 端代码调整

先将原来的 peer[] 替换为 ESP32 串口打印的 SoftAP MAC。不要填路由器的 MAC,也不要继续使用 ESP32 的 STA MAC。

然后将:

esp_now_add_peer(peer, ESP_NOW_ROLE_COMBO, 3, NULL, 0);

改为:

if (esp_now_add_peer(
      peer,
      ESP_NOW_ROLE_COMBO,
      WIFI_CHANNEL,
      NULL,
      0) != 0) {
  Serial.println("esp_now_add_peer failed");
  return;
}

保留本地信道设置,并检查执行结果:

if (!wifi_set_channel(WIFI_CHANNEL)) {
  Serial.println("wifi_set_channel failed");
  return;
}

Serial.print("ESP8266 STA MAC: ");
Serial.println(WiFi.macAddress());

Serial.print("ESP8266 Wi-Fi Channel: ");
Serial.println(wifi_get_channel());

这些代码应放在原来的 WiFi.mode(WIFI_STA)、WiFi.disconnect() 之后,ESP-NOW 初始化之前。WIFI_CHANNEL 必须填写 ESP32 连接成功后打印出的信道。

题中使用的是 ESP8266 旧版 API,发送回调应与头文件中的类型一致:

void onSent(uint8_t* mac, uint8_t status) {
  Serial.print("send callback status: ");
  Serial.println(status);
}

注册时直接传入函数:

esp_now_register_recv_cb(onRecv);
esp_now_register_send_cb(onSent);

如何理解测试中的异常现象

ESP32 发送成功,不代表它收到了 ESP8266 的应用数据

对于题中的单播,ESP_NOW_SEND_SUCCESS 表示发送的数据在 MAC 层得到了成功确认。它不能证明接收端的 onRecv() 已经处理数据,也不能证明反方向的 ESP-NOW 发送配置正确。ESP-IDF 文档区分了 MAC 层成功和应用层接收。

因此,ESP32 可以向 ESP8266 发送成功,同时收不到 ESP8266 主动发送的数据。要确认业务数据已经处理,应在接收回调之外安排应用层应答,并携带包序号。

“错误信道仍能接收”不能证明可以跨信道通信

应分别检查配置中的信道值、peer 记录中的信道,以及无线电当时实际使用的信道。原程序中的 WIFI_CHANNEL 与 peer 信道 3 已经存在不一致。

仅凭串口现象,无法判断旧版 ESP8266 SDK 当时是否切换了信道,或发生了其他状态变化。应先统一配置,必要时通过抓包确认,不要依赖所谓的跨信道接收。

连接不存在的 Wi-Fi 时,偶尔成功不能作为有效验证

STA 搜索 AP 时会扫描信道,无线电未必持续停留在 ESP-NOW 使用的信道。偶尔收到数据,可能是扫描时短暂经过了目标信道。

单次读到信道 14,也不足以确定整个连接过程的信道状态。调试时应为连接过程设置超时,避免在持续搜索 AP 的状态下判断 ESP-NOW 是否正常。

其他注意事项

  • 信道 1 无法自动解决此问题,两端实际工作信道必须一致。路由器自动换信道后,未连接路由器的 ESP8266 也需要更新信道。
  • esp_wifi_set_ps(WIFI_PS_NONE) 可用于排除省电影响,但会增加功耗。题中已使用 AP 与 STA 共存模式,不能直接将故障归因于省电。Wi-Fi 文档说明了相关行为。
  • 上面的 ESP32 示例沿用题中的旧式回调签名。较新的 SDK 中,接收回调可能使用 const esp_now_recv_info_t*,发送回调也可能变化。应按安装版本的 esp_now.h 修改函数,避免用强制类型转换掩盖签名不匹配。
  • 接收回调读取 data[0] 前,应先检查 len > 0。正式程序应把数据复制到队列,再在 loop() 或其他任务中打印、处理,避免长时间占用 Wi-Fi 回调任务。
  • 两端示例里的 MAC 都是占位值,需要分别填写实际地址。不要通过给 STA MAC 最后一字节加一来猜测 SoftAP MAC。

备注:内容仅供参考。