【ESP32笔记】SmartConfig一键配网

本节目标:搞懂"手机 App 如何把 WiFi 密码告诉一台还没联网的设备",并会用按键长按触发 SmartConfig,把拿到的 SSID/密码存进 NVS

一、本节要解决的问题

上一节把 SSID 和密码写死在代码里 ,这只是学习阶段的权宜之计。真实产品出厂时不可能知道用户家的 WiFi,而且大多数 IOT 设备------插座、传感器、灯------没有屏幕、没有键盘,用户没法"输入"。

把 SSID 和密码送进设备、让它连上热点接入网络,这个过程叫 配网。常见的配网方式:

方式 做法 特点
AP 配网 设备先开热点,手机连上设备的 AP,打开网页填写 WiFi 信息 稳定但步骤多
蓝牙配网 手机通过 BLE 把凭据发给设备 需要 App 支持,交互体验好
SmartConfig 手机 App 把密码"广播"出去,设备在混杂模式下嗅探 用户操作最简单,只需在 App 输密码

本节讲 SmartConfig:用户只要在手机上输入 WiFi 密码、点一下确定,设备就能自己"听"到密码并联网。

二、硬件基础

本节的主角是板载 WiFi,外部只用到一个按键 (长按 3 秒触发配网):按键未按下时 GPIO 被拉高读到 1,按下时 GPIO 短接到地读到 0,所以按键配置里 active_level = 0 表示"低电平为按下"。

注意 :仓库 main.c 里按键用的是 GPIO_NUM_39,在 ESP32 上 GPIO34~39 是输入专用引脚,芯片内部没有上拉/下拉电阻 。button.c 里虽然写了 pull_up_en = GPIO_PULLUP_ENABLE,对 GPIO39 实际不起作用,真正把它拉高的是板上的外部上拉电阻。具体接线以自己开发板原理图为准。

另外还需要:一台 2.4GHz 路由器(ESP32 连不上 5GHz)、一台装了 EspTouch App 的手机(乐鑫官方提供,安卓/iOS 都有,且安卓版开源),配网时手机必须先连上目标 WiFi。

三、知识点

3.1 核心矛盾:密码怎么"隔空"传进去?

配网的本质难点是:设备还没连上路由器,手机和设备之间没有任何现成的通道。

那能不能让手机直接发一个包含密码的 UDP 广播包,设备收到就解析?不行 。因为在 802.11 协议里,MAC 帧的数据域(MSDU)是加密的,没连上 WiFi 的设备只能拿到密文,读不出内容。

SmartConfig 的巧妙之处在于:内容读不到,但长度是明文可读的------于是就用"包的长度"来编码信息。

3.2 SmartConfig 原理:长度编码

设一个 UDP 广播包(802.11 里是 MAC 帧,数据域是 IP 报文)由三部分组成:

复制代码
IP 报文长度 = 20 字节 IPv4 头 + 8 字节 UDP 头 + UDP 内容

定义 明文长度 = IP 报文长度 + 算法常量(算法常量由 App 和芯片厂商约定,固定值)。

教程的例子:假如算法常量是 10,App 想传一个数值 1234,只需发一个 IP 报文总长 = 1234 − 10 = 1224 字节 的 UDP 广播包(内容随便填)。设备收到后:1224 + 10 = 1234,数据就还原出来了。同理,算法常量是 16 时,要传字符 't',就发一个长度为 't' − 16 的包。

一次完整的字符传输流程(教程原例,发 "test",算法常量 16):

  1. App 连续发 3 个 UDP 广播包,内容都是前导码;
  2. 发 1 个包,长度对应字符 t;
  3. 发 1 个包,长度对应字符 e;
  4. 发 1 个包,长度对应字符 s;
  5. 发 1 个包,长度对应字符 t;
  6. 切换 WiFi 信道,重复以上步骤。

两个配角也很关键:

  • 前导码:几个特殊字节组成的固定包,设备用它来区分"这是 SmartConfig 的包"而不是普通广播包;
  • 混杂模式 + 信道轮询 :设备没连任何 AP 时,WiFi 芯片处于混杂(promiscuous)模式,快速在各条信道上嗅探;一旦遇到正在发前导码的信道,就锁定该信道继续接收,攒够数据后解码出密码,再用它去连路由器。

每家厂商的算法常量、前导码、数据格式都不一样,所以不同厂家的 SmartConfig 互不通用。ESP32 只能用支持 ESP-TOUCH 协议的 App(如乐鑫 EspTouch)来配网。

3.3 ESP32 里的 SmartConfig 事件流程

ESP32 上的 SmartConfig 实现看不到源码(乐鑫闭源),但用起来很简单,整个过程也是一串事件:

