ESP32-S3 WiFi 连接踩坑:reason=15 不是密码错了

ESP32-S3 连 WiFi 提示 reason=15 4WAY_HANDSHAKE_TIMEOUT,密码明明是对的却连不上?本文记录从密码悬垂指针到架构重写的完整排查过程

问题现象

点击 WiFi 列表里的网络、输入密码,串口输出:

1
2
3
4
I (38676) wifi:state: init -> auth (0xb0)
I (38736) wifi:state: auth -> assoc (0x0)
I (38796) wifi:state: assoc -> run (0x10)
W (41966) main: WiFi disconnected, reason=15 (ssid=Redmi Note 12 Turbo)

reason=15 对应 WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT,看起来是 4-way handshake 超时——通常意味着密码错了

但密码是纯数字 1234567890,输了好几遍,绝对没错,为什么还是连不上?


排查阶段一:密码到底输对了没有?

项目用 lv_roller 滚轮输入密码,确实容易手滑。先加一行日志确认:

1
ESP_LOGI(TAG, "UI: password='%s'", password);

结果:日志输出 password='1234567890',密码完全正确。排除输错密码。


排查阶段二:WiFi 配置缺字段?

对比参考例程 09-wifi_scan_connect,发现例程的 wifi_config_t 多了两个字段:

1
2
3
4
5
6
7
wifi_config_t wifi_config = {
    .sta = {
        .threshold.authmode = WIFI_AUTH_WPA2_PSK,
        .sae_pwe_h2e = WPA3_SAE_PWE_BOTH,        // ← 原来没有
        .sae_h2e_identifier = "",                  // ← 原来没有
    },
};

现在的手机和路由器很多是 WPA2/WPA3 混合模式,不配置这两个字段 handshake 可能失败。

修复:加上这两个字段。

结果:仍然连不上。


排查阶段三:多余的 esp_wifi_disconnect()

原始代码连接前调了 esp_wifi_disconnect()

1
2
3
esp_wifi_disconnect();              // ← 参考例程里没有这行
esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
esp_wifi_connect();

修复:删掉这行。

结果:还是连不上,但日志出现了更诡异的现象。


排查阶段四:密码变成了状态标签文本!(根因之一)

日志突然变成这样:

1
I (38666) page_wifi: wifi_connect_task: SSID='Redmi Note 12 Turbo' PASS='正在连接 Redmi Note 12 Turbo...'

密码变成了 "正在连接 Redmi Note 12 Turbo..."?!

这不是密码,这是状态标签的文本!

根因分析

看回调函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static void wifi_connect_cb(lv_event_t *e)
{
    const char *password = lv_textarea_get_text(s_ta_pass);  // ① 获取密码指针

    lv_obj_del(s_pwnd);                                       // ② 删除弹窗!
    // s_ta_pass 是 s_pwnd 的子对象,删除弹窗后内部缓冲区被释放

    lv_label_set_text_fmt(s_status_label, "正在连接 %s...", s_selected_ssid);  // ③ 写状态标签
    // 释放的内存可能被 LVGL 内存管理器重用了

    xQueueSend(s_wifi_queue, password, ...);  // ④ 发送的是悬空指针指向的垃圾数据!
}

问题很清晰:

  1. lv_textarea_get_text() 返回的是对象内部缓冲区的指针
  2. lv_obj_del(s_pwnd) 删除弹窗,连带释放了 s_ta_pass 的缓冲区
  3. 后面 lv_label_set_text_fmt() 写状态标签时,那块内存可能被重用了
  4. xQueueSend() 发送的 password 已经指向了状态标签的文本!

修复

先复制密码到局部缓冲区,再删除弹窗:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
static void wifi_connect_cb(lv_event_t *e)
{
    const char *password = lv_textarea_get_text(s_ta_pass);
    if (!password || password[0] == '\0') return;

    /* ✅ 先复制密码到局部缓冲区,再删除弹窗 */
    char pass_copy[64];
    strncpy(pass_copy, password, sizeof(pass_copy) - 1);
    pass_copy[sizeof(pass_copy) - 1] = '\0';

    ESP_LOGI(TAG, "UI: password='%s'", pass_copy);  // 确认密码正确

    lv_obj_del(s_pwnd);  // 现在可以安全删除弹窗了
    s_pwnd = NULL;

    /* 通过队列发送给后台任务 */
    wifi_account_t account;
    strncpy(account.ssid, s_selected_ssid, sizeof(account.ssid) - 1);
    strncpy(account.password, pass_copy, sizeof(account.password) - 1);  // ✅ 使用安全副本
    xQueueSend(s_wifi_queue, &account, portMAX_DELAY);
}

结果:密码正确传输了,但 WiFi 仍然连不上。继续排查。


排查阶段五:UI 回调里直接调 WiFi API(根因之二)

前面的修复解决了密码传输问题,但连接仍然失败 reason=15

再仔细对比参考例程,发现核心差异不在某一行代码,而在架构

项目 我们的项目 参考例程
连接触发 UI 事件回调中直接 esp_wifi_connect() UI 只发队列,后台任务处理连接
同步方式 没有同步,直接返回 xEventGroupWaitBits 等待结果
重试逻辑 没有 断开时自动重试 3 次
任务分离 UI 和 WiFi 混在一起 UI 线程 ↔ 队列 ↔ WiFi 线程

