Zephyr下使用LVGL-增加输入设备

Creative Commons
本作品采用知识共享署名

本文说明如何为Zephyr内的LVGL增加输入设备。

Zephyr下使用LVGL指南一文中提到过对lvgl的输入Zephyr原生只移植了KSCAN,Zephyr将Kscan做为lvgl的pointer类型输入,但对于实际应用情况这远远不够,我们知道lvgl输入设备类型支持下面4种,这几乎已经涵盖了常用的输入形式

  • LV_INDEV_TYPE_POINTER: 指针,触摸屏
  • LV_INDEV_TYPE_KEYPAD:键盘
  • LV_INDEV_TYPE_BUTTON:外部实体按键
  • LV_INDEV_TYPE_ENCODER:旋转编码器
    本文以旋转编码器为例,说明如何为Zephyr内的LVGL增加新的输入设备。

配置使用旋转编码器

Zephyr本身并不支持旋转编码器驱动,如何添加请参考Zephyr添加旋转编码器驱动。本文默认已经添加该驱动,只说如何配置使用:

  1. 设备树指定输入设备硬件信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
input_encoder: rotary_encoder {
compatible = "rotary-encoder";
status = "okay";
label = "INPUT_ENCODER";
a-gpios = <&gpio1 22 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
b-gpios = <&gpio1 23 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
ppr = <15>;
spp = <2>;
};

gpio_keys {
compatible = "gpio-keys";
input_button: button_encoder {
label = "INPUT_ENCODER_BTN";
gpios = <&gpio2 30 GPIO_ACTIVE_LOW>;
};
};
  1. 添加配置项
    1
    2
    3
    4
    5
    # 旋转编码器使用的是传感器API的抽象,因此要启用传感器
    CONFIG_SENSOR=y

    # 启用旋转编码器
    CONFIG_ROTARY_ENCODER=y

因为我们使用旋转编码器做为LVGL的输入,因此不能不要开启 CONFIG_LVGL_POINTER_KSCAN

LVGL使用旋转编码器

旋转编码器的驱动是以Zephyr的传感器驱动抽象,读取旋转量,因此用Zephyr的传感器驱动来实现lvgl的LV_INDEV_TYPE_ENCODER访问接口并完成注册即可。该代码可以写在Zephyr应用程序中,而无需改动Zephyr对lvgl的移植代码文件。

实现lvgl输入驱动

lvgl的输入驱动是以callback注册,形式如下

1
bool (*read_cb)(struct _lv_indev_drv_t * indev_drv, lv_indev_data_t * data);

旋转编码器的数据结果如下,只会使用enc_diff和state

1
2
3
4
5
6
7
8
typedef struct {
lv_point_t point; /**< For LV_INDEV_TYPE_POINTER the currently pressed point*/
uint32_t key; /**< For LV_INDEV_TYPE_KEYPAD the currently pressed key*/
uint32_t btn_id; /**< For LV_INDEV_TYPE_BUTTON the currently pressed button*/
int16_t enc_diff; /**< For LV_INDEV_TYPE_ENCODER number of steps since the previous read*/

lv_indev_state_t state; /**< LV_INDEV_STATE_REL or LV_INDEV_STATE_PR*/
} lv_indev_data_t;

  • enc_diff: 为旋转变化步长,顺时针为正,逆时针为负
  • state: 为旋转编码器按键,有按下和释放两个状态
    旋转部分的驱动如下注释:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //读取旋转编码器设备树中的lable name
    #if DT_NODE_HAS_STATUS(DT_INST(0, rotary_encoder), okay)
    #define ENCODER_DEV_NAME DT_LABEL(DT_INST(0, rotary_encoder))
    #endif

    //通过lable获取device
    dev_rotary = device_get_binding(ENCODER_DEV_NAME);

    //读取当前旋转的度数作为初始化值,以后作为比较
    sensor_channel_get(dev_rotary, SENSOR_CHAN_ROTATION, &val);
    init_level = val.val1;

    //注册旋转编码器callback,在旋转发生时会触发callback
    sensor_trigger_set(dev_rotary, NULL, encoder_callback);

