Zephyr上添加触屏驱动

本文简要介绍Zephyr下kscan驱动模型,并说明如何在kscan模型下实现触屏驱动。

Zephyr下kscan(keyboard scan matrix)驱动程序用于检测矩阵键盘或带有按钮的设备中的按键。 由于kscan驱动模型并不定义键值,而是通过按键的行列坐标来标识按键,因此kscan驱动模型也适用于触摸屏。

KSCAN驱动模型 链接到标题

kscan驱动模型定义了一组很简洁的接口和回调,方便应用使用驱动。

外部接口 链接到标题

接口列表 链接到标题

int kscan_config(struct device *dev,kscan_callback_t callback);

注册一个callback,当按键发生时通过callback通知应用。

int kscan_enable_callback(struct device *dev);

应用注册callback后,通过该API使能callback,使能后有按键发生才会调用callback。

int kscan_disable_callback(struct device *dev);

禁止callback,当按键发生时不调用callback,相当于是禁止键盘使用

Callback 链接到标题

当按键发生时,驱动会调用注册的callback来通知应用有按键发生,callback的形式如下

typedef void (*kscan_callback_t)(struct device *dev, u32_t row, u32_t column, bool pressed);

row 按键所在行 column 按键所在列 pressed true按键按下, false按键松开 callback的参数不指定固定的按键值二十使用行和列一个非常大的好处就是灵活性很高,几乎可以兼容任何类矩阵输入设备,例如触屏和鼠标。

内部接口 链接到标题

驱动模型内部定义了下面三种函数指针类型,对应于外部的三个接口

typedef int (*kscan_config_t)(struct device *dev,
			    kscan_callback_t callback);
typedef int (*kscan_disable_callback_t)(struct device *dev);
typedef int (*kscan_enable_callback_t)(struct device *dev);

struct kscan_driver_api {
	kscan_config_t config;
	kscan_disable_callback_t disable_callback;
	kscan_enable_callback_t enable_callback;
};

在驱动实现时实现这三个内部函数,并通过kscan_driver_api注册即可完成驱动,基本原理和方法可以参考zephyr驱动模型Zephyr驱动模型实现方式

触屏驱动实现 链接到标题

上一节所列出来的引用文章,是单针对驱动接口的实现,在实际的驱动添加中还有诸如dts,kconfig的添加和修改,本节以gt917s为例一步一步说明如何完成一个kscan驱动的添加。注意,本文是基于Zephyr2.20进行,设备树的宏在代码中体现的还是完整的宏,2.30后Zephyr已经引入了设备树宏生成API,两者之间在DTS宏的体现上不一样。

kscan的代码主要是在下面三个地方: 头文件 include/driver/kscan.h 定义了驱动接口,不需要修改 设备树 dts/binding/kscan/ 放置不同类型驱动的绑定文件,和硬件相关 源代码 drivers/kscan/ 放置不同类型驱动的源代码和kconfig文件

Step1 增加设备树绑定文件 链接到标题

Zephyr设备树原理和使用方法参考Zephyr设备树生成流程Zephyr设备树生成C宏规则。要添加设备树绑定需要先了解硬件,下图为gt917s触摸屏的接口原理图: touchif 接口驱动方法需要详细阅读gt917s的规格书,本文的重点是如何添加驱动,因此只列出和添加需要相关的内容,这里简要列出接口的使用方法:

  • I2C接口:用于写入和读取gt917s的寄存器,以获得触摸信息
  • INT: 触摸发生时会通过INT通知
  • RST:触摸屏初始化时和INT配合决定触摸屏I2C地址
  • 触摸屏范围可配置:触摸屏的最大范围宽高可根据需求配置

从上面的信息分析可以知道需要使用到I2C接口和两个GPIO,因此在绑定中描述这些硬件信息。触摸屏的宽高作为硬件驱动的配置信息我们也通过绑定来描述。在dts/binding/kscan/下新建绑定文件coodix,gt9x.yaml如下

description: GT9x capacitive touch panel

compatible: "coodix,gt9x"

include: [kscan.yaml, i2c-device.yaml]

properties:
    width:
      type: int
      required: true
      description: touch panel width
    height:
      type: int
      required: true
      description: touch panel height
    int-gpios:
      type: phandle-array
      required: false
    reset-gpios:
      type: phandle-array
      required: false

其中 include kscan.yaml是所有kscan的属性,目前只有lable include i2c-device.yaml,是因为包含i2c设备,将其属性include进来 新添加的width和height属性指定触摸屏的宽和高 新添加的int-gpios指定触摸屏的中断gpio 新添加的reset-gpios指定触摸屏的reset gpio

