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

雖然lvgl官方提供了有關linux framebuffer操作的庫函數,但是我決定自己試一下能否自己實現這部分操作

實際項目中應優先採用官方庫函數,官方實現程式碼位於文件夾lv_drivers/display下fbdev.c。

這篇文章則記錄這整個過程。
文章中若有言論及操作不妥之處,還望各位不吝賜教,批評指正。

項目地址://gitee.com/JensenHua/lvgl_fbdev_evdev

最終效果

在這裡插入圖片描述

[video(video-l4Rijug5-1616507869486)(type-bilibili)(url-//player.bilibili.com/player.html?aid=332141380)(image-//ss.csdn.net/p?//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工程創建的可以跳過這部分)

Tags: