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_AXES
                    evdev_root_y += in.value;
                #else
                    evdev_root_x += in.value;
                #endif
            else if(in.code == REL_Y)  //相对Y位移
                #if EVDEV_SWAP_AXES
                    evdev_root_x += in.value;
                #else
                    evdev_root_y += in.value;
                #endif
        } else if(in.type == EV_ABS) {  //绝对位置
            if(in.code == ABS_X)  //绝对X位置
                #if EVDEV_SWAP_AXES
                    evdev_root_y = in.value;
                #else
                    evdev_root_x = in.value;
                #endif
            else if(in.code == ABS_Y)  //绝对Y位置
                #if EVDEV_SWAP_AXES
                    evdev_root_x = in.value;
                #else
                    evdev_root_y = in.value;
                #endif
            else if(in.code == ABS_MT_POSITION_X)
                #if EVDEV_SWAP_AXES
                    evdev_root_y = in.value;
                #else
                    evdev_root_x = in.value;
                #endif
            else if(in.code == ABS_MT_POSITION_Y)
                #if EVDEV_SWAP_AXES
                    evdev_root_x = in.value;
                #else
                    evdev_root_y = in.value;
                #endif
            else 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) {   //鼠标按键或触摸按下,都属于POINTER
                if(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_XKB
            data->key = xkb_process_key(in.code, in.value != 0);
    #else
                switch(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_CALIBRATE
    data->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);
#else
    data->point.x = evdev_root_x;
    data->point.y = evdev_root_y;
#endif

    data->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标志决定要不要继续向上冒泡。在这里就不进一步分析了(超出本文讨论范围)。

### 不同输入设备上的 LVGL 移植方法 LVGL 是一款功能强大的嵌入式 GUI 库,支持多种输入设备的移植。以下是针对不同输入设备进行移植的具体方法: #### 输入设备概述 LittlevGL 提供了一个灵活的架构来处理各种输入设备,例如触摸屏、按键、鼠标和编码器等[^3]。这些输入设备通过 `lv_indev_t` 类型的对象管理。 --- #### 显示驱动初始化 在硬件初始化完成后,需调用以下函数完成 LVGL 的基本初始化: ```c lv_init(); // 初始化 LVGL 核心模块 lv_port_disp_init(); // 配置显示接口并注册到 LVGL 中 ``` 上述代码片段负责设置显示相关的参数,并将其绑定至 LVGL 系统中[^1]。 --- #### 输入设备模板配置 为了适配不同的输入设备,在项目目录下的 `porting` 文件夹中可以找到两个重要文件: - **`lv_port_indev_template.c`** - **`lv_port_indev_template.h`** 这两个文件提供了输入设备的基础框架,开发者可以根据实际需求修改其内容以匹配目标硬件[^4]。 --- #### 编写自定义输入回调函数 每种输入设备都需要实现特定的读取逻辑,并将该逻辑封装成一个回调函数传递给 LVGL。具体步骤如下: 1. 定义输入设备类型(如触摸屏或键盘)。 2. 实现对应的读取函数,返回当前输入状态。 3. 将读取函数注册到 LVGL 系统中。 ##### 示例:触摸屏输入设备 假设使用的是电阻式触摸屏,则需要编写类似的代码: ```c static bool touchpad_read(lv_indev_data_t *data) { uint16_t touch_x, touch_y; bool is_pressed; /* 调用底层驱动获取触摸坐标 */ get_touch_point(&touch_x, &touch_y, &is_pressed); if (is_pressed) { // 如果屏幕被按下 data->point.x = touch_x; // 设置 X 坐标 data->point.y = touch_y; // 设置 Y 坐标 data->state = LV_INDEV_STATE_PR; // 表示按压状态 } else { data->state = LV_INDEV_STATE_REL; // 表示释放状态 } return false; // 返回 false 继续轮询 } ``` 接着,将此函数注册为输入设备的一部分: ```c /* 创建一个新的输入设备对象 */ lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type = LV_INDEV_TYPE_POINTER; // 设定为指针类输入设备 indev_drv.read_cb = touchpad_read; // 注册读取回调函数 lv_indev_t *touch_indev = lv_indev_drv_register(&indev_drv); // 注册设备 ``` --- #### 键盘输入设备 如果目标平台包含物理键盘或其他离散按钮作为输入方式,则可参考以下代码: ```c static bool keypad_read(lv_indev_data_t *data) { static uint32_t last_key = 0; // 上次按键记录 uint32_t current_key = read_keyboard(); // 获取当前按键值 if (current_key != 0 && current_key != last_key) { data->key = current_key; // 更新键值 data->state = LV_INDEV_STATE_PR; // 按下状态 last_key = current_key; } else if (current_key == 0) { data->state = LV_INDEV_STATE_REL; // 释放状态 } return false; } // 注册键盘输入设备 lv_indev_drv_t indev_kb_drv; lv_indev_drv_init(&indev_kb_drv); indev_kb_drv.type = LV_INDEV_TYPE_KEYPAD; indev_kb_drv.read_cb = keypad_read; lv_indev_t *kb_indev = lv_indev_drv_register(&indev_kb_drv); ``` --- #### 工程结构优化 为了减少错误发生概率,建议按照推荐的文件夹结构组织工程文件[^5]。这种结构不仅便于维护,还能有效避免因路径问题引发的编译失败。 --- #### 总结 通过对 `lv_port_indev_template.c` 和 `lv_port_indev_template.h` 进行定制开发,能够轻松实现对各类输入设备的支持。无论是触摸屏还是传统按键,只需提供相应的读取逻辑即可无缝集成到 LVGL 平台之上。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值