Step2 修改kconfig 链接到标题

kconfig文件用于选择配置kscan驱动和配置kscan驱动纯软件层面的参数。这里添加gt917s驱动,那么就要修改kconfig让其具有gt917s的配置选项,修改方法如下

增加Kconfig.gt9x 链接到标题

在drivers/kscan下新建文件Kconfig.gt9x文件,该文件包含了针对驱动的纯软件配置项

menuconfig KSCAN_GT9X
	bool "GT91X capacitive touch panel driver"
	depends on I2C && HAS_DTS_I2C
	help
	  Enable driver for the GT91X capacitive touch panel controller.

#触摸屏采样周期,默认每10ms读一次触摸屏硬件,看是否有按键发生
config KSCAN_GT9X_PERIOD
	int "Sample period (ms)"
	default 10
	depends on KSCAN_GT9X

#配置是否启用中断,启用后将在中断发生时读取触摸屏硬件,看按键发生情况
config KSCAN_GT9X_INTERRUPT
	bool "Enable interrupt"
	depends on KSCAN_GT9X

修改Kconfig 链接到标题

修改drivers/kscan/Kconfig, 让其引用Kconfig.gt9x,在Kconfig中添加如下即可

source "drivers/kscan/Kconfig.gt9x"

Step3 添加驱动代码 链接到标题

增加源文件 链接到标题

在drivers/kscan/下增加驱动的源代码文件kscan_gt9x.c,在drivers/kscan/CMakefiles.txt中增加下面内容,让编译系统可以编译源文件

zephyr_library_sources_ifdef(CONFIG_KSCAN_GT9X		kscan_gt9x.c)

驱动编写 链接到标题

具体驱动gt917s的代码spec有关不是本文说明的内容,本小节罗列驱动的主要框架,说明zephyr下如何实现kscan驱动,省略掉和spec细节相关的内容。 驱动工作的流程如下:

  • 上电时Zephyr调用 gt9x_init,对驱动进行初始化
  • 建立一个work queue,接受按键处理事件
  • 在非中断模式下建立一个timer,安装配置的时间进行触发(例如10ms)
  • 当timer发生时或者中断到来,在timer/中断中发送按键处理事件通知work queue处理按键
  • work queue收到按键处理事件后调用gt9x_read,通过i2c从gt917s中读出数据判断是否有实际按键发生
  • gt9x_read发现有实际按键发生时,呼叫callback将touch点发送给上层

实现的细节说明见注释:

#include <drivers/kscan.h>
#include <drivers/i2c.h>
#include <drivers/gpio.h>
#include <logging/log.h>

LOG_MODULE_REGISTER(gt9x, CONFIG_KSCAN_LOG_LEVEL);

//定义gt917s使用的硬件结构体,用于管理驱动gt917s使用的硬件
struct gt9x_config {
	char *i2c_name;         //i2c device name
	u8_t i2c_address;       //gt917s的i2c地址
	char *int_port ;        //中断gpio port
	u32_t int_pin;          //中断gpio pin
	u32_t int_flags;        //中断gpio配置的flag信息
	char *rst_port;         //reset gpio port
	u32_t rst_pin;          //reset gpio pin
	u32_t rst_flags;        //reset gpio配置的flag
};

//定义gt917s驱动运行时数据信息
struct gt9x_data {
	struct device *i2c;             //i2c驱动handle
	struct device *gpio_int;        //int gpio驱动handle
	struct device *gpio_rst;        //reset gpio驱动handle
	kscan_callback_t callback;      //保存上层注册的callback
	struct k_work work;             //处理按键的work queue handle
#ifdef CONFIG_KSCAN_GT9X_INTERRUPT	
	struct gpio_callback int_gpio_cb;   //使用中断时需要注册的中断callback
#else
	struct k_timer timer;               //不使用中断,使用轮询的timer
#endif
	struct device *dev;                 //gt917s设备驱动的handle
	
};

//gt917s中断处理函数
#ifdef CONFIG_KSCAN_GT9X_INTERRUPT
static void gt9x_isr_handler(struct device *dev,
				  struct gpio_callback *cb, u32_t pins)
{
	printk("touch int\n");
	const struct gt9x_config *config = dev->config->config_info;
	struct gt9x_data *drv_data =
		CONTAINER_OF(cb, struct gt9x_data, int_gpio_cb);

	gpio_pin_interrupt_configure(dev, config->int_pin,
		GPIO_INT_DISABLE);

    //中断发生时说明有touch发生,通知work queue处理
	k_work_submit(&drv_data->work);
}
#endif

