设计指南
代码目录
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
2
3
4
5
6
7
8
9
LVGL整体流程
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在头文件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*/
2
3
创建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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
用户只需要实现三个函数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界面的初始化
LVGL层次结构
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,永远不会被遮挡
父子结构
LVGL是面向对象的基于父子结构的设计,每一个对象都包含一个父对象(screen对象除外),但是一个父对象可以包含任意数量的子对象。
/*
* 创建对象的时候,需要传入父对象的指针,
* 如果父对象对NULL, 表示创建的是screen对象
*/
lv_obj_create(NULL);
2
3
4
5
父子对象一起移动
子对象超出父对象部分不可见
显示对接
主要包括三部分:
1.绘制buffer初始化
2.flush_cb对接
3.2D硬件加速对接
绘制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大小。
flush_cb对接
flush_cb回调函数的处理流程,我们以双缓冲为例进行说明,绘制模式有full_refresh和direct_mode两种:
1.全刷新模式:每一帧都刷新整个显示屏。
在虚线框中为flush_cb中处理部分,在全刷新的流程中,直接通过pan_display接口送当前绘制buffer到显示,然后等待vsync中断,等到中断后,当前的绘制buffer就真正的在显示屏中显示出来,然后调用lv_disp_flush_ready
通知LVGL框架已经flush结束,最后在LVGL框架中会进行绘制buffer的交换。
2.局部刷新:每一帧只刷新需要更新的无效区域(可以有多个无效区域)。
上图中的示例,为了方便描述每一帧都有两个无效区域(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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
2
3
4
5
6
在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;
}
2
3
4
5
6
7
8
9
10
11
先调用lv_draw_sw_init_ctx函数把所有绘制操作都初始化为软件实现,然后对可以硬件加速的接口重新实现,覆盖原来的软件实现。
显示驱动注册
所有的显示相关功能都包含在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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
上述代码中表示目前是局部刷新模式:
disp_drv.full_refresh = 0;
disp_drv.direct_mode = 1;
2
全刷新模式参数配置如下:
disp_drv.full_refresh = 1;
disp_drv.direct_mode = 0;
2
硬件解码对接
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);
}
2
3
4
5
6
7
8
9
10
绘制函数draw_img_decoded需要的解码后数据,需要通过注册解码器回调去获取,这是我们默认的图片处理流程:
- 采用此流程需要额外申请一块解码buffer,占用内存增加。
- 缓存解码后的buffer,下次再显示同样的image,不用重复解码,加快了UI加载速度。
当绘制函数为draw_img
的时候,硬件解码在函数draw_img
内部,无需注册解码回调函数,我们默认不采用此方法,当在内存受限的场景下,可以评估此方法是否可满足场景需求。
采用此流程无需额外申请解码buffer,直接解码到绘制buffer
当需要进行alpha blending的时候,此方法不可行
每次都要重新对image解码,速度不如draw_img_decoded
当硬件解码不支持裁剪的时进行局部更新,此方法不可行
图片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流畅性。
第三方库支持
1.freetype库支持:
- 使用
scons --menuconfig
或me
进入配置菜单,选中freetype包。
Local packages options --->
Third-party packages options --->
[*] freetype ---
2
3
- 在头文件
luban-lite/packages/artinchip/lvgl-ui/lv_conf.h
打开宏定义:
#define LV_USE_FREETYPE 1
- freetype字体调用示例如下:
void lv_example_freetype_1(void)
{
/*Create a font*/
static lv_ft_info_t info;
/*FreeType uses C standard file system, so no driver letter is required.*/
// 需要设置字库文件在系统目录下的路径
info.name = "/rodata/lvgl_data/font/Lato-Regular.ttf";
info.weight = 24;
info.style = FT_FONT_STYLE_NORMAL;
info.mem = NULL;
if(!lv_ft_font_init(&info)) {
LV_LOG_ERROR("create failed.");
}
/*Create style with the new font*/
static lv_style_t style;
lv_style_init(&style);
lv_style_set_text_font(&style, info.font);
lv_style_set_text_align(&style, LV_TEXT_ALIGN_CENTER);
/*Create a label with the new style*/
lv_obj_t * label = lv_label_create(lv_scr_act());
lv_obj_add_style(label, &style, 0); // 添加字库style到label控件
lv_label_set_text(label, "Hello world\nI'm a font created with FreeType");
lv_obj_center(label);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
LVGL demo
目前支持base demo、meter demo等demo。
base demo
对png、jpg硬件解码,以及build-in的图片使用方式进行演示,UI界面如下:
此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);
2
3
4
5
6
7
8
9
10
11
12
13
14
2.背景图片通过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);
2
3
4
3.菜单图片也通过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);
2
3
4.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数据源
2
3
4
5
6
7
5.build-in image是通过数组变量在程序中表示图像,图片转换成.c文件的工具参考官网: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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
meter demo
此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);
2
3
4
2.指针和光影通过贴图和硬件任意角度旋转来实现,其中前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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
3.UI怎么设计更高效?
针对本demo场景给出两种流程:
(1)UI设计方案1
需要光条、指针、光圈、底图四张
首先光圈和背景进行alpha blending,然后光条和背景进行alpha blending,最后指针和背景进行alpha blending
每一个角度的旋转都需要进行3次 alpha blending
(2)UI设计方案2
- 光条和指针合并为一张图。
- 光圈合并到背景图中。
- 需要光条和指针图片和底图2张图片。
- 每一个角度的旋转都只需要光条和指针图片与背景图进行一次alpha blending。
显然UI设计方案2更高效,实现同样的界面效果,简化流程速度可以提升一倍以上。