LVGL输入设备管理
LVGL版本:8.1
原理框图
在LVGL中,官方定义了四种输入设备(除了未定类型):
//lv_hal_invdv.h
typedef enum {LV_INDEV_TYPE_NONE, /* 未定类型 */LV_INDEV_TYPE_POINTER, /* 触摸屏、鼠标 */LV_INDEV_TYPE_KEYPAD, /* 键盘 */LV_INDEV_TYPE_BUTTON, /* 硬件按键,和屏幕上的某个部件或点联动 */LV_INDEV_TYPE_ENCODER, /* 三个键值的解码器,通常为“左”、“右”和“按下” */
} lv_indev_type_t;
这些输入设备都是LVGL统一管理的,其代码结构都是类似的,可以用以下原理图来表示:
输入设备管理中,最核心的就是LVGL定时查询设备状态并获取数据,在图上为红色字体的lv_indev_reader,该模块承上启下,收集设备的数据并向上传递。原理图层级从上到下分别是:
- 用户层(User):把这层定义为用户注册设备驱动以及用户操作设备和显示之间的交互行为,其中特定输入设备仅归属于一个显示器,而显示器不绑定任何输入设备。
- 核心层(Core):reader定时(timing)采集输入设备的状态和数据,并整理成LVGL所需要的数据逻辑,再以事件形式分发到相关对象,如定义了光标,还需要在显示器上绘制光标图案。
- 事件驱动层(Event):连接LVGL核心和操作系统的部分,通过设备驱动提供的接口读取特定格式的数据,并初步分类为有意义的输入设备状态和数据,如鼠标、键盘等类型的数据。
- 设备驱动层(Device):通常不属于LVGL的部分,和硬件的设备驱动模块相关联,如Linux系统下使用设备驱动和输入设备之间传递数据。
源码逻辑
LVGL查询输入设备的方式是轮询的,周期默认LV_INDEV_DEF_READ_PERIOD设置为30ms,即33Hz刷新率。在用户使用驱动注册新的输入设备时,就为新设备创建了定时器任务lv_indev_read_timer_cb()。
//lv_hal_indev.c
lv_indev_t * lv_indev_drv_register(lv_indev_drv_t * driver)
{if(driver->disp == NULL) driver->disp = lv_disp_get_default(); //设备和显示器绑定if(driver->disp == NULL) {LV_LOG_WARN("lv_indev_drv_register: no display registered hence can't attach the indev to ""a display");return NULL;}lv_indev_t * indev = _lv_ll_ins_head(&LV_GC_ROOT(_lv_indev_ll)); //取下一个设备指针用于注册新设备if(!indev) {LV_ASSERT_MALLOC(indev);return NULL;}lv_memset_00(indev, sizeof(lv_indev_t));indev->driver = driver; //为新设备"安装"驱动indev->proc.reset_query = 1;indev->driver->read_timer = lv_timer_create(lv_indev_read_timer_cb, LV_INDEV_DEF_READ_PERIOD, indev); //为新设备创建定时器任务return indev;
}
该定时器任务就是上面原理图中的lv_indev_rader,其作用就是读取输入设备的数据,并组织成LVGL所需要的数据逻辑进行处理,起到承上启下的作用。
//lv_indev.c
void lv_indev_read_timer_cb(lv_timer_t * timer)
{INDEV_TRACE("begin");lv_indev_data_t data;indev_act = timer->user_data; //这里的user_data就是输入设备if(indev_act->driver->disp == NULL) return; /* 设备没有绑定到任何显示器,就没有意义 */indev_proc_reset_query_handler(indev_act); /* 如果有reset请求,优先处理并复位 */if(indev_act->proc.disabled) return;bool continue_reading;do {_lv_indev_read(indev_act, &data); //从设备中读取数据continue_reading = data.continue_reading;indev_proc_reset_query_handler(indev_act); /* 在读取函数中,有可能设置了复位,在这里还要处理一次 */indev_obj_act = NULL;indev_act->proc.state = data.state; //状态只有按下和松开两种,仅对有按键的设备有效/* 记录上一次活跃的时间 */if(indev_act->proc.state == LV_INDEV_STATE_PRESSED) {indev_act->driver->disp->last_activity_time = lv_tick_get();}else if(indev_act->driver->type == LV_INDEV_TYPE_ENCODER && data.enc_diff) {indev_act->driver->disp->last_activity_time = lv_tick_get();}if(indev_act->driver->type == LV_INDEV_TYPE_POINTER) {indev_pointer_proc(indev_act, &data);}else if(indev_act->driver->type == LV_INDEV_TYPE_KEYPAD) {indev_keypad_proc(indev_act, &data);}else if(indev_act->driver->type == LV_INDEV_TYPE_ENCODER) {indev_encoder_proc(indev_act, &data);}else if(indev_act->driver->type == LV_INDEV_TYPE_BUTTON) {indev_button_proc(indev_act, &data);}indev_proc_reset_query_handler(indev_act); /* 如果处理过程中产生了复位请求,还要再做一次reset */} while(continue_reading);/* 结束输入设备处理函数,将活跃输入设备指针清空 */indev_act = NULL;indev_obj_act = NULL;INDEV_TRACE("finished");
}
其中读取设备数据的函数_lv_indev_read():
//lv_hal_indev.c
void _lv_indev_read(lv_indev_t * indev, lv_indev_data_t * data)
{lv_memset_00(data, sizeof(lv_indev_data_t));/* For touchpad sometimes users don't set the last pressed coordinate on release.* So be sure a coordinates are initialized to the last point */if(indev->driver->type == LV_INDEV_TYPE_POINTER) {data->point.x = indev->proc.types.pointer.last_raw_point.x;data->point.y = indev->proc.types.pointer.last_raw_point.y;}/*Similarly set at least the last key in case of the user doesn't set it on release*/else if(indev->driver->type == LV_INDEV_TYPE_KEYPAD) {data->key = indev->proc.types.keypad.last_key;}/*For compatibility assume that used button was enter (encoder push)*/else if(indev->driver->type == LV_INDEV_TYPE_ENCODER) {data->key = LV_KEY_ENTER;}if(indev->driver->read_cb) {INDEV_TRACE("calling indev_read_cb");indev->driver->read_cb(indev->driver, data); //输入事件驱动函数,即读取设备数据函数}else {LV_LOG_WARN("indev_read_cb is not registered");}
}
在函数的最后,使用输入事件驱动读取数据read_cb(),在注册设备驱动时会传入该函数,可以使用如下的通用读取函数evdev_read()来读取不同类型的输入设备。
void evdev_read(lv_indev_drv_t * drv, lv_indev_data_t * data)
{struct input_event in;while(read(evdev_mouse_fd, &in, sizeof(struct input_event)) > 0) {if(in.type == EV_REL) { //相对位置if(in.code == REL_X) //相对X位移#if EVDEV_SWAP_AXESevdev_root_y += in.value;#elseevdev_root_x += in.value;#endifelse if(in.code == REL_Y) //相对Y位移#if EVDEV_SWAP_AXESevdev_root_x += in.value;#elseevdev_root_y += in.value;#endif} else if(in.type == EV_ABS) { //绝对位置if(in.code == ABS_X) //绝对X位置#if EVDEV_SWAP_AXESevdev_root_y = in.value;#elseevdev_root_x = in.value;#endifelse if(in.code == ABS_Y) //绝对Y位置#if EVDEV_SWAP_AXESevdev_root_x = in.value;#elseevdev_root_y = in.value;#endifelse if(in.code == ABS_MT_POSITION_X)#if EVDEV_SWAP_AXESevdev_root_y = in.value;#elseevdev_root_x = in.value;#endifelse if(in.code == ABS_MT_POSITION_Y)#if EVDEV_SWAP_AXESevdev_root_x = in.value;#elseevdev_root_y = in.value;#endifelse if(in.code == ABS_MT_TRACKING_ID)if(in.value == -1)evdev_button = LV_INDEV_STATE_REL;else if(in.value == 0)evdev_button = LV_INDEV_STATE_PR;} else if(in.type == EV_KEY) { //按键类型if(in.code == BTN_MOUSE || in.code == BTN_TOUCH) { //鼠标按键或触摸按下,都属于POINTERif(in.value == 0)evdev_button = LV_INDEV_STATE_REL;else if(in.value == 1)evdev_button = LV_INDEV_STATE_PR;} else if(drv->type == LV_INDEV_TYPE_KEYPAD) { //键盘类型#if USE_XKBdata->key = xkb_process_key(in.code, in.value != 0);#elseswitch(in.code) {case KEY_BACKSPACE:data->key = LV_KEY_BACKSPACE;break;case KEY_ENTER:data->key = LV_KEY_ENTER;break;case KEY_PREVIOUS:data->key = LV_KEY_PREV;break;case KEY_NEXT:data->key = LV_KEY_NEXT;break;case KEY_UP:data->key = LV_KEY_UP;break;case KEY_LEFT:data->key = LV_KEY_LEFT;break;case KEY_RIGHT:data->key = LV_KEY_RIGHT;break;case KEY_DOWN:data->key = LV_KEY_DOWN;break;case KEY_TAB:data->key = LV_KEY_NEXT;break;default:data->key = 0;break;}#endif /* USE_XKB */if (data->key != 0) {/* Only record button state when actual output is produced to prevent widgets from refreshing */data->state = (in.value) ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;}evdev_key_val = data->key;evdev_button = data->state;return;}}}if(drv->type == LV_INDEV_TYPE_KEYPAD) {/* No data retrieved */data->key = evdev_key_val;data->state = evdev_button;return;}if(drv->type != LV_INDEV_TYPE_POINTER)return ;/*Store the collected data*/#if EVDEV_CALIBRATEdata->point.x = map(evdev_root_x, EVDEV_HOR_MIN, EVDEV_HOR_MAX, 0, drv->disp->driver->hor_res);data->point.y = map(evdev_root_y, EVDEV_VER_MIN, EVDEV_VER_MAX, 0, drv->disp->driver->ver_res);
#elsedata->point.x = evdev_root_x;data->point.y = evdev_root_y;
#endifdata->state = evdev_button;if(data->point.x < 0)data->point.x = 0;if(data->point.y < 0)data->point.y = 0;if(data->point.x >= drv->disp->driver->hor_res)data->point.x = drv->disp->driver->hor_res - 1;if(data->point.y >= drv->disp->driver->ver_res)data->point.y = drv->disp->driver->ver_res - 1;return ;
}
输入事件input_event是一种通用的Linux数据结构,
struct input_event {struct timeval time;__u16 type; //类型__u16 code; //类型下面的具体代码__s32 value; //值,通常为0/1
};
以自用的鼠标为例(测试了两款鼠标都是一致的输入数据):
左移动:type:2 code:0 value:-1(相对位置:横坐标X) + type:0 code:0 value:0(SYNC)
右移动:type:2 code:0 value:1(相对位置:横坐标X) + type:0 code:0 value:0(SYNC)
上移动:type:2 code:1 value:-1(相对位置:纵坐标Y) + type:0 code:0 value:0(SYNC)
下移动:type:2 code:1 value:1(相对位置:纵坐标Y) + type:0 code:0 value:0(SYNC)
滚轮上:type:2 code:8 value:1(相对位置:REL_WHEEL) + type:0 code:0 value:0(SYNC)
滚轮下:type:2 code:8 value:-1(相对位置:REL_WHEEL) + type:0 code:0 value:0(SYNC)
滚轮按下:type:4 code:4 value:0x90003(杂项:MSC_SCAN) + type:1 code:0x112 value:0x1(BTN-MIDDLE 按下) + type:0 code:0 value:0(SYNC)
滚轮松开:type:4 code:4 value:0x90003(杂项:MSC_SCAN) + type:1 code:0x112 value:0x0(BTN-MIDDLE 松开) + type:0 code:0 value:0(SYNC)
左键按下:type:4 code:4 value:0x90001(杂项:MSC_SCAN) + type:1 code:0x110 value:0x1(BTN-LEFT 按下) + type:0 code:0 value:0(SYNC)
左键松开:type:4 code:4 value:0x90001(杂项:MSC_SCAN) + type:1 code:0x110 value:0x0(BTN-LEFT 松开) + type:0 code:0 value:0(SYNC)
右键按下:type:4 code:4 value:0x90002(杂项:MSC_SCAN) + type:1 code:0x111 value:0x1(BTN-RIGHT 按下) + type:0 code:0 value:0(SYNC)
右键松开:type:4 code:4 value:0x90002(杂项:MSC_SCAN) + type:1 code:0x111 value:0x0(BTN-RIGHT 松开) + type:0 code:0 value:0(SYNC)
可以对照上面的evdev_read()函数,并结合Linux的输入事件代码文件“input-event-codes.h”进行学习。
在读取了输入设备数据后,就可以分门别类地进行数据处理了,这里还是以鼠标设备为例:
//lv_indev.c
static void indev_pointer_proc(lv_indev_t * i, lv_indev_data_t * data)
{lv_disp_t * disp = i->driver->disp;/* 保存当前点位置,在下一次读取设备数据时会进行比较 */i->proc.types.pointer.last_raw_point.x = data->point.x;i->proc.types.pointer.last_raw_point.y = data->point.y;//根据旋转配置进行调整if(disp->driver->rotated == LV_DISP_ROT_180 || disp->driver->rotated == LV_DISP_ROT_270) {data->point.x = disp->driver->hor_res - data->point.x - 1;data->point.y = disp->driver->ver_res - data->point.y - 1;}if(disp->driver->rotated == LV_DISP_ROT_90 || disp->driver->rotated == LV_DISP_ROT_270) {lv_coord_t tmp = data->point.y;data->point.y = data->point.x;data->point.x = disp->driver->ver_res - tmp - 1;}/* 简单的范围检测 */if(data->point.x < 0) LV_LOG_WARN("X is %d which is smaller than zero", data->point.x);if(data->point.x >= lv_disp_get_hor_res(i->driver->disp)) LV_LOG_WARN("X is %d which is greater than hor. res",data->point.x);if(data->point.y < 0) LV_LOG_WARN("Y is %d which is smaller than zero", data->point.y);if(data->point.y >= lv_disp_get_ver_res(i->driver->disp)) LV_LOG_WARN("Y is %d which is greater than hor. res",data->point.y);//如果定义了光标,则要重新根据位置绘制光标if(i->cursor != NULL &&(i->proc.types.pointer.last_point.x != data->point.x || i->proc.types.pointer.last_point.y != data->point.y)) {lv_obj_set_pos(i->cursor, data->point.x, data->point.y);}i->proc.types.pointer.act_point.x = data->point.x;i->proc.types.pointer.act_point.y = data->point.y;if(i->proc.state == LV_INDEV_STATE_PRESSED) {indev_proc_press(&i->proc); //按下按键的处理函数}else {indev_proc_release(&i->proc); //释放按键的处理函数}i->proc.types.pointer.last_point.x = i->proc.types.pointer.act_point.x;i->proc.types.pointer.last_point.y = i->proc.types.pointer.act_point.y;
}
其中按下按键时的处理函数indev_proc_press(),需要判断当前鼠标的位置是否存在LVGL组件,如果存在,则要发送按下事件LV_EVENT_PRESSED给对应的组件,进一步的,还要根据组件对象的Bubble标志决定要不要继续向上冒泡。在这里就不进一步分析了(超出本文讨论范围)。