问题现象
点击 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, ...); // ④ 发送的是悬空指针指向的垃圾数据!
}
|
问题很清晰:
lv_textarea_get_text() 返回的是对象内部缓冲区的指针
lv_obj_del(s_pwnd) 删除弹窗,连带释放了 s_ta_pass 的缓冲区
- 后面
lv_label_set_text_fmt() 写状态标签时,那块内存可能被重用了
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?
- 时序问题:WiFi 驱动在某些状态下不接受配置变更
- 事件冲突:
esp_wifi_connect() 是异步的,回调返回后事件处理可能还没完
- 缺少重试:断开后没有重试,一次失败就彻底凉了
修复 —— 重写为后台任务模式
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