获取LVGL图形库

https://github.com/lvgl/lvgl

也可以克隆它或从 GitHub 下载最新版本的库。

1
2
3


git clone https://github.com/lvgl/lvgl

图形库本身是 lvgl 目录,应将其复制到你的项目中。

获取显示和输入设备驱动程序

https://github.com/lvgl/lv_drivers

修改配置文件

有一个名为 lv_conf.h 的 LVGL 配置头文件。在这里,你可以设置库的基本行为、禁用未使用的模块和功能、在编译时调整内存缓冲区的大小等。

复制 lvgl 目录旁边的 lvgl/lv_conf_template.h 并将其重命名为 lv_conf.h。打开文件并将开头的“#if 0”更改为“#if 1”以启用其内容。

lv_conf.h 也可以复制到另一个地方,但是你应该添加 LV_CONF_INCLUDE_SIMPLE 定义到你的编译器选项(例如 -DLV_CONF_INCLUDE_SIMPLE 用于 gcc 编译器)并手动设置包含路径。 在这种情况下,LVGL 将尝试使用 #include “lv_conf.h” 简单地包含 lv_conf.h。

在配置文件的注释中解释了选项的含义。请务必根据显示器的颜色深度至少设置“LV_COLOR_DEPTH”。

初始化

要使用图形库,必须初始化它和其他组件。

初始化的顺序是:

1.调用lv_init()。

2.初始化驱动程序。

3.在 LVGL 中注册显示和输入设备驱动程序。

4.在中断中每隔 x 毫秒调用 lv_tick_inc(x) 以告知经过的时间。

5.每隔几毫秒调用lv_timer_handler()来处理LVGL相关的任务。

二、显示接口

要为 LVGL 注册一个显示器,必须初始化一个 lv_disp_draw_buf_t 和一个 lv_disp_drv_t 变量。

lv_disp_draw_buf_t 包含称为绘制缓冲区的内部图形缓冲区。

lv_disp_drv_t 包含与显示交互和操作绘图相关事物的回调函数。

绘制缓冲区

绘制缓冲区是 LVGL 用来渲染屏幕内容的简单数组。 一旦渲染准备就绪,绘制缓冲区的内容将使用显示驱动程序中设置的 flush_cb 函数发送到显示器(见下文)。

绘制绘制缓冲区可以通过“lv_disp_draw_buf_t”变量初始化,如下所示:

 1
 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


/*A static or global variable to store the buffers*/


static lv_disp_draw_buf_t disp_buf;





/*Static or global buffer(s). The second buffer is optional*/


static lv_color_t buf_1[MY_DISP_HOR_RES * 10];


static lv_color_t buf_2[MY_DISP_HOR_RES * 10];





/*Initialize `disp_buf` with the buffer(s). With only one buffer use NULL instead buf_2 */


lv_disp_draw_buf_init(&disp_buf, buf_1, buf_2, MY_DISP_HOR_RES*10);

请注意,lv_disp_draw_buf_t 需要是静态的、全局的或动态分配的,而不是超出范围时销毁的局部变量。

绘制缓冲区可以小于屏幕。在这种情况下,较大的区域将被重新绘制为适合绘制缓冲区的较小部分。 如果只有一个小区域发生变化(例如按下按钮),则只会刷新该区域。

更大的缓冲区会导致更好的性能,但超过 1/10 屏幕大小的缓冲区没有显着的性能改进。 因此,建议选择绘制缓冲区的大小至少为屏幕大小的 1/10。

如果只使用一个缓冲区,LVGL 将屏幕内容绘制到该绘制缓冲区中并将其发送到显示器。 这样 LVGL 需要等到缓冲区的内容发送到显示器,然后再在其中绘制新内容。

如果使用两个缓冲区,LVGL 可以绘制到一个缓冲区中,而另一个缓冲区的内容被发送到后台显示。 应使用 DMA 或其他硬件将数据传输到显示器,让 MCU 同时绘制。 这样,显示的渲染和刷新变得并行。

在显示驱动程序(lv_disp_drv_t)中,可以启用 full_refresh 位以强制 LVGL 始终重绘整个屏幕。这适用于 one buffer 和 two buffers 模式。

如果启用 full_refresh 并提供 2 个屏幕大小的绘制缓冲区,LVGL 的显示处理就像“传统”双缓冲一样工作。

这意味着在 flush_cb 中只有帧缓冲区的地址需要更改为提供的指针(color_p 参数)。 如果 MCU 具有 LCD 控制器外围设备而不是外部显示控制器(例如 ILI9341 或 SSD1963),则应使用此配置。

显示驱动程序

一旦缓冲区初始化准备好,lv_disp_drv_t 显示驱动程序需要

用lv_disp_drv_init(&disp_drv)初始化

它的字段需要设置