事件 含义
SC_EVENT_SCAN_DONE 设备扫描环境中的 AP 完毕
SC_EVENT_FOUND_CHANNEL 找到了手机正在发送数据的那条信道
SC_EVENT_GOT_SSID_PSWD 成功解码出 SSID 和密码,可以拿去连接路由器了
SC_EVENT_SEND_ACK_DONE 设备已向手机回复 ACK,配网可以结束

可以配的协议版本有 SC_TYPE_ESPTOUCH(V1)、SC_TYPE_AIRKISS(微信用的)、SC_TYPE_ESPTOUCH_AIRKISS、SC_TYPE_ESPTOUCH_V2。版本之间不兼容:App 选 V1、设备设成 V2 就永远配不上,两边必须一致。

整个流程串起来:esp_smartconfig_start() → 扫描完成 → 锁定信道 → 拿到 SSID/密码 → 用 wifi_config_t + esp_wifi_set_config() 去连路由器(和上一节完全一样)→ 回 ACK → 任务收到完成标志后 esp_smartconfig_stop() 收尾。

四、代码分析

4.1 工程结构

复制代码
smartconfig/
├── CMakeLists.txt
├── sdkconfig
└── main/
    ├── CMakeLists.txt      # SRCS "wifi_smartconfig.c" "main.c" "button.c"
    ├── main.c              # app_main():NVS、WiFi 初始化,注册按键,等长按事件
    ├── wifi_smartconfig.c  # WiFi 初始化 + SmartConfig 任务 + 事件回调 + NVS 读写
    ├── wifi_smartconfig.h
    ├── button.c            # 通用按键驱动:状态机 + 5ms 定时器轮询
    └── button.h            # button_config_t 与 button_event_set()

4.2 按键驱动:状态机 + 定时器

button.c 用一个 5ms 周期的 esp_timer 轮询按键电平,状态机一共四态:

c 复制代码
typedef enum
{
    BUTTON_RELEASE,             //按键没有按下
    BUTTON_PRESS,               //按键按下了,等待一点延时(消抖),然后触发短按回调事件,进入BUTTON_HOLD
    BUTTON_HOLD,                //按住状态,如果时间长度超过设定的超时计数,将触发长按回调函数,进入BUTTON_LONG_PRESS_HOLD
    BUTTON_LONG_PRESS_HOLD,     //此状态等待电平消失,回到BUTTON_RELEASE状态
}BUTTON_STATE;

工作方式(increase_cnt 每次为 5,代表 5ms):

  • 电平等于 active_level 开始计时,累计到 FILITER_TIMER(20,即 20ms)才算真的按下 ,这就完成了消抖,并触发 short_cb;
  • 继续按住,计时超过 long_press_time(结构体里给的 3000,单位 ms)就触发 long_cb,之后必须松开 才回到 BUTTON_RELEASE,避免一直重复触发;
  • 定时器创建时 dispatch_method = ESP_TIMER_TASK,回调运行在 esp_timer 任务里(不是中断),所以代码可以直接用 xEventGroupSetBits() 而不需要 FromISR 版本。

按键配置结构体一目了然:

c 复制代码
typedef struct
{
    int gpio_num;           //gpio号
    int active_level;       //按下的电平
    int long_press_time;    //长按时间
    button_press_cb short_cb;   //短按回调函数
    button_press_cb long_cb;    //长按回调函数
}button_config_t;

4.3 app_main:长按 3 秒 → 启动配网

c 复制代码
#define BTN_GPIO    GPIO_NUM_39

void app_main(void)
{
    nvs_flash_init();           //初始化NVS
    initialise_wifi();          //初始化wifi

    s_pressEvent = xEventGroupCreate();
    button_config_t btn_cfg = 
    {
        .gpio_num = BTN_GPIO,       //gpio号
        .active_level = 0,          //按下的电平
        .long_press_time = 3000,    //长按时间
        .short_cb = NULL,           //短按回调函数
        .long_cb = smartconfig_start             //长按回调函数
    };
    button_event_set(&btn_cfg);     //添加按键响应事件处理

长按回调直接绑定 smartconfig_start,所以用户长按 3 秒松手前就已经开始配网了。

4.4 initialise_wifi:STA 基础 + 从 NVS 取上次的凭据

initialise_wifi() 就是上一节 wifi_sta_init() 的翻版,只是多注册了一类事件、多了一段 NVS 读取:

c 复制代码
void initialise_wifi(void)
{
    ESP_ERROR_CHECK(esp_netif_init());
    s_wifi_event_group = xEventGroupCreate();
    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_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL) );
    ESP_ERROR_CHECK( esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL) );
    ESP_ERROR_CHECK( esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL) );

    ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) );
    ...

