深入分析 ESP32 的 WiFi 状态机

Posted tidyjiang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入分析 ESP32 的 WiFi 状态机相关的知识,希望对你有一定的参考价值。

本工程已托管到 GitHub,具体路径是 https://github.com/tidyjiang8/esp32-projects/tree/master/sta

在前一篇博客 【让 ESP32 连接到你的 WiFi 热点】 中,我们已经简单地分析了一下 WiFi 的工作流程,并简要提示了一下事件调度器/WiFi 状态机,我们将在这一篇博客中详细分析。

在 ESP-IDF 中,整个 wifi 协议栈是一个状态机,它在各个时刻都有一个状态。用户可以根据自己的需要,让协议栈在运行到某个状态时自动处理某些工作。理解清楚整个 WiFi 状态机有利于我们编写出更好的应用程序,其中最最基础的功能就是【断网重连】,这在我们的 sta 项目中已经实现了,请参考该源码。

【协议栈的状态定义】

在 ESP-IDF 中,整个网络协议栈包含的状态定义在头文件 components/esp32/include/esp_event.h中,由枚举类型 system_event_id_t 定义:

typedef enum 
    SYSTEM_EVENT_WIFI_READY = 0,           /**< ESP32 WiFi 准备就绪*/
    SYSTEM_EVENT_SCAN_DONE,                /**< ESP32 完成扫描 AP */
    SYSTEM_EVENT_STA_START,                /**< ESP32 sta 启动 */
    SYSTEM_EVENT_STA_STOP,                 /**< ESP32 sta 停止 */
    SYSTEM_EVENT_STA_CONNECTED,            /**< ESP32 sta 连接到 AP */
    SYSTEM_EVENT_STA_DISCONNECTED,         /**< ESP32 sta 从 AP 断开连接 */
    SYSTEM_EVENT_STA_AUTHMODE_CHANGE,      /**< ESP32 sta 所连接的 AP 的授权模式改变了 */
    SYSTEM_EVENT_STA_GOT_IP,               /**< ESP32 sta 从 AP 获取到 IP 地址 */
    SYSTEM_EVENT_STA_WPS_ER_SUCCESS,       /**< ESP32 sta wps succeeds in enrollee mode */
    SYSTEM_EVENT_STA_WPS_ER_FAILED,        /**< ESP32 sta wps fails in enrollee mode */
    SYSTEM_EVENT_STA_WPS_ER_TIMEOUT,       /**< ESP32 sta wps timeout in enrollee mode */
    SYSTEM_EVENT_STA_WPS_ER_PIN,           /**< ESP32 sta wps pin code in enrollee mode */
    SYSTEM_EVENT_AP_START,                 /**< ESP32 soft-AP 启动*/
    SYSTEM_EVENT_AP_STOP,                  /**< ESP32 soft-AP 停止*/
    SYSTEM_EVENT_AP_STACONNECTED,          /**< 有 sta 连接到 ESP32 soft-AP */
    SYSTEM_EVENT_AP_STADISCONNECTED,       /**< 有 sta 从 ESP32 soft-AP 断开连接 */
    SYSTEM_EVENT_AP_PROBEREQRECVED,        /**< soft-AP 接口接收到探测请求报文*/
    SYSTEM_EVENT_AP_STA_GOT_IP6,           /**< ESP32 sta/ap 接口获取到 IPv6 地址 */
    SYSTEM_EVENT_ETH_START,                /**< ESP32 ethernet start */
    SYSTEM_EVENT_ETH_STOP,                 /**< ESP32 ethernet stop */
    SYSTEM_EVENT_ETH_CONNECTED,            /**< ESP32 ethernet phy link up */
    SYSTEM_EVENT_ETH_DISCONNECTED,         /**< ESP32 ethernet phy link down */
    SYSTEM_EVENT_ETH_GOT_IP,               /**< ESP32 ethernet got IP from connected AP */
    SYSTEM_EVENT_MAX
 system_event_id_t;

【查看 ESP32 连接到 AP 时经历的各个状态】

