美文网首页RTOS和GUI_基于英飞凌tc2x及stm32开发板
littlevgl_7.11源码分析(1)--Apple的学习笔

littlevgl_7.11源码分析(1)--Apple的学习笔

作者: applecai | 来源:发表于2021-05-11 21:04 被阅读0次

    一,前言

    玩玩littlevgl移植到stm32--Apple的学习笔记已经提到输入和显示都移植了,然后我又移植了下文件系统,结果一调试open file出错。最后查到void *file_d = drv->open_cb(drv, real_path, mode);是3个参数,但是fs_porting.c中4个参数导致的。难道是我下载的lvgl版本问题。后来查了下,我下载的是master开发版本,所以故障率高。并且在看官网help的时候,里面举例的函数参数与我当前的8.0.0版本不同,因为它是7.11版本。所以为了安全的是使用源码,我将版本替换为了7.11正式释放版本。所以我的代码分析也会围绕7.11版本进行。 至于文件系统的移植,后续还会进行的。

    二,源码模块分析

    我在想源码分析需要从框架开始分析,或者说从某对象开始分析。而官网的help中Overview中就是一个个对象的简单描述,这就给我打开了一扇门,我就从对象开始分析吧。
    1. object
    它是所有对象的基类。c语言也可以设计为面向对象,这个和linux驱动是一样的,其实对c语言而且就是结构体可以看做c++中的类,而结构体中包含基类的指针,这样就可以访问基类,可以看为是c++中的继承,里面比较有特点的就是和linux设备驱动一样,有一个parent指向父类的指针,这个很重要,因为GUI引擎基本上都是一个node挂一个子node的这种方式处理的。而c++通过继承基类的子类中的虚方法在c语言中通过指针函数注册来实现方法解耦。
    而之前游戏设计方法中提到的为了减少内存而进行的共享,就是把texture和module都抽象为基类,然后绑定到树的网格或者绑定到山川的网格,再配合上配置参数大小,位置等信息,就能构建出对象。
    这里的object也是类似的,object的数据结构体lv_obj_t抽象出Position,Size,Parent,Drag enable,Click enable这些共同的属性用来给子类继承。
    坐标系说下,左上角是(0,0),x向右值增大,y向下值增大。而之前说的GUI的node参考系依赖parent指针来找到。这里子object的坐标系是参考父object,当做为object为画布。
    比如2个矩阵,矩形1为父object,矩形2为子object。父的坐标为(10,10),转为统一视图坐标系,则为(10,10),而子object的坐标为(0,0),它是相对父坐标来定的offset。所以对于统一的视图坐标系,它也是(10,10)。子object的视图坐标=子object坐标+父object的坐标。另外,子object的显示不能超过父object的边框外。
    2. Layer
    层的概念是相对父obj来说的,lv_obj_set_parent(obj, new_parent)那么可以理解为父和子2个参考对象的层次关系为前后关系。父在后面作为背景,子在前面作为前景。那么对于一个父类有多个子类,那么创建越晚的子类在最上层。lv_obj_set_top(obj, true)对于obj或任意子类,只要选择则可以移动到最上层,会为此v_obj_t对象的top成员赋值为true。而lv_obj_move_foreground(obj)lv_obj_move_background(obj)可以移动层级。好了,这2个API是官网这样描述的,但是对于我来说我是要分析源码的,当然要去看看这API内的实现机制。注意:我分析的是7.11版本!分析后添加了中文注释如下

    void lv_obj_move_foreground(lv_obj_t * obj)
    {
        LV_ASSERT_OBJ(obj, LV_OBJX_NAME);
        /* 获取选中对象的父对象 */
        lv_obj_t * parent = lv_obj_get_parent(obj);
        /* 此父对象最上层(head)的对象为此obj,则不需要移动到前景。
        这个我看到return,就猜到child应该是头插法,而head就是显示在最上层的子对象。*/
        /*Do nothing of already in the foreground*/
        if(_lv_ll_get_head(&parent->child_ll) == obj) return;
        /* 设置对象区域无效,便于重绘 */
        lv_obj_invalidate(parent);
        /* 将obj从parent->child_ll中先移除,然后添加到parent->child_ll中head的位置,因为这里是true 
           head我刚刚已经猜测过了,这样又特别移动到head,说明我的猜测正确,head指向了显示在最上层的子obj */
        _lv_ll_chg_list(&parent->child_ll, &parent->child_ll, obj, true);
    
        /*Notify the new parent about the child*/
        parent->signal_cb(parent, LV_SIGNAL_CHILD_CHG, obj);
    
        lv_obj_invalidate(parent);
    }
    

    signal_cb绑定了lv_obj_signal函数,lv_obj_t中有一个protect成员。我理解在后面的lv_obj_invalidate设置区域无效重绘过程中,对于受到保护的成员应该不会重绘。但是如下只是读取受保护状态,并没有关心res返回值。说明这里功能没有完善。

        if(sign == LV_SIGNAL_CHILD_CHG) {
            /*Return 'invalid' if the child change signal is not enabled*/
            if(lv_obj_is_protected(obj, LV_PROTECT_CHILD_CHG) != false) res = LV_RES_INV;
        }
    

    关于Top and sys layers,官网说sys在Top显示层上面。这2层总是被可见的。Top用来给用户发menu或pop窗口因为总是可见。然后sys层用来放鼠标显示。
    3. Event
    这个输入识别,就不多说了,只是里面有手工设置event的函数。lv_event_send(mbox, LV_EVENT_VALUE_CHANGED, &btn_id);然后还有重新绘制也可以用event来通知lv_event_send(obj, LV_EVENT_REFRESH, NULL),它打包了lv_event_send_refresh函数。面向对象的c++中大家喜欢叫方法。从c这个过程控制语言来说,大家喜欢叫函数。我的主要工作语言是嵌入式c语言,所以我喜欢叫函数。
    4. Sytle
    lv_style_t又是一个被抽象出来的类,不过很容易理解,html+ccs的概念来说它就是ccs,专门用来做样式的。不同的样式应用到相同的对象,则这个对象就会呈现出不同形象了。这个算是GUI引擎的一个重要功能了,之后需要详细分析。
    5. Input
    Input模块很好理解了。触摸呀,按钮呀,鼠标呀。
    6. Disp
    显示模块。特点是支持多显示器,只需初始化更多的显示缓冲区,并为每个显示屏注册另一个驱动程序。创建 UI 时,请使用该信息告诉库在哪个显示屏上创建对象。lv_disp_set_default(disp) 。同时也支持拆分图像,就是一个缓冲区的完整大图,拆分成多个小图在不同屏幕上显示。
    7. Image/Font/Animations/FileSystem
    图形和字体,动画和文件系统模块。这个就不多说了。
    8. task
    这个自建task我没有用过。
    9. draw
    绘图模块flush_cb是主要函数,然后说了支持双framebuffer,同时一个framebuffer也可以同时写入及显示,比如DMA支持情况下。然后描述了内置的mask功能。

    三,先挑选一个方向进行相关源码分析

    lv_obj_invalidate->lv_obj_invalidate_area->_lv_inv_area是关于重绘的,刚刚上文提到的将一个obj移动到最前端,内部调用过程中也用到了这个函数。

    void lv_obj_invalidate(const lv_obj_t * obj)
    {
        LV_ASSERT_OBJ(obj, LV_OBJX_NAME);
    
        /*Truncate the area to the object*/
        lv_area_t obj_coords;
        /* 绘图的扩大区域 */
        lv_coord_t ext_size = obj->ext_draw_pad;
        lv_area_copy(&obj_coords, &obj->coords);
        /* 对obj区域进行扩大 */
        obj_coords.x1 -= ext_size;
        obj_coords.y1 -= ext_size;
        obj_coords.x2 += ext_size;
        obj_coords.y2 += ext_size;
        /* 标记无效区域 */
        lv_obj_invalidate_area(obj, &obj_coords);
    
    }
    

    lv_obj_invalidate_area函数中判断是否不可见,若不可见,则不用再标记了。若是可见而无效的区域,需要标记,请问它要重绘的。判断visible的设计可以理解为一种性能优化。

    void lv_obj_invalidate_area(const lv_obj_t * obj, const lv_area_t * area)
    {
        LV_ASSERT_OBJ(obj, LV_OBJX_NAME);
    
        lv_area_t area_tmp;
        lv_area_copy(&area_tmp, area);
        /* 获取是否此obj可见 */
        bool visible = lv_obj_area_is_visible(obj, &area_tmp);
        /* 若可见,则设置无效区域 */
        if(visible) _lv_inv_area(lv_obj_get_disp(obj), &area_tmp);
    }
    

    先分析lv_obj_area_is_visible函数。lv_obj_t中的hidden成员属性说明obj被隐藏则不显示,直接返回不可见。
    但是这个函数中的_lv_area_intersect子函数我不太理解,直接看上面的注释就是若是当前或之前显示的screen内容,则比如传入无效区域和当前区域有没有重合。若没重合则说明此区域不在此obj上,则返回不可见。然后还要归递的检查obj的父类在此区域是否可见,原理是相同的。
    我不理解2个内容。
    1是若这个if都4种情况都不成立else应该返回false,为什么返回true。
    2是_lv_area_intersect函数传入的参数前面对area就做了ext_draw_pad的扩展,所以一定是重合的,那么我下方标注的2行代码应该是多余的吧,因为我理解第一个is_common = _lv_area_intersect(area, area, &obj_coords);的结果就是area。难道这是它的bug。

    bool lv_obj_area_is_visible(const lv_obj_t * obj, lv_area_t * area)
    {
        /* 此obj被隐藏则直接返回不可见 */
        if(lv_obj_get_hidden(obj)) return false;
    
        /*Invalidate the object only if it belongs to the current or previous'*/
        lv_obj_t * obj_scr = lv_obj_get_screen(obj);
        lv_disp_t * disp   = lv_obj_get_disp(obj_scr);
        if(obj_scr == lv_disp_get_scr_act(disp) ||
           obj_scr == lv_disp_get_scr_prev(disp) ||
           obj_scr == lv_disp_get_layer_top(disp) ||
           obj_scr == lv_disp_get_layer_sys(disp)) {
    
            /*Truncate the area to the object*/
            lv_area_t obj_coords;
            lv_coord_t ext_size = obj->ext_draw_pad;
            lv_area_copy(&obj_coords, &obj->coords);
            obj_coords.x1 -= ext_size;
            obj_coords.y1 -= ext_size;
            obj_coords.x2 += ext_size;
            obj_coords.y2 += ext_size;
    
            bool is_common;
    
            is_common = _lv_area_intersect(area, area, &obj_coords);   // 多余吧
            if(is_common == false) return false;  /*The area is not on the object*/  //多余吧
    
            /*Truncate recursively to the parents*/
            lv_obj_t * par = lv_obj_get_parent(obj);
            while(par != NULL) {
                is_common = _lv_area_intersect(area, area, &par->coords);
                if(is_common == false) return false;       /*If no common parts with parent break;*/
                if(lv_obj_get_hidden(par)) return false; /*If the parent is hidden then the child is hidden and won't be drawn*/
    
                par = lv_obj_get_parent(par);
            }
        }
    
        return true;
    }
    

    接下来分析_lv_inv_area函数。里面的参数disp是连接到显示驱动的,而area是它的一个成员属性。目的是在disp中将area区域设置为无效,用来重绘的。若disp是NULL默认就是一个显示器,若area为NULL默认没有要重绘的部分。lv_task_set_prio函数也是一种优化,它设计为通过实践触发的方式来redraw。等于不是每个周期都要redraw整个显示区域的,只是在被通知到有无效区域后,并且保存后,才会对这个区域进行刷新。

    void _lv_inv_area(lv_disp_t * disp, const lv_area_t * area_p)
    {
        if(!disp) disp = lv_disp_get_default();
        if(!disp) return;
    
        /*Clear the invalidate buffer if the parameter is NULL*/
        if(area_p == NULL) {
            disp->inv_p = 0;
            return;
        }
        /* 获取显示区域 */
        lv_area_t scr_area;
        scr_area.x1 = 0;
        scr_area.y1 = 0;
        scr_area.x2 = lv_disp_get_hor_res(disp) - 1;
        scr_area.y2 = lv_disp_get_ver_res(disp) - 1;
    
        lv_area_t com_area;
        bool suc;
        /* _lv_area_intersect 函数就是获取显示和传入区的公共区域大小的,放入com_area */
        suc = _lv_area_intersect(&com_area, area_p, &scr_area);
    
        /*The area is truncated to the screen*/
        if(suc != false) {
            /* rounder_cb我没找到赋值的地方,头文件中注释就是添加绘图扩展区域 */
            if(disp->driver.rounder_cb) disp->driver.rounder_cb(&disp->driver, &com_area);
    
            /*Save only if this area is not in one of the saved areas*/
            uint16_t i;
            /* disp中inv_p是用来保存要重绘的区域数量,inv_areas数组是保存对应的重绘区间 */
            for(i = 0; i < disp->inv_p; i++) {
                /* 检查com_area是否完全在disp->inv_areas区域内,若是true,则说明已经保存过要重绘的区域,则退出此函数 */
                if(_lv_area_is_in(&com_area, &disp->inv_areas[i], 0) != false) return;
            }
    
            /*Save the area*/
            /* inv_p最大值为为32,若当前保存的区域小于32则将本次的com重绘区域进行保存,否则从0开始覆盖之前的保存区域 */
            if(disp->inv_p < LV_INV_BUF_SIZE) {
                lv_area_copy(&disp->inv_areas[disp->inv_p], &com_area);
            }
            else {   /*If no place for the area add the screen*/
                disp->inv_p = 0;
                lv_area_copy(&disp->inv_areas[disp->inv_p], &scr_area);
            }
            disp->inv_p++;
            /* 设置刷新优先级,我理解启动专门的对需要区域的刷新吧!_lv_disp_refr_task函数中会设置此task为OFF */
            lv_task_set_prio(disp->refr_task, LV_REFR_TASK_PRIO);
        }
    }
    

    lv_task_set_prio函数来看下disp->refr_task是传入的任务,LV_REFR_TASK_PRIO是此任务的优先级,此函数就是将就绪任务入栈,等待执行。而refr_task初始化注册的是disp->refr_task = lv_task_create(_lv_disp_refr_task, LV_DISP_DEF_REFR_PERIOD, LV_REFR_TASK_PRIO, disp);_lv_disp_refr_task函数。

    void lv_task_set_prio(lv_task_t * task, lv_task_prio_t prio)
    {
        if(task->prio == prio) return;
    
        /*Find the tasks with new priority*/
        lv_task_t * i;
        _LV_LL_READ(LV_GC_ROOT(_lv_task_ll), i) {
            /* 优先级应该是按从大到小排序的,若找到一个优先级比当前的小,则将prio的task插入 */
            if(i->prio <= prio) {
                if(i != task) _lv_ll_move_before(&LV_GC_ROOT(_lv_task_ll), task, i);
                break;
            }
        }
        /* 若这个task的优先级是task优先级中最小的,则将此task放入尾巴处 */
        /*There was no such a low priority so far then add the node to the tail*/
        if(i == NULL) {
            _lv_ll_move_before(&LV_GC_ROOT(_lv_task_ll), task, NULL);
        }
        task_list_changed = true;
    
        task->prio = prio;
    }
    

    好了从最上层的用户API将某一个obj设置到前景,我已经一步步走到函数内层进行了剖析。除了_lv_area_is_in小函数我没有具体分析,其它主要函数基本都分析完了。
    刚刚等于设置了无效的待重绘区域,接着启动了task要进行重绘,关于设置task继续分析下。猜测task的执行应该会向RTOS一样,周期扫描_lv_task_ll链表中高优先级的task后进行执行。所以搜索_lv_task_ll,果然找到了lv_task_handler函数,这个函数中果然有优先级的判断,包括lv_task_exec(LV_GC_ROOT(_lv_task_act)),用来执行注册的具体task的。而这个lv_task_handler函数就是我在main函数的时候周期调用的。好了,从触发收到命令进行无效区域设置,到更新绘图区域的执行都连接起来了。

    四,小结

    今天等于分析了一小点的源码,还是有收获的。说不定还捕获到了源码的bug,将来有机会我实际调试验证下的我想法,若有bug那么我就去github提issue。

    相关文章

      网友评论

        本文标题:littlevgl_7.11源码分析(1)--Apple的学习笔

        本文链接:https://www.haomeiwen.com/subject/mfindltx.html