# 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-07-12 ## 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`) - 支持 320x240 横屏显示 - 模块化设计,每个屏幕独立封装 - 通过 Kconfig 可独立控制每个屏幕的编译开关及屏幕尺寸 - LVGL v9.5.0 API 实现(依赖已锁定 `lvgl/lvgl: 9.5.0`) ## 架构:显示层与驱动层解耦 ``` +-------------------+ +-------------------+ | lvgl_ui | | 应用层 | | (显示层/UI引擎) | | (display 等驱动) | +-------------------+ +-------------------+ | - LVGL 初始化 | --注入--> | display_* 函数 | | - 页面管理 | lvgl_ui_ | 注册到 | | - 线程安全 | display_ | lvgl_ui_display_ | | - flush 回调 | driver_t | driver_t | +-------------------+ +-------------------+ | | 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 │ └── ui_weather_clock_modern.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 │ └── ui_weather_clock_modern.c └── examples/ ├── welcome_demo/ └── weather_clock_demo/ ``` ## 包含的屏幕 | 屏幕名称 | 头文件 | Kconfig 开关 | 描述 | |---------|--------|-------------|------| | Welcome | `lvgl_ui_welcome_init()` | `LVGL_UI_INCLUDE_WELCOME` | 欢迎页面,居中显示标题文字、进度条和状态信息 | | Weather Clock (Classic) | `lvgl_ui_weather_clock_init()` | `LVGL_UI_INCLUDE_WEATHER_CLOCK` | 实时时钟经典样式,左右双栏布局 | | Weather Clock (Modern) | `lvgl_ui_weather_clock_init()` | `LVGL_UI_INCLUDE_WEATHER_CLOCK_MODERN` | 实时时钟现代样式,卡片式自动轮播面板,玻璃拟态风格 | ## 安装方法 ### 方式一:复制到 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 // 注意:draw_area 契约为 bool(true=成功);display_draw_area 返回 esp_err_t(ESP_OK==0), // 必须做 ==ESP_OK 转换,否则成功时会被当成失败触发双重 flush_ready。 #include "esp_err.h" static bool draw_area_wrapper(int x, int y, int w, int h, const void *data) { return display_draw_area(x, y, w, h, data) == ESP_OK; } lvgl_ui_display_driver_t drv = { .is_ready = display_is_ready, .get_io = display_get_io, .get_width = display_get_width, // 旋转后返回有效分辨率(如 320) .get_height = display_get_height, // (如 240) .draw_area = draw_area_wrapper, }; 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); /* 更新时间(含农历和四柱) */ clock_data_t time_data = { .hour = 14, .minute = 30, .second = 0, .day = 6, .month = 7, .year = 2026, .is_24h_format = true, .show_seconds = true, .day_name = "星期一", .lunar_date = "甲子鼠年正月初一", .four_pillars = "甲子年甲子月甲子日甲子时", }; lvgl_ui_weather_clock_update_time(clock, &time_data); /* 更新天气数据 */ weather_data_t weather = { .current = WEATHER_SUNNY, .temp_current = 28, .temp_high = 32, .temp_low = 22, .humidity = 60, .wind_speed = 15, .wind_direction = "东北", .location = "天津", .forecast = { { .type = WEATHER_SUNNY, .temp_high = 32, .temp_low = 22, .day_name = "今天" }, { .type = WEATHER_CLOUDY, .temp_high = 30, .temp_low = 21, .day_name = "明天" }, { .type = WEATHER_RAINY, .temp_high = 28, .temp_low = 19, .day_name = "后天" }, }, }; lvgl_ui_weather_clock_update_weather(clock, &weather); lvgl_ui_weather_clock_set_24h_format(clock, false); ``` **布局说明**:页面分为左右两栏,左栏 184px 显示时钟信息,右栏 136px 显示天气信息。 ``` ┌─ 左面板 184px ────────┬─ 右面板 136px ────┐ │ 00:00:00 │ 天津 │ │ 2026年07月06日 星期一 │ 晴 28°C │ │ 甲子鼠年正月初一 │ 湿度 60% │ │ 甲子年 甲子月 │ 东北风 15 km/h │ │ 甲子日 甲子时 │ 今天 晴 32/22°C │ │ │ 明天 阴 30/21°C │ │ │ 后天 雨 28/19°C │ └────────────────────────┴──────────────────┘ ``` ### 页面切换 ```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 任务中,已持有锁。 > ⚠️ **线程安全约束(重要)** > > 1. **依赖递归锁**:`lvgl_ui_lock()` 底层调用 `lv_lock()`,**要求 LVGL 的 `lv_lock()` 必须实现为递归互斥锁**(FreeRTOS 默认使用 `xSemaphoreCreateRecursiveMutex`,安全)。若外部项目替换了 LVGL OSAL 层或使用 `LV_OS_NONE`,可能导致死锁。 > > 2. **禁止已在锁中再调 API**:外部代码不应在已持有 `lv_lock()` 或 `lvgl_ui_lock()` 的情况下调用 lvgl_ui 公开 API(导致重入锁)。所有 lvgl_ui API 内部已自动加锁,外部无需预先取锁。 > > 3. **事件回调 / timer 回调中调 API**:LVGL timer 和事件回调运行在 handler 任务上下文中,此时 handler 任务已持有 `lv_lock()`。在这些回调中调用 lvgl_ui API 会触发 `lv_lock()` 重入,**必须确保 `lv_lock()` 为递归锁**。 > > 4. **`lvgl_ui_deinit()` 中 `lv_obj_del` 触发事件回调**:`cleanup_active_page()` 内部调用 `lv_obj_del()` 删除页面对象树,可能触发外部注册的 LVGL 事件回调。若回调中调用了 lvgl_ui API,同一任务将重入 `lv_lock()`。同样依赖递归锁安全。 ### 核心 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_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_time(obj, data)` | 更新时间数据(含农历、四柱) | | `lvgl_ui_weather_clock_update_weather(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_delete(obj)` | 删除天气时钟 | `clock_data_t` 新增字段: | 字段 | 类型 | 说明 | |------|------|------| | `lunar_date[32]` | char[] | 农历日期,如 `"甲子鼠年正月初一"` | | `four_pillars[48]` | char[] | 四柱,如 `"甲子年甲子月甲子日甲子时"` | > 四柱字符串若为 12 个汉字(36 字节),内部自动折行显示为 `"甲子年 甲子月\n甲子日 甲子时"` 两行。 ## 配置选项 通过 `idf.py menuconfig` 配置: | 配置项 | 默认值 | 说明 | |--------|--------|------| | `LVGL_UI_SCREEN_WIDTH` | 320 | 逻辑屏幕宽度 | | `LVGL_UI_SCREEN_HEIGHT` | 240 | 逻辑屏幕高度 | | `LVGL_UI_INCLUDE_WELCOME` | y | 是否编译欢迎页面 | | `LVGL_UI_INCLUDE_WEATHER_CLOCK` | y | 是否编译经典天气时钟 | | `LVGL_UI_INCLUDE_WEATHER_CLOCK_MODERN` | y | 是否编译现代天气时钟 | | `LVGL_UI_CHINESE_FONT_16` | y | 是否包含 16px 中文字体 | | `LVGL_UI_CHINESE_FONT_20` | y | 是否包含 20px 中文字体 | | `LVGL_UI_CHINESE_FONT_24` | y | 是否包含 24px 中文字体 | | `LVGL_UI_CHINESE_FONT_40` | y | 是否包含 40px 中文字体(抗锯齿,~450KB flash) | 所有配置项均支持通过 `sdkconfig.defaults` 覆写默认值,例如只启用经典天气时钟: ```ini CONFIG_LVGL_UI_INCLUDE_WELCOME=y CONFIG_LVGL_UI_INCLUDE_WEATHER_CLOCK=y CONFIG_LVGL_UI_INCLUDE_WEATHER_CLOCK_MODERN=n ``` ## 横屏模式说明 驱动层(display 组件)以**物理分辨率 (240x320)** 初始化,`display_set_rotation(DISPLAY_ROTATION_90)` 会同时: 1. 操作 ST7789 硬件 MADCTL 寄存器完成坐标映射(swap_xy + mirror); 2. **自动交换有效分辨率**,使 `display_get_width()/get_height()` 返回旋转后的逻辑尺寸(横屏 320x240)。 因此显示层(lvgl_ui)在 320x240 逻辑空间渲染,并**直接取 `display_get_width()/get_height()`** 即可得到正确尺寸,无需在应用层维护本地横屏尺寸。LVGL flush 回调把逻辑坐标直接传给 `display_draw_area()`,硬件侧 swap_xy 已开启,自动完成物理地址映射。 > ⚠️ **调用顺序铁律**:`display_set_rotation(90)` 必须在 `lvgl_ui_display_attach()` **之前**调用,否则 LVGL 仍按 240x320 建显示设备,横屏右侧刷新区会被 `validate_area()` 拒绝而卡死。 > > ⚠️ **色序**:ST7789V 2.0" 屏多为 **BGR** 走线。若整屏红蓝互换,在 `sdkconfig.defaults` 设 `CONFIG_DISPLAY_RGB_ELEMENT_ORDER_BGR=y`(或 `display_config_t.rgb_order = DISPLAY_RGB_ORDER_BGR`);若改完更糟则改回 RGB。 ## 示例项目 ### 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. 在 `Kconfig` 中添加配置开关,在 `CMakeLists.txt` 中添加条件编译,在 `lvgl_ui.h` 中添加条件 `#include` ## 依赖项 - ESP-IDF v5.5+ - LVGL v9.5.0(依赖锁定,详见 `idf_component.yml`) - [display](https://gitee.com/NetADs/display) - ST7789 显示驱动组件 ## 兼容性 ESP32 / ESP32-S2 / ESP32-S3 / ESP32-C3 ## 许可证 MIT License ## 更新日志 ### v1.4.0 - 精简接口:移除 `get_panel` 回调字段,驱动接口仅保留 5 个必要字段 - 移除未使用的异步信号量机制,简化 init/deinit 流程 - 移除 `LVGL_UI_ERR_SEMAPHORE_FAILED` 状态码 - 移除天气时钟未实现的桩函数(`update_weather`、`set_theme`、`set_brightness`) - 移除 `weather_to_string()`、`weather_to_icon()` 未使用内联函数 - 移除 `theme_variant_t`、`brightness_mode_t` 未使用枚举类型 - 修正 `lvgl_ui_deinit()` 资源释放顺序:先显存后任务后 lv_deinit - 统一屏幕尺寸引用,`ui_welcome.c` 改为使用 `ui_config.h` - 移除 `ui_weather_clock_bak.c` 未编译备份文件 ### 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()` - 新增异步信号量机制 - 新增错误码 `LVGL_UI_ERR_SEMAPHORE_FAILED` - 屏幕文件(ui_welcome / ui_weather_clock)所有公开函数加锁保护 ### v1.1.0 - 新增统一初始化/反初始化接口 - 新增页面管理引擎 - 新增页面预释放回调约定 ### v1.0.0 - 初始版本