ESP32 的日志默认级别是 INFO,即只有级别大于等于 INFO 级别的消息才会被打印到串口上,我们要查看 WiFi 连接过程中的各个状态,需要修改日志的打印级别,这是在配置菜单中完成的。

运行命令 make menuconfig 进入图形化配置菜单,然后依次选择 Component config --->Log output --->Default log verbosity (Info) --->,然后选择打印级别为Debug

日志的最低级别明明是 Verbose,我们选择的级别为啥是 Debug?请继续看后续分解^_^

选择好日志级别后,退出配置界面,保存配置,然后执行命令make flash monitor重新编译、烧写程序并查看串口输出。

为了避免文章太过冗长,下面截取了一部分与 WiFi 相关的日志:

I (720) wifi: Init dynamic tx buffer num: 32
I (720) wifi: Init dynamic rx buffer num: 64
I (720) wifi: wifi driver task: 3ffbd668, prio:23, stack:3584
I (730) wifi: Init static rx buffer num: 10
I (730) wifi: Init dynamic rx buffer num: 64
I (740) wifi: Init rx ampdu len mblock:7
I (740) wifi: Init lldesc rx ampdu entry mblock:4
I (740) wifi: wifi power manager task: 0x3ffc2a30 prio: 21 stack: 2560
I (750) wifi: wifi timer task: 3ffc3ab0, prio:22, stack:3584
I (810) wifi: mode : sta (30:ae:a4:04:80:84)
D (810) event: SYSTEM_EVENT_STA_START

I (840) app_sta: Connecting to AP...
I (960) wifi: n:1 0, o:1 0, ap:255 255, sta:1 0, prof:1
I (1940) wifi: state: init -> auth (b0)
I (1950) wifi: state: auth -> assoc (0)
I (1960) wifi: state: assoc -> run (10)
I (1990) wifi: connected with test, channel 1
D (1990) event: SYSTEM_EVENT_STA_CONNECTED, ssid:test, ssid_len:4, bssid:d0:5b:a8:c2:91:7e, channel:1, authmode:3
D (2324) event: SYSTEM_EVENT_STA_GOTIP, ip:192.168.1.120, mask:255.255.255.0, gw:192.168.1.1
I (2324) event: ip: 192.168.1.120, mask: 255.255.255.0, gw: 192.168.1.1
I (2324) app_sta: Connected.
I (9964) wifi: state: run -> auth (3a0)
I (9964) wifi: n:1 0, o:1 0, ap:255 255, sta:1 0, prof:1
D (9964) event: SYSTEM_EVENT_STA_DISCONNECTED, ssid:test, ssid_len:4, bssid:d0:5b:a8:c2:91:7e, reason:3
I (9994) app_sta: Wifi disconnected, try to connect ...
I (18164) wifi: n:11 0, o:1 0, ap:255 255, sta:11 0, prof:1
I (18174) wifi: state: auth -> auth (b0)
I (18174) wifi: state: auth -> assoc (0)
I (18184) wifi: state: assoc -> run (10)
I (18224) wifi: connected with test, channel 11
D (18234) event: SYSTEM_EVENT_STA_CONNECTED, ssid:test, ssid_len:4, bssid:d0:5b:a8:c2:91:7e, channel:11, authmode:3
D (18894) event: SYSTEM_EVENT_STA_GOTIP, ip:192.168.1.120, mask:255.255.255.0, gw:192.168.1.1
I (18894) event: ip: 192.168.1.120, mask: 255.255.255.0, gw: 192.168.1.1
I (18894) app_sta: Connected.
I (28184) wifi: pm start, type:0

注意,为了查看更多的状态,我在连接过程中将 WiFi 热点关闭了一次,然后再打开该热点。

仔细查看日志,可以看到下面这些与状态改变相关的日志:

  • event: SYSTEM_EVENT_STA_START
  • event: SYSTEM_EVENT_STA_CONNECTED
  • event: SYSTEM_EVENT_STA_GOTIP # 获取到 IP 后,重启 WiFi 热点
  • event: SYSTEM_EVENT_STA_DISCONNECTED
  • app_sta: Wifi disconnected, try to connect ...
  • event: SYSTEM_EVENT_STA_CONNECTED
  • event: SYSTEM_EVENT_STA_GOTIP # 再次连接,并获取到 IP地址

