首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

深入分析 ESP32 的 WiFi 状态机

  • 24-03-03 14:46
  • 3022
  • 9401
blog.csdn.net

本工程已托管到 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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

【查看 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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

注意,为了查看更多的状态,我在连接过程中将 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;
  • 1
  • 2
  • 3
  • 4

在该状态来临时,我们调用了函数 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;
  • 1
  • 2
  • 3
  • 4

我们在这里再次调用了函数 esp_wifi_connect(),让 wifi 驱动再次尝试与热点建立连接。如果建立建立成功,则会再次进入 CONNECT、GOTIP 这两个状态;如果建立连接失败,会再次进入 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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

它的作用已经在注释中说的非常清楚了:初始化事件 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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

它使用 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

  • 将传入给该函数的参数 cb 和 ctx 保存到全局变量 s_event_handler_cb 和 s_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!");
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

再看看默认处理过程:

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

这里我们需要再看看 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
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

我们可以看出:

  • 当 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

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

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

【总结】

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

注:本文转载自blog.csdn.net的chunhua.jiang的文章"https://blog.csdn.net/tidyjiang/article/details/71703241"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top