4. Wi-Fi 扫描机制:主动扫描与被动扫描、扫描结果解析、信道管理

Wi-Fi 扫描,说白了就是设备在找网络。ESP32 怎么知道周围有哪些热点?信号强度怎么样?能不能连?这些信息全靠扫描来获取。

我刚开始做 Wi-Fi 产品时,总觉得扫描就是调个 API 的事。后来在量产项目上被坑过几次,才明白扫描机制选不对,连接稳定性直接崩盘。今天咱们就把这块掰开揉碎了讲。

4.1 主动扫描 vs 被动扫描

ESP32 支持两种扫描模式。它们的工作方式完全不同,适用场景也不一样。

对比项 主动扫描 被动扫描
工作方式 发送 Probe Request,等待 AP 回复 Probe Response 监听 AP 周期性发送的 Beacon 帧
扫描速度 快(通常 1-3 秒/信道) 慢(需等待 Beacon 周期,通常 100ms-1s/信道)
功耗 较高(主动发包) 较低(仅接收)
隐蔽性 低(暴露设备存在) 高(不发送任何帧)
获取信息 更丰富(可获取 AP 完整能力) 仅 Beacon 中携带的信息
适用场景 快速连接、用户主动搜索 后台扫描、低功耗巡检

核心结论:ESP32 默认使用主动扫描。如果你做的是电池供电设备,建议在后台巡检时切到被动扫描,能省 30%-50% 的扫描功耗。

4.2 扫描流程与参数配置

ESP32 的扫描 API 在 esp_wifi.h 里。我习惯用 esp_wifi_scan_start() 配合回调来处理结果。

先看一个最基础的主动扫描示例:

#include "esp_wifi.h"

void scan_networks(void) {
    wifi_scan_config_t scan_config = {
        .ssid = NULL,           // 扫描所有 SSID
        .bssid = NULL,          // 不指定 BSSID
        .channel = 0,           // 0 表示全信道扫描
        .show_hidden = true     // 显示隐藏网络
    };

    ESP_ERROR_CHECK(esp_wifi_scan_start(&scan_config, true));
    // true 表示阻塞等待扫描完成
}

这里有个细节——show_hidden 参数。我遇到过不少产品,用户说「我明明开了 Wi-Fi 但设备搜不到」。排查到最后,发现是 AP 开启了隐藏 SSID,而设备扫描时没设这个标志位。

我的习惯:量产固件里 show_hidden 一律设为 true。多花几十毫秒,少接一堆售后电话。

4.3 扫描结果解析

扫描完成后,结果存在 wifi_ap_record_t 结构体数组里。咱们看看里面到底有什么:

typedef struct {
    uint8_t bssid[6];           // AP 的 MAC 地址
    uint8_t ssid[33];           // 网络名称
    uint8_t primary;            // 主信道编号
    wifi_second_chan_t second;  // 第二信道(40MHz 带宽时使用)
    int8_t rssi;                // 信号强度(dBm)
    wifi_auth_mode_t authmode;  // 认证方式
    wifi_cipher_type_t pairwise_cipher;
    wifi_cipher_type_t group_cipher;
    uint8_t ant;                // 天线编号
    uint32_t phy_11b: 1;        // 是否支持 11b
    uint32_t phy_11g: 1;        // 是否支持 11g
    uint32_t phy_11n: 1;        // 是否支持 11n
    uint32_t phy_lr: 1;         // 是否支持 Long Range
    uint32_t wps: 1;            // 是否支持 WPS
    uint32_t reserved: 27;
    wifi_country_t country;     // 国家码信息
} wifi_ap_record_t;

这里面最关键的三个字段:

  • rssi:信号强度。我一般以 -70dBm 为分界线,低于这个值就提示用户信号弱。
  • authmode:认证方式。WPA2-PSK 最常见,但有些老 AP 还在用 WEP,连接时要注意兼容。
  • primary:主信道。这个在信道管理里会用到。

解析结果的代码也很直接:

uint16_t ap_count = 0;
wifi_ap_record_t ap_records[20];

esp_wifi_scan_get_ap_records(&ap_count, ap_records);

