本工程已托管到 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 的日志默认级别是 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 驱动再次尝试与热点建立连接。如果建立建立成功,则会再次进入 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);
它的作用已经在注释中说的非常清楚了:初始化事件 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;
}
上面这段代码主要做了如下几件事儿:
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!");
}
}
}
}
再看看默认处理过程:
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
};
我们可以看出:
START
时,会调用函数 system_event_sta_start_handle_default()
CONNECTED
时,会调用函数 system_event_sta_connected_handle_default()
GOTIP
时,会调用函数 system_event_sta_got_ip_handle_default()
DISCONNECT
时,会调用函数 system_event_sta_disconnect_handle_default()
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 驱动库没开源,所以我们没办法继续追踪源代码啦。
其实这个状态机还是蛮简单的,由用户在应用程序传递一个回调函数给系统的事件处理模块,然后在该模块内部循环地接收并处理事件——调用默认的事件处理函数和用户设置的回调函数。