6.9.5. 设计指南

6.9.5.1. 代码目录

luban-lite/packages/artinchip/lvgl-ui
├── aic_demo      // aic lvgl demo
├── lv_driver     // lvgl显示和2D加速对接
├── lvgl          // lvgl库
├── aic_ui.c      // aic demo入口
├── aic_ui.h      // aic demo头文件
├── lv_conf.h     // lvgl配置文件
├── lv_demo.c     // lvgl demo入口函数
└── SConscript

6.9.5.2. LVGL整体流程

../../../_images/key_process1.png

图 6.86 整体流程

LVGL框架的运行都是基于LVGL中定义的”Timer”定时器,系统需要给LVGL一个“心跳”, LVGL才能正常的运转起来,两个关键的函数:

  • lv_tick_get(), 获取以ms为单位的tick时间

  • lv_timer_handler(),在while循环中的基于定时器的任务处理,函数lv_task_handler会调用lv_timer_handler, lv_tick_get 决定了lv_timer_handler基于定时器的任务处理的时间的准确性

其中在文件lv_hal_tick.c中的lv_tick_get的实现代码如下:

uint32_t lv_tick_get(void)
{
#if LV_TICK_CUSTOM == 0

    /*If `lv_tick_inc` is called from an interrupt while `sys_time` is read
    *the result might be corrupted.
    *This loop detects if `lv_tick_inc` was called while reading `sys_time`.
    *If `tick_irq_flag` was cleared in `lv_tick_inc` try to read again
    *until `tick_irq_flag` remains `1`.*/
    uint32_t result;
    do {
        tick_irq_flag = 1;
        result        = sys_time;
    } while(!tick_irq_flag); /*Continue until see a non interrupted cycle*/

    return result;
#else
    return LV_TICK_CUSTOM_SYS_TIME_EXPR;
#endif
}

在头文件lv_rt_thread_conf.h中定义了上述函数中的LV_TICK_CUSTOM_SYS_TIME_EXPR

#define LV_TICK_CUSTOM 1
#define LV_TICK_CUSTOM_INCLUDE LV_RTTHREAD_INCLUDE
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (rt_tick_get_millisecond())    /*Expression evaluating to current system time in ms*/

创建LVGL线程的代码如下所示:

static void lvgl_thread_entry(void *parameter)
{
#if LV_USE_LOG
    lv_log_register_print_cb(lv_rt_log);
#endif /* LV_USE_LOG */
    lv_init();
    lv_port_disp_init();
    lv_port_indev_init();
    lv_user_gui_init();

    /* handle the tasks of LVGL */
    while(1)
    {
        lv_task_handler();
        rt_thread_mdelay(SLEEP_PERIOD);
    }
}

int lvgl_thread_init(void)
{
    rt_err_t err;

    err = rt_thread_init(&lvgl_thread, "LVGL", lvgl_thread_entry, RT_NULL,
        &lvgl_thread_stack[0], sizeof(lvgl_thread_stack), LPKG_LVGL_THREAD_PRIO, 0);
    if(err != RT_EOK)
    {
        LOG_E("Failed to create LVGL thread");
        return -1;
    }
    rt_thread_startup(&lvgl_thread);

    return 0;
}

用户只需要实现三个函数lv_port_disp_init()、lv_port_indev_init()、lv_user_gui_init():

  • 其中在函数lv_port_disp_init()中实现显示接口的对接以及硬件2D加速的对接

  • 在函数lv_port_indev_init()中实现触摸屏的对接

  • 在函数lv_user_gui_init()中适配不同UI界面的初始化

6.9.5.3. LVGL层次结构

../../../_images/struct1.png

图 6.87 层次结构

  • LVGL的display是对显示驱动的封装和抽象

  • display包含Active Screen、Top layer、System layer

  • Active Screen、Top layer、System layer是不同的screen对象,这里的screen用layer表达更准确一点, 表示的是图层的概念,其中Active Screen在最底层,System layer在最顶层

  • 一般在Active Screen实现不同的app界面,用户可以创建多个screen,但只能有一个screen设置为Active Screen

  • Top layer在Active Screen之上,可以用来创建弹出窗口,Top layer永远在Active Screen之上

  • System layer在最顶层,比如鼠标可以在System layer,永远不会被遮挡

../../../_images/layer1.png

图 6.88 图层叠加

6.9.5.4. 父子结构

LVGL是面向对象的基于父子结构的设计,每一个对象都包含一个父对象(screen对象除外), 但是一个父对象可以包含任意数量的子对象。

