Skip to content
Published at:

LVGL源码分析

文章基于LVGL V7.11.0版本https://github.com/lvgl/lvgl/tree/v7.11.0

UI框架核心流程:

对于一个UI系统来说最原始的三个需求:

  • 控件树维护:在前端叫DOM树(创建控件时,都需要传一个父控件)
  • UI显示、刷新:通过迭代控件树,调用每个控件自己的绘制函数
  • 点击事件传递到控件:TP硬件获取到点击坐标,传给UI系统,在去控件树去找到对应位置的控件,然后调用该控件的点击回调函数

问题:

  • LVGL启动流程
  • Widgets的数据是如何显示到屏幕
  • 屏幕点击事件,是如何传递到对应空间的事件回调中
  • 不同控件之间有什么关系?为什么返回的都是lv_obj_t
  • 图片解码、缓存流程,显示

提前须知:

LVGL中重要的数据结构:双向链表

c
// lv_ll.h
typedef struct {
    uint32_t n_size;
    lv_ll_node_t * head;
    lv_ll_node_t * tail;
} lv_ll_t;

最重要的宏:LV_ROOTS

c
// lv_gc.c
#if(!defined(LV_ENABLE_GC)) || LV_ENABLE_GC == 0
    LV_ROOTS
#endif /* LV_ENABLE_GC */

// lv_gc.h
#define LV_ITERATE_ROOTS(f) \
    f(lv_ll_t, _lv_task_ll)  /*Linked list to store the lv_tasks*/ \
    f(lv_ll_t, _lv_disp_ll)  /*Linked list of screens*/            \
    f(lv_ll_t, _lv_indev_ll) /*Linked list of screens*/            \
    f(lv_ll_t, _lv_drv_ll)                                         \
    f(lv_ll_t, _lv_file_ll)                                        \
    f(lv_ll_t, _lv_anim_ll)                                        \
    f(lv_ll_t, _lv_group_ll)                                       \
    f(lv_ll_t, _lv_img_defoder_ll)                                 \
    f(lv_ll_t, _lv_obj_style_trans_ll)                             \
    f(lv_img_cache_entry_t*, _lv_img_cache_array)                  \
    f(lv_task_t*, _lv_task_act)                                    \
    f(lv_mem_buf_arr_t , _lv_mem_buf)                              \
    f(_lv_draw_mask_saved_arr_t , _lv_draw_mask_list)              \
    f(void * , _lv_theme_material_styles)                          \
    f(void * , _lv_theme_template_styles)                          \
    f(void * , _lv_theme_mono_styles)                              \
    f(void * , _lv_theme_empty_styles)                             \
    f(uint8_t *, _lv_font_decompr_buf)                             \


#define LV_DEFINE_ROOT(root_type, root_name) root_type root_name;
#define LV_ROOTS LV_ITERATE_ROOTS(LV_DEFINE_ROOT)
#define LV_GC_ROOT(x) x

很懵?那换一种方式:

c
lv_ll_t                   _lv_task_ll;  /*Linked list to store the lv_tasks*/
lv_ll_t                   _lv_disp_ll;  /*Linked list of display device*/
lv_ll_t                   _lv_indev_ll; /*Linked list of input device*/
lv_ll_t                   _lv_drv_ll;
lv_ll_t                   _lv_file_ll;
lv_ll_t                   _lv_anim_ll;
lv_ll_t                   _lv_group_ll;
lv_ll_t                   _lv_img_decoder_ll;
lv_ll_t                   _lv_obj_style_trans_ll;
lv_img_cache_entry_t*     _lv_img_cache_array;
lv_task_t*                _lv_task_act;
lv_mem_buf_arr_t          _lv_mem_buf;
_lv_draw_mask_saved_arr_t _lv_draw_mask_list;
void*                     _lv_theme_material_styles;
void*                     _lv_theme_template_styles;
void*                     _lv_theme_mono_styles;
void*                     _lv_theme_empty_styles;
uint8_t*                  _lv_font_decompr_buf;

基本上是lvgl的context:task、disp、indev、drv、anim、img_decoder、style、theme、font全定义了; 使用时一般会用LV_GC_ROOT()宏去获取使用,比如: LV_GC_ROOT(_lv_task_ll)LV_GC_ROOT(_lv_disp_ll)&LV_GC_ROOT(_lv_indev_ll)

