当前位置: 首页 > ds >正文

Dive into LVGL (1) —— How LVGL works from top to down

0.briefly speaking

由于工作原因,最近开始接触到一些图形图像处理相关的知识,在这个过程中逐渐接触到了LVGL。作为一个开源的图形库,LVGL可以高效地为MCU、MPU等嵌入式设备构建美观的UI界面。我的手头也正好有一块集成了Vivante 2.5D GPU的嵌入式开发板,于是想借此机会深入了解一下LVGL->VGLite->GPU的工作过程。本文的参考资料和源代码如下:

  • LVGL中文文档——百问网(此文档有些地方显示不全)
  • HPMicro SDK
  • VGLite API Reference Manual
  • VGLite编程指南
  • LVGL模拟器——Porting to VsCode(无开发板可使用此模拟器学习LVGL)

本文将以一个最简单的控件的绘制过程为例,结合LVGL的源代码完成LVGL工作流程的分析。请注意本文并不是教大家如何用LVGL的API绘制精美的界面,这个过程也许后面会在闲暇时展开一些介绍,但本文要做一些更硬核的工作——深入LVGL的工作原理。当然,作为DEMO展示的界面,本文中也会对其中使用到的接口进行简单介绍

1.应用代码


以下应用代码节选自hpm_sdk/samples/lvgl/vglite,这段代码在MCU进入FreeRTOS内核之后启动了一个名为lvgl_task的任务,其函数调用关系如下:
/ **************************** /
// lvgl绘图主任务
lvgl_task
   // 初始化触摸屏和lcd屏,因开发板而异…
   // 通过VGLite接口初始化VGLite和GPU硬件,这是VGLite源码的一部分
   gpu_vg_lite_startup
   // 初始化LVGL的主函数
   hpm_lvgl_init
    // 初始化LVGL图形库(*)
    lv_init
/ **************************** /

完整的应用代码流程和注释如下:

static void lvgl_task(void *pvParameters)
{(void) pvParameters;uint32_t delay;// 初始化开发板上的触摸屏和lcd屏(board dependent)board_init_cap_touch();board_init_lcd();// [初始化GPU]// 这个函数调用了vg_lite_init()来完成主要的初始化动作// lv_conf.h中LV_VG_LITE_USE_GPU_INIT为0时,此函数必须要在lvgl初始化前gpu_vg_lite_startup();// [初始化lvgl(*)]hpm_lvgl_init();// 绘图函数,可以在这里绘出漂亮的图片,甚至动画// 但这不是我们本文的重点 :(// hpm_lvgl_demos();draw_something();// 保证LVGL定时任务正常进行和屏幕刷新while (1) {delay = lv_timer_handler();vTaskDelay(delay);}
}

2.自底向上——从LVGL的初始化说起

在上述的任务函数中调用了hpm_lvgl_init对LVGL进行初始化,其实现如下,其中处于核心地位的是lv_init函数,它负责初始化整个LVGL框架。随后,如果在lv_conf.h文件中配置了使用VG_LITE通用GPU进行绘图,则需要一些额外的初始化动作。最后,还有一些输入输出设备相关的初始化动作,下面一一来看。

void hpm_lvgl_init(void)
{// 初始化LVGL图形库(*)lv_init();// 如果启用了VG_LITE作为绘图后端,则需要一些额外的初始化动作
// 这里HPM自己封装了一套内存管理接口,为绘图缓冲区分配和回收内存
#if defined(LV_USE_DRAW_VG_LITE) && LV_USE_DRAW_VG_LITEdraw_buf_handlers_init();
#endif// 初始化lvgl滴答计数回调、显示器设备和输入设备(指触摸屏)hpm_lvgl_tick_init();hpm_lvgl_display_init();hpm_lvgl_indev_init();
}

2.1 LVGL的初始化——lv_init

此函数会根据用户在lv_conf.h中指定的配置,有选择性地对LVGL源码库中的组件进行包含和编译。最终这些配置将会影响到LVGL的初始化过程中需要执行的动作,并对LVGL具体执行的代码流程造成影响。

以下是LVGL库初始化过程的主函数,由于篇幅所限,我们仅对其中调用的一部分函数展开,关注的重点放在不同后端绘图硬件的初始化动作上
/ ***************************************************** /
// lvgl初始化函数
lv_init
   // 初始化全局静态变量lv_global
   LV_GLOBAL_INIT(LV_GLOBAL_DEFAULT())
   // 初始化软件(CPU)绘图单元
   lv_draw_sw_init
     // 设置任务分发函数(软件作为绘图单元)
     dispatch(lv_draw_sw.c)
      // 挑选一个合适于特定绘图单元的独立绘图任务,送入绘图单元执行
      lv_draw_get_next_available_task
     // 设置任务评估函数(软件作为绘图单元)
     evaluate(lv_draw_sw.c)
     // 初始化一个渲染线程(非裸机环境)
     lv_thread_init
   // 初始化VGLite API兼容的绘图单元
   / * 这里使用链表头插法将VGLite绘图单元置于软件绘图单元之前 * /
   lv_draw_vg_lite_init
     // 设置任务分发函数(VGLite作为绘图单元)
     draw_dispatch(lv_draw_vg_lite.c)
     // 设置任务评估函数(VGLite作为绘图单元)
     draw_evaluate(lv_draw_vg_lite.c)

/ ***************************************************** /

void lv_init(void)
{/*First initialize Garbage Collection if needed*/// 初始化垃圾回收机制,这应是一个由用户自定义的函数,从略
#ifdef LV_GC_INITLV_GC_INIT();
#endif/*Do nothing if already initialized*/// 如果LVGL已经初始化,则什么也不做if(lv_initialized) {LV_LOG_WARN("lv_init: already initialized");return;}LV_LOG_INFO("begin");/*Initialize members of static variable lv_global */// 初始化全局变量lv_global中的成员LV_GLOBAL_INIT(LV_GLOBAL_DEFAULT());// 初始化LVGL的内存管理// LVGL使用TLSF算法来管理内存,这里为LVGL初始化一块静态存储区lv_mem_init();// 为LVGL绘图缓冲区指定操作函数句柄_lv_draw_buf_init_handlers();#if LV_USE_SPAN != 0lv_span_stack_init();
#endif#if LV_USE_PROFILER && LV_USE_PROFILER_BUILTINlv_profiler_builtin_config_t profiler_config;lv_profiler_builtin_config_init(&profiler_config);lv_profiler_builtin_init(&profiler_config);
#endif// 初始化lv_global结构体中的一系列成员_lv_timer_core_init(); 	/* 定时器初始化 */_lv_fs_init();			/* 文件系统抽象层的初始化 */_lv_layout_init();		/* 布局管理模块的初始化 */_lv_anim_core_init();   /* 动画图像的初始化 */_lv_group_init();		/* 焦点组的初始化 */lv_draw_init();			/* 绘图单元的初始化(仅在有OS介入时有实际动作) *//*LVGL支持多种后端绘图硬件,从最简单的CPU绘图(LV_USE_DRAW_SW)到使用特定的GPU绘图,依据lv_conf.h配置文件的不同,会在初始化阶段对不同的硬件后端执行初始化,请参见:https://docs.lvgl.io/master/details/main-modules/draw/draw_pipeline.html
*/// 初始化软件(SW)绘图单元,也就是CPU绘图
#if LV_USE_DRAW_SWlv_draw_sw_init();
#endif// 初始化NXP VGLite GPU绘图单元
#if LV_USE_DRAW_VGLITElv_draw_vglite_init();
#endif/* 以下是一系列不同后端绘图单元的初始化,这里不再一一展开 */
#if LV_USE_DRAW_PXPlv_draw_pxp_init();
#endif#if LV_USE_DRAW_DAVE2Dlv_draw_dave2d_init();
#endif#if LV_USE_DRAW_SDLlv_draw_sdl_init();
#endif#if LV_USE_WINDOWSlv_windows_platform_init();
#endif// 初始化对象风格,关于对象风格请参照:// https://docs.lvgl.io/master/details/common-widget-features/styles/styles.html_lv_obj_style_init();/*Initialize the screen refresh system*/_lv_refr_init();#if LV_USE_SYSMON_lv_sysmon_builtin_init();
#endif/* 解码器相关的初始化 */_lv_image_decoder_init();lv_bin_decoder_init();  /*LVGL built-in binary image decoder*/// 初始化VGLITE API兼容的绘图单元
#if LV_USE_DRAW_VG_LITElv_draw_vg_lite_init();
#endif// 测试IDE是否支持UTF-8编码,以及机器的大小端/*Test if the IDE has UTF-8 encoding*/const char * txt = "Á";uint8_t * txt_u8 = (uint8_t *)txt;if(txt_u8[0] != 0xc3 || txt_u8[1] != 0x81 || txt_u8[2] != 0x00) {LV_LOG_WARN("The strings have no UTF-8 encoding. Non-ASCII characters won't be displayed.");}uint32_t endianness_test = 0x11223344;uint8_t * endianness_test_p = (uint8_t *) &endianness_test;bool big_endian = endianness_test_p[0] == 0x11;if(big_endian) {LV_ASSERT_MSG(LV_BIG_ENDIAN_SYSTEM == 1,"It's a big endian system but LV_BIG_ENDIAN_SYSTEM is not enabled in lv_conf.h");}else {LV_ASSERT_MSG(LV_BIG_ENDIAN_SYSTEM == 0,"It's a little endian system but LV_BIG_ENDIAN_SYSTEM is enabled in lv_conf.h");}/* 以下都是一些LVGL特性的初始化,这里不再展开 */#if LV_USE_ASSERT_MEM_INTEGRITYLV_LOG_WARN("Memory integrity checks are enabled via LV_USE_ASSERT_MEM_INTEGRITY which makes LVGL much slower");
#endif#if LV_USE_ASSERT_OBJLV_LOG_WARN("Object sanity checks are enabled via LV_USE_ASSERT_OBJ which makes LVGL much slower");
#endif#if LV_USE_ASSERT_STYLELV_LOG_WARN("Style sanity checks are enabled that uses more RAM");
#endif#if LV_LOG_LEVEL == LV_LOG_LEVEL_TRACELV_LOG_WARN("Log level is set to 'Trace' which makes LVGL much slower");
#endif#if LV_USE_FS_FATFS != '\0'lv_fs_fatfs_init();
#endif#if LV_USE_FS_STDIO != '\0'lv_fs_stdio_init();
#endif#if LV_USE_FS_POSIX != '\0'lv_fs_posix_init();
#endif#if LV_USE_FS_WIN32 != '\0'lv_fs_win32_init();
#endif#if LV_USE_FS_MEMFSlv_fs_memfs_init();
#endif#if LV_USE_FS_LITTLEFSlv_fs_littlefs_init();
#endif#if LV_USE_LODEPNGlv_lodepng_init();
#endif#if LV_USE_LIBPNGlv_libpng_init();
#endif#if LV_USE_TJPGDlv_tjpgd_init();
#endif#if LV_USE_LIBJPEG_TURBOlv_libjpeg_turbo_init();
#endif#if LV_USE_BMPlv_bmp_init();
#endif/*Make FFMPEG last because the last converter will be checked first and*it's superior to any other */
#if LV_USE_FFMPEGlv_ffmpeg_init();
#endif#if LV_USE_FREETYPE/*Init freetype library*/lv_freetype_init(LV_FREETYPE_CACHE_FT_GLYPH_CNT);
#endif#if LV_USE_TINY_TTFlv_tiny_ttf_init();
#endif// 设置LVGL已初始化的标志lv_initialized = true;LV_LOG_TRACE("finished");
}

2.1.1 初始化全局变量——lv_global_init

在lv_global.h头文件中定义了一个名为lv_global_t的全局结构体,其中记录着LVGL图形库的一些具体配置和运行时状态,这里不对其中每一个具体字段的含义展开分析,等到对应源码调用到时再展开。

LV_GLOBAL_INIT是用来初始化上述lv_global结构体的函数,它本质上是lv_global_init函数的宏包装,而lv_global_init函数的实现如下

static inline void lv_global_init(lv_global_t * global)
{LV_ASSERT_NULL(global);if(global == NULL) {LV_LOG_ERROR("lv_global cannot be null");return;}// 将lv_global全局变量全部清0lv_memzero(global, sizeof(lv_global_t));// 初始化显示器和输入设备链表// global->disp_ll和global->indev_ll以链表形式组织显示器和输入设备_lv_ll_init(&(global->disp_ll), sizeof(lv_display_t));_lv_ll_init(&(global->indev_ll), sizeof(lv_indev_t));// 初始化一些字段,设定随机数种子global->memory_zero = ZERO_MEM_SENTINEL;global->style_refresh = true;global->layout_count = _LV_LAYOUT_LAST;global->style_last_custom_prop_id = (uint32_t)_LV_STYLE_LAST_BUILT_IN_PROP;global->event_last_register_id = _LV_EVENT_LAST;lv_rand_set_seed(0x1234ABCD);// 如果设定了影子cache,则初始化其他的一些变量
#if defined(LV_DRAW_SW_SHADOW_CACHE_SIZE) && LV_DRAW_SW_SHADOW_CACHE_SIZE > 0global->sw_shadow_cache.cache_size = -1;global->sw_shadow_cache.cache_r = -1;
#endif
}

2.1.2 初始化软件绘图后端——lv_draw_sw_init

此函数用来对软件绘图后端进行初始化,在此函数中主要完成了

void lv_draw_sw_init(void)
{// 如果打开了软件绘制复杂图形的开关
// 在这里初始化LV_GLOBAL_DEFAULT()->draw_info.circle_cache_mutex互斥量
#if LV_DRAW_SW_COMPLEX == 1lv_draw_sw_mask_init();
#endifuint32_t i;// 初始化指定数量的绘图单元for(i = 0; i < LV_DRAW_SW_DRAW_UNIT_CNT; i++) {// 分配一个新的软件绘图单元结构体,并将其使用[头插法]插入lv_global->draw_info链表中lv_draw_sw_unit_t * draw_sw_unit = lv_draw_create_unit(sizeof(lv_draw_sw_unit_t));// 设置软件绘图分发函数及评估函数// 分发函数(dispatch)负责完成具体的绘图动作// 评估函数(evaluate)负责评估绘图单元自身对于任务的“擅长程度”draw_sw_unit->base_unit.dispatch_cb = dispatch;draw_sw_unit->base_unit.evaluate_cb = evaluate;// 设置软件绘图单元编号和删除函数draw_sw_unit->idx = i;draw_sw_unit->base_unit.delete_cb = LV_USE_OS ? lv_draw_sw_delete : NULL;// 如果存在操作系统,则在此处创建一个线程/任务来接管上述创建好的绘图单元
// 任务主函数是render_thread_cb
#if LV_USE_OSlv_thread_init(&draw_sw_unit->thread, LV_THREAD_PRIO_HIGH, render_thread_cb, 8 * 1024, draw_sw_unit);
#endif}// 一些特殊处理
#if LV_USE_VECTOR_GRAPHIC && LV_USE_THORVGtvg_engine_init(TVG_ENGINE_SW, 0);
#endif
}
2.1.2.1 软件绘图的任务分发函数——dispatch(lv_draw_sw.c)

dispatch函数对于每一个可用的后端绘图都有重载的版本,它主要完成了以下几件事情:

  • 从图层layer中获取可由当前绘图单元执行的绘图任务(lv_draw_task_t)
  • 如有必要,为当前的图层layer分配绘图缓冲区(layer->draw_buf)
  • 唤醒渲染线程开始工作(裸机/操作系统)

作为补充,我们看看LVGL文档对Dispatching过程的描述:


While collecting Draw Tasks LVGL frequently dispatches the collected Draw Tasks to their assigned Draw Units. This is handled via the dispatch_cb of the Draw Units.
翻译:在收集绘图任务的同时,LVGL会持续地将收集到的绘图任务分发给它们指定的绘图单元(Draw Unit),这是由绘图单元的回调函数dispatch_cb来处理的。

If a Draw Unit is busy with another Draw Task, it just returns. However, if it is available it can take a Draw Task.
翻译:如果一个绘图单元正忙于处理另一个绘图任务,那么分发函数将直接返回。然而,如果当前绘图单元可用(空闲),则可以接收一个新的绘图任务。

lv_draw_get_next_available_task(layer, previous_task, draw_unit_id) is a useful helper function which is used by the dispatch_cb to get the next Draw Task it should act on. If it handled the task, it sets the Draw Task’s state field to LV_DRAW_TASK_STATE_READY (meaning “completed”). “Available” in this context means that has been queued and assigned to a given Draw Unit and is ready to be carried out. The ramifications of having multiple drawing threads are taken into account for this.

翻译:lv_draw_get_next_available_task(layer, previous_task, draw_unit_id)是一个很有用的功能函数,可以被分发回调函数调用以确定下一个应该执行的绘图任务。如果完成了这个任务,则将会把绘图任务的状态域(state filed)设置为LV_DRAW_TASK_STATE_READY(表示已完成)。这里上下文中的“可用”(Available)意味着任务已经排队,并已经分配给特定的绘图单元,并已经准备好执行。拥有多个绘图线程可能带来的影响(同步问题)也已经考虑在内。


所以本质上,dispatch函数是让当前的绘图单元(draw_unit)认领特定图层(layer)的绘图任务(lv_draw_task_t),并唤醒对应的渲染线程开始工作的过程(execute_drawing_unit),以下是软件绘图单元的默认分发函数实现:

static int32_t dispatch(lv_draw_unit_t * draw_unit, lv_layer_t * layer)
{LV_PROFILER_BEGIN;// 取出软件绘图单元lv_draw_sw_unit_t * draw_sw_unit = (lv_draw_sw_unit_t *) draw_unit;/*Return immediately if it's busy with draw task*/// 如果当前软件单元正在执行绘图任务,则直接返回if(draw_sw_unit->task_act) {LV_PROFILER_END;return 0;}// 获取软件绘图单元可以执行的下一个任务lv_draw_task_t * t = NULL;t = lv_draw_get_next_available_task(layer, NULL, DRAW_UNIT_ID_SW);// 如果没有获取到,则直接返回-1if(t == NULL) {LV_PROFILER_END;return -1;}/* 如果执行至此,说明已经领取到了一个绘图任务 */// 为当前图层分配缓冲区void * buf = lv_draw_layer_alloc_buf(layer);if(buf == NULL) {LV_PROFILER_END;return -1;}// 改变当前任务的状态为LV_DRAW_TASK_STATE_IN_PROGRESS,表示正在处理t->state = LV_DRAW_TASK_STATE_IN_PROGRESS;// 设定当前绘图单元的目标图层,和裁剪区域draw_sw_unit->base_unit.target_layer = layer;draw_sw_unit->base_unit.clip_area = &t->clip_area;// 设定当前绘图单元正在执行的任务,此任务将在execute_drawing_unit函数/渲染线程中被识别和进一步处理draw_sw_unit->task_act = t;// 如果有操作系统,通过信号量告知渲染线程开始工作
#if LV_USE_OS/*Let the render thread work*/if(draw_sw_unit->inited) lv_thread_sync_signal(&draw_sw_unit->sync);// 在裸机情况下,直接调用execute_drawing_unit完成绘制
#elseexecute_drawing_unit(draw_sw_unit);
#endifLV_PROFILER_END;return 1;
}
2.1.2.2 软件绘图的任务评估函数——evaluate(lv_draw_sw.c)

和分发函数一起被指定的,还有一个评估函数evaluate。评估函数的作用是在一个绘图任务刚刚被添加到队列时,每个绘图单元用来评价自身对于当前任务处理能力的函数

首先,不同绘图单元设置的评估函数会判断自身是否有能力完成此任务,若能完成且比之前的绘图单元更快(通过preference_score来衡量),则更新绘图任务的默认绘图单元为自身,这进而也会反过来影响到上述lv_draw_get_next_available_task函数的判断。

作为补充,这里也将LVGL文档中关于评估函数(evaluate)的说明,在此一并给出:


When each Draw Task is created, each existing Draw Unit is “consulted” as to its “appropriateness” for the task. It does this through an “evaluation callback” function pointer (a.k.a. evaluate_cb), which each Draw Unit sets (for itself) during its initialization. Normally, that evaluation:

  • optionally examines the existing “preference score” for the task mentioned above
  • if it can accomplish that type of task (e.g. line drawing) faster than other Draw Units that have already reported, it writes its own “preference score” and “preferred Draw Unit ID” to the respective fields in the task

翻译:当一个绘图任务被创建时,每一个绘图单元都会被“询问”它自己对于任务的适配度(appropriateness)。它通过“评估回调函数”函数指针(也就是evaluate_cb)来完成这个任务,这个函数是在每个绘图单元初始化时为自己设定的。一般情况下,这个评估过程会:

  • 检查上述提到的任务现存的“偏好得分”(preference score)
  • 如果它(指绘图单元)可以比已报告的其他绘图单元更快的速度完成此种类型的任务,则它将自己的“偏好得分”和“偏好绘图单元ID”写入任务的对应字段(这本质上意味着更新了任务的最优绘图单元)

In this way, by the time the evaluation sequence is complete, the task will contain the score and the ID of the Drawing Unit that will be used to perform that task when it is dispatched.

翻译: 这样一来,等待评估过程一结束,任务就会自然得到它即将要被分发到的绘图单元的ID号和得分。

This logic, of course, can be overridden or redefined, depending on system design.

As a side effect, this also ensures that the same Draw Unit will be selected consistently, depending on the type (and nature) of the drawing task, avoiding any possible screen jitter in case more than one Draw Unit is capable of performing a given task type.

The sequence of the Draw Unit list (with the Software Draw Unit at the end) also ensures that the Software Draw Unit is the “buck-stops-here” Draw Unit: if no other Draw Unit reported it was better at a given drawing task, then the Software Draw Unit will handle it.

翻译:评估函数的逻辑,自然而然的,可以被重载和优化,这取决于系统的设计。

一个带来的附加作用(附加好处)是,这同时也确保了(对于一个特定的绘图任务)同一个绘图单元会被连续地选择(因为这只取决于绘图任务的类型的和其本质),这避免了多个绘图单元均可执行同一个类型的任务时可能带来的屏幕抖动现象

绘图单元列表的默认顺序(即保证软件绘图单元位于最后),同样也确保了软件绘图单元会作为“兜底”的绘图单元。如果没有其他绘图单元声称它在给定的绘图任务上表现更好,则软件绘图单元会接管它


下面结合具体代码,看看任务评估函数的逻辑是如何实现的:

static int32_t evaluate(lv_draw_unit_t * draw_unit, lv_draw_task_t * task)
{LV_UNUSED(draw_unit);/* 根据当前要执行的任务类型,来判断是否有不支持的渲染操作如果有,则直接返回,表明当前绘图单元没有能力支持指定的渲染动作,自然也不能执行此任务 */switch(task->type) {case LV_DRAW_TASK_TYPE_IMAGE:case LV_DRAW_TASK_TYPE_LAYER: {lv_draw_image_dsc_t * draw_dsc = task->draw_dsc;/* not support skew */// 软件绘图单元不支持画歪斜线(两条不共面的斜线)if(draw_dsc->skew_x != 0 || draw_dsc->skew_y != 0) {return 0;}bool transformed = draw_dsc->rotation != 0 || draw_dsc->scale_x != LV_SCALE_NONE ||draw_dsc->scale_y != LV_SCALE_NONE ? true : false;bool masked = draw_dsc->bitmap_mask_src != NULL;// 如果在图形变换的同时启用了蒙版,这也是不支持的if(masked && transformed)  return 0;// 蒙版和一些特定的颜色格式同时启用也是不支持的lv_color_format_t cf = draw_dsc->header.cf;if(masked && (cf == LV_COLOR_FORMAT_A8 || cf == LV_COLOR_FORMAT_RGB565A8)) {return 0;}}break;default:break;}/* 如果至此还没有退出,说明当前绘图单元有能力执行此绘图任务 */// 如果当前绘图单元的执行速度比软件慢,则替换默认绘图单元为软件// preference_score描述了绘图单元执行绘图任务速度的相对值,软件绘图速度默认为100,也是基准值// 越小,表示速度越快if(task->preference_score >= 100) {// 设置当前任务的执行速度为100,则将默认绘图单元更新为软件task->preference_score = 100;task->preferred_draw_unit_id = DRAW_UNIT_ID_SW;}return 0;
}
2.1.2.3 创建一个软件渲染线程——lv_thread_init(lv_freertos.c)

在非裸机的开发板上运行LVGL(开启LV_USE_OS),许多操作都需要经过操作系统的抽象来实现。LVGL为此专门提供了操作系统抽象层(Operating System Abstraction Layer, OSAL)来用统一的接口兼容各种不同的操作系统。这里我们以对FreeRTOS的适配作为例子,浅析一下渲染线程的初始化过程。

首先是LVGL线程的初始化过程,对于FreeRTOS而言,这一步本质上是通过调用xTaskCreate来完成的:

// [后面备注了调用时传入的参数]
lv_result_t lv_thread_init(lv_thread_t * pxThread,			// &draw_sw_unit->thread  lv_thread_prio_t xSchedPriority,	// LV_THREAD_PRIO_HIGHvoid (*pvStartRoutine)(void *), 	// render_thread_cbsize_t usStackSize,				// 8 * 1024void * xAttr)					// draw_sw_unit
{/*********************************/// pxThread是LVGL封装的线程类型,其定义如下://	typedef struct {//		void (*pvStartRoutine)(void *);       /*< Application thread function. *///		void * xTaskArg;                      /*< Arguments for application thread function. *///		TaskHandle_t xTaskHandle;             /*< FreeRTOS task handle. *///	} lv_thread_t;/*********************************/// 设置线程要执行的函数主体和参数pxThread->xTaskArg = xAttr;pxThread->pvStartRoutine = pvStartRoutine;BaseType_t xTaskCreateStatus = xTaskCreate(prvRunThread,		// pvTaskCode, 入口函数指针pcTASK_NAME,		// pcName, 任务的描述性名称// uxStackDepth, 任务堆栈的字数(configSTACK_DEPTH_TYPE)(usStackSize / sizeof(StackType_t)), (void *)pxThread,					// pvParameters, 传入函数的参数tskIDLE_PRIORITY + xSchedPriority,	// uxPriority, 任务优先级&pxThread->xTaskHandle);			// pxCreatedTask, 记录下创建的任务句柄/* Ensure that the FreeRTOS task was successfully created. */if(xTaskCreateStatus != pdPASS) {LV_LOG_ERROR("xTaskCreate failed!");return LV_RESULT_INVALID;}return LV_RESULT_OK;
}

接下来,我们看看被注册到FreeRTOS任务中的渲染线程的主函数逻辑,也就是render_thread_cb函数的实现。实际上,渲染动作也是依赖execute_drawing_unit函数完成的,这和裸机环境下是保持一致的,我们将在后面对此函数进行解析。除此之外,渲染线程会在绘图任务没有到来时,始终在信号量上等待

static void render_thread_cb(void * ptr)
{lv_draw_sw_unit_t * u = ptr;// 初始化线程同步结构体lv_thread_sync_init(&u->sync);u->inited = true;// 渲染任务循环体...while(1) {// 如果绘图单元当前没有正在执行的任务while(u->task_act == NULL) {// 如果当前软件绘图单元准备取消// 则直接退出当前循环,不再等待任务的到来if(u->exit_status) {break;}// 等待信号量唤醒此渲染进程lv_thread_sync_wait(&u->sync);}// 如果当前绘图单元取消,则退出此函数if(u->exit_status) {LV_LOG_INFO("ready to exit software rendering thread");break;}// 执行绘图动作execute_drawing_unit(u);}// 上述循环一旦退出,意味着软件绘图单元已经失效u->inited = false;lv_thread_sync_delete(&u->sync);LV_LOG_INFO("exit software rendering thread");
}

2.1.3 初始化VGLite API兼容的绘图后端——lv_draw_vg_lite_init

此函数和lv_draw_sw_init的大体轮廓是一致的,也都包括分发函数和评估函数的设定。与上面的软件绘图单元不同的是,这里少了许多线程创建和同步的步骤,而更加专注于GPU硬件的初始化和VGLite中一些变量的初始化。

void lv_draw_vg_lite_init(void)
{
// P.S.: 
// 如果LVGL初始化时GPU硬件还没有初始化好,则应该打开LV_VG_LITE_USE_GPU_INIT宏
// 让LVGL调用gpu_init(本质上就是vg_lite_init)完成GPU硬件初始化
// HPM-SDK已经在gpu_vg_lite_startup中完成了GPU初始化,因此无需打开LV_VG_LITE_USE_GPU_INIT宏
#if LV_VG_LITE_USE_GPU_INITextern void gpu_init(void);static bool inited = false;if(!inited) {gpu_init();inited = true;}
#endif// 打印GPU硬件信息,和VGLite API版本信息lv_vg_lite_dump_info();// 初始化vglite专用的绘图缓冲区操作函数width_to_stride_cb// 和lv_init中的_lv_draw_buf_init_handlers相呼应,哪里初始化的是缓冲区操作通用函数lv_draw_buf_vg_lite_init_handlers();// 这里创建了新的VGLite GPU绘图单元,// 并使用"头插法"将其插入draw_info链表// <!-- 这意味这VGLite GPU会先于软件绘图单元被调用 -->lv_draw_vg_lite_unit_t * unit = lv_draw_create_unit(sizeof(lv_draw_vg_lite_unit_t));// 和上面软件绘图单元一致,初始化分发、评估函数unit->base_unit.dispatch_cb = draw_dispatch;unit->base_unit.evaluate_cb = draw_evaluate;unit->base_unit.delete_cb = draw_delete;// 初始化VGLite的一些机制lv_vg_lite_image_dsc_init(unit);	/* 初始化图像描述符 */lv_vg_lite_grad_init(unit);			/* 初始化渐变相关机制 */lv_vg_lite_path_init(unit);			/* 初始化全局路径 */lv_vg_lite_decoder_init();			/* 初始化图片解码器 */
}
2.1.3.1 VGLite后端任务分发函数——draw_dispatch(lv_draw_vg_lite.c)

和上面软件绘图后端类似,任务分发函数的主要任务还是让绘图单元从就绪任务中可以认领的任务,并完成绘图的动作(draw_execute),关于绘图的部分,我们将在第二部分深入。下面是分发函数的具体实现:

static int32_t draw_dispatch(lv_draw_unit_t * draw_unit, lv_layer_t * layer)
{lv_draw_vg_lite_unit_t * u = (lv_draw_vg_lite_unit_t *)draw_unit;/* Return immediately if it's busy with draw task. */// 如果当前绘图单元有正在执行的任务,则直接退出if(u->task_act) {return 0;}/* Try to get an ready to draw. */// 尝试找出适合VGLite绘图单元的任务lv_draw_task_t * t = lv_draw_get_next_available_task(layer, NULL, VG_LITE_DRAW_UNIT_ID);/* Return 0 is no selection, some tasks can be supported by other units. */// 如果没有找到合适的任务,或是当前任务的首选绘图单元并非VGLite// 则直接返回if(!t || t->preferred_draw_unit_id != VG_LITE_DRAW_UNIT_ID) {lv_vg_lite_finish(u);return -1;}// 尝试为当前图层分配绘图缓冲区void * buf = lv_draw_layer_alloc_buf(layer);if(!buf) {return -1;}/* Return if target buffer format is not supported. */// 如果目标缓冲区的颜色格式(color format, cf)不受支持,则直接返回if(!lv_vg_lite_is_dest_cf_supported(layer->draw_buf->header.cf)) {return -1;}// 将绘图任务信息设置到绘图单元字段中,表示接管此任务t->state = LV_DRAW_TASK_STATE_IN_PROGRESS;u->base_unit.target_layer = layer;u->base_unit.clip_area = &t->clip_area;u->task_act = t;// 开始执行此绘图任务draw_execute(u);/* 运行至此,则绘图任务已执行完毕 */// 设置任务状态为已完成,标记绘图单元为空闲u->task_act->state = LV_DRAW_TASK_STATE_READY;u->task_act = NULL;/*The draw unit is free now. Request a new dispatching as it can get a new task*/// 绘图单元已经空闲,此时可以请求派发下一个绘图任务lv_draw_dispatch_request();return 1;
}
2.1.3.2 VGLite后端任务评估函数——draw_evaluate(lv_draw_vg_lite.c)

与上面一样,评估函数做的事情就是根据当前绘图单元的能力来判断是否有能力处理当前任务,如果可以支持,则将偏好得分修改为80分。这意味这它比软件基准速度更快,因而也更适合处理当前的绘图任务

static int32_t draw_evaluate(lv_draw_unit_t * draw_unit, lv_draw_task_t * task)
{LV_UNUSED(draw_unit);// 判断VGLite是否可以支持指定的绘图任务switch(task->type) {case LV_DRAW_TASK_TYPE_LABEL:case LV_DRAW_TASK_TYPE_FILL:case LV_DRAW_TASK_TYPE_BORDER:
#if LV_VG_LITE_USE_BOX_SHADOWcase LV_DRAW_TASK_TYPE_BOX_SHADOW:
#endifcase LV_DRAW_TASK_TYPE_LAYER:case LV_DRAW_TASK_TYPE_LINE:case LV_DRAW_TASK_TYPE_ARC:case LV_DRAW_TASK_TYPE_TRIANGLE:case LV_DRAW_TASK_TYPE_MASK_RECTANGLE:
#if LV_USE_VECTOR_GRAPHICcase LV_DRAW_TASK_TYPE_VECTOR:
#endifbreak;// 如果是绘制图片的任务,判断VGLite是否支持图片的色彩格式case LV_DRAW_TASK_TYPE_IMAGE: {if(!check_image_is_supported(task->draw_dsc)) {return 0;}}break;default:/*The draw unit is not able to draw this task. */return 0;}/* The draw unit is able to draw this task. */// 直接设定偏好得分为80,偏好的绘图单元为VGLite// 比软件基准值100更小,表示速度更快task->preference_score = 80;task->preferred_draw_unit_id = VG_LITE_DRAW_UNIT_ID;return 1;
}

3.自顶向下——从绘制一条线段说起

上面从自底向上的视角认识了LVGL是如何从开始一点点初始化的,我们看到了VGLite和LVGL是如何初始化的,也简单了介绍了LVGL中的任务派发函数(dispatch)任务评估函数(evaluate)的概念。接下来我们从图形的绘制过程,自顶向下地解释LVGL接口是如何调用不同绘图单元后端提供的接口,完成绘图任务的

static void lvgl_task(void *pvParameters)
{/* 以上是GPU和LVGL初始化的过程 */// 绘图函数,可以在这里绘出漂亮的图片,甚至动画draw_something();// 保证LVGL定时任务正常进行和屏幕刷新while (1) {delay = lv_timer_handler();vTaskDelay(delay);}
}

让我们回顾一下上面绘制图案的FreeRTOS任务代码,我们一般会在draw_somenthing()这里调用一些LVGL的API完成一些图案的绘制,比如我们可以用下面的代码绘制一个灰色的对钩

void draw_check(void)
{static lv_style_t style;lv_style_init(&style);lv_style_set_line_color(&style, lv_palette_main(LV_PALETTE_GREY));lv_style_set_line_width(&style, 6);lv_style_set_line_rounded(&style, true);/*Create an object with the new style*/lv_obj_t * obj = lv_line_create(lv_screen_active());lv_obj_add_style(obj, &style, 0);static lv_point_precise_t p[] = {{10, 30}, {30, 50}, {100, 0}};lv_line_set_points(obj, p, 3);lv_obj_center(obj);
}

上面的代码我没有逐行注解,因为这里大部分都是LVGL中的接口函数。我们的重点在于,这个图形到底是如何被绘制到显示器上的,它又是如何与我们在初始化时注册的任务分发函数关联上的。

因此,我们用GDB在函数draw_dispatch处(vg_lite绘图后端注册的任务分发函数)打一个断点,命中后查看函数调用的backtrace,以下是经过整理的函数调用路径(#num表示调用的函数栈帧编号):

// output by GDB
// 以下函数调用栈帧从下向上阅览	
// 截止到#0栈帧,我们调用到了初始化时vg_lite绘图后端注册的draw_dispatch函数
[#0]  in lv_draw_dispatch_layer at lvgl/src/draw/lv_draw.c:255
[#1]  in lv_draw_dispatch at lvgl/src/draw/lv_draw.c:161
[#2]  in lv_draw_finalize_task_creation at lvgl/src/draw/lv_draw.c:138
[#3]  in lv_draw_line at lvgl/src/draw/lv_draw_line.c:66
/* 通过发送绘图事件,异步触发绘图动作 */ 
[#4]  in lv_line_event at lvgl/src/widgets/line/lv_line.c:213
[#5]  in lv_obj_event_base at lvgl/src/core/lv_obj_event.c:86
[#6]  in event_send_core at lvgl/src/core/lv_obj_event.c:359 	
[#7]  in lv_obj_send_event at lvgl/src/core/lv_obj_event.c:64
/*              递归调用               */
[#8]  in lv_obj_redraw at lvgl/src/core/lv_refr.c:110 		
[#9]   in refr_obj at lvgl/src/core/lv_refr.c:892
[#10]  in lv_obj_redraw at lvgl/src/core/lv_refr.c:161							^
[#11]  in refr_obj at lvgl/src/core/lv_refr.c:892								|
/*              递归调用               */										|
[#12]  in refr_obj_and_children at lvgl/src/core/lv_refr.c:790					|
[#13]  in refr_area_part at lvgl/src/core/lv_refr.c:723							|
[#14]  in refr_area at lvgl/src/core/lv_refr.c:619								|
[#15]  in refr_invalid_areas at lvgl/src/core/lv_refr.c:566						|
[#16]  in _lv_display_refr_timer at lvgl/src/core/lv_refr.c:374					|
[#17]  in lv_timer_exec at lvgl/src/misc/lv_timer.c:300							|
[#18] in lv_timer_handler at lvgl/src/misc/lv_timer.c:105						|

主目录

这是一张非常重要的路线图,在它的指引下,我们下面开启第二部分的内容,首先给出此部分的目录:
/ ***************************************************** /
// LVGL的时钟处理函数,随着tick递增而执行各个定时器动作
lv_timer_handler
// 判断计时器是否超时,超时则调用回调函数
lv_timer_exec
// 显示器的超时刷新回调函数
_lv_display_refr_timer
/* 以下内容是逐层细化的刷新动作 /
// 刷新未被合并的无效区域
refr_invalid_areas
// 依据不同渲染模式刷新无效区域
refr_area
// 刷新无效子区域
refr_area_part
// 刷新对象及其子控件
refr_obj_and_children
// 控件重绘(),这里实际通过事件机制触发了绘图动作
lv_obj_redraw
/* 刷新动作结束,下面进入绘图动作 /
// 通过LVGL的事件机制触发绘图
关于LVGL事件机制的概述
// 创建线段绘制任务
lv_draw_line
// 任务创建结束,评估并尝试分发(任务评估函数在此被调用)
lv_draw_finalize_task_creation
// 绘图任务分发主函数
lv_draw_dispatch
// 分发特定的图层到绘图单元(任务分发函数在此被调用)
lv_draw_dispatch_layer
/ 至此,绘图动作已被分发到绘图单元 */

/ ***************************************************** /

代码详解部分

以下沿着上述调用栈帧的顺序,对其中的函数进行研究:

[#18] LVGL的绘图的入口——时钟驱动的处理函数lv_timer_handler

在上面GDB打印出来的函数调用栈中可以看到,真正触发绘图任务的起点是LVGL中的时钟处理函数lv_timer_handler。在对此函数进行深入分析之前,首先需要对LVGL中的工作过程做出解释,以下是LVGL官方文档中对于LVGL绘图过程的刻画
在这里插入图片描述
详细来说,LVGL通过函数接口lv_tick_inc获得系统时钟,一般来说它会在系统时钟中断中被调用,用来告知LVGL系统时间的流逝。

“知道时间已经过了多久”对于LVGL非常重要,因为显示过程中很多事件的发生需要时间作为判据,比如动画的刷新、定时事件的触发等。而lv_timer_handler就是在获悉时间流逝之后,要具体完成做什么动作,所以我们可以看到真正的绘图渲染动作都是以lv_timer_handler为入口的。用户的绘图函数(例如上面的绘制对钩的代码)仅仅只是告诉LVGL“要怎么画,在哪里画”,完全是应用层的逻辑。

所以,LVGL是一套"时间驱动"的绘图框架。

接下来看看lv_timer_handler的通用代码实现(in misc/lv_timer.c),这个函数完成了如下几个事情:

  • 对已经注册的定时器timer链表进行遍历,并尝试执行每一个定时器所绑定的任务(详见下面lv_timer_exec函数)
  • 计算下一次timer事件触发的最短时间
  • 对LVGL性能执行简单的分析,计算繁忙时间和空闲时间的比率等
LV_ATTRIBUTE_TIMER_HANDLER uint32_t lv_timer_handler(void)
{LV_TRACE_TIMER("begin");lv_timer_state_t * state_p = &state;/*Avoid concurrent running of the timer handler*/// 并发保护,防止此函数正在被其他线程执行if(state_p->already_running) {LV_TRACE_TIMER("already running, concurrent calls are not allow, returning");return 1;}// 进入之后将标志置位,防止其他线程进入state_p->already_running = true;// 如果timer没有使能,那么此函数也要退出if(state_p->lv_timer_run == false) {state_p->already_running = false; /*Release mutex*/return 1;}LV_PROFILER_BEGIN;// 获取当前LVGL的tick计时// 我们上面说过tick计时由lv_tick_inc来更新,一般基于[系统时钟(system timer)]来提供时钟源// 其实tick的获取也可以被tick_get_cb重载,对于HPM SDK,它从RV-32 CPU的性能寄存器来获取计时uint32_t handler_start = lv_tick_get();// 如果得到的tick是0,且超过一定次数// 说明LVGL没有从一个稳定的时钟获取tick,输出警告信息if(handler_start == 0) {state.run_cnt++;if(state.run_cnt > 100) {state.run_cnt = 0;LV_LOG_WARN("It seems lv_tick_inc() is not called.");}}/*Run all timer from the list*/lv_timer_t * next;lv_timer_t * timer_active;lv_ll_t * timer_head = timer_ll_p;// 扫描所有LVGL注册的软件定时器(timers)// 对于需要定期刷新的任务,LVGL都会注册一个软件定时器,来定期刷新它们的执行do {// 恢复是否有定时器被创建和删除的标志位为假// 表明当前没有发生定时器的创建或删除state_p->timer_deleted             = false;state_p->timer_created             = false;// 遍历注册的timer链表,逐个执行timer绑定的定时任务timer_active = _lv_ll_get_head(timer_head);while(timer_active) {/*The timer might be deleted if it runs only once ('repeat_count = 1')*So get next element until the current is surely valid*/// 获取指向下一个timer的指针next = _lv_ll_get_next(timer_head, timer_active);// 执行当前timer所绑定的定时任务// 如果在执行过程中发生了定时器的添加或删除,重新开始一轮遍历if(lv_timer_exec(timer_active)) {/*If a timer was created or deleted then this or the next item might be corrupted*/if(state_p->timer_created || state_p->timer_deleted) {LV_TRACE_TIMER("Start from the first timer again because a timer was created or deleted");break;}}// 指向下一个timer timer_active = next; /*Load the next timer*/}} while(timer_active);// 计算下一个计时器被触发的时间// 这应该是下一次timer_handler被调用的时间uint32_t time_until_next = LV_NO_TIMER_READY;next = _lv_ll_get_head(timer_head);while(next) {// 如果timer没有暂停,则所有timer剩余时间的最小值就应该是下一次被触发的时间if(!next->paused) {uint32_t delay = lv_timer_time_remaining(next);if(delay < time_until_next)time_until_next = delay;}next = _lv_ll_get_next(timer_head, next); /*Find the next timer*/}// 性能分析,观察这一次timer_handler执行过程的耗时相对于空闲时长的比例state_p->busy_time += lv_tick_elaps(handler_start);uint32_t idle_period_time = lv_tick_elaps(state_p->idle_period_start);if(idle_period_time >= IDLE_MEAS_PERIOD) {// 计算繁忙时间相对于整个周期的比例state_p->idle_last         = (state_p->busy_time * 100) / idle_period_time;  /*Calculate the busy percentage*/state_p->idle_last         = state_p->idle_last > 100 ? 0 : 100 - state_p->idle_last; /*But we need idle time*/state_p->busy_time         = 0;state_p->idle_period_start = lv_tick_get();}// 设置下一次触发timer_handler的时机state_p->timer_time_until_next = time_until_next;state_p->already_running = false; /*Release the mutex*/LV_TRACE_TIMER("finished (%" LV_PRIu32 " ms until the next timer call)", time_until_next);LV_PROFILER_END;// 返回下一次触发handler的时间return time_until_next;
}

跳回主目录

[#17] 执行每个定时器对应的定时任务——lv_timer_exec

此函数完成的功能非常直观,就是检查当前计时器是否已经到时,如果到时就触发其超时回调函数。同时,定时器每超时一次都会使得剩余执行次数(timer->repeat_count)减少一次。当剩余执行次数减少到0时,会根据标志位(timer->auto_delete)的设置选择是删除当前定时器,还是暂停当前计时器,暂停计时器可以有效减小LVGL运行过程中因为动态分配和回收定时器带来的开销。

完整的源代码如下:

static bool lv_timer_exec(lv_timer_t * timer)
{// 如果该定时器已经暂停,则直接返回if(timer->paused) return false;bool exec = false;// 如果计时器到时,则执行回调函数if(lv_timer_time_remaining(timer) == 0) {/* Decrement the repeat count before executing the timer_cb.* If any timer is deleted `if(timer->repeat_count == 0)` is not executed below* but at least the repeat count is zero and the timer can be deleted in the next round*/// 递减repeat_countint32_t original_repeat_count = timer->repeat_count;if(timer->repeat_count > 0) timer->repeat_count--;// 记录当前时间,last_run会在lv_timer_time_remaining函数中使用// 用以判断当前定时器是否已经超时timer->last_run = lv_tick_get();LV_TRACE_TIMER("calling timer callback: %p", *((void **)&timer->timer_cb));// 当剩余次数不为0时,调用此定时器绑定的超时回调函数,也就是下面的_lv_display_refr_timerif(timer->timer_cb && original_repeat_count != 0) timer->timer_cb(timer);// 如果定时器在回调函数中没有被删除,那么打印出回调函数的地址,并提醒完成if(!state.timer_deleted) {LV_TRACE_TIMER("timer callback %p finished", *((void **)&timer->timer_cb));}else {LV_TRACE_TIMER("timer callback finished");}LV_ASSERT_MEM_INTEGRITY();// 执行完成的标志exec = true;}// 如果定时器的重复次数已经用完,且设置了自动删除的标记,则删掉计时器if(state.timer_deleted == false) { /*The timer might be deleted by itself as well*/if(timer->repeat_count == 0) { /*The repeat count is over, delete the timer*/if(timer->auto_delete) {LV_TRACE_TIMER("deleting timer with %p callback because the repeat count is over", *((void **)&timer->timer_cb));lv_timer_delete(timer);}// 否则只是暂停当前计时器,这可以免除反复创建和删除定时器带来的时间开销else {LV_TRACE_TIMER("pausing timer with %p callback because the repeat count is over", *((void **)&timer->timer_cb));lv_timer_pause(timer);}}}return exec;
}

跳回主目录

[#16] 显示器超时刷新回调函数——_lv_display_refr_timer

此回调函数是在函数lv_display_create中绑定到显示器对应的定时器上的,对应的代码如下:

// in src/display/lv_display.c:95
// lv_timer_t * lv_timer_create(lv_timer_cb_t timer_xcb, uint32_t period, void * user_data)
// timer超时回调函数为_lv_display_refr_timer,传入的user_data为显示器disp
disp->refr_timer = lv_timer_create(_lv_display_refr_timer, LV_DEF_REFR_PERIOD, disp);

以下是显示器超时回调函数的具体实现,它主要完成的事情有以下几件:

  • 刷新屏幕(screen)和屏幕图层(screen layer)的布局
  • 合并要重新绘制的无效区域,减小绘制时的开销(lv_refr_join_area)
  • 刷新无效区域(refr_invalid_areas),这是真正完成绘图的地方
  • 如果是直显模式+双缓冲区,要同步双缓冲区的内容,防止画面撕裂
void _lv_display_refr_timer(lv_timer_t * tmr)
{LV_PROFILER_BEGIN;LV_TRACE_REFR("begin");if(tmr) {// 取出对应的显示器对象disp_refr = tmr->user_data;/* Ensure the timer does not run again automatically.* This is done before refreshing in case refreshing invalidates something else.* However if the performance monitor is enabled keep the timer running to count the FPS.*/// 如果没有开启性能监视器,则暂停当前的timer
// 防止在屏幕上重复刷新相同的内容,进而造成浪费
#if !(defined(LV_USE_PERF_MONITOR) && LV_USE_PERF_MONITOR)lv_timer_pause(tmr);
#endif}else {disp_refr = lv_display_get_default();}// 如果没有获取到显示器对象,则直接返回if(disp_refr == NULL) {LV_LOG_WARN("No display registered");return;}// 如果没有获取到有效的绘图缓冲区,则直接返回lv_draw_buf_t * buf_act = disp_refr->buf_act;if(!(buf_act && buf_act->data && buf_act->data_size)) {LV_LOG_WARN("No draw buffer");return;}// 发送刷新开始事件提醒,所有监控LV_EVENT_REFR_START事件的函数都会执行对应的动作// 这关于事件系统,在此暂不详细展开lv_display_send_event(disp_refr, LV_EVENT_REFR_START, NULL);/*Refresh the screen's layout if required*/// 刷新屏幕布局LV_PROFILER_BEGIN_TAG("layout");lv_obj_update_layout(disp_refr->act_scr);if(disp_refr->prev_scr) lv_obj_update_layout(disp_refr->prev_scr);// 刷新屏幕图层(screen layer)的布局// bottom_layer位于最底层,用于保留背景效果// active screen一般是我们正在维护的UI控件树// top layer位于我们的屏幕层之上,用来设置一些弹窗或遮罩效果// sys layer由LVGL使用,位于top layer之上,用于系统组件(例如光标)lv_obj_update_layout(disp_refr->bottom_layer);lv_obj_update_layout(disp_refr->top_layer);lv_obj_update_layout(disp_refr->sys_layer);LV_PROFILER_END_TAG("layout");/*Do nothing if there is no active screen*/if(disp_refr->act_scr == NULL) {disp_refr->inv_p = 0;LV_LOG_WARN("there is no active screen");goto refr_finish;}// 尝试将需要重绘的无效区域合并,减少绘制时的开销lv_refr_join_area();// 刷新同步区域// [仅在直显模式+双frame buffer缓冲的情况下,此函数才会执行]refr_sync_areas();// 在这里真正刷新未合并的无效区域(*)// 请注意,合并之后的区域也会被标记成未合并区域,并在之后refr_invalid_areas中被重绘refr_invalid_areas();if(disp_refr->inv_p == 0) goto refr_finish;// 如果当前处于直显模式且开启了双缓冲区// 则需要将此次重绘的区域,同步拷贝给off-screen buffer一份,否则两个缓冲区不同步// 会出现画面撕裂/*If refresh happened ...*/lv_display_send_event(disp_refr, LV_EVENT_RENDER_READY, NULL);if(!lv_display_is_double_buffered(disp_refr) ||disp_refr->render_mode != LV_DISPLAY_RENDER_MODE_DIRECT) goto refr_clean_up;/*With double buffered direct mode synchronize the rendered areas to the other buffer*//*We need to wait for ready here to not mess up the active screen*/wait_for_flushing(disp_refr);uint32_t i;for(i = 0; i < disp_refr->inv_p; i++) {if(disp_refr->inv_area_joined[i])continue;lv_area_t * sync_area = _lv_ll_ins_tail(&disp_refr->sync_areas);*sync_area = disp_refr->inv_areas[i];}// 渲染完成,恢复无效区域与合并结果
refr_clean_up:lv_memzero(disp_refr->inv_areas, sizeof(disp_refr->inv_areas));lv_memzero(disp_refr->inv_area_joined, sizeof(disp_refr->inv_area_joined));disp_refr->inv_p = 0;refr_finish:#if LV_DRAW_SW_COMPLEX == 1_lv_draw_sw_mask_cleanup();
#endif// 提示刷新结果已经就绪lv_display_send_event(disp_refr, LV_EVENT_REFR_READY, NULL);LV_TRACE_REFR("finished");LV_PROFILER_END;
}

跳回主目录

[#15] 刷新未被合并的无效区域——refr_invalid_areas

此函数作用非常简单,就是在合并完绘图区域之后,对那些还未合并的无效区域进行刷新,因此其核心代码就是一个for循环来对未合并的无效区域进行刷新,真正的刷新动作是通过调用refr_area函数来完成的,这将会在下面慢慢展开。完整代码如下:

/*** Refresh the joined areas*/
static void refr_invalid_areas(void)
{if(disp_refr->inv_p == 0) return;LV_PROFILER_BEGIN;/*Find the last area which will be drawn*/// 倒序遍历,找到最后一个需要被重绘的区域编号,记录下来// 这在后面会作为判断循环是否结束的标记int32_t i;int32_t last_i = 0;for(i = disp_refr->inv_p - 1; i >= 0; i--) {if(disp_refr->inv_area_joined[i] == 0) {last_i = i;break;}}/*Notify the display driven rendering has started*/// 提醒“渲染已开始”lv_display_send_event(disp_refr, LV_EVENT_RENDER_START, NULL);// 初始化控制变量,同时将正在渲染的标志rendering_in_progress置为1disp_refr->last_area = 0;disp_refr->last_part = 0;disp_refr->rendering_in_progress = true;// 开始遍历无效区域,找出其中没有合并的区域,刷新它们for(i = 0; i < (int32_t)disp_refr->inv_p; i++) {/*Refresh the unjoined areas*/if(disp_refr->inv_area_joined[i] == 0) {// 如果当前已经是最后一个刷新区域,则无需再遍历下去if(i == last_i) disp_refr->last_area = 1;disp_refr->last_part = 0;// 在此处真正刷新未被合并的无效区域(*)refr_area(&disp_refr->inv_areas[i]);}}// 渲染结束,恢复渲染标志disp_refr->rendering_in_progress = false;LV_PROFILER_END;
}

跳回主目录

[#14] 依据不同渲染模式刷新无效区域——refr_area

此函数会根据显示器指定的渲染模式(LV_DISPLAY_RENDER_MODE_PARTIAL、LV_DISPLAY_RENDER_MODE_FULL、LV_DISPLAY_RENDER_MODE_DIRECT)的不同对将要重绘的子区域(disp_area和sub_area)进行初始化,并最终调用refr_area_part函数完成真正的无效区域重绘。

值得注意的是,FULL渲染模式总是刷新整个显示器上的像素,而DIRECT模式虽然也存储了整个显示器上的所有像素点,但是只需要刷新指定的区域,也就是我们作为入口参数传递进去的area_p。PARTIAL模式下缓冲区开销最小,它将要渲染的区域沿Y轴方向继续切分成更细的子区域,并逐块向后渲染。

/*** Refresh an area if there is Virtual Display Buffer* @param area_p  pointer to an area to refresh*/
static void refr_area(const lv_area_t * area_p)
{LV_PROFILER_BEGIN;lv_layer_t * layer = disp_refr->layer_head;layer->draw_buf = disp_refr->buf_act;/*With full refresh just redraw directly into the buffer*//*In direct mode draw directly on the absolute coordinates of the buffer*/// 三种渲染模式介绍如下(from LVGL doc.):// 1.LV_DISPLAY_RENDER_MODE_PARTIAL: 使用的缓冲区往往比显示器的尺寸要小(建议至少要有1/10的屏幕大小)// 在刷新回调函数中渲染的图片需要被拷贝到显示器的指定区域// 2.LV_DISPLAY_RENDER_MODE_DIRECT: 使用的缓冲区大小与显示器大小一致,LVGL每次都将渲染的结果写入到缓冲区// 的指定位置,所以缓冲区常常包含整个显示器图像。如果提供了双缓冲区,那么被渲染的区域总会在刷新之后拷贝到另一个缓冲区// 3.LV_DISPLAY_RENDER_MODE_FULL: 使用的缓冲区大小与显示器大小一致,LVGL总是会重新绘制整个屏幕// 哪怕只有一个像素改变。使用双缓冲区时,和传统意义上的缓冲区一样。// -----------------------------------------------------------------------------// 如果当前模式是LV_DISPLAY_RENDER_MODE_DIRECT或LV_DISPLAY_RENDER_MODE_FULL// 这两种渲染模式下,缓冲区大小都是整个显示器大小if(disp_refr->render_mode != LV_DISPLAY_RENDER_MODE_PARTIAL) {// 设置当前图层的缓冲区大小为整个显示器的大小layer->buf_area.x1 = 0;layer->buf_area.y1 = 0;layer->buf_area.x2 = lv_display_get_horizontal_resolution(disp_refr) - 1;layer->buf_area.y2 = lv_display_get_vertical_resolution(disp_refr) - 1;// 将图层的绘图缓冲区(大小、步长)设置到layer->draw_buf的头部layer_reshape_draw_buf(layer);// 初始化一块显示区域,其大小为显示器大小lv_area_t disp_area;lv_area_set(&disp_area, 0, 0, lv_display_get_horizontal_resolution(disp_refr) - 1,lv_display_get_vertical_resolution(disp_refr) - 1);// 如果渲染模式是LV_DISPLAY_RENDER_MODE_FULL// 则last_part置为1,表示是最后一块要刷新的区域(因为FULL渲染模式下本身就是全屏刷新)// 同时渲染的范围就是整个屏幕范围if(disp_refr->render_mode == LV_DISPLAY_RENDER_MODE_FULL) {disp_refr->last_part = 1;layer->_clip_area = disp_area;refr_area_part(layer);}// 否则只需要渲染屏幕数据中修改的部分,也就是传入的area_pelse if(disp_refr->render_mode == LV_DISPLAY_RENDER_MODE_DIRECT) {disp_refr->last_part = disp_refr->last_area;layer->_clip_area = *area_p;refr_area_part(layer);}LV_PROFILER_END;return;}// 以下就是LV_DISPLAY_RENDER_MODE_PARTIAL渲染模式下的处理逻辑/*Normal refresh: draw the area in parts*//*Calculate the max row num*/// 计算渲染区域的长宽以及末端纵坐标int32_t w = lv_area_get_width(area_p);int32_t h = lv_area_get_height(area_p);int32_t y2 = area_p->y2 >= lv_display_get_vertical_resolution(disp_refr) ?lv_display_get_vertical_resolution(disp_refr) - 1 : area_p->y2;// 计算每次刷新的Y方向的行块步长int32_t max_row = get_max_row(disp_refr, w, h);int32_t row;int32_t row_last = 0;lv_area_t sub_area;for(row = area_p->y1; row + max_row - 1 <= y2; row += max_row) {/*Calc. the next y coordinates of draw_buf*/// 设置要绘制的子区域的信息,和上面类似sub_area.x1 = area_p->x1;sub_area.x2 = area_p->x2;sub_area.y1 = row;sub_area.y2 = row + max_row - 1;layer->draw_buf = disp_refr->buf_act;layer->buf_area = sub_area;layer->_clip_area = sub_area;layer_reshape_draw_buf(layer);if(sub_area.y2 > y2) sub_area.y2 = y2;row_last = sub_area.y2;if(y2 == row_last) disp_refr->last_part = 1;refr_area_part(layer);}/*If the last y coordinates are not handled yet ...*/// 如果最后还剩下一部分没有被整除的区域,在此做一个收尾if(y2 != row_last) {/*Calc. the next y coordinates of draw_buf*/sub_area.x1 = area_p->x1;sub_area.x2 = area_p->x2;sub_area.y1 = row;sub_area.y2 = y2;layer->draw_buf = disp_refr->buf_act;layer->buf_area = sub_area;layer->_clip_area = sub_area;layer_reshape_draw_buf(layer);disp_refr->last_part = 1;refr_area_part(layer);}LV_PROFILER_END;
}

跳回主目录

[#13] 刷新无效子区域——refr_area_part

这段代码是负责执行刷新指定小块区域缓冲的函数,这段代码完成了以下几件事情:

  • 根据显示器是否支持透明度通道来判断是否需要提前清空draw_buf
  • 搜索当前活跃screen上一个screen中包含当前要更新区域(_clip_area)的最小控件元素,并将其按照draw_prev_over_act指定的顺序(即谁覆盖谁)刷新到layer上
  • 将top layer和system layer的控件内容无条件地刷新到layer上
  • 当前图层上的所有内容刷新到显示器上 (draw_buf_flush),此步骤之后才会在显示器上会看到具体样式变化

值得注意的是,绘图动作其实在函数refr_obj_and_children中就已经完成。draw_buf_flush仅仅是将当前图层draw_buffer中的内容刷新到显示器的帧缓冲区里进行显示,因此当GDB断点在执行此函数前后,显示器上会将我们绘制的图像刷新在显示器上

static void refr_area_part(lv_layer_t * layer)
{LV_PROFILER_BEGIN;// refreshed_area中记录的是实际要完成刷新的子区域disp_refr->refreshed_area = layer->_clip_area;/* In single buffered mode wait here until the buffer is freed.* Else we would draw into the buffer while it's still being transferred to the display*/// 如果是单缓冲区,则需要等待刷新过程完毕if(!lv_display_is_double_buffered(disp_refr)) {wait_for_flushing(disp_refr);}/*If the screen is transparent initialize it when the flushing is ready*/// 如果显示器支持透明度通道,则要将重绘区域的透明度信息清空if(lv_color_format_has_alpha(disp_refr->color_format)) {lv_area_t a = disp_refr->refreshed_area;// 如果渲染模式是PARTIAL,则要将刷新区域坐标进行变换到(0,0)if(disp_refr->render_mode == LV_DISPLAY_RENDER_MODE_PARTIAL) {/*The area always starts at 0;0*/lv_area_move(&a, -disp_refr->refreshed_area.x1, -disp_refr->refreshed_area.y1);}// 将要刷新的区域像素清空lv_draw_buf_clear(layer->draw_buf, &a);}lv_obj_t * top_act_scr = NULL;lv_obj_t * top_prev_scr = NULL;/*Get the most top object which is not covered by others*/// 尝试获取可以覆盖_clip_area完整区域的位于当前screen和上一个screen的顶层对象// [这里所谓的previous screen只有在动画场景下才会被记录]top_act_scr = lv_refr_get_top_obj(&layer->_clip_area, lv_display_get_screen_active(disp_refr));if(disp_refr->prev_scr) {top_prev_scr = lv_refr_get_top_obj(&layer->_clip_area, disp_refr->prev_scr);}/*Draw a bottom layer background if there is no top object*/// 如果两者都没有找到,则刷新bottom layer的内容if(top_act_scr == NULL && top_prev_scr == NULL) {refr_obj_and_children(layer, lv_display_get_layer_bottom(disp_refr));}// 判断是否需要在当前活动的屏幕"上"绘制前一层screenif(disp_refr->draw_prev_over_act) {if(top_act_scr == NULL) top_act_scr = disp_refr->act_scr;// 先刷新当前活跃screen的内容refr_obj_and_children(layer, top_act_scr);/*Refresh the previous screen if any*/// 如果存在上一个screen的内容,则将其内容也刷新到屏幕上if(disp_refr->prev_scr) {if(top_prev_scr == NULL) top_prev_scr = disp_refr->prev_scr;refr_obj_and_children(layer, top_prev_scr);}}// 否则颠倒绘图顺序,先画上一个screen的内容,然后再画当前活跃的screen的内容else {/*Refresh the previous screen if any*/if(disp_refr->prev_scr) {if(top_prev_scr == NULL) top_prev_scr = disp_refr->prev_scr;refr_obj_and_children(layer, top_prev_scr);}// 刷新当前active screen的UI树(*)// 这里实质是完成绘图动作的入口if(top_act_scr == NULL) top_act_scr = disp_refr->act_scr;refr_obj_and_children(layer, top_act_scr);}/*Also refresh top and sys layer unconditionally*/// 将top screen和system screen内容也刷新到当前layer上refr_obj_and_children(layer, lv_display_get_layer_top(disp_refr));refr_obj_and_children(layer, lv_display_get_layer_sys(disp_refr));// 将draw_buf里的所有内容刷新到显示器上// 此函数里的刷新回调函数,真正将绘图缓冲区中的内容放置到了显示器的帧缓冲区draw_buf_flush(disp_refr);LV_PROFILER_END;
}

跳回主目录

[#12] 刷新对象及其子控件——refr_obj_and_children

此函数是统摄整个刷新动作的核心函数,从一个控件节点作为出发点,它的刷新分为两个方向来进行

  • 一个方向是沿当前对象沿着控件树向叶子节点方向深度遍历并刷新
  • 另一个方向是沿着当前控件节点向上,刷新比它年轻(图层位于它之上)的控件节点及其祖先

这段代码还是非常精妙的,详细代码注释如下:

static void refr_obj_and_children(lv_layer_t * layer, lv_obj_t * top_obj)
{/*Normally always will be a top_obj (at least the screen)*but in special cases (e.g. if the screen has alpha) it won't.*In this case use the screen directly*/// 如果没有传入顶层对象,则将当前活跃的屏幕作为对象if(top_obj == NULL) top_obj = lv_display_get_screen_active(disp_refr);if(top_obj == NULL) return;  /*Shouldn't happen*/LV_PROFILER_BEGIN;/*Refresh the top object and its children*/// 刷新顶层对象和它的子对象,这是在控件树中向叶子结点方向刷新refr_obj(layer, top_obj);/*Draw the 'younger' sibling objects because they can be on top_obj*/// 刷新与当前组件同级,但是更年轻的兄弟控件// 因为它们的图层(draw layer)可能位于当前空间上方lv_obj_t * parent;lv_obj_t * border_p = top_obj;parent = lv_obj_get_parent(top_obj);/*Do until not reach the screen*/// 向上逐层循环,直至扫描到最顶级控件(树的根节点),也就是屏幕while(parent != NULL) {bool go = false;uint32_t i;uint32_t child_cnt = lv_obj_get_child_count(parent);// 只刷新与当前控件同级的,但是更加年轻的兄弟节点// 没找到之前跳过更加年长(图层位于当前控件下方)的控件for(i = 0; i < child_cnt; i++) {lv_obj_t * child = parent->spec_attr->children[i];if(!go) {if(child == border_p) go = true;}// 刷新兄弟节点else {/*Refresh the objects*/refr_obj(layer, child);}}/*Call the post draw draw function of the parents of the to object*/// 发送一些后处理绘制事件lv_obj_send_event(parent, LV_EVENT_DRAW_POST_BEGIN, (void *)layer);lv_obj_send_event(parent, LV_EVENT_DRAW_POST, (void *)layer);lv_obj_send_event(parent, LV_EVENT_DRAW_POST_END, (void *)layer);/*The new border will be the last parents,*so the 'younger' brothers of parent will be refreshed*/// 继续沿控件树向上遍历border_p = parent;/*Go a level deeper*/parent = lv_obj_get_parent(parent);}LV_PROFILER_END;
}

接下来应该继续分析refr_obj的实现,但是因为一般情况下图层类型均为LV_LAYER_TYPE_NONE,因此refr_obj会直接调用lv_obj_redraw完成控件的重新绘制,因此我们下面直接分析lv_obj_redraw的实现

跳回主目录

[#10] 控件重绘(*)——lv_obj_redraw

此函数完成了一个控件对象和其子控件的刷新,整体上来说这是一个树形数据结构典型先序遍历的写法

  • 首先,它首先重绘控件自己,这是通过发送事件LV_EVENT_DRAW_MAIN到对象obj来异步触发绘图动作完成的。关于LVGL中的事件机制,由于篇幅限制这篇文章不展开介绍。
  • 在绘制完对象自身之后,它会递归调用refr_obj函数,完成所有子控件的刷新。特别需要留意的是[#11-#10]栈帧和[#9-#8]栈帧出现了重复,这就是递归调用的结果。实际上经过调试发现,第一次进入lv_obj_draw时绘制的是一个抽象对象obj(lv_obj.c:lv_obj_draw),之后在递归绘制子对象时才真正绘制我们描述的内容
void lv_obj_redraw(lv_layer_t * layer, lv_obj_t * obj)
{// 取出当前图层的裁剪区域,裁剪区域外的内容无法看到lv_area_t clip_area_ori = layer->_clip_area;lv_area_t clip_coords_for_obj;/*Truncate the clip area to `obj size + ext size` area*/// 计算当前要绘制的对象的坐标范围(算上要扩展的区域大小,如阴影和光影效果)lv_area_t obj_coords_ext;lv_obj_get_coords(obj, &obj_coords_ext);int32_t ext_draw_size = _lv_obj_get_ext_draw_size(obj);lv_area_increase(&obj_coords_ext, ext_draw_size, ext_draw_size);// 如果当前要绘制的对象不在裁剪区域内// 那么它不可见,不用绘制,直接返回if(!_lv_area_intersect(&clip_coords_for_obj, &clip_area_ori, &obj_coords_ext)) return;/*If the object is visible on the current clip area*/// 更新要绘制的区域范围为控件与裁剪区域的交集layer->_clip_area = clip_coords_for_obj;// (*)发送绘图事件到obj,在这里实际上异步完成了对象自身的绘制// 实际完成绘制的图形lv_obj_send_event(obj, LV_EVENT_DRAW_MAIN_BEGIN, layer);lv_obj_send_event(obj, LV_EVENT_DRAW_MAIN, layer);lv_obj_send_event(obj, LV_EVENT_DRAW_MAIN_END, layer);
#if LV_USE_REFR_DEBUG// 从略...
#endif/* 从这里之后,进入子控件的绘制 */	// 判断是否允许子对象超出边界绘制内容const lv_area_t * obj_coords;// 如果可以溢出,则使用扩展后的坐标,否则使用原始坐标if(lv_obj_has_flag(obj, LV_OBJ_FLAG_OVERFLOW_VISIBLE)) {obj_coords = &obj_coords_ext;}else {obj_coords = &obj->coords;}// 判断是否需要绘制子对象// 如果父对象与裁剪对象交集不为空,则需要绘制子对象lv_area_t clip_coords_for_children;bool refr_children = true;if(!_lv_area_intersect(&clip_coords_for_children, &clip_area_ori, obj_coords)) {refr_children = false;}// 如果需要绘制子对象if(refr_children) {uint32_t i;uint32_t child_cnt = lv_obj_get_child_count(obj);// 如果当前已经到达了叶子对象(也就是没有孩子控件),则触发LV_EVENT_DRAW_POST事件if(child_cnt == 0) {/*If the object was visible on the clip area call the post draw events too*/layer->_clip_area = clip_coords_for_obj;/*If all the children are redrawn make 'post draw' draw*/lv_obj_send_event(obj, LV_EVENT_DRAW_POST_BEGIN, layer);lv_obj_send_event(obj, LV_EVENT_DRAW_POST, layer);lv_obj_send_event(obj, LV_EVENT_DRAW_POST_END, layer);}// 如果仍有孩子节点需要绘制else {// 更新图层的裁剪区域为孩子控件的坐标范围layer->_clip_area = clip_coords_for_children;// 这里是裁剪区域转角风格的相关判断bool clip_corner = lv_obj_get_style_clip_corner(obj, LV_PART_MAIN);int32_t radius = 0;if(clip_corner) {radius = lv_obj_get_style_radius(obj, LV_PART_MAIN);if(radius == 0) clip_corner = false;}// (*)如果无需裁剪转角,那么直接递归绘制子控件// 注:refr_obj最终也会调用到lv_obj_redraw自身/***************************************************/if(clip_corner == false) {for(i = 0; i < child_cnt; i++) {lv_obj_t * child = obj->spec_attr->children[i];refr_obj(layer, child);}// 后处理,首先恢复图层的裁剪区域大小/*If the object was visible on the clip area call the post draw events too*/layer->_clip_area = clip_coords_for_obj;/*If all the children are redrawn make 'post draw' draw*/lv_obj_send_event(obj, LV_EVENT_DRAW_POST_BEGIN, layer);lv_obj_send_event(obj, LV_EVENT_DRAW_POST, layer);lv_obj_send_event(obj, LV_EVENT_DRAW_POST_END, layer);}// 绘制裁剪区域转角外观的逻辑,这里略去不深究else {// 绘制裁剪区域转角的代码从略...}}}// 恢复图层的裁剪区域为原始值layer->_clip_area = clip_area_ori;
}

跳回主目录

[#7-#4] LVGL的事件机制——从略


这几个栈帧都关于LVGL的事件机制,这是另一个比较大的话题,在下一篇文章中我会用一个独立的篇幅来梳理LVGL的事件机制,这里只对其原理做一个引子。

简而言之,在创建一个对象(例如线段对象line)时,就注册了一个接收事件的回调函数(例如lv_line_event)到这个对象中。它会根据事件类型LV_EVENT_DRAW_MAIN触发绘图动作,例如线段的绘图动作函数就是lv_draw_line

在上面lv_obj_redraw函数的实现中,就通过发送事件LV_EVENT_DRAW_MAIN触发了特定对象的重绘动作。lv_obj_send_event->event_send_core->lv_obj_event_base->lv_line_event就是完整的函数调用链。

跳回主目录

[#3] 创建线段绘制任务——lv_draw_line

这个函数的任务非常简单,就是创建了一个绘制线段line的绘图任务,并将其挂在了图层任务链表的尾部。紧接着,此函数将会在最后尝试让各个绘图单元评估此任务,并分发任务到特定的绘图单元(in lv_draw_finalize_task_creation)。

void LV_ATTRIBUTE_FAST_MEM lv_draw_line(lv_layer_t * layer, const lv_draw_line_dsc_t * dsc)
{// 计算绘图区域范围LV_PROFILER_BEGIN;lv_area_t a;a.x1 = (int32_t)LV_MIN(dsc->p1.x, dsc->p2.x) - dsc->width;a.x2 = (int32_t)LV_MAX(dsc->p1.x, dsc->p2.x) + dsc->width;a.y1 = (int32_t)LV_MIN(dsc->p1.y, dsc->p2.y) - dsc->width;a.y2 = (int32_t)LV_MAX(dsc->p1.y, dsc->p2.y) + dsc->width;// 创建并初始化一个绘图任务,并将其挂在图层绘图任务的链表尾部lv_draw_task_t * t = lv_draw_add_task(layer, &a);// 将线段描述符拷贝到绘图任务重t->draw_dsc = lv_malloc(sizeof(*dsc));lv_memcpy(t->draw_dsc, dsc, sizeof(*dsc));t->type = LV_DRAW_TASK_TYPE_LINE;// 绘图任务创建收尾,即将准备评估和分发到绘图单元lv_draw_finalize_task_creation(layer, t);LV_PROFILER_END;
}

跳回主目录

[#2] 任务创建结束,评估并尝试分发——lv_draw_finalize_task_creation

这里是我们故事的世界线收敛的第一个地方,在这个函数中调用了所有我们最早在LVGL初始化时注册的评估函数,用来给即将要执行的绘图任务确定一个最优绘图单元。之后,如果当前没有正在执行任务,则此函数同时准备分发这个任务到具体的绘图单元进行绘制

void lv_draw_finalize_task_creation(lv_layer_t * layer, lv_draw_task_t * t)
{LV_PROFILER_BEGIN;lv_draw_dsc_base_t * base_dsc = t->draw_dsc;base_dsc->layer = layer;lv_draw_global_info_t * info = &_draw_info;/*Send LV_EVENT_DRAW_TASK_ADDED and dispatch only on the "main" draw_task*and not on the draw tasks added in the event.*Sending LV_EVENT_DRAW_TASK_ADDED events might cause recursive event sends and besides*dispatching might remove the "main" draw task while it's still being used in the event*/// 如果没有任务正在执行if(info->task_running == false) {// 判断当前需要绘制的对象是否正在监听任务创建事件LV_EVENT_DRAW_TASK_ADDEDif(base_dsc->obj && lv_obj_has_flag(base_dsc->obj, LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS)) {info->task_running = true;lv_obj_send_event(base_dsc->obj, LV_EVENT_DRAW_TASK_ADDED, t);info->task_running = false;}/*Let the draw units set their preference score*/// 给当前的绘图任务初始化一个默认的绘图单元t->preference_score = 100;t->preferred_draw_unit_id = 0;lv_draw_unit_t * u = info->unit_head;// 遍历所有已经注册的绘图单元,判断哪个是最高效最合适的绘图单元while(u) {// [我们终于到达了一个交汇点,这里终于调用到了我们在初始化时注册的任务评估函数!]if(u->evaluate_cb) u->evaluate_cb(u, t);u = u->next;}// 尝试分发此任务到绘图单元lv_draw_dispatch();}// 如果当前有任务正在运行,那么只执行评估过程,不具体分发任务else {/*Let the draw units set their preference score*/t->preference_score = 100;t->preferred_draw_unit_id = 0;lv_draw_unit_t * u = info->unit_head;while(u) {if(u->evaluate_cb) u->evaluate_cb(u, t);u = u->next;}}LV_PROFILER_END;
}

跳回主目录

[#1] 绘图任务分发主函数——lv_draw_dispatch

此函数是绘图任务分发的主函数,请注意,它并没有入口参数。而是遍历所有显示器所有图层,并逐一绘制,绘制的主要动作发生在函数lv_draw_dispatch_layer中。

void lv_draw_dispatch(void)
{LV_PROFILER_BEGIN;bool render_running = false;lv_display_t * disp = lv_display_get_next(NULL);// 沿显示器链表遍历所有显示器while(disp) {// 遍历当前显示器下挂的所有图层lv_layer_t * layer = disp->layer_head;while(layer) {// 分发具体图层的绘图任务到绘图单元if(lv_draw_dispatch_layer(disp, layer))render_running = true;layer = layer->next;}// 如果绘图任务没有开始,则在此发出请求分配任务的信号if(!render_running) {lv_draw_dispatch_request();}// 遍历下一块显示器disp = lv_display_get_next(disp);}LV_PROFILER_END;
}

跳回主目录

[#0] 分发特定的图层到绘图单元——lv_draw_dispatch_layer

这是所有故事的结尾,我们最终走入了具体图层绘制任务的分发过程。这个函数主要完成了三部分内容:

  • 首先此函数会遍历当前要绘制的layer下挂载的所有绘图任务,并回收释放其中已经完成的任务
  • 如果发现当前图层下所有绘图任务已经全部执行完成,且当前层需要合并回父层,则更新任务状态并触发分发函数
  • 如果绘图任务还没有全部完成,那么就遍历任务列表,并让每一个绘图单元认领一个最适合自己的任务(这与前面我们在初始化阶段注册的回调函数相呼应)
bool lv_draw_dispatch_layer(lv_display_t * disp, lv_layer_t * layer)
{LV_PROFILER_BEGIN;/*Remove the finished tasks first*/lv_draw_task_t * t_prev = NULL;lv_draw_task_t * t = layer->draw_task_head;/* PART1: 遍历并释放所有已完成的绘图任务 */while(t) {lv_draw_task_t * t_next = t->next;// 这里遍历layer上的绘图任务draw_task链表,并清除已经完成的绘图任务if(t->state == LV_DRAW_TASK_STATE_READY) {// 通过移除链表节点来实现任务删除/*Remove by it by assigning the next task to the previous*/if(t_prev) t_prev->next = t->next;      /*If it was the head, set the next as head*/else layer->draw_task_head = t_next;    /*If it was layer drawing free the layer too*/// 如果当前的绘制任务是绘制一个图层,那么连带其图层一起删掉// 这里可能有风险,子任务可能会删除父任务还没有用完的图层if(t->type == LV_DRAW_TASK_TYPE_LAYER) {lv_draw_image_dsc_t * draw_image_dsc = t->draw_dsc;lv_layer_t * layer_drawn = (lv_layer_t *)draw_image_dsc->src;// 释放图层的绘图缓冲区draw_buffer,并更新空闲内存信息if(layer_drawn->draw_buf) {int32_t h = lv_area_get_height(&layer_drawn->buf_area);int32_t w = lv_area_get_width(&layer_drawn->buf_area);uint32_t layer_size_byte = h * lv_draw_buf_width_to_stride(w, layer_drawn->color_format);_draw_info.used_memory_for_layers_kb -= get_layer_size_kb(layer_size_byte);LV_LOG_INFO("Layer memory used: %" LV_PRIu32 " kB\n", _draw_info.used_memory_for_layers_kb);lv_draw_buf_destroy(layer_drawn->draw_buf);layer_drawn->draw_buf = NULL;}/*Remove the layer from  the display's*/// 将此图层节点从屏幕中记录的图层链表中删去if(disp) {lv_layer_t * l2 = disp->layer_head;while(l2) {if(l2->next == layer_drawn) {l2->next = layer_drawn->next;break;}l2 = l2->next;}// 解初始化并释放图层本身if(disp->layer_deinit) disp->layer_deinit(disp, layer_drawn);lv_free(layer_drawn);}}  // end of if(t->state == LV_DRAW_TASK_STATE_READY)// 如果当前绘图任务包含标签,那么还需要清空标签里的文本字段lv_draw_label_dsc_t * draw_label_dsc = lv_draw_task_get_label_dsc(t);if(draw_label_dsc && draw_label_dsc->text_local) {lv_free((void *)draw_label_dsc->text);draw_label_dsc->text = NULL;}// 释放任务内部记录的描述符,和任务结构体本身// 注意先释放子对象,否则会内存泄漏lv_free(t->draw_dsc);lv_free(t);}else {t_prev = t;}t = t_next;}bool render_running = false;/* PART2: 如果图层绘图任务全部完成,尝试提醒父图层 *//*This layer is ready, enable blending its buffer*/// 如果当前这个layer的所有绘图任务都已经结束,且其有父图层正在等待它// 则更新父对象中任务状态为LV_DRAW_TASK_STATE_QUEUED,提醒父对象自己已经绘制完成if(layer->parent && layer->all_tasks_added && layer->draw_task_head == NULL) {/*Find a draw task with TYPE_LAYER in the layer where the src is this layer*/lv_draw_task_t * t_src = layer->parent->draw_task_head;while(t_src) {if(t_src->type == LV_DRAW_TASK_TYPE_LAYER && t_src->state == LV_DRAW_TASK_STATE_WAITING) {lv_draw_image_dsc_t * draw_dsc = t_src->draw_dsc;if(draw_dsc->src == layer) {t_src->state = LV_DRAW_TASK_STATE_QUEUED;// 尝试触发任务分发,提醒父图层可以合并自己lv_draw_dispatch_request();break;}}t_src = t_src->next;}}/* PART3: 执行当前图层下的所有绘图任务 *//*Assign draw tasks to the draw_units*/// 否则,尝试将任务分配到所有已经注册的绘图单元中else {/*Find a draw unit which is not busy and can take at least one task*//*Let all draw units to pick draw tasks*/lv_draw_unit_t * u = _draw_info.unit_head;// 这里是另外一个交汇点,这对应到我们初始化时对应的分发回调函数while(u) {// 分发回调函数会遍历layer中的每一个绘图任务,领取一个适合自己的并开始绘制// 每一个绘图单元都会尝试领取一个适合自己执行的绘图任务int32_t taken_cnt = u->dispatch_cb(u, layer);if(taken_cnt >= 0) render_running = true;u = u->next;}}LV_PROFILER_END;return render_running;
}

跳回主目录

总结

此文至此已经接近5万字,在本文中我们介绍和梳理了LVGL绘制一个简单图形的全过程,这个过程中有很多难点和需要理解的概念,比如:

  • 任务评估函数与任务分发函数的作用。
  • 一系列容易混淆的LVGL概念,例如display(显示器)screen(屏幕)layer(图层,其实有三种释义(drawing layer、screen layer、layer order))绘图缓冲区(draw_buffer)绘图任务(draw_task)帧缓冲区(frame buffer)等等等等,崩溃吧? 理解这些概念之后,阅读上述代码和文章才能得到更好的理解。
  • 事件机制,需要理解LVGL是如何通过发送事件给特定对象,从而异步实现具体的绘制任务的,这可能会在后面的文章中深入介绍。
  • 理解时间对于LVGL框架的重要性。
http://www.xdnf.cn/news/5237.html

相关文章:

  • 期货反向跟单—数据分析误区(二)盘手排名
  • 60分钟示范课设计-《Python循环语句的奥秘与应用进阶》
  • 第J7周:对于ResNeXt-50算法的思考
  • 网上商城系统
  • 【嵌入式系统设计师(软考中级)】第二章:嵌入式系统硬件基础知识——⑤电源及电路设计
  • 全国青少年信息素养大赛 Python编程挑战赛初赛 内部集训模拟试卷四及详细答案解析
  • 解决librechat 前端界面没有google gemini 2.5模型的选项
  • 【c语言】动态内存管理
  • 各种注解含义及使用
  • 心 光 -中小企实战运营和营销工作室博客
  • 微机控制高温扭转试验机
  • 关于AI 大数据模型的基础知识 杂记
  • 数字化与信息化的关系
  • 4.3 Thymeleaf案例演示:图书管理
  • 军事目标无人机视角坦克检测数据集VOC+YOLO格式4003张1类别
  • 44.辐射发射整改简易摸底测试方法
  • 企业名录搜索软件哪家好?
  • 6.01 Python中打开usb相机并进行显示
  • 动态创建链表(头插法、尾插法)
  • RISC-V CLINT、PLIC及芯来ECLIC中断机制分析 —— RISC-V中断机制(一)
  • Linux探秘坊-------12.库的制作与原理
  • java-----------------多态
  • 跨平台编码规范文档
  • c++:标准模板库 STL(Standard Template Library)
  • 【Go底层】http标准库服务端实现原理
  • 设计模式-迭代器模式
  • 【MySQL数据库】--SQLyog创建数据库+python连接
  • 26考研——中央处理器_CPU 的功能和基本结构(5)
  • 机器学习-数据集划分和特征工程
  • Rust 中的 `PartialEq` 和 `Eq`:深入解析与应用