# LVGL_UI **Repository Path**: NetADs/lvgl_ui ## Basic Information - **Project Name**: LVGL_UI - **Description**: 保存LVGL的UI界面。 创建在ESP32-S3-N16R8开发板上,显示在2寸320x240SPI接口,ST7789V驱动的TFT_LCD屏。嵌入代码用ESP-IDF架构,用LVGL9.5.0组件开发UI界面。UI架构使用当前流行的简洁清晰易于维护的架构。 创建一个主屏幕,以时钟显示为主“00:00:00”大字体显示。日期黄历,当天天气及三天预报交替显示为辅。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-27 - **Last Updated**: 2026-06-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # LVGL UI Component Library 可移植的 LVGL UI 组件库,为 ESP-IDF 项目提供通用 UI 屏幕集合。与 display 组件配合,从硬件驱动到 UI 渲染全链路封装。 ## 功能特性 - 标准 ESP-IDF 组件结构 - 统一初始化/反初始化 (`lvgl_ui_init()` / `lvgl_ui_deinit()`) - 通过抽象驱动接口挂载任意显示驱动 (`lvgl_ui_display_attach(&drv)`) - 不直接依赖特定显示硬件,适配 ST7789 / ILI9341 / 模拟器等任意驱动 - 页面管理引擎 (`lvgl_ui_page_switch()`) - 内部线程安全:所有公开 API 自动加锁 (`lvgl_ui_lock` / `lvgl_ui_unlock`) - 异步信号量同步机制(`_ui_async_*` 内部接口) - 支持 320x240 横屏显示 - 模块化设计,每个屏幕独立封装 - 通过 Kconfig 可配置屏幕尺寸 - LVGL v9.x+ API 实现 ## 架构:显示层与驱动层解耦 ``` +-------------------+ +-------------------+ | lvgl_ui | | 应用层 | | (显示层/UI引擎) | | (display 等驱动) | +-------------------+ +-------------------+ | - LVGL 初始化 | --注入--> | display_* 函数 | | - 页面管理 | lvgl_ui_ | 注册到 | | - 线程安全 | display_ | lvgl_ui_display_ | | - 异步信号量 | driver_t | driver_t | | - flush 回调 | | | +-------------------+ +-------------------+ | | v v lvgl_ui_display_attach(&drv) display_draw_area() 回调读取分辨率/发送像素 发送像素到硬件 SPI drv.get_width() / get_height() drv.draw_area() ``` - **驱动层** (display): 管理物理硬件,始终使用物理分辨率 (240x320)。通过填充 `lvgl_ui_display_driver_t` 结构体将函数指针注入 lvgl_ui - **显示层** (lvgl_ui): 管理 LVGL 逻辑渲染空间 (320x240 横屏),通过回调接口驱动任意显示硬件,不直接依赖特定驱动 ## 目录结构 ``` lvgl_ui/ ├── CMakeLists.txt ├── Kconfig ├── idf_component.yml ├── README.md ├── LICENSE ├── include/ │ ├── lvgl_ui.h │ ├── colors.h │ ├── ui_config.h │ ├── weather_types.h │ ├── lvgl_chinese_font.h │ └── screens/ │ ├── ui_welcome.h │ └── ui_weather_clock.h ├── src/ │ ├── lvgl_ui.c │ ├── fonts/ │ │ ├── lv_font_chinese_16.c │ │ ├── lv_font_chinese_20.c │ │ ├── lv_font_chinese_24.c │ │ └── lv_font_chinese_40.c │ └── screens/ │ ├── ui_welcome.c │ └── ui_weather_clock.c └── examples/ ├── welcome_demo/ └── weather_clock_demo/ ``` ## 包含的屏幕 | 屏幕名称 | 头文件 | 描述 | |---------|--------|------| | Welcome | `lvgl_ui_welcome_init()` | 欢迎页面,居中显示标题文字、进度条和状态信息 | | Weather Clock | `lvgl_ui_weather_clock_init()` | 天气时钟,显示时间/日期/天气/2天预报 | ## 安装方法 ### 方式一:复制到 components 目录 ```bash cp -r lvgl_ui /path/to/your/project/components/ cp -r display /path/to/your/project/components/ ``` ### 方式二:使用 ESP-IDF 组件管理器(推荐) 在项目的 `idf_component.yml` 中添加: ```yaml dependencies: display: git: https://gitee.com/NetADs/display.git lvgl_ui: git: https://gitee.com/NetADs/lvgl_ui.git ``` ## 快速开始 ```c #include "display.h" #include "lvgl_ui.h" void app_main(void) { // 1. 驱动层:初始化硬件显示(物理分辨率 240x320) display_config_t cfg = { .width = 240, .height = 320, }; ESP_ERROR_CHECK(display_init(&cfg)); // 2. 驱动层:旋转到横屏 (仅操作 MADCTL 寄存器,不修改逻辑尺寸) display_set_rotation(DISPLAY_ROTATION_90); // 3. 显示层:初始化 LVGL 引擎(tick + handler 任务 + 信号量) lvgl_ui_init(); // 4. 显示层:构建驱动接口并挂载到 LVGL lvgl_ui_display_driver_t drv = { .is_ready = display_is_ready, .get_panel = display_get_panel, .get_io = display_get_io, .get_width = display_get_width, .get_height = display_get_height, .draw_area = display_draw_area, }; lvgl_ui_display_attach(&drv); // 5. 创建并切换页面 lv_obj_t* welcome = lvgl_ui_welcome_init(NULL); lvgl_ui_page_switch(welcome); lvgl_ui_welcome_set_text(welcome, "Hello!"); while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } } ``` ## 使用示例 ### 欢迎页面 + 进度条 ```c lv_obj_t* welcome = lvgl_ui_welcome_init(NULL); lvgl_ui_page_switch(welcome); lvgl_ui_welcome_set_status_text(welcome, "正在连接 WiFi..."); lvgl_ui_welcome_set_progress(welcome, 10); vTaskDelay(pdMS_TO_TICKS(2000)); lvgl_ui_welcome_set_status_text(welcome, "WiFi 连接成功"); lvgl_ui_welcome_set_progress(welcome, 25); ``` ### 天气时钟 ```c lv_obj_t* clock = lvgl_ui_weather_clock_init(NULL); lvgl_ui_page_switch(clock); weather_data_t weather = { .current = WEATHER_SUNNY, .temp_current = 26, .temp_high = 28, .temp_low = 22, .humidity = 65, .wind_speed = 12, .visibility = 10, .forecast = { {WEATHER_CLOUDY, 24, 20, "明天"}, {WEATHER_SUNNY, 27, 21, "后天"} } }; lvgl_ui_weather_clock_update_weather(clock, &weather); ``` ### 页面切换 ```c // 切换到新页面(自动删除旧页面:pre_free → lv_obj_del → free) lv_obj_t* clock = lvgl_ui_weather_clock_init(NULL); lvgl_ui_page_switch(clock); // 手动删除 lvgl_ui_welcome_delete(welcome); ``` ## API 参考 ### 线程安全 | 函数 | 描述 | |------|------| | `lvgl_ui_lock()` | 获取 LVGL 互斥锁 | | `lvgl_ui_unlock()` | 释放 LVGL 互斥锁 | 所有公开 API 内部自动加锁/解锁。外部任务调用 API 无需手动处理锁。内部回调(如 lv_timer、flush callback)运行在 handler 任务中,已持有锁。 ### 核心 API | 函数 | 描述 | |------|------| | `lvgl_ui_init()` | 初始化 LVGL、创建 tick/handler 任务和异步信号量 | | `lvgl_ui_deinit()` | 清理所有资源 | | `lvgl_ui_display_attach(driver)` | 挂载显示驱动接口,创建 LVGL 显示设备 | | `lvgl_ui_page_switch(new_page)` | 切换活动页面 | **状态码:** | 枚举值 | 说明 | |--------|------| | `LVGL_UI_OK` | 操作成功 | | `LVGL_UI_ERR_TICK_TASK_FAILED` | tick 任务创建失败 | | `LVGL_UI_ERR_HANDLER_TASK_FAILED` | handler 任务创建失败 | | `LVGL_UI_ERR_DISPLAY_FAILED` | 显示驱动初始化失败 | | `LVGL_UI_ERR_INVALID_PARAM` | 参数无效 | | `LVGL_UI_ERR_SEMAPHORE_FAILED` | 异步信号量创建失败 | **可配置宏(在包含头文件前定义):** | 宏 | 默认值 | 说明 | |----|--------|------| | `LVGL_UI_TICK_TASK_STACK` | 4096 | tick 任务栈大小 | | `LVGL_UI_HANDLER_TASK_STACK` | 8192 | handler 任务栈大小 | | `LVGL_UI_TICK_TASK_PRIO` | 5 | tick 任务优先级 | | `LVGL_UI_HANDLER_TASK_PRIO` | 5 | handler 任务优先级 | | `LVGL_UI_DISPLAY_BUF_LINES` | 40 | 每缓冲区像素行数 | ### 欢迎页面 API | 函数 | 描述 | |------|------| | `lvgl_ui_welcome_init(parent)` | 创建欢迎页面 | | `lvgl_ui_welcome_set_text(obj, text)` | 更新标题文字 | | `lvgl_ui_welcome_set_progress(obj, pct)` | 设置进度条 (0-100) | | `lvgl_ui_welcome_set_status_text(obj, text)` | 设置状态文字 | | `lvgl_ui_welcome_delete(obj)` | 删除欢迎页面 | ### 天气时钟 API | 函数 | 描述 | |------|------| | `lvgl_ui_weather_clock_init(parent)` | 创建天气时钟 | | `lvgl_ui_weather_clock_update_weather(obj, data)` | 更新天气数据 | | `lvgl_ui_weather_clock_update_time(obj, data)` | 更新时间数据 | | `lvgl_ui_weather_clock_set_24h_format(obj, enable)` | 24/12 小时制 | | `lvgl_ui_weather_clock_show_seconds(obj, enable)` | 显示/隐藏秒数 | | `lvgl_ui_weather_clock_set_theme(obj, theme)` | 设置主题 | | `lvgl_ui_weather_clock_set_brightness(obj, mode)` | 设置亮度模式 | | `lvgl_ui_weather_clock_delete(obj)` | 删除天气时钟 | ## 配置选项 通过 `idf.py menuconfig` 配置: | 配置项 | 默认值 | 说明 | |--------|--------|------| | `LVGL_UI_SCREEN_WIDTH` | 320 | 逻辑屏幕宽度 | | `LVGL_UI_SCREEN_HEIGHT` | 240 | 逻辑屏幕高度 | ## 横屏模式说明 驱动层(display 组件)以物理分辨率 (240x320) 初始化,显示层(lvgl_ui)在 320x240 逻辑空间渲染。`display_set_rotation(DISPLAY_ROTATION_90)` 仅操作 ST7789 硬件 MADCTL 寄存器完成坐标映射,不修改物理尺寸。LVGL flush 回调中坐标直接传递给 `display_draw_area()`,硬件侧 swap_xy 已开启,自动完成物理地址映射。 ## 示例项目 ### welcome_demo ```bash cd examples/welcome_demo idf.py set-target esp32s3 idf.py build flash monitor ``` ### weather_clock_demo ```bash cd examples/weather_clock_demo idf.py set-target esp32s3 idf.py build flash monitor ``` ## 添加新屏幕 ### 1. 创建头文件 `include/screens/ui_yourscreen.h`: ```c #pragma once #include "lvgl.h" lv_obj_t* lvgl_ui_yourscreen_init(lv_obj_t* parent); void lvgl_ui_yourscreen_delete(lv_obj_t* screen); ``` ### 2. 创建实现文件 `src/screens/ui_yourscreen.c`: ```c #include "screens/ui_yourscreen.h" #include "lvgl_ui.h" typedef struct { lvgl_ui_page_pre_free_fn pre_free; lv_obj_t* container; } ui_yourscreen_t; static void your_pre_free(void* user_data) { } lv_obj_t* lvgl_ui_yourscreen_init(lv_obj_t* parent) { lvgl_ui_lock(); if (parent == NULL) parent = lv_scr_act(); ui_yourscreen_t* page = calloc(1, sizeof(ui_yourscreen_t)); if (page == NULL) { lvgl_ui_unlock(); return NULL; } page->pre_free = your_pre_free; page->container = lv_obj_create(parent); lv_obj_set_size(page->container, 320, 240); lv_obj_set_user_data(page->container, page); lvgl_ui_unlock(); return page->container; } void lvgl_ui_yourscreen_delete(lv_obj_t* obj) { if (obj == NULL) return; lvgl_ui_lock(); ui_yourscreen_t* page = lv_obj_get_user_data(obj); if (page) { if (page->pre_free) page->pre_free(page); lv_obj_del(obj); free(page); } else { lv_obj_del(obj); } lvgl_ui_unlock(); } ``` ### 3. 更新 `CMakeLists.txt` 和 `lvgl_ui.h` ## 依赖项 - ESP-IDF v5.5+ - LVGL v9.x+ - [display](https://gitee.com/NetADs/display) - ST7789 显示驱动组件 ## 兼容性 ESP32 / ESP32-S2 / ESP32-S3 / ESP32-C3 ## 许可证 MIT License ## 更新日志 ### v1.3.0 - 显示驱动解耦:`lvgl_ui_display_attach()` 改为接受 `lvgl_ui_display_driver_t` 回调结构体 - 移除对 display 组件的编译依赖,CMakeLists.txt 不再 `REQUIRES display` - flush 回调改为异步 DMA 模式,DMA 完成后才通知 LVGL - handler 任务循环间隔从 10ms 降至 5ms - 新增 `lvgl_ui_display_driver_t` 显示驱动抽象接口定义 ### v1.2.0 - 集成 display 组件:`lvgl_ui_display_attach()` 无参数,内部通过 `display_get_panel()` 等读取硬件信息 - flush 回调改用 `display_draw_area()` 发送像素,不再直接操作 `esp_lcd_panel_handle_t` - 线程安全:所有公开 API 内部封装 `lvgl_ui_lock()` / `lvgl_ui_unlock()` - 新增异步信号量机制:`_ui_async_create_sem` / `_ui_async_take_sem` / `_ui_async_give_sem` / `_ui_async_delete_sem` - 新增 `s_async_attach_sem` 和 `s_async_welcome` 内部状态 - 新增错误码 `LVGL_UI_ERR_SEMAPHORE_FAILED` - 屏幕文件(ui_welcome / ui_weather_clock)所有公开函数加锁保护 ### v1.1.0 - 新增统一初始化/反初始化接口 - 新增页面管理引擎 - 新增页面预释放回调约定 ### v1.0.0 - 初始版本