ESP32S3开发笔记:实现WiFi扫描与连接功能

一、项目概述

本文将详细讲解如何在ESP32S3开发板上实现WiFi扫描和连接功能。我们将使用LVGL图形库创建一个友好的用户界面,实现:

  • 扫描并显示周围WiFi名称
  • 点击WiFi名称后进入密码输入界面
  • 输入密码连接WiFi

整个项目基于ESP-IDF框架开发,是一个非常实用的应用案例。无论你是嵌入式开发新手还是有经验的工程师,都能从中学到ESP32的WiFi功能实现方法。

二、硬件与软件准备

硬件需求

  • ESP32S3开发板
  • USB数据线
  • 带显示屏的开发板(本例中使用320x240液晶屏)

软件环境

  • ESP-IDF开发环境(本例使用v5.4.2)
  • VSCode编辑器
  • ESP-IDF VSCode插件

三、项目结构解析

与普通ESP-IDF项目相比,本项目有几个特殊之处:

  1. 自定义分区表:使用了自定义的分区表文件partitions.csv,为存储中文字库提供更大空间
  2. 中文字库文件:添加了中文字体支持,以便正确显示中文WiFi名称
  3. LVGL界面:使用LVGL图形库创建用户界面

3.1 项目目录结构

项目根目录
├── main
│   ├── app_ui.c            // 应用程序UI相关代码
│   ├── app_ui.h            // UI头文件
│   ├── esp32_s3_szp.c      // 开发板BSP文件
│   ├── font_alipuhui20.c   // 中文字库文件
│   ├── main.c              // 主程序入口
│   └── CMakeLists.txt      // 编译配置文件
├── partitions.csv          // 自定义分区表
└── CMakeLists.txt          // 主CMakeLists文件

四、分区表详解

4.1 为什么需要自定义分区表?

在ESP32开发中,分区表负责为Flash划分不同的区域,存放不同类型的文件。默认分区表为应用程序分配的空间是1MB,但由于我们的项目包含中文字库,体积较大(约3MB),因此需要自定义分区表,增加应用程序的存储空间。

4.2 分区表内容

# Name,   Type, SubType, Offset,  Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     ,        0x1000,
factory,  app,  factory, ,        7M,

与默认分区表相比,我们将factory分区的大小从1MB增加到了7MB,这样就能容纳我们的应用程序和中文字库。

4.3 配置使用自定义分区表

在menuconfig中,需要选择Custom partition table CSV选项:

  1. 在VSCode中打开终端
  2. 运行idf.py menuconfig
  3. 进入Partition Table选项
  4. 选择Custom partition table CSV
  5. 设置分区表文件名为partitions.csv

五、中文字库制作

5.1 为什么需要中文字库?

ESP32默认不支持中文显示。由于WiFi名称可能包含中文字符,我们需要添加中文字体支持。本项目使用阿里普惠字体,它是免费可商用的。

5.2 字库制作步骤

1. 获取字体文件
2. 使用LVGL在线工具制作字库

访问LVGL字体转换工具,按以下步骤设置:

  1. 基本设置

    • Name: font_alipuhui20(自定义名称)
    • Size: 20(像素大小)
    • Bpp: 4(每个像素的位数,影响显示效果和文件大小)
  2. 添加中文字体

    • 选择阿里普惠字体文件
    • Range: 0x20-0x2FA1F(包含所有中文字符的Unicode范围)
  3. 添加WiFi图标字体

    • 点击"Include another font"
    • 选择fa-soild-900.ttf文件
    • Range: 0xf1eb(WiFi图标的Unicode码)
  4. 点击"Submit",等待生成字体文件

注意:生成过程可能需要10分钟左右,如果浏览器提示"页面无响应",选择"等待"即可。

5.3 在项目中使用字库

  1. 将生成的font_alipuhui20.c文件复制到main文件夹

  2. 修改文件开头的包含头文件部分:

    // 修改前
    #ifdef LV_LVGL_H_INCLUDE_SIMPLE
    #include "lvgl.h"
    #else
    #include "lvgl/lvgl.h"
    #endif
    
    // 修改后
    #include "lvgl.h"
    
  3. 在CMakeLists.txt中添加字体文件:

    idf_component_register(SRCS "font_alipuhui20.c" "app_ui.c" "esp32_s3_szp.c" "main.c"
                        INCLUDE_DIRS ".")
    
  4. 在使用字体的文件中声明:

    LV_FONT_DECLARE(font_alipuhui20);
    
  5. 使用字体:

    lv_obj_set_style_text_font(label, &font_alipuhui20, 0);
    

六、应用程序设计

6.1 主程序流程

主程序入口app_main()函数的代码如下:

