← 返回教程库

让 ESP32-S3 连上 WiFi

最后更新 2026-06-22
L3 · 联网与 IoT ⏱ 约 18 分钟 🟢 软件/低风险
你将学到
  • 用一套标准 wifi_init_sta() 骨架让 ESP32-S3 连上 2.4GHz WiFi
  • 真正理解 ESP-IDF 的"事件循环 + 回调 + 事件组"为什么取代了 Arduino 的 while 轮询
  • 在事件回调里实现断线自动重连、拿到 IP 后再继续往下跑
  • 知道连不上时怎么从 disconnected 事件的 reason code、2.4GHz 限制、供电几个方向排查

你已经会让 ESP32-S3 闪灯了,但闪灯的板子和一个 9 块钱的定时器没本质区别——它没"出过门"。这一节让它真正联网:连上你家路由器、拿到一个 IP 地址。拿到 IP 之后,你的手机、电脑、云服务器才能找到它,后面的网页控制、MQTT 上云、远程开关,全都从这一步长出来。

但我先把一句话放在最前面,这是本篇最值钱的东西:ESP-IDF 的联网是"事件驱动"的,不是 Arduino 那种 while(WiFi.status()!=WL_CONNECTED) 的轮询。 如果你之前用过 Arduino 的 WiFi.begin,先把那套"发起→死循环里查状态"的肌肉记忆放一边。在 ESP-IDF 里,连上、断开、拿到 IP 这些事,都是系统在后台"抛事件",你写一个回调函数坐等接招。这个范式转变,是从"玩具写法"跨到"产品写法"的真正分水岭,也是本篇要讲透的核心。

读这篇前,你需要已经跑通过点亮第一个 LED——也就是 ESP-IDF 环境装好了、idf.py build flash monitor 这条链路通了、能看懂串口日志。WiFi 这节不接任何线,纯软件,但调试全靠串口日志(ESP_LOGI 打出来的那些),所以你得熟悉怎么用 idf.py monitor 看日志。


第一步:先把整段代码跑通

下面这段是完整可烧录的程序——它用一套叫 wifi_init_sta() 的标准骨架连上 WiFi,连不上自动重连五次,连上后把拿到的 IP 打印出来。把你家 2.4GHz WiFi 的名字和密码填进 WIFI_SSID / WIFI_PASS,直接放进工程的 main/main.c

#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"

#define WIFI_SSID "你的WiFi名"
#define WIFI_PASS "你的WiFi密码"
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT      BIT1

static const char *TAG = "wifi";
static EventGroupHandle_t s_wifi_event_group;
static int s_retry = 0;

static void event_handler(void* arg, esp_event_base_t base, int32_t id, void* data) {
    if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();                       // 启动后主动发起连接
    } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_retry < 5) { esp_wifi_connect(); s_retry++; ESP_LOGI(TAG, "重连中 %d/5", s_retry); }
        else xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
    } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* e = (ip_event_got_ip_t*) data;
        ESP_LOGI(TAG, "拿到 IP: " IPSTR, IP2STR(&e->ip_info.ip));   // 成功标志
        s_retry = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

void wifi_init_sta(void) {
    s_wifi_event_group = xEventGroupCreate();
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL));
    wifi_config_t wc = { .sta = { .ssid = WIFI_SSID, .password = WIFI_PASS } };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wc));
    ESP_ERROR_CHECK(esp_wifi_start());
    // 用事件组阻塞等"连上"或"彻底失败",替代轮询 status 的 while 死等
    xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
}

void app_main(void) {
    ESP_ERROR_CHECK(nvs_flash_init());   // WiFi 射频校准数据存 NVS,连网前必做
    wifi_init_sta();
    ESP_LOGI(TAG, "WiFi 初始化流程结束");
}

在工程目录下一条命令编译、烧录、看日志:

idf.py build flash monitor

(第一次用要先 idf.py set-target esp32s3 选好芯片型号。按 Ctrl + ] 退出监视。)

你应该看到什么

idf.py monitor 的日志里,会按顺序滚出这么几条(中间夹着一堆系统自己打的 wifi: 开头的底层日志,正常):

