LVGL 集成与应用
AC792 SDK 在 sdk/apps/common/ 下同时集成了 LVGL v8 与 v9 两套图形库源码及其中间层适配代码,为应用提供独立 UI 任务、事件驱动刷新、按键/触摸/编码器输入通道、资源文件系统挂载与 RPC 跨线程调用等能力。
Purpose and Scope
本文档说明 LVGL 在 fw-AC792_SDK 中的集成方式与上层应用机制,覆盖:
sdk/apps/common/lvgl_v9/(v9 主线)与sdk/apps/common/lvgl_v8/(v8 兼容线)的目录结构与差异;- LVGL 主任务的创建、消息队列驱动的事件循环与
lv_timer_handler()刷新机制; - 按键(含长按/短按拆分、编码器)、触摸中断与轮询定时器的输入通道实现;
- 跨线程安全调用 LVGL 的 RPC 通道(
lvgl_rpc_post_func); - UI 任务挂起/恢复(低功耗联动)与全屏脏矩形刷新;
- GUI 初始化流程、演示应用、bin 图片/RLE 字体资源加载与 Monkey 自动化测试。
以下主题不在本页范围:具体 LCD 驱动(SPI/MCU 接口)、触摸屏硬件驱动、文件系统/存储设备挂载细节、具体 App 的页面逻辑,它们属于各自的驱动与应用文档。
Overview
LVGL(Light and Versatile Graphics Library)是面向嵌入式设备的开源图形库,提供控件、样式、动画与输入设备抽象。在 AC792 SDK 中,LVGL 被封装为一个独立的任务(lvgl_main_task),而非在中断或其它任务中直接调用,其设计动机是:
- 线程安全:LVGL 本身不是完全线程安全的,所有
lv_*API 必须集中在 UI 任务中执行;其它任务(如蓝牙、音频、按键扫描)只能通过消息队列或 RPC 通道间接驱动 UI。 - 事件驱动刷新:UI 任务平时阻塞在
os_taskq_pend_timeout()上,由触摸、按键、定时器超时等消息唤醒,仅在需要时执行lv_timer_handler(),避免空转消耗 CPU 与功耗。 - 低功耗协同:通过
lvgl_ui_suspend()/lvgl_ui_resume()与系统电源管理联动,挂起期间丢弃输入事件并停止刷新。
SDK 提供 lvgl_v8 与 lvgl_v9 两套源码树,由编译宏选择使用;本页以 v9 的实现为主说明机制,v8 结构与思路一致。
Architecture
下图展示了 LVGL 在 AC792 SDK 中的整体架构与数据流(基于 sdk/apps/common/lvgl_v9/lvgl_main.c 与 lvgl_v9_main_task_init.c 的实际代码):
flowchart TD
subgraph sg_Events["事件产生源(其他任务/中断)"]
KeyScan["按键扫描任务<br/>key_event"]
TouchDev["触摸中断<br/>lcd_touch_interrupt_event"]
AppTask["业务任务<br/>lvgl_rpc_post_func"]
SysTimer["系统定时器<br/>sys_hi_timeout_add"]
end
subgraph sg_Adapter["中间层适配(lvgl_main.c)"]
KeyH1["lvgl_key_event_handler"]
KeyH2["lvgl_key_event_handler_2<br/>长按/短按拆分"]
TouchH["lcd_touch_interrupt_event<br/>+ 轮询定时器"]
RPC["lvgl_rpc_post_func<br/>函数指针投递"]
TimeoutH["lvgl_timer_event_timeout"]
SusRes["_lvgl_ui_task_suspend_resume<br/>信号量同步"]
end
subgraph sg_UITask["LVGL 主任务(LVGL_TASK_NAME)"]
Loop["lvgl_main_task 消息循环<br/>os_taskq_pend_timeout"]
Dispatch["UI_MSG_* 分发<br/>TOUCH/KEY/ENCODER/TIMEOUT/SUSPEND/RESUME/RPC"]
TimerH["lv_timer_handler()<br/>定时器+刷新"]
GUIInit["lvgl_v9_gui_init<br/>lv_init + port 初始化 + demo"]
end
subgraph sg_LVGL["LVGL 图形库"]
Core["lv_init / lv_timer / lv_obj"]
DispPort["lv_port_disp_init 显示驱动"]
IndevPort["lv_port_indev_init 输入驱动"]
FsPort["lv_port_fs_init 文件系统"]
end
subgraph sg_Demo["应用层"]
Demo["lv_demo_widgets / benchmark"]
UserUI["用户 UI(create_my_label 等)"]
Monkey["LV_USE_MONKEY 自动化测试"]
end
KeyScan --> KeyH1
KeyScan --> KeyH2
TouchDev --> TouchH
AppTask --> RPC
SysTimer --> TimeoutH
KeyH1 -->|"os_taskq_post_type Q_USER"| Loop
KeyH2 -->|"os_taskq_post_type Q_USER"| Loop
TouchH -->|"UI_MSG_TOUCH"| Loop
RPC -->|"UI_MSG_RPC_FUNC"| Loop
TimeoutH -->|"UI_MSG_TIMER_TIMEOUT"| Loop
SusRes -->|"UI_MSG_SUSPEND/RESUME"| Loop
Loop --> Dispatch
Dispatch --> TimerH
Dispatch --> GUIInit
GUIInit --> Core
Core --> DispPort
Core --> IndevPort
Core --> FsPort
GUIInit --> Demo
GUIInit --> UserUI
GUIInit --> Monkey
UserUI -->|"lv_timer_create 周期刷新"| TimerH
架构要点说明:
- 事件产生源(按键扫描任务、触摸中断、业务任务、系统定时器)均处于 UI 任务之外;它们唯一的职责是构造
int消息数组并投递到LVGL_TASK_NAME的任务队列(Q_USER或Q_MSG),绝不直接调用lv_*API。 - 中间层适配(
lvgl_main.c)承担“事件 → 消息”的翻译,包括按键去重/补发、长按短按拆分、触摸轮询定时器启停、RPC 参数打包、挂起恢复的同步等待。 - LVGL 主任务是唯一允许触碰 LVGL 内部状态的上下文:它从队列取消息、分发到对应处理分支、周期调用
lv_timer_handler()驱动动画/控件刷新,并在挂起时跳过刷新。 - 端口层(
lv_port_disp_init/lv_port_indev_init/lv_port_fs_init)把 LVGL 与具体硬件(LCD 帧缓冲、触摸、存储设备)解耦,是移植到不同硬件板卡的唯一需要改动的地方。 - 应用层通过弱函数钩子
lvgl_v9_gui_init/lvgl_v9_main_task_hook接入自定义 UI,也可编译进官方 demo 与 Monkey 自动化测试。
主任务与事件循环
UI 主任务入口为 lvgl_main_task(void *priv),其核心结构(基于 lvgl_main.c):
void lvgl_main_task(void *priv)
{
lvgl_suspend_flag = 0;
int msg[8] = {0};
int ret;
lvgl_touch_timer_status = 0;
char lvgl_task_pend_timeout_refr_flag = 0;
u32 time_till_next;
lvgl_v9_gui_init();
//刷新第一帧不需要事件触发
time_till_next = lv_timer_handler();
if (time_till_next != 0xFFFFFFFF) {
sys_hi_timeout_add(NULL, lvgl_timer_event_timeout, time_till_next);
}
while (1) {
ret = os_taskq_pend_timeout(msg, ARRAY_SIZE(msg), portMAX_DELAY);
if (ret != OS_TASKQ && ret != OS_TIMEOUT) {
printf("lvgl_main_task os_taskq_pend err=%d %d\n", ret, msg[0]);
continue;
}
lvgl_v9_main_task_hook();
...
}
}
Source: lvgl_main.c
执行流程:
- 任务启动后先清挂起标志,初始化触摸轮询状态;
- 调用
lvgl_v9_gui_init()完成 LVGL 内核与端口初始化,并创建首屏 UI; - 主动刷新第一帧:直接调用一次
lv_timer_handler(),得到下次需要刷新的时间time_till_next,若有效(非0xFFFFFFFF)则通过sys_hi_timeout_add注册一个到期的内核定时器回调lvgl_timer_event_timeout—— 这是“事件驱动刷新”的起点:LVGL 自身的lv_timer到期由系统定时器换算成队列消息,而不是让 UI 任务忙等; - 进入
while(1)无限循环,阻塞在os_taskq_pend_timeout(msg, ..., portMAX_DELAY)上等待消息或定时器超时; - 每次被唤醒都先调用弱函数钩子
lvgl_v9_main_task_hook()(应用可重写实现周期性业务),再按msg[0]的类型分发处理。
设计意图:把“LVGL 定时器到期”与“外部事件”统一抽象为队列消息,使 UI 任务可以在没有任何事件时完全休眠,仅在需要时醒来做一次 lv_timer_handler(),从而把 UI 刷新功耗降到最低——这是嵌入式 UI 与桌面 GUI 最本质的差异。
消息类型与事件分发
所有进入 UI 任务的消息都通过 msg[0] 携带类型标识。类型枚举定义于 lvgl_main.c:
enum {
UI_MSG_TOUCH = 1, // 触摸事件(首次触摸或轮询使能)
UI_MSG_ENCODER, // 编码器旋钮事件(RDEC_UP/RDEC_DOWN)
UI_MSG_KEY, // 普通按键事件
UI_MSG_TIMER_TIMEOUT, // LVGL 定时器到期(由 sys_hi_timeout 触发)
UI_MSG_SUSPEND, // 挂起 UI(进入低功耗)
UI_MSG_RESUME, // 恢复 UI(全屏刷新)
UI_MSG_RPC_FUNC, // 跨线程函数调用(RPC 通道)
};
Source: lvgl_main.c
此外还有一组“用户类型”用于消息池回收管理,其取值特意从 0x10000 开始并注释“不可更改”,因为该值决定了删除消息池时如何识别 LVGL 专用消息:
enum LVGL_Q_USER_TYPE {
LVGL_Q_USER_TYPE_TOUCH = 0x10000,//从这个立即数开始是为了删消息池使用,不可更改
LVGL_Q_USER_TYPE_KEY = 0x20000,
};
Source: lvgl_main.c
分发逻辑位于 lvgl_main_task 的消息循环中:msg[0] 为 UI_MSG_KEY/UI_MSG_ENCODER/UI_MSG_TOUCH 时调用 lv_indev_* 系列接口注入输入;UI_MSG_TIMER_TIMEOUT 时调用 lv_timer_handler() 并重新注册下一个超时;UI_MSG_SUSPEND/UI_MSG_RESUME 时切换 lvgl_suspend_flag 并回调应用回调;UI_MSG_RPC_FUNC 时调用 lvgl_rpc_post_func_deal 解包函数指针与参数执行。
按键事件处理
直接投递模式:lvgl_key_event_handler
该处理器把按键事件原样投递给 UI 任务,但对消息丢失做了幂等与补发保护(lvgl_main.c):
int lvgl_key_event_handler(struct sys_event *event)
{
if (lvgl_suspend_flag) {
return 0; // UI 挂起时丢弃按键
}
struct key_event *key = (struct key_event *)event->payload;
if (key->key_intercept) {
return 0; // 被其它模块截获的按键不再投递
}
if ((key->action == KEY_EVENT_UP) && (key->value != s_last_key_value)) {
puts("lvgl_key_event_handler post failed 1");
return -EINVAL;
}
if (key_event_drop_timeout_id) {
puts("lvgl_key_event_handler post failed 2");
return -EINVAL;
}
if (lvgl_key_msg_remain_cnt > 2) {
puts("lvgl_key_event_handler post failed 3");
goto _retry;
}
int msg[2 + sizeof(struct key_event) / 4];
msg[0] = UI_MSG_KEY;
memcpy(&msg[1], key, sizeof(struct key_event));
if (os_taskq_post_type(LVGL_TASK_NAME, Q_USER, ARRAY_SIZE(msg), msg)) {
puts("lvgl_key_event_handler post failed 4");
goto _retry;
} else {
s_last_key_value = key->value;
++lvgl_key_msg_remain_cnt;
}
return -EINVAL;
_retry:
//由于LVGL不可丢失按键抬起消息, 判断丢失的消息是按键抬起消息才做重发动作
if (key->action == KEY_EVENT_UP && key->value == s_last_key_value) {
memcpy(&s_drop_key_event, key, sizeof(struct key_event));
key_event_drop_timeout_id = sys_timeout_add_to_task(LVGL_TASK_NAME, NULL, key_event_drop_timeout_handler, 30);
}
return -EINVAL;
}
Source: lvgl_main.c
要点:
- 挂起丢弃:
lvgl_suspend_flag为真时直接返回,避免低功耗状态下被唤醒; - 拦截过滤:
key_intercept标志表示按键已被系统其它模块消费; - 抬起匹配:
KEY_EVENT_UP必须与上一次记录的键值一致,防止“没有按下就抬起”的异常消息; - 消息积压保护:
lvgl_key_msg_remain_cnt > 2时进入_retry,避免队列被按键淹没; - 抬起消息不可丢:LVGL 的按键状态机要求 down/up 成对出现,因此当投递失败且丢失的是抬起消息时,用 30ms 延迟定时器
key_event_drop_timeout_handler补发一次。补发逻辑见 lvgl_main.c。
长按/短按拆分模式:lvgl_key_event_handler_2
当同一按键需要区分短按与长按的 UI 功能时使用该处理器。其注释明确说明了设计动机(lvgl_main.c):
/*
* down事件不发给UI,等到按键抬起后up事件拆分为2次事件发给UI
* 这样做的好处是能区分出短按和长按,例如按键短按和长按分别有不同UI功能时
* */
int lvgl_key_event_handler_2(struct sys_event *event)
Source: lvgl_main.c
处理规则:
- 编码器优先:
KEY_EVENT_RDEC_UP/KEY_EVENT_RDEC_DOWN直接打包为UI_MSG_ENCODER投递,不参与按键拆分; - 按下暂存:
KEY_EVENT_DOWN只记录s_last_key_value与s_last_key_action,不发给 UI; - 抬起拆分:按键抬起时把一次 up 拆成
down → up两次事件按序投递,UI 因此能感知“完整的按下-抬起”过程;若此前发生过KEY_EVENT_HOLD/KEY_EVENT_LONG,则只发一次 up(长按后的抬起 UI 只响应一次); - 长按/保持/单击直发:
KEY_EVENT_LONG、KEY_EVENT_HOLD、KEY_EVENT_CLICK属于派生事件,直接投递; - 与模式一相同,投递失败且丢失抬起消息时走 30ms 补发重试。
这种“抬起时才分发”的策略把短按/长按的判定交给按键扫描层,UI 层收到的始终是语义化的事件序列,降低了 UI 状态机的复杂度。
触摸事件处理
触摸通道由中断入口 lcd_touch_interrupt_event 与轮询定时器配合实现(lvgl_main.c):
int lcd_touch_interrupt_event(const char *tp_task_name, u16 x, u16 y, u8 status)
{
int err;
if (lvgl_suspend_flag) {
return 0;
}
if (status) {
int msg[6];
//检测到触摸,创建定时器启用轮询
if (!lvgl_touch_timer_status) {//这个标志是用来防止多次发消息
msg[0] = UI_MSG_TOUCH;
msg[1] = (int)x;
msg[2] = (int)y;
msg[3] = (int)status;
err = os_taskq_post_type(LVGL_TASK_NAME, Q_USER, ARRAY_SIZE(msg), msg);
if (err) {
printf("lvgl touch msg drop2 err = %d\n", err);
return err;
}
lvgl_touch_timer_status = 1;
} else {
//确保定时器创建成功
if (lv_indev_set_touch_timer_check() == false) {
msg[0] = UI_MSG_TOUCH;
...
}
}
return 0;
}
return 0;
}
Source: lvgl_main.c
工作原理:
- 中断只做“第一下”:触摸按下中断到达时,若轮询定时器尚未开启(
lvgl_touch_timer_status == 0),则投递UI_MSG_TOUCH消息并在 LVGL 侧创建轮询定时器;此后手指移动/持续按压期间由定时器周期读取坐标,中断不再逐点投递,显著降低中断频率; - 防重入:
lvgl_touch_timer_status标志防止重复投递“启动轮询”消息; - 兜底重试:若定时器实际未创建成功(
lv_indev_set_touch_timer_check() == false),再次以消息方式通知 UI 任务; - 坐标读取结果通过
lv_port_get_touch_x_y_status()(见 lvgl_main.c)从struct touch_event中取出 x/y/action,再经lv_indev_timer_read_touch()注入 LVGL 输入设备(lvgl_main.c)。
设计意图:电阻/电容触摸在持续按压时会产生大量坐标数据,若全部走中断与消息队列会挤占 UI 任务带宽;采用“中断启动 + 定时器轮询”的分层策略,让触摸在静止时几乎零开销,按压过程中也只有一次轮询周期内的刷新。
定时器驱动与刷新机制
LVGL 自身的 lv_timer 与动画时间轴由 lv_timer_handler() 推进。SDK 采用“系统定时器 → 队列消息 → UI 任务”的三级桥接,把 LVGL 的时间需求翻译为嵌入式 RTOS 的事件(lvgl_main.c):
static void lvgl_timer_event_timeout(void *p)
{
int err;
int msg[2];
msg[0] = UI_MSG_TIMER_TIMEOUT;
lvgl_timeout_id = 0;
err = task_queue_post_event(LVGL_TASK_NAME, msg, ARRAY_SIZE(msg));
if (err) {
printf("lvgl_timer_event_timeout post_ui_msg err=%d\n", err);
} else {
lvgl_timerout_msg_remain_flag = 1;
}
}
Source: lvgl_main.c
闭环流程:
- UI 任务调用
lv_timer_handler()返回“距离下一个定时器/动画到期的时间time_till_next”; - 若该时间有效(不等于
0xFFFFFFFF),注册系统高精度定时器sys_hi_timeout_add(NULL, lvgl_timer_event_timeout, time_till_next); - 到期后系统定时器回调投递
UI_MSG_TIMER_TIMEOUT消息; - UI 任务收到消息后再次调用
lv_timer_handler(),重复步骤 1。
这样 LVGL 的动画、控件闪烁、lv_timer_create 创建的周期任务(如 1 秒刷新一次的 Label)都无需 UI 任务忙等,刷新时机完全由事件驱动。lv_tick_set_cb(&timer_get_ms) 把 LVGL 的毫秒时钟绑定到 SDK 的 timer_get_ms()(见 lvgl_main.c)。
RPC 跨线程调用通道
业务任务(如蓝牙、音频、网络)经常需要驱动 UI,但直接调用 lv_* API 存在线程安全问题。SDK 提供 lvgl_rpc_post_func 把“函数指针 + 最多 5 个 int 参数”打包成消息投递到 UI 任务,在 UI 上下文中执行(lvgl_main.c):
int lvgl_rpc_post_func(void (*rpc_func)(void *, ...), int argc, ...)
{
int err;
if (lvgl_suspend_flag) {
printf("lvgl_rpc_post suspended \n");
return -1;
}
int msg[8];
msg[0] = UI_MSG_RPC_FUNC;
msg[1] = argc;
msg[2] = (int)rpc_func;
ASSERT(argc <= ARRAY_SIZE(msg) - 3, "%s argc too long!\n", __FUNCTION__);
va_list argptr;
va_start(argptr, argc);
for (int i = 0; i < argc; i++) {
msg[i + 3] = va_arg(argptr, int);
}
va_end(argptr);
_RETRY:
err = os_taskq_post_type(LVGL_TASK_NAME, Q_USER, ARRAY_SIZE(msg), msg);
if (err) {
printf("lvgl_rpc_post_func err=%d\n", err);
msleep(20);
goto _RETRY;
}
return 0;
}
Source: lvgl_main.c
UI 侧的解包执行函数 lvgl_rpc_post_func_deal 依据 argc(0~5)把 msg[2] 中的函数指针与 msg[3..] 中的参数按相应原型调用(lvgl_main.c)。
设计意图与约束:
- 参数只能是 int 宽度的值(指针地址、枚举、小整数),传递
void*时需自行保证指针生命周期; - 投递失败会以 20ms 间隔无限重试(
_RETRY),因此在挂起状态下调用会直接返回-1,调用方需自行决定是否重试; - 这是 SDK 推荐的“任意任务驱动 UI”的唯一官方通道,也是后续 App 页面收到业务事件的标准入口。
UI 挂起与恢复(低功耗协同)
_lvgl_ui_task_suspend_resume 是内部实现,通过 UI_MSG_SUSPEND/UI_MSG_RESUME 消息与信号量实现跨任务同步(lvgl_main.c):
void _lvgl_ui_task_suspend_resume(uint8_t state, void *cb)
{
OS_SEM sem_wait;
int err;
int msg[4];
u8 is_need_wait = 1;
msg[0] = state ? UI_MSG_RESUME : UI_MSG_SUSPEND;
msg[1] = (int)cb;
msg[2] = state;
msg[3] = (int)&sem_wait;
if (!strcmp(os_current_task(), LVGL_TASK_NAME)) {
//当前如果不在lvgl线程则需要等待
is_need_wait = 0;
msg[3] = 0;
}
os_sem_create(&sem_wait, 0);
err = os_taskq_post_type(LVGL_TASK_NAME, Q_MSG, ARRAY_SIZE(msg), msg);
if (err) {
printf("lvgl_ui_start_stop post err=%d\n", err);
} else {
if (is_need_wait) {
err = os_sem_pend(&sem_wait, 300);
ASSERT(err != OS_TIMEOUT, "%s %s no response\n", __FUNCTION__, state ? "RESUME" : "SUSPEND");
}
}
os_sem_del(&sem_wait, OS_DEL_ALWAYS);
}
Source: lvgl_main.c
对外 API(lvgl_main.c):
void lvgl_ui_suspend(void) { _lvgl_ui_task_suspend_resume(0, _lv_ui_suspend_resume_cb); }
void lvgl_ui_resume(void) { _lvgl_ui_task_suspend_resume(1, _lv_ui_suspend_resume_cb); }
u8 lvgl_ui_is_suspended(void) { return lvgl_suspend_flag; }
Source: lvgl_main.c
行为说明:
- 挂起时
lvgl_suspend_flag = 1,按键/触摸/RPC 入口全部短路丢弃,UI 任务不再刷新,屏幕进入静态画面或由电源管理关闭背光; - 恢复时回调
_lv_ui_suspend_resume_cb(1),其内部把整个屏幕区域(从(0,0)到(hor_res-1, ver_res-1))标记为全屏脏矩形(_lv_inv_area),保证恢复后强制重绘一帧,避免残影(lvgl_main.c); - 调用方若不在 UI 任务内,会阻塞等待 UI 任务响应(300ms 超时并
ASSERT),保证“挂起/恢复完成”后才返回——这对系统休眠流程至关重要,避免屏幕/背光状态与 UI 状态不一致。
GUI 初始化与演示应用
lvgl_v9_gui_init 是应用可重写的强符号(非 weak 版本在 lvgl_v9_main_task_init.c 中),弱符号默认实现位于 lvgl_main.c,两者都执行同一套初始化序列:
void lvgl_v9_gui_init(void)
{
puts("lvgl_v9_gui_init \n\n");
lv_init();
lv_port_disp_init();
lv_port_indev_init();
lv_port_fs_init();
lv_tick_set_cb(&timer_get_ms);
#if LV_USE_MONKEY != 0
/*Create pointer monkey test*/
lv_monkey_config_t monkey_pointer_config;
monkey_pointer_config.type = LV_INDEV_TYPE_POINTER;
monkey_pointer_config.period_range.min = 20;
monkey_pointer_config.period_range.max = 20;
lv_monkey_set_enable(lv_monkey_create(&monkey_pointer_config), true);
/*Create keypad monkey test*/
lv_monkey_config_t monkey_keypad_config;
monkey_keypad_config.type = LV_INDEV_TYPE_KEYPAD;
monkey_keypad_config.period_range.min = 20;
monkey_keypad_config.period_range.max = 20;
lv_monkey_set_enable(lv_monkey_create(&monkey_keypad_config), true);
/*Create encoder monkey test*/
lv_monkey_config_t monkey_encode_config;
monkey_encode_config.type = LV_INDEV_TYPE_ENCODER;
monkey_encode_config.period_range.min = 20;
monkey_encode_config.period_range.max = 20;
monkey_encode_config.input_range.min = -5;
monkey_encode_config.input_range.max = 5;
lv_monkey_set_enable(lv_monkey_create(&monkey_encode_config), true);
#endif
/*lv_demo_benchmark();*/
lv_demo_widgets();
/*lv_demo_keypad_encoder();*/
}
Source: lvgl_v9_main_task_init.c
初始化顺序的用意:
lv_init()初始化 LVGL 内核(对象系统、样式、定时器、内存);lv_port_disp_init()注册显示驱动(帧缓冲、分辨率、刷新回调);lv_port_indev_init()注册输入设备(触摸指针、按键组、编码器),并建立与UI_MSG_TOUCH/UI_MSG_KEY/UI_MSG_ENCODER消息的映射;lv_port_fs_init()挂载 LVGL 文件系统驱动,使lv_img_set_src(img, "mnt/sdfile/...")等路径可以直接从存储设备读图;lv_tick_set_cb(&timer_get_ms)提供毫秒时基;LV_USE_MONKEY编译开关打开时创建 pointer/keypad/encoder 三种 Monkey 自动化输入(20ms 周期,编码器输入范围为 -5~5),用于压测 UI 稳定性;- 最后按需启动官方 demo(
lv_demo_widgets/lv_demo_benchmark/lv_demo_keypad_encoder),实际产品中应用应重写该函数替换为自己的页面。
任务入口的初始化函数 lvgl_main_task_init(lvgl_v9_main_task_init.c)由系统 late_initcall 机制(示例中被注释的 late_initcall(lvgl_v9_test))或 App 启动流程调用,创建 LVGL_TASK_NAME 任务并进入消息循环。
核心时序
下图展示一次典型的“按键 → UI 刷新”与“业务任务 RPC 驱动 UI”的完整时序(节点名称与函数均取自源码):
sequenceDiagram
participant KS as 按键扫描任务
participant TP as 触摸中断
participant APP as 业务任务
participant Q as LVGL 任务队列
participant UI as lvgl_main_task
participant LV as LVGL(lv_timer_handler)
participant HW as 显示驱动/屏幕
Note over KS,UI: 短按拆分模式 lvgl_key_event_handler_2
KS->>KS: KEY_EVENT_DOWN(暂存, 不投递)
KS->>KS: KEY_EVENT_UP(拆分 down+up)
KS->>Q: os_taskq_post_type(UI_MSG_KEY, Q_USER)
Q->>UI: 唤醒 os_taskq_pend_timeout
UI->>UI: lvgl_v9_main_task_hook()
UI->>LV: lv_indev 注入 down/up 事件
UI->>LV: lv_timer_handler() 处理动画/重绘
LV->>HW: flush 回调输出帧
HW-->>LV: 刷新完成
Note over APP,UI: RPC 跨线程调用
APP->>Q: lvgl_rpc_post_func(UI_MSG_RPC_FUNC)
Q->>UI: 唤醒
UI->>UI: lvgl_rpc_post_func_deal 解包
UI->>LV: 调用 rpc_func(业务更新控件)
Note over TP,UI: 触摸中断启动轮询
TP->>Q: lcd_touch_interrupt_event(UI_MSG_TOUCH)
Q->>UI: 唤醒
UI->>LV: lv_indev_timer_read_touch + 开启触摸轮询定时器
UI->>LV: lv_timer_handler() 周期性读坐标并刷新
流程要点:
- 按键扫描任务把物理事件整理成
struct key_event,在lvgl_key_event_handler_2中完成拆分/过滤; - 消息经
Q_USER队列唤醒 UI 任务;UI 任务先执行lvgl_v9_main_task_hook()钩子,再按消息类型处理; - 输入注入后调用
lv_timer_handler(),LVGL 内部完成对象状态推进、脏矩形收集与显示驱动 flush; - 业务任务驱动 UI 必须走 RPC 通道,避免跨线程直接操作控件;
- 触摸按下只发一次启动消息,持续按压由轮询定时器读取,降低中断压力。
使用示例
示例 1:创建动态刷新 Label(lv_timer 周期回调)
lvgl_v9_main_task_init.c 中的 create_my_label 展示了标准 UI 创建流程:设置屏幕背景/边框、创建顶层 overlay、创建 Label、用 lv_timer_create 每秒刷新计数(lvgl_v9_main_task_init.c):
static void create_my_label(void)
{
lv_obj_t *scr = lv_screen_active();
lv_obj_t *overlay;
lv_obj_t *label;
/* The root screen stays at the compose color key so only the real widgets cover the camera. */
lv_obj_set_style_bg_color(scr, lv_color_hex(0x55aaa5), LV_PART_MAIN);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_border_opa(scr, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_outline_opa(scr, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_shadow_opa(scr, LV_OPA_TRANSP, LV_PART_MAIN);
overlay = lv_obj_create(scr);
lv_obj_remove_style_all(overlay);
lv_obj_set_size(overlay, 280, 64);
lv_obj_align(overlay, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_bg_color(overlay, lv_color_hex(0x000000), 0);
lv_obj_set_style_bg_opa(overlay, LV_OPA_80, 0);
lv_obj_set_style_border_color(overlay, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_border_width(overlay, 2, 0);
lv_obj_set_style_radius(overlay, 8, 0);
lv_obj_clear_flag(overlay, LV_OBJ_FLAG_SCROLLABLE);
label = lv_label_create(overlay);
lv_label_set_text(label, "UI refresh test");
lv_obj_set_size(label, 240, LV_SIZE_CONTENT);
lv_obj_center(label);
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_font(label, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
lv_timer_create(label_update_timer_cb, 1000, label);
update_test_label(label);
}
static void label_update_timer_cb(lv_timer_t *timer)
{
lv_obj_t *label = timer ? lv_timer_get_user_data(timer) : NULL;
if (label == NULL) {
return;
}
update_test_label(label);
}
static void update_test_label(lv_obj_t *label)
{
static uint32_t refresh_cnt;
refresh_cnt++;
lv_label_set_text_fmt(label, "UI refresh ok\ncount: %04" LV_PRIu32, refresh_cnt);
}
Source: lvgl_v9_main_task_init.c
该示例验证了:定时器回调由 UI 任务在 lv_timer_handler() 中执行,因此回调内可以直接调用 lv_* API;同时 lv_timer_get_user_data 取出创建时传入的 label 指针作为上下文,是 LVGL 定时器惯用的数据传递方式。
示例 2:从文件系统加载 bin 图片与 RLE 压缩字体
lv_example_img_bin 演示了产品化资源加载方式——把图片与字体以 bin 文件放在 mnt/sdfile/EXT_RESERVED/uipackres/ui/ 目录,运行期加载而非编译进固件(lvgl_v9_main_task_init.c):
static void lv_example_img_bin(void)
{
LV_IMAGE_DECLARE(img_benchmark_cogwheel_argb);
static lv_style_t style_common;
lv_style_reset(&style_common);
lv_obj_t *img = lv_img_create(lv_scr_act());
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_obj_remove_style_all(img);
lv_obj_add_style(img, &style_common, 0);
//Img
lv_obj_set_pos(img, 330, 189);
//C数组
/* lv_img_set_src(img, &img_benchmark_cogwheel_argb); */
//bin文件
lv_img_set_src(img, "mnt/sdfile/EXT_RESERVED/uipackres/ui/logoargb.bin");
//Font
/* lv_font_t *font_bin = lv_font_load_bin("mnt/sdfile/EXT_RESERVED/uipackres/ui/font1.bin"); //不压缩字体 */
lv_font_t *font_bin = lv_font_load_bin("mnt/sdfile/EXT_RESERVED/uipackres/ui/font1.rle");//压缩字体
lv_obj_set_style_text_font(label, font_bin, LV_PART_MAIN);
/* lv_obj_set_style_text_font(label,&lv_font_montserrat_24, LV_PART_MAIN);//C数组 */
lv_label_set_text(label, "123");
lv_obj_set_pos(label, 100, 100);
}
Source: lvgl_v9_main_task_init.c
设计意图:注释中的对比说明了资源策略——C 数组方式(如 img_benchmark_cogwheel_argb、lv_font_montserrat_24)适合小资源/调试,bin 文件方式(含 .rle RLE 压缩字体)适合大资源产品化,可在不改固件的情况下替换 UI 资源,配合 lv_port_fs_init() 挂载的文件系统路径直接访问。示例同时提示:bin 文件加载依赖 SD 卡等存储设备挂载完成(文件开头有被 #if 0 屏蔽的 storage_device_ready() 等待逻辑,实际产品需要自行保证时序)。
API 参考
| 函数 | 职责 | 关键参数/返回值 |
|---|---|---|
lvgl_main_task(void *priv) | UI 主任务入口:初始化 GUI、进入消息循环、周期 lv_timer_handler | priv:任务参数,无返回 |
lvgl_key_event_handler(struct sys_event *event) | 按键事件直接投递模式 | 返回 -EINVAL 表示事件已消费/过滤 |
lvgl_key_event_handler_2(struct sys_event *event) | 长按/短按拆分模式,编码器事件走 UI_MSG_ENCODER | 同上 |
lcd_touch_interrupt_event(const char *tp_task_name, u16 x, u16 y, u8 status) | 触摸中断入口,首次按下启动轮询定时器 | 投递失败返回错误码 |
lv_set_touch_timer_status(u8 status) | 设置触摸轮询定时器开关状态 | status:0/1 |
lv_port_get_touch_x_y_status(struct touch_event *e, u16 *x, u16 *y, u8 *status) | 从触摸事件读取坐标与动作 | 输出 x/y/status |
lvgl_timer_event_timeout(void *p) | 系统定时器到期 → 投递 UI_MSG_TIMER_TIMEOUT | 无 |
lvgl_rpc_post_func(void (*rpc_func)(void *, ...), int argc, ...) | 跨线程投递函数调用(0~5 个 int 参数) | 挂起时返回 -1;成功返回 0 |
_lvgl_ui_task_suspend_resume(uint8_t state, void *cb) | 挂起/恢复内部实现,带信号量同步 | state:1 恢复 0 挂起 |
lvgl_ui_suspend(void) / lvgl_ui_resume(void) | 对外挂起/恢复 API | 无返回 |
lvgl_ui_is_suspended(void) | 查询 UI 是否挂起 | 返回 lvgl_suspend_flag |
lvgl_v9_gui_init(void) | 应用可重写:初始化 LVGL 内核/端口/demo | 弱符号默认实现见 lvgl_main.c |
lvgl_v9_main_task_hook(void) | 每次消息循环被唤醒时调用的弱钩子 | 应用可重写做周期性业务 |
lvgl_main_task_init(void) | 创建并启动 LVGL 主任务 | 无返回 |
配置选项
| 配置项 | 类型 | 默认/示例 | 说明 |
|---|---|---|---|
USE_LVGL_V9_UI_DEMO | 编译宏 | 定义时启用 v9 入口 | 控制 lvgl_v9_main_task_init.c 是否参与编译(见 lvgl_v9_main_task_init.c) |
LV_USE_MONKEY | LVGL 配置宏 | 非 0 时启用 | 创建 pointer/keypad/encoder 三种 Monkey 自动化输入,周期 20ms |
LV_TASK_NAME | 宏 | LVGL 任务名 | 任务队列投递目标名(os_taskq_post_type(LVGL_TASK_NAME, ...)) |
lvgl_suspend_flag | 静态变量 | 1(启动后清 0) | UI 挂起标志,为真时丢弃所有输入事件 |
lvgl_touch_timer_status | 静态变量 | 0 | 触摸轮询定时器开关,0=关闭 1=开启 |
lvgl_key_msg_remain_cnt | 静态变量 | 0 | 队列中未消费按键消息计数,>2 触发丢弃保护 |
key_event_drop_timeout_id | 静态变量 | 0 | 补发定时器句柄,非 0 表示正在补发 |
| 资源路径 | 字符串 | mnt/sdfile/EXT_RESERVED/uipackres/ui/ | bin 图片与 RLE 字体的默认存放目录 |
故障模式、边界情况与并发
消息丢失与补发
LVGL 的按键状态机要求 down/up 成对,因此抬起消息不可丢失。代码通过两层保护实现:
lvgl_key_msg_remain_cnt > 2时暂停投递(防积压);- 投递失败且丢失的是抬起消息时,
_retry分支把事件存入s_drop_key_event,并用sys_timeout_add_to_task(..., 30)在 30ms 后经key_event_drop_timeout_handler补发(lvgl_main.c)。
设计权衡:补发只针对抬起事件,是因为丢 down 只会让 UI 少一次按下响应,而丢 up 会让控件“卡在按下状态”,对交互影响更大。
线程安全与跨任务调用
- 铁律:所有
lv_*API 只能在lvgl_main_task上下文中调用; - 业务任务若直接调用
lv_*属于未定义行为,必须改走lvgl_rpc_post_func; - RPC 通道在挂起时返回
-1,且投递失败会无限重试(20ms 间隔),调用方要避免在会阻塞 UI 的临界区中调用,防止死锁; _lvgl_ui_task_suspend_resume通过 300ms 超时信号量等待 UI 任务响应,若 UI 任务长时间不处理Q_MSG消息会触发ASSERT——因此不要在 UI 任务的回调(如lv_timer回调、lvgl_v9_main_task_hook)中调用挂起/恢复,否则会自锁(代码中os_current_task() == LVGL_TASK_NAME时已跳过等待,正是为了处理部分此类场景)。
挂起期间的输入丢弃
lvgl_key_event_handler、lvgl_key_event_handler_2、lcd_touch_interrupt_event、lvgl_rpc_post_func 四处在入口都检查 lvgl_suspend_flag。这意味着:
- 睡眠唤醒过程中产生的按键会被静默丢弃,产品需在 App 层决定是否需要“补读键”逻辑;
- RPC 调用在挂起时直接返回
-1,业务任务应根据返回值决定是否在恢复后重试。
触摸轮询状态机
lvgl_touch_timer_status 只允许 0/1 两态:0 表示轮询关闭,1 表示轮询开启。lcd_touch_interrupt_event 依赖它做防重入;若中断在“轮询已开启但定时器尚未创建成功”的窗口内到达,lv_indev_set_touch_timer_check() == false 分支会再次投递消息兜底。该状态机保证了触摸从“按下”到“抬起”期间恰好只启动一次轮询。
消息数组宽度限制
- UI 消息统一为
int msg[8],RPC 参数最多 5 个 int(ASSERT(argc <= ARRAY_SIZE(msg) - 3)); key_event通过int msg[2 + sizeof(struct key_event)/4]按 4 字节对齐拷贝,依赖struct key_event为 4 的整数倍字节大小,改动该结构体时需同步检查。
资源加载时序
bin 图片/字体依赖存储设备挂载。源码中以 #if 0 保留了 storage_device_ready() 等待示例,提示实际产品必须在文件系统就绪后再创建依赖资源的 UI,否则 lv_img_set_src("mnt/sdfile/...") 会加载失败。
性能与运维考量
- 事件驱动刷新:UI 任务无事件时完全阻塞(
portMAX_DELAY),动画期间也只按lv_timer_handler()返回的下一次到期时间唤醒,CPU 占用随 UI 活跃度自适应; - 触摸轮询:按压期间以轮询周期读取坐标,静止时零开销;轮询周期需与触摸屏采样率匹配(SDK 内定,未开放配置时通过
lv_indev_timer_read_touch/lv_indev_set_touch_timer_en控制); - 恢复全屏重绘:
_lv_inv_area全屏脏矩形会带来一帧较高的绘制开销,适合偶发休眠/唤醒场景;若产品频繁切换页面,建议改为局部失效区域; - Monkey 压测:
LV_USE_MONKEY打开后三种输入设备以 20ms 周期随机操作,可用于出货前的稳定性/内存泄漏回归(配合lv_demo_benchmark可同时评估帧率); - 调试手段:源码中保留了
puts/printf形式的投递失败日志(post failed 0..4、touch msg drop2/3、lvgl_rpc_post_func err),产品阶段可按需裁剪。
扩展点
| 扩展点 | 机制 | 用途 |
|---|---|---|
lvgl_v9_gui_init | 强符号覆盖弱符号 | 替换官方 demo,创建产品首屏页面 |
lvgl_v9_main_task_hook | 弱函数钩子 | 每次消息循环唤醒时执行周期性业务(如状态轮询、电量刷新) |
lv_port_disp_init / lv_port_indev_init / lv_port_fs_init | 端口层接口 | 适配新 LCD/触摸/存储设备,是移植新板卡的唯一入口 |
lvgl_key_event_handler vs lvgl_key_event_handler_2 | 两种按键策略 | 需要短按/长按区分时选用 handler_2 |
lvgl_rpc_post_func | RPC 通道 | 业务任务安全驱动 UI 的官方途径 |
LV_USE_MONKEY / demo 宏 | 编译开关 | 自动化测试与性能评估 |
lv_album_img_show | 预留接口(FIXME 空实现) | 相册/图片预览类功能接入点(见 lvgl_v9_main_task_init.c) |
v8 与 v9 双版本共存说明
仓库同时存在 sdk/apps/common/lvgl_v8/ 与 sdk/apps/common/lvgl_v9/ 两套源码树,v8 目录同样包含 lvgl_main.c 与 lvgl.h。两套版本的中间层机制(消息队列驱动、事件分发、挂起恢复)保持一致,主要差异在于 LVGL 内部 API 命名与控件模型(例如 v9 的 lv_screen_active() 对应 v8 的 lv_scr_act(),源码中两个版本各自适配)。实际编译时由构建系统/app_config.h 选择启用哪套,应用代码应针对所选版本编写,避免混用两代 API。
相关链接
- lvgl_main.c — 主任务、事件处理与 RPC 实现
- lvgl_v9_main_task_init.c — GUI 初始化、演示与资源加载示例
- lvgl_v9/lvgl.h — v9 对外头文件
- lvgl_v9/lvgl_private.h — v9 私有头文件
- lvgl_v8/lvgl_main.c — v8 版本的中间层实现
- 相关主题:LCD 显示驱动、触摸驱动、文件系统挂载(
mnt/sdfile)、电源管理(UI 挂起/恢复联动)请参阅各自专题文档。