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整体流程¶
 
图 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层次结构¶
 
图 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,永远不会被遮挡 
 
图 6.88 图层叠加¶
6.9.5.4. 父子结构¶
LVGL是面向对象的基于父子结构的设计,每一个对象都包含一个父对象(screen对象除外), 但是一个父对象可以包含任意数量的子对象。
/*
 * 创建对象的时候,需要传入父对象的指针,
 * 如果父对象对NULL, 表示创建的是screen对象
 */
lv_obj_create(NULL);
6.9.5.5. 显示对接¶
主要包括三部分:
- 绘制buffer初始化 
- flush_cb对接 
- 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大小 
 
图 6.91 双缓冲¶
6.9.5.5.2. flush_cb对接¶
flush_cb回调函数的处理流程,我们以双缓冲为例进行说明,绘制模式有full_refresh和direct_mode两种:
- 全刷新模式,每一帧都刷新整个显示屏 
 
图 6.92 全刷新模式¶
在虚线框中为flush_cb中处理部分,在全刷新的流程中,直接通过 pan_display接口送当前绘制buffer到显示,然后等待vsync中断, 等到中断后,当前的绘制buffer就真正的在显示屏中显示出来,然后调用lv_disp_flush_ready通知LVGL框架已经flush结束, 最后在LVGL框架中会进行绘制buffer的交换。
- 局部刷新,每一帧只刷新需要更新的无效区域(可以有多个无效区域) 
 
图 6.93 无效区域¶
 
图 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结构体中:
- 通过lv_disp_drv_init来初始化lv_disp_drv_t结构体 
- 通过lv_disp_draw_buf_init初始化绘制buffer 
- 通过回调flush_cb来注册显示接口 
- 通过lv_draw_aic_ctx_init来注册2D硬件加速相关接口 
- 通过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需要的解码后数据,需要通过注册解码器回调去获取, 这是我们默认的图片处理流程:
 
图 6.95 draw_img_decoded¶
- 采用此流程需要额外申请一块解码buffer,占用内存增加 
- 缓存解码后的buffer,下次再显示同样的image,不用重复解码,加快UI加载速度 
当绘制函数为draw_img的时候,硬件解码在函数draw_img内部,无需注册解码回调函数,我们默认不采用此方法, 当在内存受限的场景下,可以评估此方法是否可满足场景需求。
 
图 6.96 draw_img¶
- 采用此流程无需额外申请解码buffer,直接解码到绘制buffer 
- 当需要进行alpha blending的时候,此方法不可行 
- 每次都要重新对image解码,速度不如draw_img_decoded 
- 当硬件解码不支持裁剪的时进行局部更新,此方法不可行 
6.9.5.6.2. 图片cache机制¶
- 采用lv_img_decoder_t提供的接口注册的解码器可以采用LVGL内部的图片缓冲机制, 在lv_conf.h 中宏定义LV_IMG_CACHE_DEF_SIZE为1的时候,表示打开图片缓冲机制, 当LV_IMG_CACHE_DEF_SIZE为0的时候,图片缓冲机制关闭。 
- 通过void lv_img_cache_set_size(uint16_t entry_cnt)来设置缓冲的图片张数,图片以张数为单位进行缓存。 
- 当图片缓存到设置的最大张数的时候,如果需要新的缓存,图片缓存机制内部会进行图片缓存价值的判断, 例如:如果某一张图片解码的时间比较久,或者某一张图片使用的更频繁,那么这种图片的缓存价值打分会更高, 优先缓存这些缓存价值更高的图片。 
如果一些图片的读取时间或者解码时间比较长,采用图片缓存机制可以提升UI流畅性
6.9.5.7. LVGL demo¶
目前支持base demo、meter demo等demo
6.9.5.7.1. base demo¶
对png、jpg硬件解码,以及build-in的图片使用方式进行演示,UI界面如下:
 
图 6.97 base demo¶
此demo一共有4四个页面, 第一个页面为仪表演示、第二个页面为音乐播放演示、第三个页面为菜单演示、 第四个页面为播放器演示。播放器演示页面需要打开base_ui.c中的宏定义VIDEO_PLAYER
- 不同页面通过滑动操作切换,页面滑动使用了控件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);
- 背景图片通过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);
- 菜单图片也通过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);
- 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数据源
- 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演示了硬件旋转,以及仪表盘的设计参考
 
图 6.98 meter demo¶
- 其中各种控件的动作通过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);
- 指针和光影通过贴图和硬件任意角度旋转来实现,其中前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;
}
- UI怎么设计更高效? - 针对本demo场景给出两种流程: 
(1)UI设计方案1
 
图 6.99 UI设计方案1¶
- 需要光条、指针、光圈、底图四张 
- 首先光圈和背景进行alpha blending,然后光条和背景进行alpha blending,最后指针和背景进行alpha blending 
- 每一个角度的旋转都需要进行3次 alpha blending 
(2)UI设计方案2
 
图 6.100 UI设计方案2¶
- 光条和指针合并为一张图 
- 光圈合并到背景图中 
- 需要光条和指针图片和底图2张图片 
- 每一个角度的旋转都只需要光条和指针图片与背景图进行一次alpha blending 
显然UI设计方案2更高效,实现同样的界面效果,简化流程速度可以提升一倍以上
 
