lvgl移植—Linux fbdev&evdev(基于LVGL v7)

时间:2024-03-12 10:24:22

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视频链接

要做的事,写在最前面

  1. 搭建LVGL基本框架

  2. 实现并注册显示函数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也可以使用了。

  1. 实现并注册输入函数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_initmy_touchpad_thread

先看第一个函数my_touchpad_init
这个函数仅仅是打开了/dev/input/event1,并设置了pollfd结构体数组的fdeventsrevents
函数代码

/**
 * 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项中的链接,如果你实在看不懂,可以配合翻译工具做一个大体了解。
在这里插入图片描述

工程准备(了解LVGL工程创建的可以跳过这部分)