为什么 UI 回调里不能直接连 WiFi?

  1. 时序问题:WiFi 驱动在某些状态下不接受配置变更
  2. 事件冲突esp_wifi_connect() 是异步的,回调返回后事件处理可能还没完
  3. 缺少重试:断开后没有重试,一次失败就彻底凉了

修复 —— 重写为后台任务模式

 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/* 队列和 EventGroup */
static QueueHandle_t s_wifi_queue = NULL;
static EventGroupHandle_t s_wifi_event_group = NULL;
#define WIFI_CONNECTED_BIT  BIT0
#define WIFI_FAIL_BIT       BIT1

typedef struct {
    char ssid[32];
    char password[64];
} wifi_account_t;

/* ---------- 后台连接任务 ---------- */
static void wifi_connect_task(void *arg)
{
    wifi_account_t account;
    while (true) {
        if (xQueueReceive(s_wifi_queue, &account, portMAX_DELAY)) {
            wifi_config_t wifi_config = {
                .sta = {
                    .threshold.authmode = WIFI_AUTH_WPA2_PSK,
                    .sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
                    .sae_h2e_identifier = "",
                },
            };
            strcpy((char *)wifi_config.sta.ssid, account.ssid);
            strcpy((char *)wifi_config.sta.password, account.password);

            ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
            esp_wifi_connect();

            EventBits_t bits = xEventGroupWaitBits(
                s_wifi_event_group,
                WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
                pdFALSE, pdFALSE, portMAX_DELAY);

            if (bits & WIFI_CONNECTED_BIT) {
                ESP_LOGI(TAG, "WiFi connected OK");
            } else if (bits & WIFI_FAIL_BIT) {
                ESP_LOGW(TAG, "WiFi connect failed");
            }
        }
    }
}

/* ---------- UI 回调:只发队列 ---------- */
static void wifi_connect_cb(lv_event_t *e)
{
    const char *password = lv_textarea_get_text(s_ta_pass);
    if (!password || password[0] == '\0') return;

    char pass_copy[64];
    strncpy(pass_copy, password, sizeof(pass_copy) - 1);
    pass_copy[sizeof(pass_copy) - 1] = '\0';

    lv_obj_del(s_pwnd);  // 关闭弹窗
    s_pwnd = NULL;

    wifi_account_t account;
    strncpy(account.ssid, s_selected_ssid, sizeof(account.ssid) - 1);
    strncpy(account.password, pass_copy, sizeof(account.password) - 1);
    xQueueSend(s_wifi_queue, &account, portMAX_DELAY);  // ✅ 发给后台任务
}

/* ---------- 初始化时创建任务 ---------- */
void page_wifi_init(void)
{
    s_wifi_event_group = xEventGroupCreate();
    s_wifi_queue = xQueueCreate(2, sizeof(wifi_account_t));
    xTaskCreatePinnedToCore(wifi_connect_task, "wifi_conn", 4*1024, NULL, 5, NULL, 1);
}

同时加上重试机制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
static int s_wifi_retry_num = 0;
#define WIFI_MAX_RETRY  3

static void wifi_event_handle(...)
{
    // ...
    case WIFI_EVENT_STA_DISCONNECTED:
        if (s_wifi_retry_num < WIFI_MAX_RETRY) {
            esp_wifi_connect();  // 自动重试
            s_wifi_retry_num++;
        } else {
            page_wifi_on_failed();  // 通知 UI
        }
        break;
    // ...
    case IP_EVENT_STA_GOT_IP:
        s_wifi_retry_num = 0;  // 重置重试计数
        page_wifi_on_connected();
        break;
}

结果:连接成功!


所有修复点汇总

# 问题 修复
1 wifi_config_t 缺 WPA3 字段 .sae_pwe_h2e = WPA3_SAE_PWE_BOTH.sae_h2e_identifier = ""
2 多余的 esp_wifi_disconnect() 删除这行
3 悬垂指针:删弹窗后缓冲区失效 先复制密码到局部变量,再删弹窗
4 UI 回调直接操作 WiFi 改用队列 + 后台 FreeRTOS 任务
5 缺少自动重试 事件处理中加 WIFI_MAX_RETRY

关键教训

1. LVGL 对象删除后指针立即失效

1
2
3
4
5
6
7
8
9
// ❌ 错误:先删对象,再用指针
lv_obj_del(parent);
xQueueSend(q, child_text);   // 指向已释放内存

// ✅ 正确:先复制数据,再删对象
char buf[64];
strncpy(buf, child_text, sizeof(buf)-1);
lv_obj_del(parent);
xQueueSend(q, buf);          // 安全

lv_obj_get_text() / lv_textarea_get_text() 返回的是对象内部缓冲区的指针,对象删除后立即失效。

2. UI 线程只做 UI 的事

WiFi 连接涉及异步事件、状态机、超时重试,这些都应该放在后台任务。UI 只负责:

  • 显示界面
  • 收集用户输入
  • 通过队列/事件通知后台

3. 看参考例程要看架构

不要只看参考例程的某一行代码,要看整体:

  • 任务怎么划分的
  • 同步用什么机制
  • 初始化顺序
  • 错误怎么处理

硬件:立创实战派 ESP32-S3 开发板 | 环境:ESP-IDF v5.5 + LVGL 8.3

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计