前几行和上一节的 STA 初始化一样(多了 xEventGroupCreate()),关键差别是第三个注册:SC_EVENT(SmartConfig 事件)也交给同一个 event_handler 统一处理,这样配网过程中的状态变化和三方事件就都在一个回调里分流了。

读 NVS 的两个函数用的是命名空间 "DEV_WIFI"、键名 "ssid" / "password",读成功后填进 s_ssid_value[33] 和 s_password_value[65] 两个缓存:

c 复制代码
    //从NVS中读出SSID
    read_nvs_ssid(s_ssid_value,32);

    //从NVS中读取PASSWORD
    read_nvs_password(s_password_value,64);

4.5 smartconfig_start 与配网任务

c 复制代码
void smartconfig_start(void)
{
    if(!s_is_smartconfig)
    {
        s_is_smartconfig = true;
        esp_wifi_disconnect();
        xTaskCreate(smartconfig_example_task, "smartconfig_example_task", 4096, NULL, 3, NULL);
    }
}
  • s_is_smartconfig 是运行标志,防止重复启动 SmartConfig;
  • 先 esp_wifi_disconnect() 断开现有连接,让 WiFi 芯片腾出手去"嗅探";
  • 单独开一个 4096 字节栈的任务做配网(配网要保存密码、发 ACK,栈别给太小)。
c 复制代码
static void smartconfig_example_task(void * parm)
{
    EventBits_t uxBits;
    ESP_ERROR_CHECK( esp_smartconfig_set_type(SC_TYPE_ESPTOUCH_V2) );           //设定SmartConfig版本
    smartconfig_start_config_t cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
    ESP_ERROR_CHECK( esp_smartconfig_start(&cfg) ); //启动SmartConfig
    while (1) {
        uxBits = xEventGroupWaitBits(s_wifi_event_group, CONNECTED_BIT | ESPTOUCH_DONE_BIT, true, false, portMAX_DELAY);
        if(uxBits & CONNECTED_BIT) {
            ESP_LOGI(TAG, "WiFi Connected to ap");
        }
        if(uxBits & ESPTOUCH_DONE_BIT) {    //收到smartconfig配网完成通知
            ESP_LOGI(TAG, "smartconfig over");
            esp_smartconfig_stop();         //停止smartconfig配网
            write_nvs_ssid(s_ssid_value);   //将ssid写入NVS
            write_nvs_password(s_password_value);   //将password写入NVS
            s_is_smartconfig = false;       
            vTaskDelete(NULL);              //退出任务
        }
    }
}

三个要点:

  • esp_smartconfig_set_type() 必须在 esp_smartconfig_start() 之前调用,这里选 V2,所以手机 App 上也要选 V2;
  • SMARTCONFIG_START_CONFIG_DEFAULT() 是默认启动配置(不打日志、不加密);
  • 任务同时等两个事件位:CONNECTED_BIT(拿到 IP)和 ESPTOUCH_DONE_BIT(配网完成)。xEventGroupWaitBits 的 pdTRUE, pdFALSE 表示任一位满足即返回,并把置上的位清掉。

4.6 事件回调:四类事件汇于一处

回调里同时处理 WIFI、IP、SmartConfig 三类事件,最核心的两个分支:

c 复制代码
    } else if (event_base == SC_EVENT && event_id == SC_EVENT_GOT_SSID_PSWD) {
        //smartconfig 获取到SSID和密码
        ESP_LOGI(TAG, "Got SSID and password");

        smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t *)event_data;
        wifi_config_t wifi_config;
        uint8_t ssid[33] = { 0 };
        uint8_t password[65] = { 0 };
        //从event_data中提取SSID和密码
        bzero(&wifi_config, sizeof(wifi_config_t));
        memcpy(wifi_config.sta.ssid, evt->ssid, sizeof(wifi_config.sta.ssid));
        memcpy(wifi_config.sta.password, evt->password, sizeof(wifi_config.sta.password));
        ...
        snprintf(s_ssid_value,33,"%s",(char*)ssid);
        snprintf(s_password_value,65,"%s",(char*)password);
        //重新连接WIFI
        ESP_ERROR_CHECK( esp_wifi_disconnect() );
        ESP_ERROR_CHECK( esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
        esp_wifi_connect();
    } else if (event_base == SC_EVENT && event_id == SC_EVENT_SEND_ACK_DONE) {
        //smartconfig 已发起回应
        xEventGroupSetBits(s_wifi_event_group, ESPTOUCH_DONE_BIT);
    }
  • SC_EVENT_GOT_SSID_PSWD 的 event_data 是 smartconfig_event_got_ssid_pswd_t *,里面装着解码出来的 ssid[32]、password[64](还有 bssid、token 等)。把密码拷进 wifi_config_t,set_config 后 esp_wifi_connect()------后面的路和上一节 STA 完全一致,这就是配网与联网的衔接点;
  • 回调里同时把凭据缓存到 s_ssid_value / s_password_value,供后面的任务写 NVS;
  • SC_EVENT_SEND_ACK_DONE 表示设备已回 ACK 给手机,置位 ESPTOUCH_DONE_BIT 通知配网任务收尾。

