目录
[使用 FreeRTOS 优化 ESP32-S3 LVGL 界面刷新并实现屏幕亮度调节](#使用 FreeRTOS 优化 ESP32-S3 LVGL 界面刷新并实现屏幕亮度调节)
[1. LVGL 工作机制与 FreeRTOS 的必要性](#1. LVGL 工作机制与 FreeRTOS 的必要性)
[2. 创建 LVGL 刷新任务(绑定 Core 1)](#2. 创建 LVGL 刷新任务(绑定 Core 1))
[2.1 任务函数声明](#2.1 任务函数声明)
[2.2 在 setup() 中创建任务](#2.2 在 setup() 中创建任务)
[2.3 实现任务函数](#2.3 实现任务函数)
[3. 实现屏幕亮度调节(滑块控制 PWM)](#3. 实现屏幕亮度调节(滑块控制 PWM))
[3.1 SquareLine Studio 中的设计](#3.1 SquareLine Studio 中的设计)
[3.2 编写滑块事件回调](#3.2 编写滑块事件回调)
[3.3 全局变量 Set 的定义](#3.3 全局变量 Set 的定义)
[3.4 硬件注意事项](#3.4 硬件注意事项)
[4. 效果与总结](#4. 效果与总结)
[5. 扩展思考](#5. 扩展思考)
使用 FreeRTOS 优化 ESP32-S3 LVGL 界面刷新并实现屏幕亮度调节
在嵌入式 GUI 开发中,LVGL 是一个轻量且强大的图形库,但它需要定期调用 lv_timer_handler() 来处理屏幕刷新、触摸事件、动画等任务。如果直接在 Arduino 的 loop() 中执行耗时操作(如网络请求、文件读写),就会导致界面卡顿。借助 FreeRTOS 的多任务特性,我们可以将 LVGL 刷新任务独立出来,并绑定到专用 CPU 核心,从而保证界面流畅。本文以 ESP32-S3 为例,介绍如何创建 LVGL 刷新任务,并通过滑块动态调节屏幕亮度。
1. LVGL 工作机制与 FreeRTOS 的必要性
LVGL 是一个轮询驱动 的 GUI 库,核心函数 lv_timer_handler() 需要每隔几毫秒(通常 5ms)调用一次,以便:
-
刷新屏幕(将脏区域数据推送到显示器)
-
处理触摸/按键输入并触发事件回调
-
执行动画进度更新
-
运行内部定时器(如长按检测、滚动惯性)
在简单程序中,可以直接在 loop() 中调用:
cs
cpp
void loop() {
lv_timer_handler();
delay(5);
}
但这种方式的局限性很明显:
-
阻塞问题 :如果
loop()中有耗时代码(如WiFi.scanNetworks()),LVGL 刷新就会被推迟,界面会卡顿。 -
多任务冲突:无法同时处理 WiFi 连接、音频播放、传感器采集等任务。
-
代码维护困难:所有逻辑挤在一个循环中,耦合度高。
FreeRTOS 的解决方案:
-
将 LVGL 刷新、网络通信、音频播放等拆分为独立任务,互不阻塞。
-
利用优先级调度,确保高优先级的 GUI 任务始终流畅。
-
充分利用 ESP32-S3 的双核架构:将 LVGL 刷新任务固定在 Core 1,其他任务放在 Core 0,实现真正的并行处理。
2. 创建 LVGL 刷新任务(绑定 Core 1)
2.1 任务函数声明
在 main.cpp 顶部声明任务函数:
cs
cpp
void lvgl_task(void *pt);
2.2 在 setup() 中创建任务
完成 LVGL、显示器、触摸初始化后,调用 ui_init() 加载 SquareLine Studio 设计的界面,然后创建 FreeRTOS 任务:
cs
cpp
void setup() {
Serial.begin(115200);
// 初始化 LVGL、显示器、触摸等...
lv_init();
tft.begin();
touch.begin();
ui_init(); // 加载 SquareLine Studio 设计的界面
// 创建 LVGL 刷新任务,绑定到 Core 1
xTaskCreatePinnedToCore(
lvgl_task, // 任务函数
"lvgl_task", // 任务名称
1024 * 15, // 栈大小(15KB,LVGL 需要较大栈空间)
NULL, // 任务参数
2, // 优先级(数字越大优先级越高,GUI 建议设为 2)
NULL, // 任务句柄(不需要时可为 NULL)
1 // 绑定到 Core 1(ESP32-S3 的第二个核心)
);
Serial.println("Setup done");
}
// loop() 可留空,所有任务由 FreeRTOS 调度
void loop() {}
2.3 实现任务函数
在任务函数中无限循环调用 lv_timer_handler(),并使用 vTaskDelay() 让出 CPU:
cs
cpp
void lvgl_task(void *pt) {
while (1) {
lv_timer_handler(); // LVGL 核心刷新
vTaskDelay(5); // 延时 5ms 并让出 CPU 给其他任务
}
}
关键点:
-
任务必须是一个永不退出的无限循环。
-
vTaskDelay(5)相当于 Arduino 的delay(5),但不会阻塞其他任务。 -
栈大小根据实际需求调整,LVGL 界面复杂时可适当增加。
3. 实现屏幕亮度调节(滑块控制 PWM)
3.1 SquareLine Studio 中的设计


在 SquareLine Studio 中,我们可以在"设置"屏幕(如 ui_Set)中添加一个滑块(Slider) 和一个**标签(Label)**用于显示亮度百分比。命名约定:
-
滑块对象名:
ui_SliderLight -
显示数值的标签:
ui_LabelLightValue
3.2 编写滑块事件回调
在 SquareLine Studio 生成的事件文件(如 ui_events.c 或直接在 ui.c 中)中,找到或添加滑块的事件回调函数。当滑块值变化时(LV_EVENT_VALUE_CHANGED),我们需要:
-
获取滑块当前值(0-100)。
-
更新标签文本(可用
_ui_slider_set_text_value辅助函数)。 -
将亮度值转换为 PWM 占空比(0-255),通过
analogWrite输出到背光引脚(本例中为 GPIO 16)。
示例代码(SquareLine Studio 生成的代码风格可能略有不同,但逻辑一致):
cs
cpp
void ui_event_SliderLight(lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
lv_obj_t * target = lv_event_get_target(e);
if (event_code == LV_EVENT_VALUE_CHANGED) {
// 更新标签显示当前亮度值(带 % 符号)
_ui_slider_set_text_value(ui_LabelLightValue, target, "", "%");
// 获取滑块值(0-100)并保存到全局变量
Set.screen_light = lv_slider_get_value(target);
// 通过 PWM 控制背光引脚(值映射到 0-255)
if (Set.screen_light > 0) {
analogWrite(16, Set.screen_light * 2.55); // 2.55 = 255/100
} else {
analogWrite(16, 0); // 关闭背光
}
}
}
3.3 全局变量 Set 的定义
我们需要在某个头文件(如 config.h)中定义保存亮度值的结构体,并声明全局变量:
cpp
c
// config.h
typedef struct {
int screen_light; // 屏幕亮度 0-100
// 其他设置项(如 WiFi 配置)暂时省略
} cfg_set;
extern cfg_set Set;
在对应的 .c 文件(如 config.c)中初始化:
cpp
c
#include "config.h"
cfg_set Set = {
.screen_light = 100, // 默认亮度 100%
// 其他成员初始化...
};
然后在 main.cpp 中包含该头文件即可使用 Set.screen_light。
3.4 硬件注意事项
-
背光引脚 :本例中背光接在 GPIO 16,需确保该引脚支持 PWM 输出(ESP32-S3 所有 GPIO 均可通过
analogWrite实现软件 PWM,或使用硬件 LEDC 通道)。 -
PWM 初始化 :如果使用
analogWrite,通常无需额外初始化(Arduino 框架会自动处理)。但为了确保正确,可以在setup()中设置引脚模式:cppcpp pinMode(16, OUTPUT); -
亮度映射:滑块值范围 0-100,PWM 值范围 0-255,因此乘以 2.55。当亮度为 0 时直接输出 0 关闭背光。
4. 效果与总结
编译并烧录程序后,你会看到:
-
LVGL 界面流畅刷新,不受任何后台任务影响。
-
滑动亮度滑块时,屏幕背光实时变化,同时标签显示当前亮度百分比。
-
所有任务和谐共存:LVGL 刷新在 Core 1,其他任务(如未来要添加的 WiFi 扫描、音乐播放)可在 Core 0 上运行,互不干扰。
为什么要这样做?
-
用户体验:界面永远不卡,响应及时。
-
代码结构:任务分离,逻辑清晰,易于维护和扩展。
-
硬件潜力:充分利用双核性能,让嵌入式设备也能拥有流畅的图形交互。
5. 扩展思考
-
如果遇到 PWM 输出不稳定,可以改用 ESP32 的 LEDC 硬件 PWM 库,精度更高且不占用 CPU 时间。
-
如需保存亮度设置到非易失存储(如 Preferences),可在滑块事件中写入,并在启动时读取恢复。
-
后续我们将继续添加 WiFi 连接、天气更新、音乐播放等功能,并分别创建独立任务,敬请期待!