需要在LVGL中用lv_disp_drv_register(&disp_drv)注册

请注意,lv_disp_drv_t 也需要是静态的、全局的或动态分配的,而不是超出范围时销毁的局部变量。

必须要适配的部分

在最简单的情况下,只需要设置lv_disp_drv_t的以下字段:

指向初始化的 lv_disp_draw_buf_t 变量的 draw_buf 指针。

hor_res 显示器的水平分辨率(以像素为单位)。

ver_res 显示器的垂直分辨率(以像素为单位)。

flush_cb 一个回调函数,用于将缓冲区的内容复制到显示器的特定区域。

lv_disp_flush_ready(&disp_drv) 需要在刷新准备好时调用。 LVGL 可能会以多个块呈现屏幕,因此多次调用 flush_cb。要查看当前是否是渲染的最后一个块,请使用 lv_disp_flush_is_last(&disp_drv)。

可选的部分

有一些可选的数据字段:

color_chroma_key 将在镀铬键控图像上绘制为透明的颜色。从lv_conf.h默认设置为LV_COLOR_CHROMA_KEY。

anti_aliasing 使用抗锯齿(边缘平滑)。如果在 lv_conf.h 中将 LV_COLOR_DEPTH 设置为至少 16,则默认启用。

rotated 和 sw_rotate 请参阅下面的 Rotation 部分。

screen_transp 如果 1 屏幕本身也可以具有透明度。 LV_COLOR_SCREEN_TRANSP 需要在 lv_conf.h 中启用并且需要 LV_COLOR_DEPTH 32。

user_data 驱动程序的自定义void 用户数据…

一些其他可选的回调,使处理单色、灰度或其他非标准 RGB 显示器更容易、更优化:

rounder_cb 四舍五入要重绘的区域的坐标。例如。 2x2 px 可以转换为 2x8。 如果显示控制器只能刷新具有特定高度或宽度的区域(单色显示器通常为 8 像素高度),则可以使用它。

set_px_cb 一个自定义函数来写入绘制缓冲区。如果显示器具有特殊的颜色格式,它可用于将像素更紧凑地存储在绘图缓冲区中。 (例如 1 位单色、2 位灰度等) 这样,lv_disp_draw_buf_t 中使用的缓冲区可以更小,以仅容纳给定区域大小所需的位数。请注意,使用set_px_cb 渲染比普通渲染慢。

monitor_cb 一个回调函数,告诉我们在多长时间内刷新了多少像素。当最后一个块被渲染并发送到显示器时调用。

clean_dcache_cb 用于清理与显示相关的任何缓存的回调。

查看原文

LVGL 内置了对多个 GPU 的支持(参见 lv_conf.h),但如果需要其他功能,这些函数可用于使 LVGL 使用 GPU:

gpu_fill_cb 用颜色填充内存中的一个区域。

gpu_wait_cb 如果在 GPU 仍在工作时任何 GPU 函数返回,LVGL 将在需要时使用此函数以确保 GPU 渲染准备就绪。

示例

 1
 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


static lv_disp_drv_t disp_drv;          /*一个用来控制驱动程序的变量。必须是静态的或全局的。*/


lv_disp_drv_init(&disp_drv);            /*基本初始化*/


disp_drv.draw_buf = &disp_buf;          /*设置一个初始化的缓冲区*/


disp_drv.flush_cb = my_flush_cb;        /*设置刷新回调以绘制到显示*/


disp_drv.hor_res = 320;                 /*以像素为单位设置水平分辨率*/


disp_drv.ver_res = 240;                 /*以像素为单位设置垂直分辨率*/





lv_disp_t * disp;


disp = lv_disp_drv_register(&disp_drv); /*注册驱动程序并保存创建的显示对象*/

三、输入设备接口

输入设备的类型

要注册输入设备,必须初始化一个 lv_indev_drv_t 变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18


lv_indev_drv_t indev_drv;


lv_indev_drv_init(&indev_drv);      /*Basic initialization*/


indev_drv.type =...                 /*See below.*/


indev_drv.read_cb =...              /*See below.*/


/*Register the driver in LVGL and save the created input device object*/


lv_indev_t * my_indev = lv_indev_drv_register(&indev_drv);

type 可以是

LV_INDEV_TYPE_POINTER 触摸板或鼠标

LV_INDEV_TYPE_KEYPAD 键盘或小键盘

LV_INDEV_TYPE_ENCODER 编码器,带有左/右转和推动选项

LV_INDEV_TYPE_BUTTON 外部按钮虚拟按下屏幕

read_cb 是一个函数指针,它将被定期调用以报告输入设备的当前状态。

触摸板、鼠标或任何指针

可以点击屏幕上的点的输入设备属于这一类。

 1
 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


indev_drv.type = LV_INDEV_TYPE_POINTER;


