← 返回教程库

配网:让用户自己给设备设 WiFi

最后更新 2026-06-22
L3 · 联网与 IoT ⏱ 约 25 分钟 🟢 软件/低风险
你将学到
  • 理解为什么真实产品必须做配网,不能把 WiFi 写死在
  • 用 ESP-IDF 的 esp_smartconfig 实现"手机广播凭据→设备嗅探解码→自动联网"的完整流程
  • 在 SC_EVENT 回调里把收到的 ssid/password 填进 wifi_config_t、发起连接
  • 知道 SmartConfig / SoftAP / BLE 三种配网怎么选,以及连不上时怎么排查

上一节你让 ESP32-S3 连上了 WiFi,名字和密码是写死在 #define WIFI_SSID "你的WiFi名" 里的。这在你自己桌面上跑得很爽,但只要你把这块板子送给别人,或者你自己换了路由器、改了密码,麻烦就来了——你得插上数据线、改两行字符串、重新 idf.py build flash。一台设备这么干还行,十台呢?卖给客户的那台,难道让客户也装一套 ESP-IDF?

这就是配网要解决的问题。配网,就是让用户在不碰代码的情况下,把自己家的 WiFi 账号密码告诉设备。市面上每一个智能插座、智能灯泡、扫地机器人,开箱第一件事都是配网——你用 App 把家里 WiFi 填进去,设备就记住了。这是产品从"我自己玩的东西"变成"能交付的东西"的一道必修关。

读这篇前,请先跑通让 ESP32-S3 连上 WiFi。你需要已经理解那套 wifi_init_sta() 骨架——esp_netif_init / esp_event_loop_create_default / esp_wifi_init、事件回调、FreeRTOS 事件组怎么等。配网本质上就是把"密码从哪来"这件事,从 #define 改成用户输入,WiFi 连接那套骨架完全复用上一节的,本篇只在它外面包一层"先从空中拿到密码"。


配网是什么,三条主流路线

设备出厂时不知道用户家的 WiFi 叫什么、密码是多少。配网就是设备第一次上电(或者连不上旧网络)时,开放一个临时通道让用户输入这些信息,输入完存进 NVS,以后自动连。

ESP-IDF 圈子里主流是三条路,各有取舍:

路线一:SmartConfig(乐鑫叫 ESP-TOUCH)。 设备进入"嗅探"模式(射频混杂模式,把空中所有 WiFi 包都收下来),用户在手机 App 里输入自家 WiFi 密码,App 把密码按特定规则编码进一串 UDP 广播/组播包,设备从空中"听"到这些包、解出 SSID 和密码。好处是用户手机全程不用切换 WiFi,体验顺。坏处也实在:必须配一个专门的 App(乐鑫官方 App 或微信小程序),而且成功率受路由器影响大——有些路由器对广播包做了限制、信号弱时容易失败。它适合已经有 App 生态的成熟产品。本篇正文以 SmartConfig 给完整代码。

路线二:SoftAP 配网。 设备自己开一个 WiFi 热点(AP 模式,就是上一节末尾提的 WIFI_MODE_AP),用户手机连上这个热点,在网页或小程序里选自家 WiFi、填密码、点保存,设备就连上了。好处是不依赖广播包、成功率高、调试直观;代价是用户得手动切一次 WiFi。ESP-IDF 官方的 wifi_provisioning 组件就支持这种方式。

路线三:BLE 配网。 走蓝牙把凭据传给设备,不占 WiFi 信道、抗干扰好,但要搭蓝牙协议栈、App 也更重。wifi_provisioning 同样支持它。

我的建议: 如果你想要最省心、成功率最稳的方案,用官方 wifi_provisioning(SoftAP/BLE),它把存凭据、加密、超时这些都封装好了——这块够单独一篇,上一节让 ESP32-S3 连上 WiFi末尾已经指过路。本篇专讲 SmartConfig,因为它最能体现"无切网"的丝滑体验,而且代码量小、正好复用你刚学的事件驱动骨架,把"配网怎么挂到 WiFi 事件上"这件事讲透。


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