整个过程非常清晰吧!而且我们还可以看到断网自动重连的现象。

除了app_sta: Wifi disconnected, try to connect ...这句话是由我们的应用程序打印的外,其它 Log 都是由系统自己 打印的。

【状态转换过程】

在我们的应用程序中,当我们调用函数 esp_wifi_start() 后,wifi 状态机就会开始运转。第一个状态是 START,此时我们的 event_handler() 函数会被调用一次,并进入 case SYSTEM_EVENT_STA_START 分支:

    case SYSTEM_EVENT_STA_START:
        ESP_LOGI(TAG, "Connecting to AP...");
        esp_wifi_connect();
        break;

在该状态来临时,我们调用了函数 esp_wifi_connect(); 让 ESP32 去连接 WiFi 热点。调用该函数后,wifi 驱动会尝试通过 IEEE 802.11 与热点建立连接。如果建立连接成功,则进入 CONNECTED 状态,并通过某种协议从 AP 处获取一个 IP 地址,如果获取成功,则进入 GOTIP 状态;如果建立连接失败,则会进入 DISCONNECTED 状态。

当我们将 WiFi 热点关闭时,wifi 驱动会发现与热点的通信失败(IEEE 802.11 有心跳机制),然后进入 DISCONNECTED 状态,此时我们的 event_handler() 函数会被调用一次,并进入 case SYSTEM_EVENT_STA_DISCONNECTED 分支:

    case SYSTEM_EVENT_STA_DISCONNECTED:
        ESP_LOGI(TAG, "Wifi disconnected, try to connect ...");
        esp_wifi_connect();
        break;

我们在这里再次调用了函数 esp_wifi_connect(),让 wifi 驱动再次尝试与热点建立连接。如果建立建立成功,则会再次进入 CONNECTGOTIP 这两个状态;如果建立连接失败,会再次进入 DISCONNECT 状态,依次反复循环,直到连接成功为止。这就是所谓的断网重连!

【深入分析状态机源码】

通过上面的分析,我们已经基本清楚了整个状态机的转换过程,但是这个状态机是如何工作的呢?我们需要继续分析源码,拿 esp_event_loop_init() 这个函数开刀。

【函数原型】

这里需要看两个函数的函数原型,其中一个是 esp_event_loop_init(),另一个是需要传递给该函数的 回调函数(callback)

先看 esp_event_loop_init()

/**
  * @brief  Initialize event loop
  *         Create the event handler and task
  *
  * @param  system_event_cb_t cb : application specified event callback, it can be modified by call esp_event_set_cb
  * @param  void *ctx : reserved for user
  *
  * @return ESP_OK : succeed
  * @return others : fail
  */
esp_err_t esp_event_loop_init(system_event_cb_t cb, void *ctx);

它的作用已经在注释中说的非常清楚了:初始化事件 loop,创建事件的 handler 和任务。

包含两个参数:

  • system_event_cb_t cb,即回调函数,是一个函数指针,当状态机中有某个状态改变时,会调用这个回调函数。
  • void *ctx,回调函数相关的上下文(context),即系统在调用回调函数时需要传递给回调函数的参数。

再看看 system_event_cb_t

/**
  * @brief  Application specified event callback function
  *
  * @param  void *ctx : reserved for user
  * @param  system_event_t *event : event type defined in this file
  *
  * @return ESP_OK : succeed
  * @return others : fail
  */
typedef esp_err_t (*system_event_cb_t)(void *ctx, system_event_t *event);

它使用 typedef 定义了一个函数指针类型system_event_cb_t,它有参数:

  • void *ctx:回调函数相关的上下文(context),这个是由应用程序指定的,即传递给函数 esp_event_loop_init() 的第二个参数。
  • system_event_t *event:事件,可以理解为状态机的状态。回调函数被调用时,会根据这个参数来判断当前的状态机处于哪个状态。所以,在回调函数内部,是一个 switch…case… 结构。

【初始化过程】

下面开始具体分析源码。