另外两个分支也值得留意:

c 复制代码
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        if(s_ssid_value[0] != 0)
            esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        //WIFI断开连接后,再次发起连接
        if(!s_is_smartconfig)
            esp_wifi_connect();
        //清除连接标志位
        xEventGroupClearBits(s_wifi_event_group, CONNECTED_BIT);

断开重连时加了 if(!s_is_smartconfig) 判断 :配网过程中 WiFi 本来就会被反复断开重连,如果这时还去 esp_wifi_connect(),会和嗅探打架,所以配网期间不重连。这个细节比上一节的无脑重连更贴近实用。

五、关键 API 速查

API 作用
esp_smartconfig_set_type(SC_TYPE_ESPTOUCH_V2) 选择配网协议版本,必须在 start 之前调用
esp_smartconfig_start(&cfg) 启动 SmartConfig,开始嗅探广播包
esp_smartconfig_stop() 停止配网并释放资源(无论成功与否都应调用)
SMARTCONFIG_START_CONFIG_DEFAULT() 生成默认配网启动配置
esp_event_handler_register(SC_EVENT, ...) 注册 SmartConfig 事件回调
esp_wifi_disconnect() 配网前断开现有连接
esp_wifi_set_config(WIFI_IF_STA, &cfg) 把解码出的 SSID/密码写入驱动
button_event_set(&cfg) 注册按键 GPIO 及长/短按回调
xEventGroupCreate() / xEventGroupWaitBits() / xEventGroupSetBits() / xEventGroupClearBits() 事件组的创建、等待、置位、清除
nvs_open() / nvs_set_str() / nvs_get_str() / nvs_commit() / nvs_close() NVS 的打开、写、读、提交、关闭
esp_timer_create() / esp_timer_start_periodic() 创建并启动周期定时器(按键轮询用)

六、编译与现象验证

bash 复制代码
cd esp32-board/smartconfig
idf.py build                      # sdkconfig 已指定 CONFIG_IDF_TARGET="esp32"
idf.py -p COM<端口> flash monitor

操作步骤:

  1. 手机连上家里的 2.4GHz WiFi;

  2. 打开 EspTouch App,协议版本选 V2 (与代码里的 SC_TYPE_ESPTOUCH_V2 对应),输入 WiFi 密码,点击确定(此时 App 开始反复广播);

  3. 开发板长按按键 3 秒 ,串口开始打印 Scan done、Found channel 即可松手;

  4. 稍等片刻完成配网,串口大致打印:

    I (xxx) smartconfig_example: Scan done
    I (xxx) smartconfig_example: Found channel
    I (xxx) smartconfig_example: Got SSID and password
    I (xxx) smartconfig_example: SSID:xxxx
    I (xxx) smartconfig_example: PASSWORD:xxxxxxxx
    I (xxx) smartconfig_example: WiFi Connected to ap
    I (xxx) smartconfig_example: smartconfig over

如果一直停在 Scan done/Found channel 不进 Got SSID and password:检查 App 与设备的协议版本是否一致、手机是否连着 2.4G WiFi、密码是否正确。

相关推荐
霖霖7142 小时前
【ESP32笔记】WIFI连接—STA模式
笔记·单片机·mcu·物联网
HRTOS2 小时前
HRTOS 4.0 实测:时间片模式与调度位模式的任务切换性能对比
经验分享·单片机·嵌入式硬件·51单片机
学电子她就能回来吗2 小时前
LeetCode Hot 100|滑动窗口专题零基础学习笔记
笔记·学习·leetcode
我先去打把游戏先2 小时前
Git-Rebase使用指南-从原理到实战
服务器·git·嵌入式硬件·物联网·ubuntu
一个风轻云淡3 小时前
扒开系统调用的三层皮:从 getpid 到手写 int 0x80
stm32·单片机·嵌入式硬件
Xudde.3 小时前
DC-1 靶机渗透测试(Vulnhub Writeup)
笔记·学习·安全·web安全
海绵宝宝转agent4 小时前
2026-10-14算法+面经总结
java·笔记·算法·缓存
HRTOS4 小时前
HRTOS 4.0 实测:8051 中断响应延迟与中断嵌套性能测试
单片机·嵌入式硬件·51单片机
skywalk81634 小时前
Monty / 千脑理论(Thousand Brains Project)学习笔记
笔记·学习·monty·千脑理论