SmartConfig 的逻辑是这样的:照常初始化 WiFi(STA 模式)→ 在 WIFI_EVENT_STA_START 事件里不直接连,而是起一个 smartconfig 任务进入嗅探 → 手机 App 一发凭据,系统抛 SC_EVENT_GOT_SSID_PSWD 事件,你在回调里把收到的 ssid/password 填进 wifi_config_tesp_wifi_connect() → 连上拿到 IP,再 esp_smartconfig_stop() 收尾。

下面这段是完整可烧录的程序,直接放进工程的 main/main.c

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_smartconfig.h"   // SmartConfig 的全部 API 都在这

static const char *TAG = "smartconfig";
static EventGroupHandle_t s_wifi_event_group;
#define CONNECTED_BIT      BIT0   // 拿到 IP
#define ESPTOUCH_DONE_BIT  BIT1   // 配网整套流程结束

// 嗅探任务:进入 SmartConfig,然后等"配网完成"的信号灯再退出
static void smartconfig_task(void *parm) {
    EventBits_t bits;
    ESP_ERROR_CHECK(esp_smartconfig_set_type(SC_TYPE_ESPTOUCH));   // 用 ESP-TOUCH 协议
    smartconfig_start_config_t cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_smartconfig_start(&cfg));                  // 开始嗅探
    while (1) {
        // 阻塞等:连上了 且 收到了 SEND_ACK_DONE,才算彻底完成
        bits = xEventGroupWaitBits(s_wifi_event_group,
                                   CONNECTED_BIT | ESPTOUCH_DONE_BIT,
                                   pdTRUE, pdFALSE, portMAX_DELAY);
        if (bits & CONNECTED_BIT)     ESP_LOGI(TAG, "WiFi 已连上");
        if (bits & ESPTOUCH_DONE_BIT) {
            ESP_LOGI(TAG, "SmartConfig 完成,停止嗅探");
            esp_smartconfig_stop();          // 收尾:关掉混杂模式、释放资源
            vTaskDelete(NULL);               // 任务自己退出
        }
    }
}

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(),而是起嗅探任务等用户发凭据
        xTaskCreate(smartconfig_task, "sc", 4096, NULL, 3, NULL);

    } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
        esp_wifi_connect();                  // 断了重连(凭据已在 wifi_config 里)

    } 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));   // 成功标志
        xEventGroupSetBits(s_wifi_event_group, CONNECTED_BIT);

    } else if (base == SC_EVENT && id == SC_EVENT_SCAN_DONE) {
        ESP_LOGI(TAG, "嗅探中,等手机 App 发凭据…");

    } else if (base == SC_EVENT && id == SC_EVENT_GOT_SSID_PSWD) {
        // 核心:从事件数据里取出手机广播过来的 SSID / 密码
        smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t *) data;
        wifi_config_t wc = { 0 };
        memcpy(wc.sta.ssid, evt->ssid, sizeof(wc.sta.ssid));
        memcpy(wc.sta.password, evt->password, sizeof(wc.sta.password));
        ESP_LOGI(TAG, "收到凭据,SSID=%s,去连接", wc.sta.ssid);
        ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wc));     // 凭据落进 wifi_config
        esp_wifi_connect();                                        // 发起连接

    } else if (base == SC_EVENT && id == SC_EVENT_SEND_ACK_DONE) {
        // 设备已把"我连上了"回执发给手机 App,配网到此结束
        xEventGroupSetBits(s_wifi_event_group, ESPTOUCH_DONE_BIT);
    }
}

static void wifi_init_sta_smartconfig(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));
    // 比上一节多注册一类事件:SC_EVENT(SmartConfig 自己的事件源)
    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));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(SC_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL));
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());      // 注意:这里没 set_config,凭据等用户发
}

void app_main(void) {
    ESP_ERROR_CHECK(nvs_flash_init());      // WiFi 射频校准数据存 NVS,连网前必做
    wifi_init_sta_smartconfig();
}

在工程目录下编译、烧录、看日志:

idf.py build flash monitor

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

你应该看到什么