indev_drv.read_cb = my_input_read;





...





void my_input_read(lv_indev_drv_t * drv, lv_indev_data_t*data)


{


  if(touchpad_pressed) {


    data->point.x = touchpad_x;


    data->point.y = touchpad_y;


    data->state = LV_INDEV_STATE_PRESSED;


  } else {


    data->state = LV_INDEV_STATE_RELEASED; 


  }


}

要设置鼠标光标,请使用 lv_indev_set_cursor(my_indev, &img_cursor)。 (my_indev 是 lv_indev_drv_register 的返回值)

四、心跳接口

LVGL 需要一个系统滴答来了解动画和其他任务所用的时间。

需要定期调用 lv_tick_inc(tick_period) 函数并提供以毫秒为单位的调用周期。例如,lv_tick_inc(1) 每毫秒调用一次。

lv_tick_inc 应该在比 lv_task_handler() 更高优先级的例程中调用(例如在中断中),以精确知道经过的毫秒数,即使 lv_task_handler 的执行需要更多时间。

使用 FreeRTOS,可以在 vApplicationTickHook 中调用 lv_tick_inc。

在基于 Linux 的操作系统(例如在 Raspberry Pi 上)可以在如下线程中调用 lv_tick_inc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21


void * tick_thread (void *args)


{


      while(1) {


        usleep(5*1000);   /*睡眠5毫秒*/


        lv_tick_inc(5);      /*告诉LVGL已经过了5毫秒*/


    }


}

任务处理器

要处理 LVGL 的任务,您需要以下列方式之一定期调用 lv_timer_handler():

main() 函数的 while(1)

定时器定期中断(比 lv_tick_inc() 优先级低)

定期执行操作系统任务

时间并不重要,但它应该是大约 5 毫秒以保持系统响应。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12


while(1) {


  lv_timer_handler();


  my_delay_ms(5);


}

睡眠管理

当没有用户输入发生时,MCU 可以进入睡眠状态。在这种情况下,主要的 while(1) 应该是这样的:

 1
 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


while(1) {


  /*不活动时间<1秒时正常运行(无睡眠)*/


  if(lv_disp_get_inactive_time(NULL) < 1000) {


	  lv_task_handler();


  }


  /*休息1秒钟后再睡眠*/


  else {


	  timer_stop();   /*在调用lv_tick_inc()的位置停止计时器*/


	  sleep();		    /*让MCU睡眠*/


  }


  my_delay_ms(5);


}

你还应该将以下几行添加到你的输入设备读取功能中,以表示发生了唤醒(按下、触摸或点击等):

1
2
3
4
5
6
7
8
9


lv_tick_inc(LV_DISP_DEF_REFR_PERIOD);  /*唤醒时强制执行任务*/


timer_start();                         /*重新启动调用lv_tick_inc()的计时器*/


lv_task_handler();                     /*手动调用'lv_task_handler()'来处理唤醒事件*/

除了 lv_disp_get_inactive_time() 之外,你还可以检查 lv_anim_count_running() 以查看是否所有动画都已完成。

五、LVGL完整初始化

  1
  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
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132


int main(int argc,char *argv[])


{


    /*LittlevGL init*/


    lv_init();


  


   /*Linux frame buffer device init*/


    fbdev_init();





   /*A small buffer for LittlevGL to draw the screen's content*/


    static lv_color_t buf[DISP_BUF_SIZE];


    static lv_color_t buf1[DISP_BUF_SIZE];





   /*Initialize a descriptor for the buffer*/


    static lv_disp_draw_buf_t disp_buf;


    lv_disp_draw_buf_init(&disp_buf, buf, buf1, DISP_BUF_SIZE);





   /*Initialize and register a display driver*/


    static lv_disp_drv_t disp_drv;


    lv_disp_drv_init(&disp_drv);


    disp_drv.draw_buf   = &disp_buf;


    disp_drv.flush_cb   = fbdev_flush;


    disp_drv.hor_res    = 1024;


    disp_drv.ver_res    = 600;


    lv_disp_drv_register(&disp_drv);


  


    lv_indev_drv_t indev_drv;


    lv_indev_drv_init(&indev_drv);





    indev_drv.type = LV_INDEV_TYPE_POINTER;


    indev_drv.read_cb = (void (*)(struct _lv_indev_drv_t *, lv_indev_data_t *))evdev_read; // defined in lv_drivers/indev/evdev.h


    lv_indev_t *lv_indev = lv_indev_drv_register(&indev_drv);


    if (!lv_indev)


    {


        printf("lv_indev rregister error %d \r\n", __LINE__);


        return 0;


    }


  


   /*Handle LitlevGL tasks (tickless mode)*/


    while(1) {


        lv_task_handler();


        usleep(5000);


    }


    return 0;


}