显示时Widget之间的关系:父子关系

todo:

Task任务

todo:

LVGL启动流程

基本上程序都会分为两个阶段:一个是正常正常前的初始化阶段,把个各模块做相应的初始化;第二个是正常运行阶段,各个模块相互协作,完成任务,完成事件流阶段

  • 初始化
    • lv_init()核心初始化
    • hal_init()硬件初始化,及硬件和LVGL UI关联上
    • lv_demo_widgets()绘制自定义的UI
  • 正常运行、周期性调度Task阶段(死循环)
    • LVGL UI变化(更新)显示到屏幕(硬件)
    • 触摸事件(TP硬件)响应到LVGL UI系统,找到对应的Widget,调用相应的事件回调

Widgets的数据是如何显示到屏幕

前提:

  1. 屏幕刷新有对应UI task,周期性的执行:_lv_disp_refr_task
c
// UI 刷新task启动:
// main.c main() --> hal_init() --> lv_disp_drv_register(&disp_drv);
// lv_hal_disp.c --> lv_disp_drv_register函数中
disp->refr_task = lv_task_create(_lv_disp_refr_task, LV_DISP_DEF_REFR_PERIOD, LV_REFR_TASK_PRIO, disp);
  1. UI的刷新不是每次全部刷新,而是只刷新更新数据的区域

流程:

  1. Widget在设置数据的时候,会告诉lv_disp_t“显示屏”哪块区域需要重新绘制(标记)
  2. 当UI task开始执行时,lv_disp_t“显示屏”会调用driver驱动,去刷新对应的区域

流程详解:

  • 无论是lv_lable_t的lv_label_set_text(label, text),还是lv_img_t的lv_img_set_src(img, src_img);;都会去调用lv_obj_invalidate(obj);,标记需要重新绘制
  • “区域”会保存到lv_disp_tinv_areas数组字段中,inv_p会记录个数,也就是数组长度;
  • 当UI的task开始执行里,
    • 调用_lv_disp_refr_task UI刷新task
      • 调用lv_refr_vdb_flush 把内容刷新到UI上
        • 调用driver去刷新屏幕:disp->driver.flush_cb(&disp->driver, &vdb->area, vdb->buf_act);

下面是结构体lv_disp_t,保留了关键字段:

c
typedef struct _disp_t {
    lv_disp_drv_t driver; /**< Driver to the display*/

    // ...

    /** Invalidated (marked to redraw) areas*/
    lv_area_t inv_areas[LV_INV_BUF_SIZE];
    uint8_t   inv_area_joined[LV_INV_BUF_SIZE];
    uint32_t  inv_p : 10;
} lv_disp_t;

这个driver.flush_cb()看着是不是有几分眼熟?对,就是main.c中初始化hal层hal_init(),设置的disp_drv.flush_cb = xxx_flush;。LVGL也不知道内容显示到哪里去,所以暴露了出来

这里只是简单的概括下流程,实际情况会复杂些,包括显示的数据内容,以及多个相交的区域处理。以及有的控件重绘,会导致其它的区域也需要重绘

屏幕点击事件,是如何传递到对应空间的事件回调中

我点击了屏幕TP,如何传递并调用我设置的回调函数lv_obj_set_event_cb(obj, event_cb);

前提:

  1. 对应有一个task任务在不停的获取点击屏幕的坐标

流程:

  1. 读取点击到的坐标x、y,以及状态state(press/release)
  2. 在press按压的时候搜索对应的Widget,递归查找
    • 三层上搜索:sys layer > top layer > scr_act layer
    • Widgets之间的关系:子父关系。root根是屏幕,一个屏幕可以对应一个或多个子widgets
  3. 按不同的情况触发不同的事件:lv_event_send(indev_obj_act, LV_EVENT_XXX, NULL);
  4. 内部会调用到创建Widget时设置的回调函数