/*
 * 创建对象的时候,需要传入父对象的指针,
 * 如果父对象对NULL, 表示创建的是screen对象
 */
lv_obj_create(NULL);

6.9.5.4.1. 父子对象一起移动

../../../_images/move1.png

图 6.89 父子对象移动

6.9.5.4.2. 子对象超出父对象部分不可见

../../../_images/outside1.png

图 6.90 子对象可见区域

6.9.5.5. 显示对接

主要包括三部分:

  1. 绘制buffer初始化

  2. flush_cb对接

  3. 2D硬件加速对接

6.9.5.5.1. 绘制buffer初始化

绘制buffer初始化函数如下:

void lv_disp_draw_buf_init(lv_disp_draw_buf_t * draw_buf, void * buf1, void * buf2, uint32_t size_in_px_cnt)
  • buf1:当为单缓冲或多缓冲的时候,都要设置此buffer

  • buf2:当选择双缓冲的时候,需要配置此buffer,单缓冲不需要

  • size_in_px_cnt: 以像素为单位的buf大小

../../../_images/double_frame1.png

图 6.91 双缓冲

6.9.5.5.2. flush_cb对接

flush_cb回调函数的处理流程,我们以双缓冲为例进行说明,绘制模式有full_refresh和direct_mode两种:

  1. 全刷新模式,每一帧都刷新整个显示屏

../../../_images/full_flush_cb1.png

图 6.92 全刷新模式

在虚线框中为flush_cb中处理部分,在全刷新的流程中,直接通过 pan_display接口送当前绘制buffer到显示,然后等待vsync中断, 等到中断后,当前的绘制buffer就真正的在显示屏中显示出来,然后调用lv_disp_flush_ready通知LVGL框架已经flush结束, 最后在LVGL框架中会进行绘制buffer的交换。

  1. 局部刷新,每一帧只刷新需要更新的无效区域(可以有多个无效区域)

../../../_images/invalid_area1.png

图 6.93 无效区域

../../../_images/direct_flush_cb1.png

图 6.94 局部刷新模式

上图中的示例,为了方便描述每一帧都有两个无效区域(invalid area0 和invalid area1),LVGL可以支持更多的无效区域,到了最后一个无效区域, 说明当前帧的数据已经处理完,才把绘制buffer送显示,然后进行buffer交换

flush_cb的实现代码fbdev_flush如下:

static void fbdev_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t *color_p)
{
    int index = 0;
    lv_disp_t * disp = _lv_refr_get_disp_refreshing();
    lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp);

    if (!disp->driver->direct_mode || draw_buf->flushing_last) {
        if (disp->driver->direct_mode)
            aicos_dcache_clean_invalid_range((unsigned long *)info.framebuffer, (unsigned long)info.smem_len * 2);
        else
            aicos_dcache_clean_invalid_range((unsigned long *)color_p, (unsigned long)info.smem_len);

        if ((void *)color_p == (void *)info.framebuffer)
            index = 0;
        else
            index = 1;

        mpp_fb_ioctl(g_fb, AICFB_PAN_DISPLAY , &index);
        mpp_fb_ioctl(g_fb, AICFB_WAIT_FOR_VSYNC, 0);

        if (drv->direct_mode == 1) {
            for (int i = 0; i < disp->inv_p; i++) {
                if (disp->inv_area_joined[i] == 0) {
                    sync_disp_buf(drv, color_p, &disp->inv_areas[i]);
                }
            }
        }

        lv_disp_flush_ready(drv);
    }
    else {
        lv_disp_flush_ready(drv);
    }
}

6.9.5.5.3. 2D硬件加速对接

2D加速主要对接 lv_draw_ctx_t中的绘制函数

成员

说明

是否硬件加速

void *buf

当前要绘制的buffer

-

const lv_area_t * clip_area

绘制区域裁剪(以屏幕为参考的绝对坐标)

-

void (*draw_rect)()

绘制矩形(包括圆角、阴影、渐变等)

void (*draw_arc)()

绘制弧形

void (*draw_img_decoded)()

绘制已经解码后的图像

lv_res_t (*draw_img)()

绘制图像(包括图片解码)

void (*draw_letter)()

绘制文字

void (*draw_line)()

绘制直线

void (*draw_polygon)()

绘制多边形

在lv_draw_aic_ctx_t(重定义了lv_draw_sw_ctx_t)结构体中包含lv_draw_ctx_t和blend函数:

