一,前言
玩玩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。
网友评论