杰理 SDK 文档中心
首页
首页
  • 概述与快速开始

    • SDK 总览与芯片能力
    • 环境搭建与编译构建
    • 烧录与固件升级
    • 文档与版本资源
  • 应用与示例方案

    • demo 示例工程
    • WiFi 摄像头方案 (wifi_camera)
    • WiFi 音箱方案 (wifi_soundbox)
    • WiFi 婴儿监护方案 (wifi_bbm)
    • 公共应用模块库
    • 示例代码库 (example)
  • 系统架构与平台

    • 总体架构与工程分层
    • 系统启动与运行框架
    • 芯片驱动与板级适配
    • 设备管理与文件系统
    • 系统工具库与算法
  • 音频子系统

    • 音频框架与处理节点
    • 音频编解码与音效
    • 播放器与录音器
    • 语音交互与 AI 唤醒
    • LE Audio 与蓝牙音频
    • 音频调试与歌词
  • 视频与显示子系统

    • 摄像头驱动与 ISP
    • 视频编码与图像处理
    • 显示与 GPU 加速
    • 屏幕镜像 (screen_mirror)
  • 无线连接与网络

    • 蓝牙协议栈 (双模蓝牙)
    • WiFi 协议栈与配网
    • 网络协议栈
    • 云平台与 IoT 协议
  • UI 子系统

    • LVGL 集成与应用
    • UI 工程与工具链
  • 配置系统

    • 功能配置
    • 板级配置
    • 网络与蓝牙配置
    • 音频配置与提示音
  • 工具与测试

    • 产测与射频测试工具
    • 固件升级与更新机制
    • 调试与日志工具
  • 硬件参考设计

    • 原理图参考设计
    • 芯片数据手册

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),而非在中断或其它任务中直接调用,其设计动机是:

  1. 线程安全:LVGL 本身不是完全线程安全的,所有 lv_* API 必须集中在 UI 任务中执行;其它任务(如蓝牙、音频、按键扫描)只能通过消息队列或 RPC 通道间接驱动 UI。
  2. 事件驱动刷新:UI 任务平时阻塞在 os_taskq_pend_timeout() 上,由触摸、按键、定时器超时等消息唤醒,仅在需要时执行 lv_timer_handler(),避免空转消耗 CPU 与功耗。
  3. 低功耗协同:通过 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

执行流程:

  1. 任务启动后先清挂起标志,初始化触摸轮询状态;
  2. 调用 lvgl_v9_gui_init() 完成 LVGL 内核与端口初始化,并创建首屏 UI;
  3. 主动刷新第一帧:直接调用一次 lv_timer_handler(),得到下次需要刷新的时间 time_till_next,若有效(非 0xFFFFFFFF)则通过 sys_hi_timeout_add 注册一个到期的内核定时器回调 lvgl_timer_event_timeout —— 这是“事件驱动刷新”的起点:LVGL 自身的 lv_timer 到期由系统定时器换算成队列消息,而不是让 UI 任务忙等;
  4. 进入 while(1) 无限循环,阻塞在 os_taskq_pend_timeout(msg, ..., portMAX_DELAY) 上等待消息或定时器超时;
  5. 每次被唤醒都先调用弱函数钩子 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

处理规则:

  1. 编码器优先:KEY_EVENT_RDEC_UP/KEY_EVENT_RDEC_DOWN 直接打包为 UI_MSG_ENCODER 投递,不参与按键拆分;
  2. 按下暂存:KEY_EVENT_DOWN 只记录 s_last_key_value 与 s_last_key_action,不发给 UI;
  3. 抬起拆分:按键抬起时把一次 up 拆成 down → up 两次事件按序投递,UI 因此能感知“完整的按下-抬起”过程;若此前发生过 KEY_EVENT_HOLD/KEY_EVENT_LONG,则只发一次 up(长按后的抬起 UI 只响应一次);
  4. 长按/保持/单击直发:KEY_EVENT_LONG、KEY_EVENT_HOLD、KEY_EVENT_CLICK 属于派生事件,直接投递;
  5. 与模式一相同,投递失败且丢失抬起消息时走 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

闭环流程:

  1. UI 任务调用 lv_timer_handler() 返回“距离下一个定时器/动画到期的时间 time_till_next”;
  2. 若该时间有效(不等于 0xFFFFFFFF),注册系统高精度定时器 sys_hi_timeout_add(NULL, lvgl_timer_event_timeout, time_till_next);
  3. 到期后系统定时器回调投递 UI_MSG_TIMER_TIMEOUT 消息;
  4. 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

初始化顺序的用意:

  1. lv_init() 初始化 LVGL 内核(对象系统、样式、定时器、内存);
  2. lv_port_disp_init() 注册显示驱动(帧缓冲、分辨率、刷新回调);
  3. lv_port_indev_init() 注册输入设备(触摸指针、按键组、编码器),并建立与 UI_MSG_TOUCH/UI_MSG_KEY/UI_MSG_ENCODER 消息的映射;
  4. lv_port_fs_init() 挂载 LVGL 文件系统驱动,使 lv_img_set_src(img, "mnt/sdfile/...") 等路径可以直接从存储设备读图;
  5. lv_tick_set_cb(&timer_get_ms) 提供毫秒时基;
  6. LV_USE_MONKEY 编译开关打开时创建 pointer/keypad/encoder 三种 Monkey 自动化输入(20ms 周期,编码器输入范围为 -5~5),用于压测 UI 稳定性;
  7. 最后按需启动官方 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() 周期性读坐标并刷新

流程要点:

  1. 按键扫描任务把物理事件整理成 struct key_event,在 lvgl_key_event_handler_2 中完成拆分/过滤;
  2. 消息经 Q_USER 队列唤醒 UI 任务;UI 任务先执行 lvgl_v9_main_task_hook() 钩子,再按消息类型处理;
  3. 输入注入后调用 lv_timer_handler(),LVGL 内部完成对象状态推进、脏矩形收集与显示驱动 flush;
  4. 业务任务驱动 UI 必须走 RPC 通道,避免跨线程直接操作控件;
  5. 触摸按下只发一次启动消息,持续按压由轮询定时器读取,降低中断压力。

使用示例

示例 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_handlerpriv:任务参数,无返回
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_MONKEYLVGL 配置宏非 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 成对,因此抬起消息不可丢失。代码通过两层保护实现:

  1. lvgl_key_msg_remain_cnt > 2 时暂停投递(防积压);
  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_funcRPC 通道业务任务安全驱动 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 挂起/恢复联动)请参阅各自专题文档。
Next
UI 工程与工具链