void app_main(void)
{
    // 初始化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 );

    bsp_i2c_init();       // I2C初始化
    pca9557_init();       // IO扩展芯片初始化
    bsp_lvgl_start();     // 初始化液晶屏lvgl接口
    app_wifi_connect();   // 运行wifi连接程序
}

程序执行流程:

  1. 初始化NVS(非易失性存储,用于存储WiFi配置)
  2. 初始化I2C总线
  3. 初始化IO扩展芯片
  4. 初始化LVGL液晶屏接口
  5. 运行WiFi连接应用

6.2 WiFi扫描与连接界面

app_wifi_connect()函数是整个应用的核心,它完成了以下几个主要功能:

void app_wifi_connect(void)
{
    lvgl_port_lock(0);
    // 创建WLAN扫描页面
    static lv_style_t style;
    lv_style_init(&style);
    lv_style_set_bg_opa( &style, LV_OPA_COVER );   // 背景透明度
    lv_style_set_border_width(&style, 0);          // 边框宽度
    lv_style_set_pad_all(&style, 0);               // 内间距
    lv_style_set_radius(&style, 0);                // 圆角半径
    lv_style_set_width(&style, 320);               // 宽
    lv_style_set_height(&style, 240);              // 高
    wifi_scan_page = lv_obj_create(lv_scr_act());
    lv_obj_add_style(wifi_scan_page, &style, 0);
    // 在WLAN扫描页面显示提示
    lv_obj_t *label_wifi_scan = lv_label_create(wifi_scan_page);
    lv_label_set_text(label_wifi_scan, "WLAN扫描中...");
    lv_obj_set_style_text_font(label_wifi_scan, &font_alipuhui20, 0);
    lv_obj_align(label_wifi_scan, LV_ALIGN_CENTER, 0, -50);
    lvgl_port_unlock();

    // 扫描WLAN信息
    wifi_ap_record_t ap_info[DEFAULT_SCAN_LIST_SIZE];  // 记录扫描到的wifi信息
    uint16_t ap_number = DEFAULT_SCAN_LIST_SIZE;
    wifi_scan(ap_info, &ap_number);                    // 扫描附近wifi

    lvgl_port_lock(0);
    // 扫描附近wifi信息成功后 删除提示文字
    lv_obj_del(label_wifi_scan);
    // 创建wifi信息列表
    wifi_list = lv_list_create(wifi_scan_page);
    lv_obj_set_size(wifi_list, lv_pct(100), lv_pct(100));
    lv_obj_set_style_border_width(wifi_list, 0, 0);
    lv_obj_set_style_text_font(wifi_list, &font_alipuhui20, 0);
    lv_obj_set_scrollbar_mode(wifi_list, LV_SCROLLBAR_MODE_OFF); // 隐藏滚动条
    // 显示wifi信息
    lv_obj_t * btn;
    for (int i = 0; i < ap_number; i++) {
        ESP_LOGI(TAG, "SSID \t\t%s", ap_info[i].ssid);   // 终端输出wifi名称
        ESP_LOGI(TAG, "RSSI \t\t%d", ap_info[i].rssi);   // 终端输出wifi信号质量
        // 添加wifi列表
        btn = lv_list_add_btn(wifi_list, LV_SYMBOL_WIFI, (const char *)ap_info[i].ssid);
        lv_obj_add_event_cb(btn, list_btn_cb, LV_EVENT_CLICKED, NULL); // 添加点击回调函数
    }
    lvgl_port_unlock();

    // 创建wifi连接任务
    xQueueWifiAccount = xQueueCreate(2, sizeof(wifi_account_t));
    xTaskCreatePinnedToCore(wifi_connect, "wifi_connect", 4 * 1024, NULL, 5, NULL, 1);  // 创建wifi连接任务
}

这个函数可以分为四个主要部分:

  1. 创建扫描提示页面(4-21行):

    • 创建全屏页面
    • 显示"WLAN扫描中…"提示
  2. 执行WiFi扫描(23-26行):

    • 调用wifi_scan()函数扫描附近WiFi
  3. 显示WiFi列表(28-46行):

    • 删除"扫描中"提示
    • 创建WiFi列表控件
    • 遍历扫描到的WiFi并添加到列表中
    • 为每个WiFi名称添加点击事件
  4. 创建WiFi连接任务(48-50行):

    • 创建消息队列用于传递WiFi账号和密码
    • 创建WiFi连接任务,等待用户输入密码后执行连接

重要提示:使用esp_lvgl_port组件时,所有LVGL相关操作都必须位于lvgl_port_lock(0)lvgl_port_unlock()之间,这确保了LVGL画面的正常显示。

6.3 密码输入界面

当用户点击WiFi名称后,会进入list_btn_cb()函数,该函数负责创建密码输入界面:

// 进入输入密码界面
static void list_btn_cb(lv_event_t * e)
{
    // 获取点击到的WiFi名称
    const char *wifi_name=NULL;
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t * obj = lv_event_get_target(e);
    if(code == LV_EVENT_CLICKED) {
        wifi_name = lv_list_get_btn_text(wifi_list, obj);
        ESP_LOGI(TAG, "WLAN Name: %s", wifi_name);
    }

    // 创建密码输入页面
    wifi_password_page = lv_obj_create(lv_scr_act());
    lv_obj_set_size(wifi_password_page, 320, 240);
    lv_obj_set_style_border_width(wifi_password_page, 0, 0); // 设置边框宽度
    lv_obj_set_style_pad_all(wifi_password_page, 0, 0);      // 设置间隙
    lv_obj_set_style_radius(wifi_password_page, 0, 0);       // 设置圆角
    
    // 创建返回按钮
    // ...
    
    // 显示选中的wifi名称
    // ...
    
    // 创建密码输入框
    ta_pass_text = lv_textarea_create(wifi_password_page);
    lv_obj_set_style_text_font(ta_pass_text, &lv_font_montserrat_20, 0);
    lv_textarea_set_one_line(ta_pass_text, true);              // 一行显示
    lv_textarea_set_password_mode(ta_pass_text, false);        // 是否使用密码输入显示模式
    lv_textarea_set_placeholder_text(ta_pass_text, "password"); // 设置提醒词
    lv_obj_set_width(ta_pass_text, 150);                       // 宽度
    lv_obj_align(ta_pass_text, LV_ALIGN_TOP_LEFT, 10, 40);     // 位置
    lv_obj_add_state(ta_pass_text, LV_STATE_FOCUSED);          // 显示光标
    
    // 创建"连接按钮"
    // ...
    
    // 创建"删除按钮"
    // ...
    
    // 创建数字、小写字母、大写字母的roller及确认按钮
    // ...
}

密码输入界面包含以下元素:

  1. 返回按钮:返回到WiFi列表
  2. WiFi名称显示
  3. 密码输入框
  4. 连接按钮(OK)
  5. 删除按钮(退格键)
  6. 三个字符选择器(roller):
    • 数字(0-9)
    • 小写字母(a-z)
    • 大写字母(A-Z)
  7. 每个roller下的确认按钮

6.4 WiFi连接实现

点击"OK"按钮后,会调用btn_connect_cb函数:

static void btn_connect_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);
    if(code == LV_EVENT_CLICKED) {
        // 获取wifi名称
        char* wifi_name = NULL;
        wifi_name = lv_label_get_text(label_wifi_name);
        ESP_LOGI(TAG, "WiFi Name: %s", wifi_name);
        
        // 获取wifi密码
        const char* pass_text = lv_textarea_get_text(ta_pass_text);
        ESP_LOGI(TAG, "WiFi Password: %s", pass_text);
        
        // 删除密码界面
        lv_obj_del(wifi_password_page);
        
        // 显示"WLAN连接中..."
        lv_obj_t *label_wifi_connect = lv_label_create(wifi_scan_page);
        lv_label_set_text(label_wifi_connect, "WLAN连接中...");
        lv_obj_set_style_text_font(label_wifi_connect, &font_alipuhui20, 0);
        lv_obj_align(label_wifi_connect, LV_ALIGN_CENTER, 0, -50);
        
        // 发送wifi账号和密码到连接任务
        wifi_account_t wifi_account;
        memset(&wifi_account, 0, sizeof(wifi_account_t));
        strcpy((char *)wifi_account.ssid, wifi_name);
        strcpy((char *)wifi_account.password, pass_text);
        if (xQueueWifiAccount != NULL) {
            xQueueSend(xQueueWifiAccount, &wifi_account, 0);
        }
    }
}

此函数完成了以下操作:

  1. 获取用户选择的WiFi名称
  2. 获取用户输入的密码
  3. 删除密码输入界面
  4. 显示"WLAN连接中…"提示
  5. 将WiFi名称和密码通过队列发送给WiFi连接任务

wifi_connect任务在接收到这些信息后会执行连接:

static void wifi_connect(void *pvParameters)
{
    wifi_account_t wifi_account;
    
    // 等待接收WiFi账号密码
    if (xQueueReceive(xQueueWifiAccount, &wifi_account, portMAX_DELAY)) {
        // 开始连接WiFi
        wifi_init_sta((char *)wifi_account.ssid, (char *)wifi_account.password);
        
        // 根据连接结果显示不同提示
        lvgl_port_lock(0);
        if (wifi_connect_status == WIFI_CONNECTED_OK) {
            // 连接成功
            lv_obj_t *label_wifi_connected = lv_label_create(wifi_scan_page);
            lv_label_set_text(label_wifi_connected, "WLAN连接成功!");
            lv_obj_set_style_text_font(label_wifi_connected, &font_alipuhui20, 0);
            lv_obj_align(label_wifi_connected, LV_ALIGN_CENTER, 0, -50);
        } else if (wifi_connect_status == WIFI_CONNECTED_FAIL) {
            // 连接失败
            lv_obj_t *label_wifi_connected = lv_label_create(wifi_scan_page);
            lv_label_set_text(label_wifi_connected, "WLAN连接失败!");
            lv_obj_set_style_text_font(label_wifi_connected, &font_alipuhui20, 0);
            lv_obj_align(label_wifi_connected, LV_ALIGN_CENTER, 0, -50);
        } else {
            // 连接异常
            lv_obj_t *label_wifi_connected = lv_label_create(wifi_scan_page);
            lv_label_set_text(label_wifi_connected, "WLAN连接异常!");
            lv_obj_set_style_text_font(label_wifi_connected, &font_alipuhui20, 0);
            lv_obj_align(label_wifi_connected, LV_ALIGN_CENTER, 0, -50);
        }
        lvgl_port_unlock();
    }
    
    vTaskDelete(NULL);
}

连接完成后,会根据连接结果在屏幕上显示相应的提示:

  • 连接成功:显示"WLAN连接成功!"
  • 连接失败:显示"WLAN连接失败!"
  • 连接异常:显示"WLAN连接异常!"

七、运行与测试

  1. 编译并下载程序到ESP32S3开发板
  2. 程序运行后,屏幕会先显示"WLAN扫描中…"
  3. 扫描完成后,显示附近WiFi的名称列表
  4. 点击想要连接的WiFi
  5. 在密码输入界面输入密码
    • 可以通过三个roller选择数字、小写字母和大写字母
    • 每选择一个字符,点击下方的确认按钮(√)添加到密码框
    • 如果输入错误,可以使用删除按钮删除
  6. 输入完成后点击"OK"按钮
  7. 程序会显示"WLAN连接中…",然后根据连接结果显示成功或失败信息

八、优化与拓展

8.1 信号强度显示

目前程序只显示了WiFi名称,没有显示信号强度。可以通过以下方式添加信号强度显示:

// 根据RSSI值选择不同的WiFi图标
const char* get_wifi_icon(int rssi)
{
    if (rssi > -50) {
        return LV_SYMBOL_WIFI;       // 信号强
    } else if (rssi > -70) {
        return LV_SYMBOL_WIFI;       // 信号中(这里可以用不同图标)
    } else {
        return LV_SYMBOL_WIFI;       // 信号弱
    }
}

// 在添加WiFi列表项时使用
btn = lv_list_add_btn(wifi_list, get_wifi_icon(ap_info[i].rssi), (const char *)ap_info[i].ssid);

8.2 密码输入增强

目前密码输入只支持数字和字母,可以添加特殊字符支持:

// 修改数字roller的选项,添加特殊字符
const char * opts_num = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n!\n@\n#\n$\n%\n^\n&\n*";

8.3 记住密码功能

可以使用NVS存储已连接的WiFi信息,下次自动连接:

// 存储WiFi信息到NVS
void save_wifi_config(const char* ssid, const char* password)
{
    nvs_handle_t my_handle;
    ESP_ERROR_CHECK(nvs_open("storage", NVS_READWRITE, &my_handle));
    ESP_ERROR_CHECK(nvs_set_str(my_handle, "ssid", ssid));
    ESP_ERROR_CHECK(nvs_set_str(my_handle, "password", password));
    ESP_ERROR_CHECK(nvs_commit(my_handle));
    nvs_close(my_handle);
}

// 在连接成功后调用
if (wifi_connect_status == WIFI_CONNECTED_OK) {
    save_wifi_config(wifi_account.ssid, wifi_account.password);
    // ...
}

九、总结

通过本文,我们详细讲解了如何在ESP32S3开发板上实现WiFi扫描和连接功能,包括:

  1. 创建自定义分区表,解决中文字库占用空间过大的问题
  2. 使用免费商用的阿里普惠字体制作中文字库,支持显示中文WiFi名称
  3. 使用LVGL图形库创建友好的用户界面
  4. 实现WiFi扫描、显示列表、密码输入和连接功能
  5. 通过任务和队列实现不同功能模块之间的通信

这个项目是物联网应用的基础,为后续开发更复杂的联网应用奠定了基础。希望这篇教程对大家有所帮助!


参考资料

  1. ESP-IDF 分区表介绍
  2. ESP-IDF NVS 介绍
  3. 阿里巴巴字体网站
  4. LVGL字体制作工具
  5. Unicode字符范围查询
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