1 LVGL描述
LVGL本身是一个图形库。
LVGL的作者是来自匈牙利的Gabor Kiss-Vamosikisvegabor,用C语言编写,是一个免费的开放源代码图形库,它提供创建具有易于使用的图形元素,精美的视觉效果和低内存占用的嵌入式GUI所需的一切。
1.1 特性描述
主要特性
- 功能强大的构建块,例如按钮,图表,列表,滑块,图像等。
- 带有动画,抗锯齿,不透明,平滑滚动的高级图形。
- 各种输入设备,例如触摸板,鼠标,键盘,编码器等。
- 支持UTF-8编码的多语言。
- 多显示器支持,如TFT,单色显示器。
- 完全可定制的图形元素。
- 独立于任何微控制器或显示器使用的硬件。
- 可扩展以使用很少的内存(64 kB闪存,16 kB RAM)进行操作。
- 操作系统,支持外部存储器和GPU,但不是必需的。
- 单帧缓冲区操作,即使具有高级图形效果。
- 用C语言编写,以实现最大的兼容性(与C ++兼容)。
- 模拟器可在没有嵌入式硬件的PC上进行嵌入式GUI设计。
- 可移植到MicroPython。
- 可快速上手的教程、示例、主题。
- 丰富的文档教程。
- 在MIT许可下免费和开源。
1.2 硬件要求
基本上,每个现代控制器(肯定必须要能够驱动显示器)都适合运行LVGL。LVGL的最低运行要求很低:
- 16、32或64位微控制器或处理器最低 16 MHz 时钟频率
- Flash/ROM::对于非常重要的组件要求 >64 kB(建议 > 180 kB)
- 静态 RAM 使用量:~2 kB,取决于所使用的功能和对象类型
- 堆栈: > 2kB(建议 > 8 kB)
- 动态数据(堆):> 2 KB(如果使用多个对象,则建议 > 16 kB)。由 lv_conf.h 中的 LV_MEM_SIZE 宏进行设置。
- 显示缓冲区:> “水平分辨率”像素(建议 > 10× “水平分辨率” )
- MCU 或外部显示控制器中的一帧缓冲区
- C99或更高版本的编译器
- 具备基本的C(或C ++)知识:指针,结构,回调…
1.3 系统框架
应用程序创建 GUI 并处理特定任务的应用程序。
LVGL 本身是一个图形库。我们的应用程序通过调用 LVGL 库来创建 GUI 。它包含一个 HAL (硬件抽象层)接口,用于注册显示和输入设备驱动程序。
驱动程序除特定的驱动程序外,它还有其他的功能,可驱动显示器到 GPU (可选)、读取触摸板或按钮的输入。
根据 MCU ,有两种典型的硬件设置。 一个带有内置 LCD/TFT 驱动器的外围设备,而另一种是没有内置 LCD/TFT 驱动器的外围设备。 在这两种情况下,都需要一个 帧缓冲区 来存储屏幕的当前图像。
- 集成了 TFT/LCD 驱动器的 MCU 如果 MCU 集成了 TFT/LCD 驱动器外围设备,则可以直接通过RGB接口连接显示器。 在这种情况下,帧缓冲区可以位于内部 RAM(如果MCU有足够的RAM)中,也可以位于外部RAM(如果MCU具有存储器接口)中。
- 如果 MCU 没有集成 TFT/LCD 驱动程序接口,则必须使用外部显示控制器(例如 SSD1963、SSD1306、ILI9341 )。 在这种情况下,MCU 可以通过并行端口,SPI 或通过 I2C 与显示控制器进行通信。 帧缓冲区通常位于显示控制器中,从而为 MCU 节省了大量 RAM 。
1.4 LVGL运行原理介绍
上图中的LittledvGL模块,都是基于其定义的“timer”定时器设计的,LVGL需要系统应答才能知道什么时候该切换其他任务,调用动画等模块经过了多少时间。所以在应用层我们需要调用两个函数:
lv_tick_inc:定期调用 函数,并以毫秒为单位告知调用周期;
lv_timer_handler:任务处理器(Task Handler);
1.4.1 lv_tick_inc
首先,我们先在LVGL源码中看下lv_tick_inc干了什么:
void lv_tick_inc(uint32_t tick_period)
{
tick_irq_flag = 0;
sys_time += tick_period;
}
这其中存在两个全局变量:
tick_irq_flag : 函数被用户调用后该变量置位
sys_time :时间,当前的 ms 数
这两个变量仅在lv_tick_get中被调用,用于记录心跳时间。
uint32_t result;
do {
itck_irq_flag = 1;
reult = sys_time;
}while(!tick_irq_flag);
如上,lv_tick_get的作用就很明确了,LVGL 需要系统滴答声才能知道动画和其他任务的经过时间。
为此我们需要定期调用 lv_tick_inc(tick_period) 函数,并以毫秒为单位告知调用周期。
1.4.1 lv_task_handler
要处理 LVGL 的任务,我们需要定期通过以下方式之一调用 lv_task_handler() :
- mian 函数中设置 while(1) 调用
- 定期定时中断(低优先级然后是 lv_tick_inc()) 中调用
- 定期执行的 OS 任务中调用
- 计时并不严格,但应保持大约5毫秒以保持系统响应。
为了精确地知道经过的毫秒数,lv_tick_inc 应该在比 lv_task_handler() 更高优先级的例程中被调用(例如在中断中),即使 lv_task_handler 的执行花费较长时间;
lv_timer_handler:
...
uint32_t handler_start = lv_tick_get();
...
busy_time += lv_tick_elaps(handler_start);
uint32_t idle_period_time = lv_tick_elaps(idle_period_start);
if(idle_period_time > IDLE_MEAS_PERIOD)
{
idle_last = (busy_time * 100) / idle_period_time;
idle_last = idle_last > 100 ? 0 : 100 - idle_last;
busy_time = 0;
idle_period_start = lv_tick_get();
}
在lv_timer_handler函数中,通过上述逻辑可以让lvgl模块判断任务执行时间执行超时及任务切换逻辑。
/*Run all timer from the list*/
lv_timer_t * next;
do {
timer_deleted = false;
timer_created = false;
LV_GC_ROOT(_lv_timer_act) = _lv_ll_get_head(&LV_GC_ROOT(_lv_timer_ll));
while(LV_GC_ROOT(_lv_timer_act)) {
/*The timer might be deleted if it runs only once ('repeat_count = 1')
*So get next element until the current is surely valid*/
next = _lv_ll_get_next(&LV_GC_ROOT(_lv_timer_ll), LV_GC_ROOT(_lv_timer_act));
if(lv_timer_exec(LV_GC_ROOT(_lv_timer_act))) {
/*If a timer was created or deleted then this or the next item might be corrupted*/
if(timer_created || timer_deleted) {
TIMER_TRACE("Start from the first timer again because a timer was created or deleted");
break;
}
}
LV_GC_ROOT(_lv_timer_act) = next; /*Load the next timer*/
}
} while(LV_GC_ROOT(_lv_timer_act));
这里会查询 _lv_timer_ll 链表上的 timer,并执行 lv_timer_exec 函数,他这里做了一些安全保护,不影响逻辑。
值得一提的是,在我们注册屏幕时(LVGL驱动初始化时)就会添加timer到任务链表中去。
/*Create a refresh timer*/
disp->refr_timer = lv_timer_create(_lv_disp_refr_timer, LV_DISP_DEF_REFR_PERIOD, disp);
LV_ASSERT_MALLOC(disp->refr_timer);
if(disp->refr_timer == NULL) {
lv_mem_free(disp);
return NULL;
}
这时LVGL注册了"_lv_disp_refr_timer"函数到timer链表上,该函数主要作用是刷新缓冲区到显示器中,值得一提的是,LVGL 的绘制,不是直接绘制到屏幕,首先是往内部缓冲区绘制,当绘图(渲染)准备好时,该缓冲区被刷到屏幕;
与直接绘制到屏幕相比,这种方法有两个主要优点:
- 避免绘制UI层时闪烁。例如,如果 LVGL 直接绘制到显示中,那么在绘制 *背景 + 按钮 + 文本 * 时,每个“阶段”都会在短时间内可见。
- 修改内部 RAM 中的缓冲区并最终仅写入一个像素一次比在每个像素访问时直接读取/写入显示更快。 (例如,通过带有 SPI 接口的显示控制器)。
请注意,此概念与“传统”双缓冲不同,后者有 2 个屏幕大小的帧缓冲区: 一个保存当前图像以显示在显示器上,渲染发生在另一个(非活动)帧缓冲区中,渲染完成后它们会被交换。 主要区别在于,使用 LVGL,不必存储 2 个帧缓冲区(通常需要外部 RAM),而只需存储更小的绘图缓冲区,也可以轻松装入内部 RAM。
上述“_lv_disp_refr_timer”中主要有两个函数需要我们注意:
lv_refr_jion_area(); 该函数会计算需要刷新的区域大小
lv_refr_areas(); 该函数会进行实际的数据刷新/拷贝
static void draw_buf_flush(void)
{
lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);
lv_color_t * color_p = draw_buf->buf_act;
draw_buf->flushing = 1;
if(disp_refr->driver->draw_buf->last_area && disp_refr->driver->draw_buf->last_part) draw_buf->flushing_last = 1;
else draw_buf->flushing_last = 0;
/*Flush the rendered content to the display*/
lv_disp_t * disp = _lv_refr_get_disp_refreshing();
if(disp->driver->gpu_wait_cb) disp->driver->gpu_wait_cb(disp->driver);
if(disp->driver->flush_cb) {
/*Rotate the buffer to the display's native orientation if necessary*/
if(disp->driver->rotated != LV_DISP_ROT_NONE && disp->driver->sw_rotate) {
draw_buf_rotate(&draw_buf->area, draw_buf->buf_act);
} else {
call_flush_cb(disp->driver, &draw_buf->area, color_p);
}
}
if(draw_buf->buf1 && draw_buf->buf2) {
if(draw_buf->buf_act == draw_buf->buf1)
draw_buf->buf_act = draw_buf->buf2;
else
draw_buf->buf_act = draw_buf->buf1;
}
}
注意上述函数的call_flush_cb(),这里是重点,这个函数是我们初始化驱动时的配置刷新回调函数,他的入参是图像的变化区域的数据。
在这个回调函数内我们可以把LVGL生成的图形根据应用的需要进行定制化操作。
2 LVGL模块封装
小结
经过这些天的对LVGL使用、封装,并实际在产品上应用的经验来讲,LVGL是一款简单、高效功能十分强大的