callback的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void encoder_callback(const struct device *dev,
struct sensor_trigger *trigger)
{
struct sensor_value val;

sensor_channel_get(dev, SENSOR_CHAN_ROTATION, &val);

//读取当前度数,和上一次的做比较,判断是正转还是翻转,将结果保存在enc_diff中
if(init_level > val.val1){
enc_diff = -1;
}else{
enc_diff = 1;
}

//更新上一次的度数
init_level = val.val1;
printk("encoder rotation at %d\n", enc_diff);
}

由于添加的旋转编码器驱动只支持旋转量,因此还需要另外添加按键的驱动, 按键驱动的本质就是在下降沿触发中断时读取GPIO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//读取按键的设备树信息
#define SW0_NODE DT_ALIAS(sw0)
#if !DT_NODE_HAS_STATUS(SW0_NODE, okay)
#error "Unsupported board: sw0 devicetree alias is not defined"
#endif
static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET_OR(SW0_NODE, gpios,
{0});
static struct gpio_callback button_cb_data;

//配置按键连接的GPIO为输入
ret = gpio_pin_configure_dt(&button, GPIO_INPUT);
if (ret != 0) {
printk("Error %d: failed to configure %s pin %d\n",
ret, button.port->name, button.pin);
return;
}

//为防抖考虑,配置按键GPIO为双边沿触发中断
ret = gpio_pin_interrupt_configure_dt(&button,
GPIO_INT_EDGE_BOTH);
if (ret != 0) {
printk("Error %d: failed to configure interrupt on %s pin %d\n",
ret, button.port->name, button.pin);
return;
}

//注册一个delay work 用于防抖
k_work_init_delayable(&button_timer, button_timeout);
//注册中断处理函数,中断时调用button_pressed
gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin));
gpio_add_callback(button.port, &button_cb_data);

对于机械按键一般有10ms左右的抖动,因此button_pressed内并不是直接读GPIO,而是通知delay work在10ms以后调用button_timeout读取GPIO上的电平

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void button_pressed(const struct device *dev, struct gpio_callback *cb,
uint32_t pins)
{
//中断触发时,通知delay work 10ms之后读GPIO
k_work_reschedule(&button_timer, K_MSEC(10));
}

static void button_timeout(struct k_work *work)
{
//根据读出的level判断按键的状态,状态保存在state。
int val = gpio_pin_get(button.port, button.pin);
printk("Button pressed at %d\n", val);
if(val > 0){
state = LV_INDEV_STATE_PR;
}else{
state = LV_INDEV_STATE_REL;
}
}

现在我们有了旋转的状态enc_diff和按压的状态state,我们按照read_cb的形式写出lvgl读按键的callback函数

1
2
3
4
5
6
7
8
9
10
11
bool encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
(void) indev_drv; /*Unused*/
//printk("encoder read\n");
data->state = state;
data->enc_diff = enc_diff;
enc_diff = 0; //读完后将enc_diff设为0,表示旋转无变化

//返回false表示没有下一个按键了,这取决于输入驱动的实现,lvgl如果发现返回值为true会一直调用read_cb,直到返回false为止
return false;
}

注册lvgl输入驱动

注册驱动非常简单,使用lvgl的接口将写好的read_cb注册给lvgl即可:

1
2
3
4
5
6
7
8
9
10
11
   lv_indev_drv_t indev_drv;
//初始化驱动结构,指定输入设备类型和read_cb
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_ENCODER;
indev_drv.read_cb = encoder_read;

//注册输入驱动
encoder_indev = lv_indev_drv_register(&indev_drv);
if (encoder_indev == NULL) {
printk("Failed to register input device.\r\n");
}

最后

从前文可以看到,LVGL的输入是个非常独立的部分,即使Zephyr没有实现的输入类型我们也可以简单的在应用中添加。