4. Wi-Fi基础与Station模式:Wi-Fi协议栈简介、ESP32 Station模式配置、Wi-Fi连接状态管理、Wi-Fi事件处理

好,咱们进入第四章。这一章讲的是Wi-Fi基础,特别是Station模式。说实话,Wi-Fi这块内容,很多做嵌入式的人觉得简单——不就是调个API连上路由器吗?但我在实际项目中踩过的坑告诉我,这里面的门道还真不少。

你想想看,一个Mesh网络里,每个节点都要靠Wi-Fi来通信。如果连Wi-Fi连接都搞不稳固,那整个Mesh网络就是空中楼阁。所以这一章,咱们把Wi-Fi的底裤扒干净。

4.1 Wi-Fi协议栈简介

Wi-Fi协议栈,说白了就是一套通信规则。ESP32用的是802.11 b/g/n标准,工作在2.4GHz频段。我刚开始做Wi-Fi项目时,总觉得协议栈这东西离应用层很远,直到有一次排查丢包问题,才发现是MAC层的重传机制没搞明白。

ESP32的Wi-Fi协议栈分三层:

  • 物理层(PHY):负责信号的调制解调,说白了就是把0和1变成无线电波。ESP32支持802.11n,最高速率150Mbps,但Mesh场景下我们一般用不到那么高。
  • MAC层:负责帧的封装、解封装,还有CSMA/CA(载波监听多点接入/冲突避免)。嗯,这里要注意,Wi-Fi的冲突避免机制和以太网不一样,它是先听后说,不是边听边说。
  • 网络层:IP协议、ARP协议这些。ESP32的Wi-Fi驱动已经帮你处理好了,你只需要关心IP地址怎么来。

核心要点:ESP32的Wi-Fi协议栈是乐鑫自己封装的,底层基于FreeRTOS的任务调度。每个Wi-Fi事件都会触发一个回调,你可以在回调里做状态管理。我个人习惯把Wi-Fi事件处理单独放在一个任务里,避免阻塞主循环。

4.2 ESP32 Station模式配置

Station模式,就是让ESP32像手机一样去连接路由器。配置起来其实很简单,但有几个细节我建议你注意。

先看代码:

#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"

void wifi_init_sta(void)
{
    // 1. 初始化NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // 2. 初始化网络接口
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    // 3. 初始化Wi-Fi
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // 4. 配置Station模式
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = "YOUR_SSID",
            .password = "YOUR_PASSWORD",
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    printf("Wi-Fi Station模式已启动,正在连接...\n");
}

这段代码看起来简单,但我曾经犯过一个低级错误——忘记初始化NVS。结果Wi-Fi配置死活存不住,每次重启都要重新配。你想想看,一个Mesh节点如果每次断电都要重新配网,那用户体验得多糟糕。

我的经验:配置Wi-Fi时,建议把SSID和密码放在结构体里统一管理。我在一个项目里遇到过SSID和密码长度超过32字节的情况,结果Wi-Fi连接失败,查了半天才发现是缓冲区溢出了。ESP32的SSID最大32字节,密码最大64字节,别超了。

4.3 Wi-Fi连接状态管理

Wi-Fi连接不是一锤子买卖。你连上路由器之后,信号可能会变差,路由器可能会重启,甚至有人会把路由器关掉。所以状态管理很重要。

ESP32的Wi-Fi状态机大概有这几个状态:

状态 含义 常见原因
WIFI_STATE_INIT Wi-Fi初始化完成 刚调用esp_wifi_init()
WIFI_STATE_START Wi-Fi已启动 调用esp_wifi_start()后
WIFI_STATE_CONNECTING 正在连接路由器 调用esp_wifi_connect()后
WIFI_STATE_CONNECTED 已连接,但还没拿到IP 关联成功,DHCP进行中
WIFI_STATE_GOT_IP 已获取IP地址 DHCP成功
WIFI_STATE_DISCONNECTED 连接断开 信号弱、路由器重启等

我个人习惯用一个全局变量来记录当前状态:

typedef enum {
    WIFI_STATE_IDLE,
    WIFI_STATE_CONNECTING,
    WIFI_STATE_CONNECTED,
    WIFI_STATE_GOT_IP,
    WIFI_STATE_DISCONNECTED
} wifi_state_t;

wifi_state_t g_wifi_state = WIFI_STATE_IDLE;

为什么要这么做?因为Mesh网络里,每个节点需要知道自己的网络状态,才能决定要不要转发数据。如果Wi-Fi断了,节点应该进入本地缓存模式,等网络恢复后再同步数据。

避坑指南:我曾经在一个项目中,Wi-Fi断开后没有及时更新状态,结果节点还在拼命往外发数据,全部丢掉了。后来我加了一个定时器,每5秒检查一次Wi-Fi状态,如果断开就停止数据发送。嗯,这个习惯我一直保留到现在。

4.4 Wi-Fi事件处理

ESP32的事件处理机制,说白了就是注册回调函数。当Wi-Fi状态发生变化时,系统会触发相应的事件,你在回调里做处理。

常用的事件有这些:

  • WIFI_EVENT_STA_START:Station模式启动完成。这时候可以调用esp_wifi_connect()去连接路由器。
  • WIFI_EVENT_STA_CONNECTED:ESP32与路由器建立了Wi-Fi连接。注意,这时候还没拿到IP地址。
  • WIFI_EVENT_STA_DISCONNECTED:连接断开。你需要在这里做重连处理。
  • IP_EVENT_STA_GOT_IP:拿到了IP地址。这时候网络才算真正可用。

看代码:

static void event_handler(void* arg, esp_event_base_t event_base,
                          int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT) {
        switch (event_id) {
            case WIFI_EVENT_STA_START:
                printf("Wi-Fi已启动,开始连接...\n");
                esp_wifi_connect();
                g_wifi_state = WIFI_STATE_CONNECTING;
                break;

            case WIFI_EVENT_STA_CONNECTED:
                printf("已连接到路由器,等待IP...\n");
                g_wifi_state = WIFI_STATE_CONNECTED;
                break;

            case WIFI_EVENT_STA_DISCONNECTED:
                printf("Wi-Fi连接断开,5秒后重连...\n");
                g_wifi_state = WIFI_STATE_DISCONNECTED;
                vTaskDelay(pdMS_TO_TICKS(5000));
                esp_wifi_connect();
                break;
        }
    } else if (event_base == IP_EVENT) {
        if (event_id == IP_EVENT_STA_GOT_IP) {
            ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
            printf("获取到IP地址: " IPSTR "\n", IP2STR(&event->ip_info.ip));
            g_wifi_state = WIFI_STATE_GOT_IP;
        }
    }
}

这里有个关键点:WIFI_EVENT_STA_DISCONNECTED事件里,我加了5秒的延时再重连。为什么?因为如果路由器只是短暂重启,你立刻重连只会加重网络负担。5秒的间隔,既不会让用户等太久,也不会把路由器搞崩溃。

核心要点:事件处理里不要做耗时操作。如果你需要在断开时保存数据,建议用队列把任务发给其他任务处理。我在一个项目里直接在事件回调里写Flash,结果导致Wi-Fi驱动卡死,整个系统都崩了。从那以后,我所有的事件回调都只做状态更新和日志打印,真正的业务逻辑放到任务里去做。

4.5 实战中的坑与经验

最后,分享几个我在项目中遇到的坑:

  1. Wi-Fi重连风暴:如果路由器信号不好,ESP32会频繁触发DISCONNECTED事件,然后你又在回调里不断重连,形成恶性循环。我的解决方案是加一个重连计数器,连续失败5次后就进入深度睡眠,等一段时间再试。
  2. DHCP超时:有些老旧路由器DHCP响应很慢,ESP32默认的超时时间可能不够。你可以通过esp_netif_set_hostname()设置主机名,或者手动配置静态IP。我个人建议Mesh节点用静态IP,省去DHCP的麻烦。
  3. 信道切换:如果路由器开启了自动信道选择,ESP32可能会在连接过程中遇到信道不匹配的问题。解决办法是在配置里指定信道,或者让ESP32做信道扫描。

嗯,这一章的内容就到这里。Wi-Fi基础打牢了,后面Mesh网络的通信才能稳如老狗。下一章咱们讲AP模式,到时候你会看到ESP32怎么从一个「客户端」变成「热点」。