for (int i = 0; i < ap_count; i++) {
    printf("SSID: %s, RSSI: %d dBm, CH: %d, Auth: %d\n",
           ap_records[i].ssid,
           ap_records[i].rssi,
           ap_records[i].primary,
           ap_records[i].authmode);
}

注意:esp_wifi_scan_get_ap_records() 的第二个参数是数组指针,数组大小要提前分配好。我见过有人传了个空指针进去,结果直接 panic。建议用 esp_wifi_scan_get_ap_num() 先获取数量,再动态分配内存。

4.4 信道管理

信道管理是 Wi-Fi 稳定性的核心。ESP32 工作在 2.4GHz 频段,共 13 个信道(中国区)。每个信道 20MHz 带宽,相邻信道有重叠。

我画了一张信道分布图,帮你理解:

2.4GHz Wi-Fi 信道分布(2.412 - 2.472 GHz) CH1 CH2 CH3 CH4 CH5 CH6 CH7 CH8 CH9 CH10 CH11 CH12 CH13 2.412 2.432 2.452 2.472 非重叠信道组:1、6、11(绿色)—— 互不干扰,推荐使用 其他信道存在重叠,同频干扰会导致重传率飙升 信道重叠干扰示意 CH1 信号 CH4 信号(重叠) CH6 信号(重叠) 频率重叠区域 → 干扰

你看,CH1、CH6、CH11 是三个互不重叠的信道。我建议在代码里做信道优选时,优先选这三个。

4.5 信道优选策略

扫描完成后,怎么选信道?我总结了一套实用策略:

  1. 信号强度优先:选 RSSI 最高的 AP 所在信道。
  2. 干扰最小化:如果多个 AP 信号强度接近,选 AP 数量最少的信道。
  3. 非重叠信道优先:CH1、CH6、CH11 优先于其他信道。
  4. 动态切换:连接后定期扫描,如果当前信道质量下降,主动切换。

实战经验:我在一个智能家居项目里,设备部署在商场。一开始固定用 CH6,结果一到周末人流量大,Wi-Fi 卡得不行。后来改成动态信道选择,每 5 分钟扫描一次,自动切到干扰最小的信道。丢包率从 15% 降到了 2% 以下。

4.6 扫描性能优化

扫描不是越快越好。太快了可能漏掉 AP,太慢了用户体验差。我一般这样调:

wifi_scan_config_t scan_config = {
    .ssid = NULL,
    .bssid = NULL,
    .channel = 0,
    .show_hidden = true,
    .scan_type = WIFI_SCAN_TYPE_ACTIVE,
    .scan_time.active.min = 100,   // 每信道最少扫描 100ms
    .scan_time.active.max = 300    // 每信道最多扫描 300ms
};

全信道扫描(13 个信道)大概需要 1.3-3.9 秒。如果你只需要连某个特定 SSID,可以指定信道,把扫描时间压缩到 200ms 以内。

我的建议:产品首次配网时做全信道扫描,后续重连时只扫描上次连接的信道及其相邻信道。这样既保证成功率,又提升速度。

4.7 常见扫描问题排查

最后列几个我踩过的坑:

  • 扫描结果为空:检查 Wi-Fi 模式是否设为 WIFI_MODE_STA,以及天线是否正常。
  • 扫描超时:esp_wifi_scan_start() 的阻塞时间不要超过 10 秒,否则任务会被看门狗复位。
  • RSSI 值异常:ESP32 的 RSSI 精度在 ±2dBm 左右,别拿它做精确定位。
  • 信道切换失败:切换信道前要确保 Wi-Fi 处于断开状态,否则会触发异常。

曾经踩过的坑:有一次我在扫描回调里直接调了 esp_wifi_connect(),结果死锁了。后来才搞清楚,扫描回调是在 Wi-Fi 任务上下文中执行的,不能在里面调用其他 Wi-Fi API。正确的做法是发事件给应用任务,由应用任务处理连接。

嗯,扫描机制这块就讲这么多。记住一句话:扫描是 Wi-Fi 连接的「眼睛」,眼睛不好使,后面跑得再快也没用。