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框架的重要性。