esp_err_t esp_event_loop_init(system_event_cb_t cb, void *ctx)

    if (s_event_init_flag) 
        // 防止重复初始化
        return ESP_FAIL;
    
    s_event_handler_cb = cb;
    s_event_ctx = ctx;
    // 创建一个 event 队列
    s_event_queue = xQueueCreate(CONFIG_SYSTEM_EVENT_QUEUE_SIZE, sizeof(system_event_t));
    // 创建 event loop 任务
    xTaskCreatePinnedToCore(esp_event_loop_task, "eventTask",
            ESP_TASKD_EVENT_STACK, NULL, ESP_TASKD_EVENT_PRIO, NULL, 0);

    s_event_init_flag = true;
    return ESP_OK;

上面这段代码主要做了如下几件事儿:

  • 将传入给该函数的参数 cb 和 ctx 保存到全局变量 s_event_handler_cbs_event_ctx
  • 创建一个事件队列 s_event_queue,这个队列用来存放事件。
  • 创建一个事件处理任务。

事件循环处理任务

在事件初始化时创建了事件循环处理任务,所以我们得继续查看该任务的代码。

static void esp_event_loop_task(void *pvParameters)

    while (1) 
        system_event_t evt;
        // 从初始化时所创建的事件队列中接收一个事件,接收的事件保存到变量 evt 中
        if (xQueueReceive(s_event_queue, &evt, portMAX_DELAY) == pdPASS) 
           // 如果接收事件成功,先调用默认的处理过程     
            esp_err_t ret = esp_event_process_default(&evt);
            if (ret != ESP_OK) 
                ESP_LOGE(TAG, "default event handler failed!");
            
            // 再将该事件转给用户的应用程序
            ret = esp_event_post_to_user(&evt);
            if (ret != ESP_OK) 
                ESP_LOGE(TAG, "post event to user fail!");
            
        
    

再看看默认处理过程:

esp_err_t esp_event_process_default(system_event_t *event)

    if (event == NULL) 
        // 先对入参进行判断。这里的入参即从事件队列中接收到的事件
        ESP_LOGE(TAG, "Error: event is null!");
        return ESP_FAIL;
    

    // 这里是一些打印信息。打印出接收到的是啥事件。我们继续追踪该函数的源码
    // 的话,可以看到它里面只是对各事件调用 ESP_LOGD() 函数而已。 这里的日志
    // 级别是 Debug,所以我们在前面改变日志级别的时候,只将级别降低到了 Debug 
    // 级别,就能看到状态机中各状态的情况
    esp_system_event_debug(event);
    // 根据事件的id,先对事件的有效性进行检查
    if ((event->event_id < SYSTEM_EVENT_MAX)) 
        // 根据事件的 id,看看该事件有没有提供默认的处理函数
        if (default_event_handlers[event->event_id] != NULL) 
            // 如果有默认的处理函数,则调用该函数
            ESP_LOGV(TAG, "enter default callback");
            default_event_handlers[event->event_id](event);
            ESP_LOGV(TAG, "exit default callback");
        
     else 
        ESP_LOGE(TAG, "mismatch or invalid event, id=%d", event->event_id);
        return ESP_FAIL;
    
    return ESP_OK;

这里我们需要再看看 default_event_handlers 这个数组,这个数组的类型是函数指针:

static const system_event_handler_t default_event_handlers[SYSTEM_EVENT_MAX] = 
#ifdef CONFIG_WIFI_ENABLED
    [SYSTEM_EVENT_WIFI_READY]          = NULL,
    [SYSTEM_EVENT_SCAN_DONE]           = NULL,
    [SYSTEM_EVENT_STA_START]           = system_event_sta_start_handle_default,
    [SYSTEM_EVENT_STA_STOP]            = system_event_sta_stop_handle_default,
    [SYSTEM_EVENT_STA_CONNECTED]       = system_event_sta_connected_handle_default,
    [SYSTEM_EVENT_STA_DISCONNECTED]    = system_event_sta_disconnected_handle_default,
    [SYSTEM_EVENT_STA_AUTHMODE_CHANGE] = NULL,
    [SYSTEM_EVENT_STA_GOT_IP]          = system_event_sta_got_ip_default,
    [SYSTEM_EVENT_STA_WPS_ER_SUCCESS]  = NULL,
    [SYSTEM_EVENT_STA_WPS_ER_FAILED]   = NULL,
    [SYSTEM_EVENT_STA_WPS_ER_TIMEOUT]  = NULL,
    [SYSTEM_EVENT_STA_WPS_ER_PIN]      = NULL,
    [SYSTEM_EVENT_AP_START]            = system_event_ap_start_handle_default,
    [SYSTEM_EVENT_AP_STOP]             = system_event_ap_stop_handle_default,
    [SYSTEM_EVENT_AP_STACONNECTED]     = NULL,
    [SYSTEM_EVENT_AP_STADISCONNECTED]  = NULL,
    [SYSTEM_EVENT_AP_PROBEREQRECVED]   = NULL,
    [SYSTEM_EVENT_AP_STA_GOT_IP6]      = NULL,
#endif
#ifdef CONFIG_ETHERNET
    [SYSTEM_EVENT_ETH_START]           = system_event_eth_start_handle_default,
    [SYSTEM_EVENT_ETH_STOP]            = system_event_eth_stop_handle_default,
    [SYSTEM_EVENT_ETH_CONNECTED]       = system_event_eth_connected_handle_default,
    [SYSTEM_EVENT_ETH_DISCONNECTED]    = system_event_eth_disconnected_handle_default,
    [SYSTEM_EVENT_ETH_GOT_IP]          = NULL,
#endif
;

我们可以看出:

  • 当 wifi 状态机的状态变为 START 时,会调用函数 system_event_sta_start_handle_default()
  • 当 wifi 状态机的状态变为 CONNECTED 时,会调用函数 system_event_sta_connected_handle_default()
  • 当 wifi 状态机的状态变为 GOTIP 时,会调用函数 system_event_sta_got_ip_handle_default()
  • 当 wifi 状态机的状态变为 DISCONNECT 时,会调用函数 system_event_sta_disconnect_handle_default()
  • 当 wifi 状态机的状态变为 STOP 时,会调用函数 system_event_sta_stop_handle_default()

如果有兴趣,可以依次查看这些函数都干了些啥。这里由于篇幅太长,就不细看了。

然后我们再看看是如何将时间传递给应用程序的:

static esp_err_t esp_event_post_to_user(system_event_t *event)

    if (s_event_handler_cb) 
        // 如果 s_event_handler_cb 不为 NULL,则调用该函数
        return (*s_event_handler_cb)(s_event_ctx, event);
    
    return ESP_OK;

so easy!直接调用在 esp_event_loop_init() 时传入的回调函数,其中第一个参数表示在 esp_event_loop_init() 时传入的由应用程序指定的上下文参数,第二个参数表示当前的事件(即状态机的状态)。

终于搞明白了,哈哈O(∩∩)O哈哈~O(∩∩)O哈哈~。

等等,事件循环处理任务从事件队列中接收事件,这个事件是从哪儿来的呢?当然是 wifi 驱动库发送到这个事件中的,不过由于 wifi 驱动库没开源,所以我们没办法继续追踪源代码啦。

【总结】

其实这个状态机还是蛮简单的,由用户在应用程序传递一个回调函数给系统的事件处理模块,然后在该模块内部循环地接收并处理事件——调用默认的事件处理函数和用户设置的回调函数。

以上是关于深入分析 ESP32 的 WiFi 状态机的主要内容,如果未能解决你的问题,请参考以下文章

1-STM32物联网开发WIFI(ESP8266)+GPRS(Air202)系统方案安全篇(来看一下怎么样监听网络数据,监听电脑上位机软件的数据)

esp32连接wifi很慢

状态机提要

esp32的wifi模块叫什么

乐鑫Esp32学习之旅 乐鑫 ESP-S2/S3 模组的实现 USB 无线网卡上网,为你的台式机装上无线WiFI上网吧。(附带源码)

乐鑫Esp32学习之旅 乐鑫 ESP-S2/S3 模组的实现 USB 无线网卡上网,为你的台式机装上无线WiFI上网吧。(附带源码)