光烧固件还不够,SmartConfig 必须手机端配合——你得装乐鑫官方的 ESPTOUCH App(iOS / 安卓应用商店搜 "EspTouch",微信里也有对应小程序)。把手机连上你家 2.4GHz WiFi,打开 App,它会自动带出当前 WiFi 名,你填上密码,点"确认/开始配网"。

正常流程下,idf.py monitor 的日志会按顺序滚出:

I (1500) smartconfig: 嗅探中,等手机 App 发凭据…
I (8200) smartconfig: 收到凭据,SSID=MyWiFi,去连接
I (9100) smartconfig: 拿到 IP: 192.168.1.123
I (9300) smartconfig: WiFi 已连上
I (9400) smartconfig: SmartConfig 完成,停止嗅探
  • 一上电,WIFI_EVENT_STA_START 触发,起了嗅探任务,esp_smartconfig_start 让设备进入混杂模式听空中的包——这时它在"等手机说话"。
  • 你在 ESPTOUCH App 点配网后,设备从广播包里解出 SSID 和密码,抛 SC_EVENT_GOT_SSID_PSWD,回调里把凭据填进 wifi_config_t 并发起连接。
  • 路由器分了 IP,IP_EVENT_STA_GOT_IP 打出 拿到 IP: 192.168.x.x——这串数字就是成功的标志
  • 设备把回执发回手机(SC_EVENT_SEND_ACK_DONE),点亮 ESPTOUCH_DONE_BIT,嗅探任务 esp_smartconfig_stop() 收尾、自删。手机 App 这时也会显示"配网成功"。

看到串口打出你家网段的 IP、手机 App 显示成功,就成了。

💡 提示

这版代码为了聚焦主线,没把凭据存进 NVS——一断电就忘了,下次开机又得重配。真做产品时,esp_wifi_set_config 设的凭据其实已经被 WiFi 驱动写进 NVS 了(前提是 esp_wifi_set_storage 用默认的 WIFI_STORAGE_FLASH),下次开机可以先尝试用存的凭据直连、连不上再进 SmartConfig。这一层"先试旧的、不行再配网"的判断,留作下面的动手挑战。


第二步:把和上一节的差异讲透

这段代码看着比上一节长,但骨架是同一套——esp_netif_init / esp_event_loop_create_default / esp_wifi_init / WIFI_MODE_STA / esp_wifi_start 一字没变。真正的差异只有三处,吃透这三处就懂了 SmartConfig 是怎么"挂"到你已有的事件驱动框架上的。

差异一:STA_START 里不直接连,而是起嗅探

上一节 WIFI_EVENT_STA_START 来了直接 esp_wifi_connect(),因为密码写死在 #define 里、esp_wifi_set_config 早就设好了。但 SmartConfig 一开始根本没有密码——密码要等用户从手机发过来。所以这里把"连接"延后:先起一个 smartconfig_task 进入嗅探,等真拿到凭据了再连。wifi_init_sta_smartconfig() 里也没有 esp_wifi_set_config,就是这个道理。

差异二:多了一个事件源 SC_EVENT

上一节你只注册了 WIFI_EVENTIP_EVENT 两个事件源。SmartConfig 自己是第三个事件源 SC_EVENT,所以代码里多了一行注册:

esp_event_handler_instance_register(SC_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL);

注册完,SmartConfig 过程中的几个关键节点就会像 WiFi 事件一样投递到你同一个 event_handler 里。重点接住两个:SC_EVENT_GOT_SSID_PSWD(收到凭据)和 SC_EVENT_SEND_ACK_DONE(配网收尾)。这正是上一节讲的"事件循环 + 回调"范式的复用——SmartConfig 没有发明新机制,它只是往你已有的事件循环里多塞了一类事件。

差异三:核心动作——把广播来的凭据填进 wifi_config_t

整篇最关键的就是 SC_EVENT_GOT_SSID_PSWD 这个分支:

smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t *) data;
wifi_config_t wc = { 0 };
memcpy(wc.sta.ssid,     evt->ssid,     sizeof(wc.sta.ssid));
memcpy(wc.sta.password, evt->password, sizeof(wc.sta.password));
esp_wifi_set_config(WIFI_IF_STA, &wc);
esp_wifi_connect();

