虽然lvgl官方提供了有关linux framebuffer操作的库函数,但是我决定自己试一下能否自己实现这部分操作
实际项目中应优先采用官方库函数,官方实现代码位于文件夹lv_drivers/display下fbdev.c。
这篇文章则记录这整个过程。
文章中若有言论及操作不妥之处,还望各位不吝赐教,批评指正。
项目地址:https://gitee.com/JensenHua/lvgl_fbdev_evdev
[video(video-l4Rijug5-1616507869486)(type-bilibili)(url-https://player.bilibili.com/player.html?aid=332141380)(image-https://ss.csdn.net/p?http://i0.hdslb.com/bfs/archive/3f35710d418cad9d0c4384f10f9f29548153c56b.jpg)(title-LVGL移植到Linux Framebuffer)]
视频预览:bilibili视频链接
搭建LVGL基本框架
实现并注册显示函数my_disp_flush
该函数原形:
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
你需要实现在屏幕上任意区域渲染的功能
函数示例
int32_t x,y; for(y = area->y1; y<=area->y2;y++) { for (x=area->x1; x<=area->x2; x++) { memcpy(fb_base + x*pixel_width + y*line_width, &color_p->full, sizeof(lv_color_t)); color_p++; } } lv_disp_flush_ready(disp);
注册驱动程序
lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.flush_cb = my_disp_flush; disp_drv.buffer = &disp_buf; lv_disp_drv_register(&disp_drv);
做到这里你就已经实现了LVGL的显示功能,即使不做输入系统的移植,LVGL也可以使用了。
bool my_touchpad_read(lv_indev_drv_t * indev, lv_indev_data_t * data)
该函数存储屏幕点击位置,以及触点处于按下还是松开状态
函数示例
bool my_touchpad_read(lv_indev_drv_t * indev, lv_indev_data_t * data) { /* store the collected data */ data->state = my_touchpad_touchdown ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL; if(data->state == LV_INDEV_STATE_PR) { data->point.x = last_x; data->point.y = last_y; } return false; }
注册驱动程序
/* register input device driver */ lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = my_touchpad_read; lv_indev_drv_register(&indev_drv);
先说下总体思想
Linux内核提供了一个名为framebuffer的设备,用户可以通过打开该设备节点,通过一系列操作,可以使自定义内容显示到输出设备上,利用这个特性我们可以完成LVGL的显示部分。
LVGL的显示部分要求在文章开头已经说过了
我实现了一个函数void my_fb_init(void)
这个函数做了些什么?
fb_var_screeninfo
做完这些操作之后你就可以在my_disp_flush
函数中使用memcpy
函数向framebuffer所在的内存中存储数据了
如何通过x, y坐标数据计算对应像素点在framebuffer中的位置?
framebuffer起始地址 + y * 横向屏幕像素宽度 + x * 像素宽度
最后附上代码
/** * Get the screen info. * mmap the framebuffer to memory. * clear the screen. * @param * @return */ void my_fb_init(void) { fd_fb = open(DEFAULT_LINUX_FB_PATH, O_RDWR); if(fd_fb < 0){ handle_error("can not open /dev/fb0"); } /* already get fd_fb */ if(ioctl(fd_fb, FBIOGET_VSCREENINFO, &var) < 0){ handle_error("can not ioctl"); } /* already get the var screen info */ line_width = var.xres * var.bits_per_pixel / 8; pixel_width = var.bits_per_pixel / 8; screen_size = var.xres * var.yres * var.bits_per_pixel / 8; /* mmap the fb_base */ fb_base = (unsigned char *)mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0); if(fb_base == (unsigned char *)-1){ handle_error("can not mmap frame buffer"); } /* alreay get the start addr of framebuffer */ memset(fb_base, 0xff, screen_size); /* clear the screen */ }
输入部分我暂时只实现了单点触摸,日后可能会实现多点触摸,使用单点触摸会限制一些交互功能,比如双指点击事件,双指放大缩小等等。
先说下总体思想
我选用的平台有一块大小为7寸的电容式触摸屏,分辨率为1024*600
。Linux操作系统中的设备驱动提供了一系列的输入事件(有关这部分,限于篇幅,不展开讨论)
,而我的这块电容式触摸屏大概有以下这么几种事件
EV_SYN
用来间隔事件
EV_KEY
压力值
BTN_TOUCH
EV_ABS
- 触点ID
ABS_MT_TRACKING_ID
- X坐标
ABS_MT_POSITION_X
ABS_X
- Y坐标
ABS_MT_POSITION_Y
ABS_Y
这些事件已经足够足够我们完成输入系统的移植了
使用异步通知方式读取输入事件时,需要提供一个信号处理函数,在本项目中名为my_touchpad_sig_handler
。在main函数中也不许要创建单独线程来读取输入事件,一切操作都由信号处理函数完成。
首先在输入设备初始化函数中进行如下设置
signal(SIGIO, my_touchpad_sig_handler); fcntl(indev_info.tp_fd, F_SETOWN, getpid()); flags = fcntl(indev_info.tp_fd, F_GETFL); fcntl(indev_info.tp_fd, F_SETFL, flags | FASYNC | O_NONBLOCK); printf("Successfully run in async mode.\n");
我使用的信号处理函数
/** * async signal handler * @param * @return */ void my_touchpad_sig_handler(int signal) { while(read(indev_info.tp_fd, &indev_info.indev_event, sizeof(struct input_event)) > 0) my_touchpad_probe_event(); }
我将事件筛选功能抽离出成为一个函数my_touchpad_probe_event
,代码如下
void my_touchpad_probe_event(void) { switch(indev_info.indev_event.type) { case EV_KEY: /* Key event. Provide the pressure data of touchscreen*/ if(indev_info.indev_event.code == BTN_TOUCH) /* Screen touch event */ { if(1 == indev_info.indev_event.value) /* Touch down */ { indev_info.touchdown = true; } else if(0 == indev_info.indev_event.value) /* Touch up */ { indev_info.touchdown = false; } else /* Unexcepted data */ { goto touchdown_err; } } break; case EV_ABS: /* Abs event. Provide the position data of touchscreen*/ if(indev_info.indev_event.code == ABS_MT_POSITION_X) { indev_info.last_x = indev_info.indev_event.value; } if(indev_info.indev_event.code == ABS_MT_POSITION_Y) { indev_info.last_y = indev_info.indev_event.value; } break; default: break; } touchdown_err: /* Do nothing. Just return and ready for next event come. */ return; }
具体代码请拉取查看git仓库
这里需要特别注意的是poll
的定时时间会直接影响屏幕的刷新速度,所以我特别建议你将该时间设置为<=5ms
,小于等于官方建议的系统相应时间。该数值越小,动画刷新越流畅。
我采用的poll定时(INPUT_SAMEPLING_TIME)为1ms
这部分的移植比我想象中的要复杂一些,我实现了两个函数,分别是my_touchpad_init
和my_touchpad_thread
。
先看第一个函数my_touchpad_init
这个函数仅仅是打开了/dev/input/event1
,并设置了pollfd
结构体数组的fd
、events
、revents
函数代码
/** * Just initialize the touchpad * @param * @return */ void my_touchpad_init(void) { tp_fd = open(DEFAULT_LINUX_TOUCHPAD_PATH, O_RDWR); if(tp_fd < 0){ handle_error("can not open /dev/input/event1"); } mpollfd[0].fd = tp_fd; mpollfd[0].events = POLLIN; mpollfd[0].revents = 0; }
再来看第二个函数my_touchpad_thread
这个函数用来处理输入事件并存储事件值,先调用poll实现poll机制
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
调用read读取输入设备中的数据
通过结构体input_event
中的type
来区分事件从而存储code
值
需要注意的是,我们应该通过一个独立的任务来处理这些数据,好在LVGL中提供了创建任务的函数
lv_task_t * lv_task_create(lv_task_cb_t task_xcb, uint32_t period, lv_task_prio_t prio, void * user_data)
我们使用该函数创建一个线程来接收输入事件
/* create a thread to collect screen input data */ lv_task_create(my_touchpad_thread, SYSTEM_RESPONSE_TIME, LV_TASK_PRIO_MID, NULL);
函数代码
/** * A thread to collect input data of screen. * @param * @return */ void my_touchpad_thread(lv_task_t *task) { (void)task; int len; len = poll(mpollfd, nfds, INPUT_SAMEPLING_TIME); if(len > 0){ /* There is data to read */ len = read(tp_fd, &my_event, sizeof(my_event)); if(len == sizeof(my_event)){ /* On success */ //printf("get event: type = 0x%x,code = 0x%x,value = 0x%x\n",my_event.type,my_event.code,my_event.value); switch(my_event.type) { case EV_SYN: /* Sync event. Do nonthing */ break; case EV_KEY: /* Key event. Provide the pressure data of touchscreen*/ if(my_event.code == BTN_TOUCH) /* Screen touch event */ { if(my_event.value == 0x1) /* Touch down */ { //printf("screen touchdown\n"); my_touchpad_touchdown = true; } else if(my_event.value == 0x0) /* Touch up */ { my_touchpad_touchdown = false; //printf("screen touchdown\n"); } else /* Unexcepted data */ //printf("Unexcepted data\n"); goto touchdown_err; } break; case EV_ABS: /* Abs event. Provide the position data of touchscreen*/ if(my_event.code == ABS_MT_POSITION_X) last_x = my_event.value; if(my_event.code == ABS_MT_POSITION_Y) last_y = my_event.value; break; default: break; } } else{ /* On error */ handle_error("read error\n"); } } else if(len == 0){ /* Time out */ /* Do nothing */ } else{ /* Error */ handle_error("poll error!"); } touchdown_err: /* Do nothing. Just return and ready for next event come. */ return; }
LVGL是一个开放源码的图形库,它提供了创建嵌入式GUI所需的一切,具有易于使用的图形元素、美观的视觉效果和较低的内存占用。
我的上一篇文章《LVGL的使用:运行LVGL的PC模拟器例程》,中简单介绍了如何在PC上运行lvgl程序
我的这篇文章可能不足以让你完成对LVGL的初步认识,但是官方文档的丰富程度让人惊讶,如果这篇文章中有你看不懂的地方,你肯定会在LVGL开发文档中找到答案
我非常建议你先阅读开发文档中的1.2.3项中的链接,如果你实在看不懂,可以配合翻译工具做一个大体了解。