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

本文说明如何为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. 设备树指定输入设备硬件信息
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. 添加配置项
# 旋转编码器使用的是传感器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注册,形式如下

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

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

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: 为旋转编码器按键,有按下和释放两个状态 旋转部分的驱动如下注释:
//读取旋转编码器设备树中的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的内容如下

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:

//读取按键的设备树信息
#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上的电平

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函数

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即可:

    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没有实现的输入类型我们也可以简单的在应用中添加。