手机广播过来的 SSID 和密码,被系统解码后放进了事件数据 smartconfig_event_got_ssid_pswd_t 这个结构体的 ssid / password 字段里。你要做的就是把它们 memcpy 进一个 wifi_config_t,然后走和上一节完全一样的两步——esp_wifi_set_config 把凭据交给 WiFi 驱动、esp_wifi_connect 发起连接。从这一行往后,连接、拿 IP、断线重连的逻辑就和上一节一模一样了。SmartConfig 的全部价值,就是替你把这两个字符串从空中"捡"回来,省掉了 #define

嗅探为什么能"听"到密码——原理一层

你可能会问:手机和 ESP32-S3 还没连上同一个网,密码是怎么传过去的?答案藏在 ESP-TOUCH 的设计里:手机 App 把密码编码进一连串 UDP 广播/组播包的"长度字段"里(包的内容是加密的看不懂,但每个包有多长是公开信息),按特定规律连续发出。ESP32-S3 这边开了混杂模式(promiscuous mode,把空中所有 802.11 包不管是不是发给自己的都收下来),它不关心包里装了啥,只数每个包的长度,按 App 约定的规律把这串长度"翻译"回原始的 SSID 和密码。

这就是为什么 SmartConfig 不需要先连网就能传密码——它走的是所有人都能收到的广播信道,靠包长度这个"边信道"来夹带信息。也正因为如此,它对路由器的广播策略很敏感:路由器要是限制了组播/广播转发,或者信号弱导致丢包,这串"长度密码"就拼不全,配网就失败。这是 SmartConfig 成功率不如 SoftAP 稳的根因。


三种配网怎么选

方式 用户操作 依赖 成功率 适合
SmartConfig(本篇) 装 App、填密码、点配网,不用切 WiFi 专门 App / 微信小程序;混杂模式 中(受路由器广播策略影响) 已有 App 生态、追求"无切网"丝滑体验
SoftAPwifi_provisioning 切到设备热点、网页/小程序填密码 AP 模式;网页或配网 SDK 高(不靠广播包) 入门、中小项目、要稳的场合
BLEwifi_provisioning App 走蓝牙配 蓝牙协议栈;更重的 App 抗 WiFi 干扰、已有蓝牙的产品

一句话决策:想稳、想省事,用官方 wifi_provisioning 的 SoftAP(上一节让 ESP32-S3 连上 WiFi末尾指过路);想要手机不切网的丝滑体验、且能接受装个 App,用本篇的 SmartConfig;做了蓝牙的产品顺手用 BLE 配网。三者底层都还是把凭据填进 wifi_config_t 那一套,区别只在"凭据从哪个通道进来"。


故障排查表

SmartConfig 的坑大半在"环境"而非代码,按这张表从上往下查:

现象 大概率原因 怎么办
App 一直转圈、设备日志停在"嗅探中" 手机连的是 5GHz SmartConfig 必须手机和设备都在 2.4GHz,把手机切到 2.4G 那个 SSID 再配
配网偶尔成功、偶尔超时 路由器限制了组播/广播转发,或信号弱丢包 离路由器近点;进路由器后台关掉"组播过滤""IGMP Snooping"等限制;多试几次
日志收到凭据但连不上、刷 STA_DISCONNECTED 密码解错(信号差时长度被丢包打乱)或确实密码错 凑近路由器重配;在 DISCONNECTED 分支把 reason code 打出来(见上一节)核实
esp_wifi_init 直接崩 没先 nvs_flash_init() 确认 app_main 第一句是 NVS 初始化
App 连不上设备 / 找不到设备 手机和设备不在同一个 2.4G 网,或 App 权限没给 确认手机连的就是要配的那个 2.4G WiFi;安卓给 App 定位权限(扫 WiFi 需要)
配过一次,重启后又要重配 本篇代码没做"先试旧凭据"逻辑 见下方动手挑战,开机先尝试用 NVS 里的凭据直连
🚧 避坑