typedef struct {
    lv_draw_ctx_t base_draw;

    /** Fill an area of the destination buffer with a color*/
    void (*blend)(lv_draw_ctx_t * draw_ctx, const lv_draw_sw_blend_dsc_t * dsc);
} lv_draw_sw_ctx_t;

在draw_rect、draw_line等操作的功能由多个步骤组成,虽然我们没有对这些接口进行硬件加速,但是这些操作的部分实现 会调用到blend,我们对blend接口进行了硬件加速对接:

void lv_draw_aic_ctx_init(lv_disp_drv_t * drv, lv_draw_ctx_t * draw_ctx)
{
    lv_draw_sw_init_ctx(drv, draw_ctx);
    lv_draw_aic_ctx_t * aic_draw_ctx = (lv_draw_aic_ctx_t *)draw_ctx;

    aic_draw_ctx->blend = lv_draw_aic_blend;
    aic_draw_ctx->base_draw.draw_img = lv_draw_aic_draw_img;
    aic_draw_ctx->base_draw.draw_img_decoded = lv_draw_aic_img_decoded;

    return;
}

先调用lv_draw_sw_init_ctx函数把所有绘制操作都初始化为软件实现,然后对可以硬件加速的接口重新实现, 覆盖原来的软件实现。

6.9.5.5.4. 显示驱动注册

所有的显示相关功能都包含在lv_disp_drv_t结构体中:

  1. 通过lv_disp_drv_init来初始化lv_disp_drv_t结构体

  2. 通过lv_disp_draw_buf_init初始化绘制buffer

  3. 通过回调flush_cb来注册显示接口

  4. 通过lv_draw_aic_ctx_init来注册2D硬件加速相关接口

  5. 通过lv_disp_drv_register来注册lv_disp_drv_t

static lv_disp_drv_t disp_drv;
void lv_port_disp_init(void)
{
    void *buf1 = RT_NULL;
    void *buf2 = RT_NULL;
    uint32_t fb_Size;
    rt_err_t result;

    g_fb = mpp_fb_open();
    if (g_fb == 0) {
        LOG_E("can't find aic framebuffer device!");
        return;
    }

    result = mpp_fb_ioctl(g_fb, AICFB_GET_SCREENINFO, &info);
    if (result != RT_EOK) {
        LOG_E("get device fb info failed!");
        return;
    }

    g_ge = mpp_ge_open();
    if (!g_ge) {
        LOG_E("ge open fail\n");
        return;
    }

    fb_Size = info.height * info.stride;
    buf1 = (void *)info.framebuffer;
    buf2 = (void *)((uint8_t *)info.framebuffer + fb_Size);
    lv_disp_draw_buf_init(&disp_buf, buf2, buf1,
                        info.width * info.height);
    lv_disp_drv_init(&disp_drv);

    /*Set a display buffer*/
    disp_drv.draw_buf = &disp_buf;

    /*Set the resolution of the display*/
    disp_drv.hor_res = info.width;
    disp_drv.ver_res = info.height;
    disp_drv.full_refresh = 0;
    disp_drv.direct_mode = 1;
    disp_drv.flush_cb = fbdev_flush;
    disp_drv.draw_ctx_init = lv_draw_aic_ctx_init;
    disp_drv.draw_ctx_deinit = lv_draw_aic_ctx_deinit;
    disp_drv.draw_ctx_size = sizeof(lv_draw_aic_ctx_t);

    /*Finally register the driver*/
    lv_disp_drv_register(&disp_drv);
}

上述代码中表示目前是局部刷新模式:

disp_drv.full_refresh = 0;
disp_drv.direct_mode = 1;

全刷新模式参数配置如下:

disp_drv.full_refresh = 1;
disp_drv.direct_mode = 0;

6.9.5.6. 硬件解码对接

6.9.5.6.1. lv_img_decoder_t注册

我们通过lv_img_decoder_t来注册硬件解码器接口,主要实现了三个接口:

函数

说明

aic_decoder_info

获取图片宽、高、图片格式信息

aic_decoder_open

申请解码输出buffer,硬件解码输出

aic_decoder_close

释放硬件解码资源(包括输出buffer)

注册解码器过程;

void aic_dec_create()
{
    lv_img_decoder_t *aic_dec = lv_img_decoder_create();

    /* init frame info lists */
    mpp_list_init(&buf_list);
    lv_img_decoder_set_info_cb(aic_dec, aic_decoder_info);
    lv_img_decoder_set_open_cb(aic_dec, aic_decoder_open);
    lv_img_decoder_set_close_cb(aic_dec, aic_decoder_close);
}

绘制函数draw_img_decoded需要的解码后数据,需要通过注册解码器回调去获取, 这是我们默认的图片处理流程:

../../../_images/draw_img_decoded1.png

图 6.95 draw_img_decoded

  • 采用此流程需要额外申请一块解码buffer,占用内存增加

  • 缓存解码后的buffer,下次再显示同样的image,不用重复解码,加快UI加载速度

当绘制函数为draw_img的时候,硬件解码在函数draw_img内部,无需注册解码回调函数,我们默认不采用此方法, 当在内存受限的场景下,可以评估此方法是否可满足场景需求。

../../../_images/draw_img1.png

图 6.96 draw_img

  • 采用此流程无需额外申请解码buffer,直接解码到绘制buffer

  • 当需要进行alpha blending的时候,此方法不可行

  • 每次都要重新对image解码,速度不如draw_img_decoded

  • 当硬件解码不支持裁剪的时进行局部更新,此方法不可行

6.9.5.6.2. 图片cache机制

  1. 采用lv_img_decoder_t提供的接口注册的解码器可以采用LVGL内部的图片缓冲机制, 在lv_conf.h 中宏定义LV_IMG_CACHE_DEF_SIZE为1的时候,表示打开图片缓冲机制, 当LV_IMG_CACHE_DEF_SIZE为0的时候,图片缓冲机制关闭。

  2. 通过void lv_img_cache_set_size(uint16_t entry_cnt)来设置缓冲的图片张数,图片以张数为单位进行缓存。

  3. 当图片缓存到设置的最大张数的时候,如果需要新的缓存,图片缓存机制内部会进行图片缓存价值的判断, 例如:如果某一张图片解码的时间比较久,或者某一张图片使用的更频繁,那么这种图片的缓存价值打分会更高, 优先缓存这些缓存价值更高的图片。

如果一些图片的读取时间或者解码时间比较长,采用图片缓存机制可以提升UI流畅性

6.9.5.7. LVGL demo

目前支持base demo、meter demo等demo

6.9.5.7.1. base demo

对png、jpg硬件解码,以及build-in的图片使用方式进行演示,UI界面如下:

../../../_images/base_demo1.png

图 6.97 base demo

此demo一共有4四个页面, 第一个页面为仪表演示、第二个页面为音乐播放演示、第三个页面为菜单演示、 第四个页面为播放器演示。播放器演示页面需要打开base_ui.c中的宏定义VIDEO_PLAYER

  1. 不同页面通过滑动操作切换,页面滑动使用了控件tabview

lv_obj_set_size(main_tabview, 1024, 600);
lv_obj_set_pos(main_tabview, 0, 0);
lv_obj_set_style_bg_opa(main_tabview, LV_OPA_0, 0);

lv_obj_t *main_tab0 = lv_tabview_add_tab(main_tabview, "main page 0");
lv_obj_t *main_tab1 = lv_tabview_add_tab(main_tabview, "main page 1");

lv_obj_set_style_bg_opa(main_tab0, LV_OPA_0, 0);
lv_obj_set_style_bg_opa(main_tab1, LV_OPA_0, 0);
lv_obj_set_size(main_tab0, 1024, 600);
lv_obj_set_size(main_tab1, 1024, 600);

lv_obj_set_pos(main_tab0, 0, 0);
lv_obj_set_pos(main_tab1, 0, 0);
  1. 背景图片通过image控件来创建,是一个名字为global_bg.png的png图片,此图片会采用注册的硬件解码器进行解码

static lv_obj_t *img_bg = NULL;
img_bg = lv_img_create(lv_scr_act());
lv_img_set_src(img_bg, LVGL_PATH(global_bg.png));
lv_obj_set_pos(img_bg, 0, 0);
  1. 菜单图片也通过image控件来创建,是png图片,此图片也会采用注册的硬件解码器进行解码

lv_obj_t *sub_image00 = lv_img_create(sub_tab0);
lv_img_set_src(sub_image00, LVGL_PATH(cook_0.jpg));
lv_obj_set_pos(sub_image00, 36, 100);
  1. fake image

fake image不是一个真实的图片,通过此方式可以方便的对一个矩形区域进行填充:包括alpha、red、green、blue

static lv_obj_t *img_bg = NULL;
FAKE_IMAGE_DECLARE(bg_dark)  // 声明(bg_dark名字可修改)