//timer处理函数,每CONFIG_KSCAN_GT9X_PERIOD执行一次
#ifndef CONFIG_KSCAN_GT9X_INTERRUPT
static void gt9x_timer_handler(struct k_timer *timer)
{
	struct gt9x_data *data =
		CONTAINER_OF(timer, struct gt9x_data, timer);
    //通知work queue查询是否有touch发生
	k_work_submit(&data->work);
}
#endif

//读gt917s
static void gt9x_read(struct device *dev)
{
	const struct gt9x_config *config = dev->config->config_info;
	struct gt9x_data *data = dev->driver_data;

    /*
        省略代码: 从gt917s中读取当前touch点信息
    */

    //读到有touch发生时,通过callback通知上层处理,这里的callback是由gt9x_configure注册
    data->callback(dev, pre_touch[i].y, pre_touch[i].x, pressed);

#ifdef CONFIG_KSCAN_GT9X_INTERRUPT
	gpio_pin_interrupt_configure(data->gpio_int,
		   config->int_pin, GPIO_INT_EDGE_TO_ACTIVE);
#endif


	return ret;
}

//work queue处理函数,work submit的时候执行
static void gt9x_work_handler(struct k_work *work)
{
	struct gt9x_data *data =
		CONTAINER_OF(work, struct gt9x_data, work);
    //work queue中读取当前gt917s信息
	gt9x_read(data->dev);
}


//注册callback
static int gt9x_configure(struct device *dev, kscan_callback_t callback)
{
	struct gt9x_data *data = dev->driver_data;

	if (!callback) {
		return -EINVAL;
	}
    //将传入的callback保存下来
	data->callback = callback;

	return 0;
}

//使能callback
static int gt9x_enable_callback(struct device *dev)
{
	struct gt9x_data *data = dev->driver_data;
	
#ifdef CONFIG_KSCAN_GT9X_INTERRUPT
    //使能中断让驱动开始读取gt917s
	gpio_add_callback(data->gpio_int, &data->int_gpio_cb);
#else
    //启动timer,让驱动开始轮询
	k_timer_start(&data->timer, K_MSEC(CONFIG_KSCAN_GT9X_PERIOD),
		      K_MSEC(CONFIG_KSCAN_GT9X_PERIOD));
#endif
	return 0;
}

//禁用callback
static int gt9x_disable_callback(struct device *dev)
{
	struct gt9x_data *data = dev->driver_data;
#ifdef CONFIG_KSCAN_GT9X_INTERRUPT
    //禁止中断,驱动停止读取gt917s
	gpio_remove_callback(data->gpio_int, &data->int_gpio_cb);
#else
    //停止timer,驱动停止轮询
	k_timer_stop(&data->timer);
#endif
	return 0;
}

//初始化gt917s
static int gt9x_init(struct device *dev)
{
	const struct gt9x_config *config = dev->config->config_info;
	struct gt9x_data *data = dev->driver_data;
	u8_t version[GT9X_VERSION_LEN];
	int ret = -1;
	u16_t check_sum;
	u16_t reg_num;

//初始化中断gpio
#ifdef DT_INST_0_COODIX_GT9X_INT_GPIOS_CONTROLLER
	data->gpio_int = device_get_binding(config->int_port);
	if (data->gpio_int == NULL) {
		LOG_ERR("Could not find INT GPIO device");
		return -EINVAL;
	}
#endif

//初始化reset gpio
#ifdef DT_INST_0_COODIX_GT9X_RESET_GPIOS_CONTROLLER
	data->gpio_rst = device_get_binding(config->rst_port);
	if (data->gpio_rst == NULL) {
		LOG_ERR("Could not find rst GPIO device");
		return -EINVAL;
	}
#endif

	/*
        省略代码:通过int和reset gpio配置gt917s的i2c地址
    */


//配置中断gpio以及gpio中断的callback
    if(data->gpio_int != NULL){
        gpio_pin_configure(data->gpio_int, config->int_pin, GPIO_INPUT | config->int_flags);
#ifdef CONFIG_KSCAN_GT9X_INTERRUPT

		ret = gpio_pin_interrupt_configure(data->gpio_int,
		   config->int_pin, GPIO_INT_EDGE_TO_ACTIVE);
		if (ret != 0) {
			LOG_ERR("Error %d: failed to configure pin interrupt %d '%s'\n",
				ret, config->int_pin, config->int_port);
			return -EINVAL;
		}
        gpio_init_callback(&data->int_gpio_cb, gt9x_isr_handler, BIT(config->int_pin));
#endif
    }

//获取使用i2c的handle
	data->i2c = device_get_binding(config->i2c_name);
	if (data->i2c == NULL) {
		LOG_ERR("Could not find I2C device");
		return -EINVAL;
	}

    /*
        省略代码:通过i2c驱动对gt917s进行配置初始化
    */

//保存本驱动的handle
	data->dev = dev;


//初始化读取touch的work queue
	k_work_init(&data->work, gt9x_work_handler);

//非中断模式,初始化timer用作轮询
#ifndef CONFIG_KSCAN_GT9X_INTERRUPT
	k_timer_init(&data->timer, gt9x_timer_handler, NULL);
#endif

	return 0;
}


//初始化驱动的API,这些API将注册进Zephyr驱动模型内,之后通过kscan.h中的API呼叫就会对应到这些API
static const struct kscan_driver_api gt9x_driver_api = {
	.config = gt9x_configure,
	.enable_callback = gt9x_enable_callback,
	.disable_callback = gt9x_disable_callback,
};

//初始化驱动的硬件信息,这些信息是DTS根据绑定文件产生,DTS的写法后文会介绍
static const struct gt9x_config gt9x_config = {
	.i2c_name = DT_INST_0_COODIX_GT9X_BUS_NAME,
	.i2c_address = DT_INST_0_COODIX_GT9X_BASE_ADDRESS,
	.int_port = DT_INST_0_COODIX_GT9X_INT_GPIOS_CONTROLLER,
	.int_pin = DT_INST_0_COODIX_GT9X_INT_GPIOS_PIN,
	.int_flags = DT_INST_0_COODIX_GT9X_INT_GPIOS_FLAGS,
	.rst_port = DT_INST_0_COODIX_GT9X_RESET_GPIOS_CONTROLLER,
	.rst_pin = DT_INST_0_COODIX_GT9X_RESET_GPIOS_PIN,
	.rst_flags = DT_INST_0_COODIX_GT9X_RESET_GPIOS_FLAGS,
};

static struct gt9x_data gt9x_data;

//注册驱动,注册后zephyr在驱动初始化阶段会自动调用gt9x_init对驱动进行初始化,注册后通过DT_INST_0_COODIX_GT9X_LABEL获得gt917s的handle,呼叫kscan.h中的API就会调用到gt9x_driver_api内对应的API
DEVICE_AND_API_INIT(GT9X, DT_INST_0_COODIX_GT9X_LABEL, gt9x_init,
		    &gt9x_data, &gt9x_config,
		    POST_KERNEL, CONFIG_KSCAN_INIT_PRIORITY,
		    &gt9x_driver_api);

KSCAN驱动使用 链接到标题

Kscan使用比较简单,配置好后,注册callback然后enable callback就可以了坐等callback接受按键了

配置 链接到标题

配置分为2部分,一是在dts中指定kscan的硬件信息,二是在prj.conf中启动配置使用kscan

dts 链接到标题

以我使用的mm_swiftio为例,在mm_swiftio.dts中添加

&i2c1 {                 //gt917s挂在i2c1下
	status = "okay";
	
	gt9x@14 {
		compatible = "coodix,gt9x";     
		reg = <0x14>;       //gt917s的i2c地址为0x14
		label = "GT9X";     //gt917s的device name为GT9X
		width = <800>;      //触摸屏宽800
		height = <480>;     //触摸屏宽480
		int-gpios = <&gpio1 19 GPIO_ACTIVE_HIGH>;   //中断使用gpio1.19
		reset-gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>;  //复位使用gpio1.2
	};
};

dts配置的信息会被zephyr的构建脚本转化为gt9x_config内使用的宏。

prj.conf 链接到标题

使用下面配置选项启用GT917S的KSCAN驱动

CONFIG_KSCAN=y                  #enable kscan
CONFIG_KSCAN_GT9X=y             #使用GT917S驱动
CONFIG_KSCAN_INIT_PRIORITY=55   #驱动初始化优先级
CONFIG_KSCAN_GT9X_PERIOD=10     #轮询GT917S的时间间隔,单位为ms
#CONFIG_KSCAN_GT9X_INTERRUPT=y  #配置该项后将只用中断通知接收按键,不再进行轮询

注意:CONFIG_KSCAN_INIT_PRIORITY不配置时默认是40,由于Kscan使用了I2C驱动,I2C驱动使用的是CONFIG_KERNEL_INIT_PRIORITY_DEVICE(50)来作为初始化优先级, 为了避免kscan使用I2C时无效,需要保证I2C驱动先初始化,初始化优先级的数字越大优先级越小。因此要配置CONFIG_KSCAN_INIT_PRIORITY>CONFIG_KERNEL_INIT_PRIORITY_DEVICE.

参考 链接到标题

https://docs.zephyrproject.org/latest/reference/peripherals/kscan.html