SmartConfig 最大的隐形门槛是 2.4GHz:ESP32-S3 射频只支持 2.4G,而且嗅探也只在 2.4G 信道上进行。只要你手机连的是 5GHz(现在很多手机在双频合一的网里默认挑 5G),设备就永远"听"不到那串广播包,表现就是 App 一直转圈、设备一直嗅探不动。配网前务必确认手机当前连的就是 2.4G 那个网


变体:超时与回退

本篇主线一直在 portMAX_DELAY 无限等用户配网,生产里得加超时,免得设备永远卡在嗅探:

// 在 smartconfig_task 里把 portMAX_DELAY 换成有限超时,例如 60 秒
bits = xEventGroupWaitBits(s_wifi_event_group,
                           CONNECTED_BIT | ESPTOUCH_DONE_BIT,
                           pdTRUE, pdFALSE, pdMS_TO_TICKS(60 * 1000));
if (bits == 0) {                       // 超时,一个 bit 都没亮
    ESP_LOGW(TAG, "配网超时,停止嗅探");
    esp_smartconfig_stop();
    // 这里可选择:重启重来、或回退到 SoftAP 配网、或点个红灯提示用户
    esp_restart();
}

成熟产品常做双通道兜底:先开 SmartConfig 等 60 秒,用户没配上就自动切到 SoftAP 配网(开个热点让用户连),覆盖"手机没装 App"或"路由器广播被限"的情况。这正好把上面那张三选一的表用起来——两条路并联,哪条通走哪条。


动手挑战

把本篇代码改成真正能交付的样子:

  1. 先试旧凭据再配网:在 app_mainesp_wifi_start() 之后,先用 esp_wifi_get_config(WIFI_IF_STA, &wc) 读 NVS 里有没有存过 SSID,有就直接 esp_wifi_connect()、跳过 SmartConfig;只有读到空 SSID(或连了几次失败)才起嗅探任务。验证:配网成功后断电重启,确认它直接连上、不再要你掏手机配
  2. 加配网超时:用上面变体里的 60 秒超时替换 portMAX_DELAY,故意不去 App 点配网,看它 60 秒后是不是按你写的打日志、然后重启或回退。
  3. 打 reason code:在 WIFI_EVENT_STA_DISCONNECTED 分支里把断开原因码打出来(写法见上一节),故意在 App 里填错一位密码,看设备收到凭据后连不上时报的是不是 15(握手超时)。
  4. 进阶:把 SC_TYPE_ESPTOUCH 换成 SC_TYPE_ESPTOUCH_V2,对照乐鑫文档看 V2 多了什么(更快、支持自定义数据),用 App 的 V2 模式重配一次。

跑通第 1 条,你的设备就真正"配一次、以后自动连"了——这是它具备产品雏形的标志。

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


小结 · 你现在掌握了什么

  • 你能用 ESP-IDF 的 esp_smartconfig 做一键配网:esp_smartconfig_set_type 选协议、esp_smartconfig_start 进嗅探、在 SC_EVENT_GOT_SSID_PSWD 回调里把手机广播来的凭据填进 wifi_config_tesp_wifi_connectSC_EVENT_SEND_ACK_DONEesp_smartconfig_stop 收尾。
  • 你看清了它和上一节的差异只有三处:STA_START 里不直接连而是起嗅探、多注册一个 SC_EVENT 事件源、核心是把广播来的 ssid/password 落进 wifi_config_t——WiFi 连接骨架完全复用 wifi_init_sta()
  • 你理解了 SmartConfig 的原理:手机把密码编码进广播包长度、设备开混杂模式嗅探解码,所以它不用先连网就能传密码,但也因此对路由器广播策略和 2.4GHz 很敏感。
  • 你知道了三种配网(SmartConfig / SoftAP / BLE)怎么选,以及连不上时从 2.4GHz、广播限制、密码这几个方向排查。

配网搞定,设备就跨过了从"自己玩"到"能交付"的那道坎。设备稳定联网之后,下一个常见需求是让它知道现在几点——上报数据要带时间戳、定时任务要按点触发。继续看用 NTP 给设备同步时间

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

📄 来源 / 自校链接

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

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

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