lvgl移植—Linux fbdev&evdev(基于LVGL v7)
虽然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也可以使用了。
- 实现并注册输入函数my_touchpad_read
该函数原形:
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)
这个函数做了些什么?
- 打开设备节点/dev/fb0
- 通过ioctl获取
fb_var_screeninfo
- 计算行宽、单像素宽度、屏幕像素数量
- 映射framebuffer到内存中
- 清除整个屏幕
做完这些操作之后你就可以在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机制(不推荐,影响动画刷新速率)
这里需要特别注意的是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程序
我选用的平台 NXP I.MX6ULL Cortex A7
我的这篇文章可能不足以让你完成对LVGL的初步认识,但是官方文档的丰富程度让人惊讶,如果这篇文章中有你看不懂的地方,你肯定会在LVGL开发文档中找到答案
我非常建议你先阅读开发文档中的1.2.3项中的链接,如果你实在看不懂,可以配合翻译工具做一个大体了解。