ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ESP32 LVGL WiFi配置界面开发:VSCode+FreeRTOS可视化配网实战

ESP32 LVGL WiFi配置界面开发:VSCode+FreeRTOS可视化配网实战 1. 为什么要在 ESP32 上折腾一套 WiFi 配置界面做过 ESP32 项目的人大概率都遇到过这个场景设备焊好、外壳装好、送到客户或者朋友手里对方问一句“怎么连我家 WiFi”你才发现自己根本没做配网入口。早期我都是用串口打印一个热点列表让人对着串口助手敲 SSID 和密码体验极差而且一旦设备装进盒子里串口都摸不到。后来试过手机 App 配网、SmartConfig 一键配网各有各的坑——App 要额外开发SmartConfig 对路由器兼容性又挑。所以这次我干脆用LVGL在 ESP32 上做一套完整的可视化 WiFi 配置界面屏幕上直接列出扫描到的热点、点一下选中、弹出软键盘输入密码、连接成功后显示 IP。整套东西在VSCode里开发配合 ESP-IDF 插件代码补全、烧录、串口监视一条龙。这篇文章就把我从零搭这套界面的完整过程拆开讲包括 LVGL 移植、FreeRTOS 任务划分、WiFi 扫描与连接的线程安全处理以及那些文档里不会写、只有踩过才知道的坑。适合谁看如果你已经会用 ESP32 点个灯、跑过 ESP-IDF 的 hello world想往上做一层带屏幕的交互这篇就是给你准备的。如果你连 LVGL 是什么都还没概念也没关系我会把关键概念用生活化的方式讲清楚你照着抄作业也能跑起来。核心关键词LVGL、ESP32、WiFi 配置界面、VSCode、可视化热点扫描会贯穿全文但重点永远落在“怎么落地”上。先说清楚这套方案解决的核心问题把“配网”这个动作从依赖外部工具变成设备自身屏幕上的一个自包含流程。用户拿到设备开机看到热点列表点选、输密码、连上全程不需要手机、不需要串口、不需要看说明书。这对做智能家居面板、工业 HMI、便携仪器的朋友来说是刚需。2. 整体方案设计与技术选型拆解2.1 为什么是 LVGL 而不是其他 GUIESP32 上能跑的 GUI 方案其实不少我列几个常见的对比一下方便你判断自己该不该跟这套方案走。方案资源占用开发效率生态与资料适合场景LVGL中等可裁剪高控件丰富极好社区活跃中大型交互界面u8g2极低中偏绘图好单色小屏、简单菜单TFT_eSPI 裸绘低低全手写一般固定画面、仪表盘SquareLine Studio LVGL中等极高可视化拖拽好快速出原型我选 LVGL 的核心理由有三个。第一它自带列表、按钮、文本域、键盘这些控件做 WiFi 配置界面几乎不用自己造轮子一个lv_list加一个lv_keyboard就能拼出完整交互。第二它支持PC 模拟器我可以在电脑上先把界面调好再往 ESP32 上搬省掉大量反复烧录的时间。第三它的样式系统很灵活做出来的界面不像“单片机味”那么重客户看着舒服。至于 SquareLine Studio我建议新手先用纯代码写一遍理解控件树和事件回调的机制再去用可视化工具否则出了问题你根本不知道生成的代码在干嘛。2.2 显示驱动与硬件搭配思路ESP32 驱动屏幕的方式主要看你的屏是什么接口。常见的有 SPI 的 ST7789、ILI9341I2C 的 SSD1306单色小屏还有并口的 RGB 屏。WiFi 配置界面至少要能显示五六行热点加一个键盘我建议分辨率不低于 240x320SPI 接口的 IPS 屏是性价比最高的选择。这里有个关键点LVGL 的刷新和 WiFi 扫描都是耗时操作绝不能放在同一个任务里串行跑。屏幕刷新要保证 30fps 左右的流畅度而 WiFi 扫描一次要 1 到 3 秒如果扫描时卡住刷新界面直接假死。所以整套架构必须建立在FreeRTOS 多任务之上这也是热词里“freertos 移植 lvgl”被频繁搜索的原因。我的任务划分是这样的LVGL 任务负责lv_timer_handler()周期调用和界面刷新优先级中等栈给 8KB。WiFi 事件任务处理扫描结果、连接状态回调通过队列把数据丢给 LVGL 任务。主任务初始化各外设创建上面两个任务后就可以歇着。2.3 VSCode 开发环境的定位为什么强调VSCode因为 ESP-IDF 官方的 Eclipse 插件体验一般而 VSCode 的 ESP-IDF 插件现在成熟度很高idf.py的所有命令都能在图形界面里点CMake 配置、menuconfig、烧录、监视串口全都有。加上 C/C 插件的智能补全写 LVGL 那种层层嵌套的控件代码时能省下大量查文档的时间。需要提醒的是VSCode 里配置 ESP-IDF 环境时Python 环境和工具链路径一定要让插件自动安装别自己手动配否则idf.py找不到编译器是家常便饭。这个坑我在“常见问题”章节会详细讲。3. 环境搭建与 LVGL 移植的关键细节3.1 VSCode ESP-IDF 环境一次配好先把地基打牢。我的操作顺序是这样的装 VSCode官网下载对应系统版本安装时勾选“添加到 PATH”。在 VSCode 扩展市场搜ESP-IDF认准 Espressif 官方那个安装。安装完成后按F1输入ESP-IDF: Configure ESP-IDF Extension选Express安装方式。选一个 ESP-IDF 版本我一般用最新的稳定版比如 v5.1 或 v5.2然后让它自动下载工具链和 Python 环境。等它跑完底部状态栏会出现 ESP-IDF 的图标说明环境就绪。这里有个细节Express 安装会把 Python 虚拟环境放在用户目录下路径里千万别有中文和空格否则后面编译时 CMake 会报一堆莫名其妙的错。我有个朋友用户名是中文折腾了一下午才发现是路径问题。装好后用ESP-IDF: Show Examples Projects打开一个hello_world例程编译烧录一遍确认工具链没问题再开始搞 LVGL。这一步别省地基不稳后面全是坑。3.2 把 LVGL 作为组件加进工程ESP-IDF 的组件机制让 LVGL 的集成变得很干净。推荐用组件管理器的方式在工程根目录建一个idf_component.ymldependencies: idf: 5.0 lvgl/lvgl: ^8.3.0然后在main/CMakeLists.txt里确保REQUIRES lvgl。执行一次idf.py build组件管理器会自动把 LVGL 拉下来。这种方式比手动 git clone 再改 CMakeLists 优雅太多升级也方便。如果你用的是 LVGL 9.xAPI 有不少变化比如lv_img改名、动画接口重构。新手我建议先用8.3 LTS 版本资料最多踩坑最少。等熟了再上 9.x。3.3 显示与输入设备的对接LVGL 本身不碰硬件它需要你提供两个回调一个告诉它怎么把像素刷到屏幕上flush_cb一个告诉它怎么读触摸或按键read_cb。以 SPI 的 ST7789 为例核心流程是// 1. 初始化 SPI 总线和屏幕 spi_bus_initialize(...); esp_lcd_new_panel_io_spi(...); esp_lcd_new_panel_st7789(...); esp_lcd_panel_init(panel); // 2. 准备 LVGL 的绘制缓冲区 static lv_color_t buf1[SCREEN_W * 40]; lv_disp_draw_buf_init(draw_buf, buf1, NULL, SCREEN_W * 40); // 3. 注册显示驱动 static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res SCREEN_W; disp_drv.ver_res SCREEN_H; disp_drv.flush_cb my_flush_cb; lv_disp_drv_register(disp_drv);my_flush_cb里做的事就是把 LVGL 画好的区域通过esp_lcd_panel_draw_bitmap推给屏幕推完调用lv_disp_flush_ready。注意绘制缓冲区大小直接决定流畅度。太小会频繁刷屏导致撕裂太大吃内存。我的经验是至少给屏幕高度的 1/10240x320 的屏给 240x40 就够用内存紧张时可以开双缓冲但只给一半大小。触摸部分如果用电容触摸如 FT6236、GT911走 I2C注册lv_indev_drv_t的read_cb即可。如果是电阻屏需要先做校准LVGL 提供了校准界面但实际项目里我一般直接写死校准参数省得用户误触。3.4 内存与性能的取舍ESP32 不带 PSRAM 的型号内部 RAM 大概 320KB 可用LVGL 加 WiFi 协议栈加 FreeRTOS很容易吃紧。几个实测有效的优化手段LVGL 的LV_MEM_SIZE在lv_conf.h里配置默认 48KB做 WiFi 界面我调到64KB比较稳。关闭不用的控件和字体比如不用图表就把LV_USE_CHART关掉字体只保留中号。绘制缓冲区用内部 DMA 内存别用 PSRAM否则刷屏速度会掉一大截。WiFi 扫描结果别全存只保留前 15 个信号最强的省内存也省列表渲染时间。如果你的板子带 PSRAM比如 ESP32-S3 加 8MB PSRAM那可以放开手脚缓冲区开大、字体多留几套界面能做得更精致。4. WiFi 配置界面的核心实现4.1 界面结构设计整个界面我用一个屏幕管理器的思路来组织分三个页面扫描页一个lv_list显示热点顶部一个刷新按钮底部一个状态栏。密码页显示选中的 SSID一个lv_textarea输入密码一个lv_keyboard软键盘。结果页显示连接状态和获取到的 IP失败时给重试按钮。页面切换用 LVGL 的lv_scr_load_anim做滑动动画体验比硬切好很多。控件树大致是这样screen_scan ├── header (label 选择WiFi) ├── list (热点列表) └── btn_refresh screen_pass ├── label_ssid ├── ta_password └── kb (键盘) screen_result ├── label_status └── btn_retry4.2 热点扫描的线程安全处理这是整套方案里最容易出 bug 的地方。WiFi 扫描是异步的esp_wifi_scan_start发起后结果通过事件回调返回。而 LVGL 的控件操作必须在 LVGL 任务里做不能在其他任务里直接lv_list_add_btn否则轻则花屏重则死机。我的做法是用一个FreeRTOS 队列把扫描结果从 WiFi 任务传到 LVGL 任务typedef struct { char ssid[33]; int8_t rssi; wifi_auth_mode_t authmode; } scan_result_t; static QueueHandle_t scan_queue; // WiFi 事件回调里 case WIFI_EVENT_SCAN_DONE: { uint16_t ap_count 0; esp_wifi_scan_get_ap_num(ap_count); wifi_ap_record_t *ap_list malloc(sizeof(wifi_ap_record_t) * ap_count); esp_wifi_scan_get_ap_records(ap_count, ap_list); for (int i 0; i ap_count i 15; i) { scan_result_t r; strncpy(r.ssid, (char *)ap_list[i].ssid, 32); r.rssi ap_list[i].rssi; r.authmode ap_list[i].authmode; xQueueSend(scan_queue, r, 0); } free(ap_list); break; }LVGL 任务里用xQueueReceive非阻塞地取取到就往列表里加一项。这样两个任务各干各的互不阻塞。提示esp_wifi_scan_get_ap_records拿到的记录里SSID 可能是空的隐藏热点渲染时要过滤掉否则列表里会出现空白项用户一脸懵。4.3 密码输入与软键盘联动LVGL 的键盘控件有个很方便的机制lv_keyboard_set_textarea(kb, ta)把键盘和输入框绑定键盘按键会自动往输入框里塞字符。密码框要设成密码模式lv_textarea_set_password_mode(ta_password, true); lv_textarea_set_one_line(ta_password, true); lv_textarea_set_max_length(ta_password, 63);63 是 WiFi 密码的最大长度别设小了。键盘布局我建议用LV_KEYBOARD_MODE_TEXT_LOWER加一个切换大小写的按钮因为很多路由器密码是大小写混合的。数字键盘虽然简洁但遇到带字母的密码就抓瞎。有个细节键盘弹出时会遮住输入框所以密码页的布局要把输入框放在屏幕上半部分键盘放底部。我一开始没注意输入框被键盘盖住用户根本看不到自己输了什么。4.4 连接流程与状态反馈用户点“连接”后流程是从输入框读出密码配置wifi_config_t。调用esp_wifi_set_config和esp_wifi_connect。监听WIFI_EVENT_STA_CONNECTED和IP_EVENT_STA_GOT_IP。成功则切到结果页显示 IP失败则弹提示并允许重试。连接过程要加超时机制我设的是 15 秒。超过就判定失败因为有些路由器密码错了会一直卡在连接中不超时的话界面永远转圈。esp_wifi_set_config(WIFI_IF_STA, wifi_config); esp_wifi_connect(); // 启动一个 15 秒的软件定时器超时回调里切到失败页状态反馈上我在扫描页底部放了一个状态标签实时显示“扫描中…”“已连接 192.168.1.100”这类信息。用户不用猜设备在干嘛体验提升明显。5. 实操过程与关键环节记录5.1 从零建工程到跑通第一个界面我把完整流程按顺序列一遍你可以直接照着走VSCode 里F1→ESP-IDF: New Project选一个模板命名wifi_config_ui。在工程根目录建idf_component.yml写入 LVGL 依赖。修改main/CMakeLists.txt加REQUIRES lvgl esp_wifi nvs_flash。写main/lv_conf.h从 LVGL 的模板拷一份改LV_COLOR_DEPTH为 16LV_MEM_SIZE为 64KB。写显示驱动初始化代码跑一个lv_label_create显示“Hello LVGL”烧录确认屏幕能亮。加 WiFi 初始化代码esp_netif_init、esp_event_loop_create_default、esp_wifi_init一套走完。把扫描、列表、键盘、连接逻辑逐步加上去。第 5 步是关键里程碑屏幕能显示文字说明 LVGL 移植成功后面就是纯业务逻辑了。很多人卡在这一步多半是flush_cb没调lv_disp_flush_ready或者 SPI 时钟设太高导致花屏。5.2 参数计算缓冲区与任务栈怎么定这部分我给出具体的计算过程别拍脑袋。绘制缓冲区屏幕 240x32016 位色一行是 240 * 2 480 字节。给 40 行就是 480 * 40 19200 字节约 19KB。双缓冲就是 38KB。ESP32 内部 RAM 够用但如果同时开 WiFi 和蓝牙建议单缓冲。LVGL 任务栈LVGL 的lv_timer_handler调用链比较深加上控件回调我实测 8KB 栈够用但为了保险给 10KB。栈溢出在 ESP32 上表现为莫名其妙的重启很难查宁可给大点。WiFi 任务栈ESP-IDF 的 WiFi 任务栈是系统自己管的不用你操心但事件回调里别做耗时操作否则会阻塞 WiFi 协议栈。队列长度扫描最多 15 个热点队列给 20 个元素每个元素约 40 字节总共 800 字节忽略不计。5.3 实测现场一次完整的配网流程烧录后我实测的流程是这样的开机 → 屏幕显示“扫描中…” → 约 2 秒后列出 8 个热点 → 点击“MyHome_5G” → 切到密码页 → 软键盘输入密码 → 点“连接” → 状态栏显示“连接中…” → 约 3 秒后切到结果页显示“已连接 IP: 192.168.1.102”。整个过程从开机到连上大概 8 秒。扫描那 2 秒是硬等待没法省但界面不卡因为扫描在后台任务跑LVGL 任务照常刷新动画。有个体验优化点扫描时在列表位置显示一个转圈的加载动画比干等一个空白列表好得多。LVGL 的lv_spinner控件正好干这个。5.4 把配置存进 NVS实现开机自动重连配网只做一次之后开机应该自动连上次的 WiFi。用 NVS 存 SSID 和密码nvs_handle_t handle; nvs_open(wifi_cfg, NVS_READWRITE, handle); nvs_set_str(handle, ssid, ssid); nvs_set_str(handle, pass, password); nvs_commit(handle); nvs_close(handle);开机时先读 NVS有记录就直接连连不上再进配置界面。这样用户平时根本看不到配网界面只有换网络时才用。这个设计让设备用起来像成品而不是半成品。注意NVS 存密码是明文的安全性要求高的场景要自己做加密。普通家用设备问题不大但心里要有数。6. 常见问题与排查技巧实录6.1 屏幕花屏、撕裂、刷新慢这是最高频的问题我整理成速查表现象可能原因解决方法花屏、雪花点SPI 时钟过高降到 40MHz 或 20MHz 试画面撕裂缓冲区太小增大到屏幕高度 1/10 以上刷新慢、卡顿缓冲区在 PSRAM改用内部 DMA 内存局部不刷新flush_cb 没调 ready检查lv_disp_flush_ready颜色错乱颜色深度不匹配LV_COLOR_DEPTH与屏幕一致我踩过最坑的一次是 SPI 时钟设了 80MHz屏幕偶尔花一下查了两天才发现是时钟问题。稳定比速度重要40MHz 对 240x320 的屏完全够用。6.2 WiFi 扫描不到热点或列表为空几个排查方向确认esp_wifi_set_mode(WIFI_MODE_STA)设对了AP 模式下扫描行为不一样。扫描前确保 WiFi 已esp_wifi_start。检查天线ESP32 模组的天线没接好信号弱到扫不到。隐藏热点扫不到是正常的需要主动指定 SSID 连接。还有一种情况是扫描到了但列表不显示多半是队列没收到数据检查xQueueSend的返回值队列满了会失败。6.3 连接一直卡在“连接中”密码错误、路由器开了 MAC 过滤、路由器只支持 5G 而 ESP32 只支持 2.4G都会导致这个问题。我的排查顺序是先用手机确认密码正确再看路由器是不是 2.4G最后查 MAC 过滤。超时机制一定要加否则用户以为死机了。6.4 VSCode 环境相关的坑idf.py找不到检查 VSCode 底部状态栏的 ESP-IDF 版本是否选中。编译报 Python 错误多半是虚拟环境路径有中文重装到纯英文路径。串口监视乱码波特率设成 115200和menuconfig里一致。烧录失败按住 BOOT 键再点烧录或者检查 USB 线是不是只能充电的那种。6.5 内存不足导致重启ESP32 上跑 LVGL 加 WiFi内存紧张是常态。用esp_get_free_heap_size()打印剩余内存如果低于 30KB 就要警惕。优化手段前面讲过核心是裁剪 LVGL 功能、缩小缓冲区、及时释放扫描结果。我遇到过一次重启查到最后是 LVGL 的LV_MEM_SIZE设太大把 WiFi 协议栈的内存挤没了。7. 一些让界面更好用的经验补充7.1 信号强度可视化光显示 SSID 不够直观我在每个列表项右边加了一个信号强度图标根据 RSSI 分四档-50 以上满格-70 三格-85 两格再低一格。LVGL 用lv_img加载四张小图标就行。用户一眼就能看出哪个热点信号好不用去猜。7.2 记住多个网络进阶玩法是存多个 SSID开机时按信号强度依次尝试。这个用 NVS 的 blob 存一个结构体数组就能实现。我做过一个项目要支持办公室和家里两个网络就是这么干的用户体验很顺。7.3 中文字体裁剪如果界面要显示中文LVGL 默认字体不含中文需要用官方的字体转换工具把用到的汉字挑出来生成子集字体。别把整个中文字库塞进去几 MB 的字体 ESP32 根本放不下。我一般只提取界面上出现的几十个字生成的字体文件才几十 KB。7.4 用 PC 模拟器加速开发LVGL 的 PC 模拟器在 Linux 或 Windows 上用 SDL 跑能让你在电脑上直接调界面改一行编译一次比烧录到 ESP32 快十倍。我的习惯是界面布局和逻辑先在模拟器上跑通再往硬件上搬硬件上只调驱动和性能。热词里“lvgl 9.x pc 模拟器”被搜得多就是因为这个流程太香了。7.5 关于蓝牙和 WiFi 共存经常有人问 ESP32 蓝牙和 WiFi 能不能一起用。答案是能但共用同一个射频会互相抢时间片导致 WiFi 吞吐下降、蓝牙延迟升高。做 WiFi 配置界面时如果同时开着蓝牙扫描会变慢。我的建议是配网阶段先关蓝牙配完再开错峰使用。这套 WiFi 配置界面我从第一版跑通到比较满意前后迭代了大概两周中间踩的坑基本都写在上面的排查表里了。最深的体会是LVGL 的难点不在界面本身而在它和 FreeRTOS、WiFi 协议栈的协作。把任务划分清楚、队列用对、内存算准剩下的就是堆控件。你要是刚开始做建议先把扫描和列表跑通别一上来就搞全套分阶段验证出问题好定位。
返回列表