/* 最后一个参数为要设置的颜色值:bit31:24 为alpha */
FAKE_IMAGE_INIT(bg_dark, 1024, 600, 0, 0x00000000);

lv_img_set_src(img_bg, FAKE_IMAGE_NAME(bg_dark)); // 设置fake image数据源
  1. build-in image是通过数组变量在程序中表示图像,图片转换成.c文件的工具参考官网:http://lvgl.io/tools/imageconverter

uint8_t circle_white_map[] = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x50, 0xff, 0x7f, 0xff,
        ........................................................};

const lv_img_dsc_t circle_white = {
    .header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA,
    .header.always_zero = 0,
    .header.reserved = 0,
    .header.w = 20,
    .header.h = 20,
    .data_size = 400 * LV_IMG_PX_SIZE_ALPHA_BYTE,
    .data = circle_white_map,
};

static lv_obj_t * circle_0 = lv_img_create(img_bg);
lv_img_set_src(circle_0, &circle_white);
lv_obj_align(circle_0, LV_ALIGN_BOTTOM_MID, -16, -28);

6.9.5.7.2. meter demo

此demo演示了硬件旋转,以及仪表盘的设计参考

../../../_images/meter_demo.png

图 6.98 meter demo

  1. 其中各种控件的动作通过timer来实现,每间隔一定的时间执行相应的callback

lv_timer_create(point_callback, 10, 0);
lv_timer_create(fps_callback, 1000, 0);
lv_timer_create(speed_callback, 100, 0);
lv_timer_create(time_callback, 1000 * 60, 0);
  1. 指针和光影通过贴图和硬件任意角度旋转来实现,其中前74张指针通过切换74张图片来实现, 从第75张开始的红色指针,采用硬件任意角度旋转来实现

static void point_callback(lv_timer_t *tmr)
{
    char data_str[64];
    (void)tmr;

    static bool first = true;
    static int id = 1;
    static int direct = 0;
    static int mode_id = 0;
    static int mode_num = sizeof(rot_mode_list) / sizeof(rot_mode_list[0]);
    static int start_id = 0;
    static int end_id = 0;

    if (first) {
        first = false;
        start_id = rot_mode_list[mode_id].start_id;
        end_id = rot_mode_list[mode_id].end_id;
    }

    direct = start_id < end_id ? 0: 1;

    if (id < 75) {
    lv_img_set_src(img_circle, LVGL_PATH(bg/small_blue.png));
    lv_obj_clear_flag(img_circle, LV_OBJ_FLAG_HIDDEN);
    } else {
    lv_obj_add_flag(img_circle, LV_OBJ_FLAG_HIDDEN);
    }

    if (id < 75) {
        sprintf(data_str, "%spoint/point_%05d.png", LVGL_DIR, id);
        lv_img_set_src(img_point, data_str);
        lv_img_set_angle(img_point, 0);

    } else {
        // id to angle
        float rot_angle = ((float)(id - 75) * 2 * 10) * 0.84;
        sprintf(data_str, "%spoint/point_%05d.png", LVGL_DIR, 75);
        lv_img_set_src(img_point, data_str);
        lv_img_set_pivot(img_point, 210, 210);
        lv_img_set_angle(img_point, (int16_t)rot_angle);
    }

    if (direct == 0) {
        id++;
    } else {
        id--;
    }

    if ((!direct && (id > end_id) ) ||
        (direct && (id < end_id))) {
        id = end_id;
        mode_id++;
        mode_id %= mode_num;
        start_id = rot_mode_list[mode_id].start_id;
        end_id = rot_mode_list[mode_id].end_id;
    }

    return;
}
  1. UI怎么设计更高效?

    针对本demo场景给出两种流程:

(1)UI设计方案1

../../../_images/meter_draw_1.png

图 6.99 UI设计方案1

  • 需要光条、指针、光圈、底图四张

  • 首先光圈和背景进行alpha blending,然后光条和背景进行alpha blending,最后指针和背景进行alpha blending

  • 每一个角度的旋转都需要进行3次 alpha blending

(2)UI设计方案2

../../../_images/meter_draw_2.png

图 6.100 UI设计方案2

  • 光条和指针合并为一张图

  • 光圈合并到背景图中

  • 需要光条和指针图片和底图2张图片

  • 每一个角度的旋转都只需要光条和指针图片与背景图进行一次alpha blending

显然UI设计方案2更高效,实现同样的界面效果,简化流程速度可以提升一倍以上