lvgl移植—Linux fbdev&evdev(基於LVGL v7)
雖然lvgl官方提供了有關linux framebuffer操作的庫函數,但是我決定自己試一下能否自己實現這部分操作
實際項目中應優先採用官方庫函數,官方實現程式碼位於文件夾lv_drivers/display下fbdev.c。
這篇文章則記錄這整個過程。
文章中若有言論及操作不妥之處,還望各位不吝賜教,批評指正。
項目地址://gitee.com/JensenHua/lvgl_fbdev_evdev
最終效果
影片預覽: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項中的鏈接,如果你實在看不懂,可以配合翻譯工具做一個大體了解。