I (1234) wifi: 拿到 IP: 192.168.1.123
I (1240) wifi: WiFi 初始化流程结束
  • 一上电,系统初始化完 WiFi、抛出 WIFI_EVENT_STA_START,你的回调里 esp_wifi_connect() 主动发起连接——这一步没打日志,但后台在握手。
  • 握手成功、路由器分了 IP,系统抛 IP_EVENT_STA_GOT_IP,回调里打出 拿到 IP: 192.168.x.x这串数字就是成功的标志——它说明 ESP32-S3 已经在你的局域网里、被路由器正式接纳、分到了门牌号。
  • 紧接着 wifi_init_sta() 里那行 xEventGroupWaitBits 等到了 WIFI_CONNECTED_BIT,函数返回,app_main 往下打出 WiFi 初始化流程结束
💡 提示

把这串 192.168.x.x 记下来。后面让手机或电脑在浏览器里访问这个 IP,就能远程控制你的设备——它是你和硬件之间的"门牌号"。如果日志里一直在刷"重连中 1/5、2/5…"最后停住,说明它连不上、走进了 WIFI_FAIL_BIT 那条路,往下看故障排查表,别急着改代码。

如果你看到的不是"拿到 IP"而是一串"重连中 N/5"刷到 5 就不动了,说明连接彻底失败——这正是事件驱动该有的样子:它不会卡死在 while 里傻等,而是按你设的上限重试完、明明白白告诉你"失败"。


第二步:把这套骨架讲透——为什么是"事件驱动"

这段代码比 Arduino 那十几行长,但它不是啰嗦,是把 Arduino 替你藏起来的东西全摊开了。理解了这套骨架,后面所有要联网的篇章(网页服务器、MQTT、OTA)都复用它。我们一块块拆。

为什么开头先 nvs_flash_init()

app_main 第一行就是 nvs_flash_init(),这不是可有可无的礼节——ESP32-S3 的 WiFi 射频校准数据存在 NVS(一块 flash 上的键值存储区)里esp_wifi_init() 启动时要去读它。你不先初始化 NVS,后面 esp_wifi_init() 会直接失败。

有个高频坑:如果 nvs_flash_init() 返回 ESP_ERR_NVS_NO_FREE_PAGES(NVS 分区被写满或版本变了),得擦了重来。产品代码一般这么写:

esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    ESP_ERROR_CHECK(nvs_flash_erase());   // 擦掉重来
    ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);

第一步那段为了聚焦主线写成了一行 ESP_ERROR_CHECK(nvs_flash_init()),真做产品时把上面这段补上更稳。

核心范式:事件循环 + 回调,不是轮询

这是全篇的眼。Arduino 里你这么判断连没连上:

// Arduino 旧写法(对比用,ESP-IDF 不这么干)
while (WiFi.status() != WL_CONNECTED) { delay(500); }  // 每 500ms 主动问一次

你在一个死循环里主动地、反复地"问":"连上了吗?没有?再等等再问。" CPU 大部分时间耗在这种无意义的轮询上。

ESP-IDF 反过来:你不"问",你"等通知"。整套机制三个角色:

  • 事件循环(event loop)esp_event_loop_create_default() 起的一个后台系统任务。WiFi 驱动、TCP/IP 协议栈一旦有动静(连上了、断了、拿到 IP 了),就往这个循环里"投递一个事件"。
  • 回调(你的 event_handler:你用 esp_event_handler_instance_register() 把自己的函数"挂"到事件循环上。事件一来,循环就调用你的函数,把"什么事件"(base + id)和"附带数据"(data)一并交给你。
  • 你只管在回调里处理WIFI_EVENT_STA_START 来了就 esp_wifi_connect()IP_EVENT_STA_GOT_IP 来了就读 IP。没事件的时候,你的代码一行都不跑,CPU 全让给别人。

打个比方:Arduino 的轮询像你站在门口每隔半秒拉开门看一眼快递到没到;ESP-IDF 的事件驱动像你装了个门铃——快递到了它响,没到你该干嘛干嘛。前者累、费电、还占着 CPU;后者省、优雅、是产品该有的样子。

esp_wifi_connect() 只是"发起"

注意一个容易看漏的点:esp_wifi_connect() 和 Arduino 的 WiFi.begin 一样,只是发起连接请求然后立刻返回,握手在后台进行。区别是后续你不靠轮询确认——连上/断开/拿 IP 全由系统抛事件、在你回调里接住。所以代码里 esp_wifi_connect() 出现在两个地方:WIFI_EVENT_STA_START(首次启动后发起)和 WIFI_EVENT_STA_DISCONNECTED(断了重新发起),都是"踢一脚",真正的结果靠后续事件回来告诉你。

事件组:xEventGroupWaitBits 替代 while 死等

可问题来了:事件驱动是好,但 wifi_init_sta() 这个函数总得"等连上了再返回"吧?不然 app_main 后面的逻辑在没网的时候就跑了。这就是 FreeRTOS 事件组(event group) 的用武之地。

事件组你可以理解成一组"信号灯"(一个个 bit)。这里用了两盏:

  • WIFI_CONNECTED_BIT(BIT0):连上并拿到 IP,由 IP_EVENT_STA_GOT_IP 回调点亮(xEventGroupSetBits)。
  • WIFI_FAIL_BIT(BIT1):重连超过上限、彻底失败,由 WIFI_EVENT_STA_DISCONNECTED 回调点亮。

wifi_init_sta() 末尾这行:

xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY);

意思是"阻塞在这,直到这两盏灯里任意一盏亮起来"。注意它和 while 轮询的本质区别:xEventGroupWaitBits 阻塞期间,当前任务是睡着的,不占 CPU——是回调(在事件循环任务里)点亮了 bit,才把它唤醒。这才是替代 while(status!=WL_CONNECTED) 的正确姿势:同样是"等连上",一个睡着等、一个忙着问。

💡 提示

pdFALSE, pdFALSE 这两个参数:第一个"等到后是否清掉 bit"(这里不清,留着给别处查),第二个"是否要所有 bit 都亮才返回"(这里是"任意一个亮就返回")。portMAX_DELAY 是"无限期等"。

拿 IP:IP2STR / IPSTR 替代 WiFi.localIP()

Arduino 里打 IP 是 Serial.println(WiFi.localIP())。ESP-IDF 里 IP 藏在 IP_EVENT_STA_GOT_IP 事件的附带数据里,你这么取:

ip_event_got_ip_t* e = (ip_event_got_ip_t*) data;
ESP_LOGI(TAG, "拿到 IP: " IPSTR, IP2STR(&e->ip_info.ip));

IPSTR 是个格式串宏(展开成 "%d.%d.%d.%d"),IP2STR() 把那个 IP 结构体拆成四个字节填进去——这对宏是 ESP-IDF 打 IP 的标准搭配,记住它。这串 IP 同样是路由器的 DHCP 服务临时分的,不是 ESP32-S3 自己定的,所以每次重连可能变——它是租来的,不是买断的。


第三步:断线自动重连——事件驱动版

真实环境里 WiFi 会抖、路由器会重启,产品级设备得能自己爬起来。在 Arduino 里你得在 loop 里每轮查一次 WiFi.status()、断了重连——又是轮询那一套。ESP-IDF 里优雅多了:断线本身就是一个事件,你已经在骨架里处理了:

} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
    if (s_retry < 5) {
        esp_wifi_connect();                       // 断了?立刻再发起
        s_retry++;
        ESP_LOGI(TAG, "重连中 %d/5", s_retry);
    } else {
        xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);   // 重试到上限,认输
    }
}

逻辑很干净:只要 WiFi 一断,系统就抛 WIFI_EVENT_STA_DISCONNECTED,你的回调被调用,里面直接 esp_wifi_connect() 重新发起。你不用在主循环里轮询、不用自己计时——断了它自己就被叫醒去重连。这里加了 s_retry < 5 的上限,避免密码错的时候无限重连刷屏;连上后在 IP_EVENT_STA_GOT_IP 里把 s_retry 清零,下次断线重新计数。

想做"永不放弃、断多久都重连"的常驻设备?把上限去掉、或者重试到上限后延时几秒再清零重来即可——但生产里更推荐带退避(每次失败把等待时间拉长一点),别一秒一次猛敲路由器。


第四步:为什么只能连 2.4GHz?

代码里没写频段,但这是最容易栽跟头的地方,得讲清楚。

ESP32-S3 的射频只支持 2.4GHz,连不上 5GHz 的网。 现在的路由器大多双频,2.4G 和 5G 常常是两个不同的名字(比如 MyWiFiMyWiFi_5G),你必须把 WIFI_SSID 填成 2.4G 那个。如果你家路由器把两个频段合成了一个名字("双频合一"/"Smart Connect"),手机会自动挑 5G,但 ESP32-S3 挑不了——这种情况要进路由器后台,临时把双频合一关掉,或单独为 2.4G 设一个名字。这个坑在日志里的表现是:一直 WIFI_EVENT_STA_DISCONNECTED、reason code 多半是 201NO_AP_FOUND,压根没扫到这个 AP)。

为什么 2.4GHz 反而是物联网的主流?因为它穿墙能力更强、覆盖更远——隔一两堵墙信号衰减比 5GHz 小得多。物联网设备要的不是高速率(传几个温度数字才几个字节),而是稳定和广覆盖,2.4GHz 正好对路。代价是这个频段更拥挤(蓝牙、微波炉、邻居的 WiFi 都挤在这),但对小数据量设备无所谓。

Station 模式:ESP32-S3 现在是"客户端"

代码里 esp_wifi_set_mode(WIFI_MODE_STA) 这行,让 ESP32-S3 工作在 Station(STA)模式——它像你的手机一样,作为客户端去连一个已有的路由器(AP,接入点)。这是最常用的模式。ESP32-S3 其实还能反过来当 AP 模式WIFI_MODE_AP,自己开一个热点让别的设备连它),或者两个一起开(WIFI_MODE_APSTA)。AP 模式正是配网(让用户不改代码就能填 WiFi 密码)的底层原理——记住这个名字。


故障排查:连不上,按这个顺序查

日志里一直刷"重连中 N/5"、最后走到 WIFI_FAIL_BIT,照这张表从上往下查,绝大多数问题在前两行。和 Arduino 不同的是:ESP-IDF 在 WIFI_EVENT_STA_DISCONNECTED 事件的 data 里带了一个 reason code(断开原因码),把它打出来能精准定位——在回调里 wifi_event_sta_disconnected_t* d = data; ESP_LOGW(TAG, "断开 reason=%d", d->reason); 即可。

现象 / 日志 最可能的原因 怎么办
一直重连,reason=15(4WAY_HANDSHAKE_TIMEOUT)或 reason=2 WiFi 密码打错 逐字核对 WIFI_PASS,注意大小写、中英文标点、首尾空格
一直重连,reason=201(NO_AP_FOUND 扫不到这个 SSID:名字打错,或连的是 5GHz 网 核对 WIFI_SSID;改连 2.4G 那个名字;双频合一的进路由器后台关掉
偶尔连上、偶尔掉,reason 杂 信号弱 / 离路由器太远 把板子凑近路由器试;中间隔太多墙也不行(RSSI 低于 -80dBm 就吃力)
esp_wifi_init 直接 ESP_ERROR_CHECK 没先 nvs_flash_init() 确认 app_main 第一句是 NVS 初始化;遇 NO_FREE_PAGES 按第二步擦了重来
串口全是乱码 monitor 波特率没对上 ESP-IDF 默认 115200,idf.py monitor 一般自动对;自己开串口工具记得选 115200
连一半板子重启,刷 Brownout detector 供电不足(见下方坑) 换好线 / 换供电足的 USB 口
🚧 避坑

SSID 和密码里的中文标点和不可见空格是头号隐形杀手。从聊天软件或网页里复制密码,常常带进一个全角空格或换行,编译进固件里你肉眼看不出来。最稳的办法:手动一个字符一个字符敲进 #define,别复制粘贴。还有一个坑——别把 WiFi 名写成路由器后台显示的"设备名",要写广播出来的那个 SSID,两者可能不一样。

📌 说明

ESP32-S3 连 WiFi 的瞬间射频电流会冲高。如果你用的是电脑某些供电弱的 USB 口、或劣质数据线,可能出现"连接到一半板子重启"的怪象(日志会刷出一段 Brownout detector was triggered,然后整个程序从头跑)。这不是你代码的 bug,是供电扛不住瞬时电流。换一根好线、或插到供电足的 USB 口 / 带独立电源的 HUB 上就好。


进阶:把密码从代码里解放出来(配网)

把 WiFi 名和密码写死在 #define有个大麻烦:换个地方用、或者要送人,就得重新改代码、重新烧录。没人愿意为了换 WiFi 找你重烧固件。

产品里的标准做法是配网(provisioning):设备第一次开机没有 WiFi 信息时,自己开一个热点或开蓝牙广播,让用户用手机连上、在网页或小程序里填好 WiFi 信息,设备把它存进 NVS,下次自动连。ESP-IDF 官方提供了现成的 wifi_provisioning 组件,支持 SoftAP 配网(设备开热点,对应前面讲的 AP 模式)和 BLE 配网(走蓝牙)两种方式;另外还有 SmartConfig(手机 App 把密码"广播"给设备)。

这块展开够单独一篇,这里只指路:要做交付给别人的成品,配网几乎是必选项,具体怎么用 wifi_provisioning 把硬编码密码替换掉,看 SmartConfig 与配网。本篇先用 #define 把"连上"这件事跑通,理解透事件驱动的骨架,配网只是在这套骨架外面再包一层"先从哪儿拿到密码"。


动手挑战

别只看,动手改一个:

  1. event_handlerWIFI_EVENT_STA_DISCONNECTED 分支里,把断开的 reason code 打出来(wifi_event_sta_disconnected_t* d = (wifi_event_sta_disconnected_t*)data; ESP_LOGW(TAG, "断开 reason=%d", d->reason);)。然后故意把密码写错一位,看它打的是不是 15
  2. 连上之后,在 app_main 末尾加一个 while(1) 循环,每 10 秒用 esp_wifi_sta_get_ap_info() 读一次当前 AP 的 RSSI 信号强度并打印(一个负数,越接近 0 越强,-50 很好、-85 很弱)。拿着板子在屋里走,看数字怎么变。

卡住了?把你的代码、idf.py monitor 的完整日志(尤其是 reason code)和想要的效果一起发给 AI,让它帮你定位——描述清楚"我看到了什么、期望看到什么",它给的答案会准得多。

本篇代码为参考实现,需结合你所用的最新 ESP-IDF 文档自校,尤其是 esp_wifi / esp_event 的 API 细节随版本可能微调,以官方文档为准。


小结 · 你现在掌握了什么

  • 你能用一套标准 wifi_init_sta() 骨架,让 ESP32-S3 连上 2.4GHz WiFi 并从日志拿到它的 IP。
  • 你理解了 ESP-IDF 的核心范式:事件循环 + 回调取代了 Arduino 的 while 轮询——连上、断开、拿 IP 都是系统抛事件、在你的 event_handler 里接住,省 CPU、更产品级。
  • 你会用 FreeRTOS 事件组xEventGroupWaitBits)阻塞等"连上或失败",会在 WIFI_EVENT_STA_DISCONNECTED 回调里做断线自动重连。
  • 你知道了为什么连网前必须 nvs_flash_init()、为什么只能连 2.4GHz、Station 和 AP 两种模式的区别,以及怎么从 reason code 和 Brownout 几个方向排查。

连上网只是拿到了"门牌号"。下一步有两条路,按你的需求挑:想让浏览器直接打开设备网页、点按钮控制它,看ESP32 当网页服务器;想让设备把数据"喊"到云上、也能收云端指令,做真正的双向 IoT,看用 MQTT 上云。这两篇都直接复用本篇这套 wifi_init_sta() 骨架——它是你后面所有联网代码的地基。

想看 L3 这一级还有哪些课、整条进阶路线长什么样,回L3 关卡总览完整路线图

📄 来源 / 自校链接

本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为公开资料的学习整理,非亲测。涉接线/花钱/合规的步骤请结合实物与官方最新资料验证,风险自负。见免责声明