背景
在上一篇中成功运行了LVGL自带的Demo,这次从Demo的程序入口进行初步分析。
入口
主函数
int main(int argc, char **argv)
// lvgl初始化
lv_init();
// 硬件初始化:包括显示设备、输入设备
hal_init();
// 选择一个Demo运行
lv_demo_widgets();
// 周期性运行lv_timer_handler接口,该接口内部主要处理:绘制、获取输入信息等
while (1)
lv_timer_handler();
usleep(5 * 1000);
lvgl具体初始化
lv_init
if(lv_initialized)
return;
// 初始化一个内存池,通过tlsf方式
lv_mem_init();
// 定时器模块初始化,_lv_timer_ll
_lv_timer_core_init();
// 文件系统初始化,_lv_fsdrv_ll
_lv_fs_init();
// 动画模块初始化,_lv_anim_ll
_lv_anim_core_init();
// 组初始化,_lv_group_ll
_lv_group_init();
// 样式初始化,_lv_obj_style_trans_ll
_lv_obj_style_init();
// 显示和输入设备初始化
_lv_ll_init(&LV_GC_ROOT(_lv_disp_ll), sizeof(lv_disp_t));
_lv_ll_init(&LV_GC_ROOT(_lv_indev_ll), sizeof(lv_indev_t));
// 图片解码初始化,_lv_img_decoder_ll
_lv_img_decoder_init();
// utf-8编码和大小端测试
// 其他模块初始化
lv_extra_init();
lv_initialized = true;
hal_init初始化
static void hal_init(void)
// 创建窗口,模拟显示设备,底层调用的SDL
monitor_init();
monitor_sdl_init();
lv_timer_create(sdl_event_handler, 10, NULL);
// 创建tick线程
SDL_CreateThread(tick_thread, "tick", NULL);
// 创建显示buffer,该大小可以不必是整个屏幕大小,如该例子就是设置的100行大小
lv_disp_draw_buf_init(&disp_buf1, buf1_1, buf1_2, MONITOR_HOR_RES * 100);
// 创建显示驱动并注册刷新回调函数
disp_drv.flush_cb = monitor_flush;
lv_disp_drv_register(&disp_drv);
// 创建鼠标设备驱动并注册鼠标读取驱动函数
indev_drv_1.read_cb = mouse_read;
lv_indev_drv_register(&indev_drv_1);
// 该线程为lvgl提供时间基准,如果在Linux这种比较完善的OS上,可以不用专门开个线程调用lv_tick_inc,可以设置LV_TICK_CUSTOM
static int tick_thread(void *data)
while (1)
SDL_Delay(5);
lv_tick_inc(5);
数据结构
链表是LVGL中最重要的数据结构,简单说一下。具体接口在lv_ll.c中
// 一个链表元素类型
typedef uint8_t lv_ll_node_t;
typedef struct {
uint32_t n_size;
lv_ll_node_t * head;
lv_ll_node_t * tail;
} lv_ll_t;
// 链表初始化
void _lv_ll_init(lv_ll_t * ll_p, uint32_t node_size);
// 增加元素
void * _lv_ll_ins_head(lv_ll_t * ll_p);
void * _lv_ll_ins_prev(lv_ll_t * ll_p, void * n_act);
void * _lv_ll_ins_tail(lv_ll_t * ll_p);
// 修改列表
void _lv_ll_remove(lv_ll_t * ll_p, void * node_p);
void _lv_ll_clear(lv_ll_t * ll_p);
void _lv_ll_chg_list(lv_ll_t * ll_ori_p, lv_ll_t * ll_new_p, void * node, bool head);
void _lv_ll_move_before(lv_ll_t * ll_p, void * n_act, void * n_after);
// 获取元素
void * _lv_ll_get_head(const lv_ll_t * ll_p);
void * _lv_ll_get_tail(const lv_ll_t * ll_p);
void * _lv_ll_get_next(const lv_ll_t * ll_p, const void * n_act);
void * _lv_ll_get_prev(const lv_ll_t * ll_p, const void * n_act);
uint32_t _lv_ll_get_len(const lv_ll_t * ll_p);
bool _lv_ll_is_empty(lv_ll_t * ll_p);
链表元素及操作本身没什么好说的,具体说一下,定义为链表类型的全局变量的定义方式。具体在lv_gc.h,以_lv_timer_ll为例
#define LV_DISPATCH(f, t, n) f(t, n)
#define LV_ITERATE_ROOTS(f) \
LV_DISPATCH(f, lv_ll_t, _lv_timer_ll) /*Linked list to store the lv_timers*/
#define LV_EXTERN_ROOT(root_type, root_name) extern root_type root_name;
LV_ITERATE_ROOTS(LV_EXTERN_ROOT)
首先,需要明确:编译时会执行LV_ITERATE_ROOTS(LV_EXTERN_ROOT)这行代码,不需要手动调用。展开步骤如下
LV_DISPATCH(f, lv_ll_t, _lv_timer_ll) -->
LV_DISPATCH(LV_EXTERN_ROOT, lv_ll_t, _lv_timer_ll) -->
LV_EXTERN_ROOT(lv_ll_t, _lv_timer_ll) -->
extern lv_ll_t _lv_timer_ll;
最终的效果是,定义了一个类型为lv_ll_t的全局变量_lv_timer_ll,其他变量的定义类似。
使用
- 从GitHub下载最新lvgl源码
git clone https://github.com/lvgl/lvgl.git
- 拷贝下载好的lvgl目录到自己的工程中,如test_prj
- lvgl/lv_conf_template.h拷贝为lv_conf.h,并打开#if 0,使能配置内容。
- 在需要使用lvgl接口函数的地方,包含lvgl/lvgl.h头文件
- 在定时器或者线程中周期调用lv_tick_inc(x),x应该为1-10ms
- 调用lv_init()
- 创建绘制缓冲区
static lv_disp_darw_buf_t draw_buf;
static lv_color_t buf1[DISP_HOR_RES * DISP_VER_RES / 10]; /*Declare a buffer for 1/10 screen size*/
lv_disp_draw_buf_init(&draw_buf, buf1, NULL, MY_DISP_HOR_RES * MY_DISP_VER_SER / 10); /*Initialize the display buffer.*/
- 注册显示驱动,用于拷贝渲染信息到显示区域
lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
disp_drv.flush_cb = my_disp_flush; /*Set your driver function*/
disp_drv.buffer = &draw_buf; /*Assign the buffer to the display*/
disp_drv.hor_res = MY_DISP_HOR_RES; /*Set the horizontal resolution of the display*/
disp_drv.hor_res = MY_DISP_VER_RES; /*Set the verizontal resolution of the display*/
lv_disp_drv_register(&disp_drv); /*Finally register the driver*/
void my_disp_flush(lv_disp_drv_t * disp, const lv_area_t * area, lv_color_t * color_p)
{
int32_t x, y;
/*It's a very slow but simple implementation.
*`set_pixel` needs to be written by you to a set pixel on the screen*/
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
set_pixel(x, y, *color_p);
color_p++;
}
}
lv_disp_flush_ready(disp); /* Indicate you are ready with the flushing*/
}
- 注册输入驱动,用于读取输入设备
lv_indev_drv_t indev_drv; /*Descriptor of a input device driver*/
lv_indev_drv_init(&indev_drv); /*Basic initialization*/
indev_drv.type = LV_INDEV_TYPE_POINTER; /*Touch pad is a pointer-like device*/
indev_drv.read_cb = my_touchpad_read; /*Set your driver function*/
lv_indev_drv_register(&indev_drv); /*Finally register the driver*/
bool my_touchpad_read(lv_indev_t * indev, lv_indev_data_t * data)
{
/*`touchpad_is_pressed` and `touchpad_get_xy` needs to be implemented by you*/
if(touchpad_is_pressed()) {
data->state = LV_INDEV_STATE_PRESSED;
touchpad_get_xy(&data->point.x, &data->point.y);
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
- 在主循环或者线程中每隔几毫秒,周期性调用
lv_timer_handler()
,该函数内将会做:重绘显示区域;读取输入设备信息;展示动画等。
总结:
- 下载lvgl源码并根据需要修改lv_conf.h
- 调用lv_init()初始化
- 调用设备自身的驱动,如LCD显示设备,Touch输入设备
- 注册显示和输入设备的驱动到lvgl,用于嫁接lvgl和不同硬件设备之间的桥梁
- 在中断函数或者线程中周期性调用lv_tick_inc(x),来告诉消逝时间,类似于系统时间。主要通过调用lv_tick_get()来获取当前tick数量,即认为当前系统时间,用其相对值。
- 在线程中周期性调用lv_timer_handler(),来处理lvgl相关任务,如需要重绘界面等。这是lvgl核心,真正动起来的地方。绘制动作,读取用户操作等。GUI的本质也就是根据用户操作,绘制相应内容。
注意事项
LVGL是非线程安全的,具体可以参考LVGL文档。我是基于V8研究的。