代码跟踪流程:

  • _lv_indev_read_task
    • _lv_indev_read
      • indev->driver.read_cb(&indev->driver, data); // mouse_read 读取点击的x、y,以及是state(press/release)
    • indev_button_proc
      • indev_proc_press
        • lv_indev_search_obj:lv_disp_get_layer_sys > lv_disp_get_layer_top > lv_disp_get_scr_act
          • 递归遍历,找到对应的obj
        • lv_event_send(indev_obj_act, LV_EVENT_XXX, NULL);
          • lv_event_send_func(obj->event_cb, obj, event, data);
            • if(event_xcb) event_xcb(obj, event); // 调用你最开始设置的回调

不同控件之间有什么关系?为什么返回的都是lv_obj_t

lv_obj_t通过一个void*拓展成不同的Widget,lv_obj_t定义共有/通用的部分,每个特别的Widget定义自己独特的部分,命名为lv_xxx_ext_t,比如lv_label_ext_tlv_img_ext_t...

lv_obj_t:

c
typedef struct _lv_obj_t {
    // ...

    void* ext_attr; // 指向

    // ...
} lv_obj_t;

lv_label_ext_t:

c
typedef struct {
    char * text;

    union {
        char * tmp_ptr;
        char tmp[LV_LABEL_DOT_NUM + 1];
    } dot;

    uint32_t dot_end;
    uint16_t anim_speed;
    lv_point_t offset;
    lv_draw_label_hint_t hint;

    uint32_t sel_start;
    uint32_t sel_end;

    lv_label_long_mode_t long_mode : 3;
    uint8_t static_txt : 1;
    uint8_t align : 2;
    uint8_t recolor : 1;
    uint8_t expand : 1;
    uint8_t dot_tmp_alloc : 1;
} lv_label_ext_t;

lv_img_ext_t:

c
typedef struct {
    const void* src;
    lv_point_t  offset;
    lv_coord_t  w;
    lv_coord_t  h;
    uint16_t    angle;
    lv_point_t  pivot;
    uint16_t    zoom;
    uint8_t     src_type : 2;
    uint8_t     auto_size : 1;
    uint8_t     cf : 5;
    uint8_t     antialias : 1;
} lv_img_ext_t;

v8.0版本更改:

lv_label_t嵌套lv_obj_t,嵌套结构体更加符合面向对象语言中的继承关系,父类有的子类会继承过来;改成嵌套结构体,可能还有一个原因:早前版本的lv_xxx_create至少会有两次malloc申请内存,一次malloc lv_obj_t,一次malloc lv_xxx_ext_t

c
typedef struct {
    lv_obj_t obj;
    char*    text;
    union {
        char* tmp_ptr;
        char  tmp[LV_LABEL_DOT_NUM + 1];
    } dot;
    uint32_t dot_end;

    lv_draw_label_hint_t hint;

    uint32_t sel_start;
    uint32_t sel_end;

    lv_point_t           offset;
    lv_label_long_mode_t long_mode : 3;
    uint8_t              static_txt : 1;
    uint8_t              recolor : 1;
    uint8_t              expand : 1;
    uint8_t              dot_tmp_alloc : 1;
} lv_label_t;

图片解码、缓存流程,显示

  1. 设置数据阶段:
  • lv_img_set_src
    • lv_img_decoder_get_info
    • 标记这块区域要刷新
  1. 刷新绘制阶段:
  • _lv_disp_refr_task
    • lv_refr_areas
      • lv_refr_area
        • lv_refr_area_part
          • lv_refr_obj
            • obj->design_cb(obj, &obj_ext_mask, LV_DESIGN_DRAW_MAIN);// 调用widget自己的绘制函数
          • lv_refr_obj_and_children
  1. 调用widget自己的design函数:lv_xxx_design
  • lv_img_design
    • lv_draw_img
      • lv_img_draw_core
        • _lv_img_cache_open 缓存 list(20)img
          • lv_img_decoder_open
            • _LV_LL_READ(LV_GC_ROOT(_lv_img_defoder_ll), d) // 尝试使用不同的解码器去解码:png/jpeg
              • info / open

内存lv_mem.c

lvgl分配内存函数void * lv_mem_alloc(size_t size);,如果你使用的内存超出了你分配的内存,不会有段错误,然后会导致整个内存分配系统不可用

c
char* p = lv_mem_alloc(8);
char* buffer = "hello lvgl !!!";
memset(p, buffer, strlen(buffer));

Updated at: