之前已经分析过了编写一个驱动程序,主要有以下几个步骤:
- 自己设定或由系统自动分配驱动设备的主设备号;
- 编写设备操作函数(drv_open、drv_read、drv_write、drv_close等);
- 构造file_operations结构体,将上一步编写的操作函数赋值给结构体内的
.open、.read、.write、.poll、.fasync
等 - 注册驱动程序,调用
register_chrdev(major, name, fops);
- 编写入口函数和出口函数。
但输入子系统驱动框架将以上步骤分开了,它是由设备层、核心层、事件层共同组成的。其中核心层提供一些设备层与事件层公用的函数,比如说注册函数、反注册函数、事件到来的处理函数等等,完成了驱动程序编写的第1-4步。但在input框架的file_operations中,只含有一个open函数,正是通过它调用特定input_handler结构体,其里面包含有根据不同次设备号映射到不同的输入设备大类的**.fops**;事件处理层其实在Linux内核里面已经帮我们写好了很多有关的事件;而设备层就是我们要新添加到输入系统的具体设备相关的程序了。
在分析输入子系统框架时,我们已经知道内核对不同类型的输入设备已经抽象出了不同的handler进行处理,device层实现纯硬件的操作,我们可以根据所实现驱动的功能对device层进行设计,主要是内容是当检测有效输入发送,调用input_event函数向handler层上报结果即可。
1、编写符合输入子系统框架的驱动程序步骤
- 分配一个
input_dev
结构体,调用input_allocate_device()
或者devm_input_allocate_device(struct input_dev*)
实现; - 编写函数,设置input_dev结构体,选择input设备的事件类型(调用
set_bit()
函数实现); - 注册input_dev结构体,调用
input_register_device(struct input_dev*)
函数实现; - 编写硬件相关代码:注册中断处理函数,比如键盘设备需要编写按键的抬起、放下,触摸屏设备需要编写按下、抬起、绝对移动,鼠标设备需要编写单击、抬起、相对移动,并且需要在必要的时候提交硬件数据(键值/坐标/状态等等),即上报输入事件。
- 当提交输入设备产生的输入事件之后,需要调用
void input_sync(struct input_dev *dev)
函数来通知输入子系统,以处理设备产生的完整事件。
Linux输入子系统支持的事件类型:(在include/linux/input.h中)
EV_SYN 0x00 同步事件 EV_KEY 0x01 按键事件 EV_REL 0x02 相对坐标(如:鼠标移动,报告相对最后一次位置的偏移) EV_ABS 0x03 绝对坐标(如:触摸屏或操作杆,报告绝对的坐标位置) EV_MSC 0x04 其它 EV_SW 0x05 开关 EV_LED 0x11 按键/设备灯 EV_SND 0x12 声音/警报 EV_REP 0x14 重复 EV_FF 0x15 力反馈 EV_PWR 0x16 电源 EV_FF_STATUS 0x17 力反馈状态 EV_MAX 0x1f 事件类型最大个数和提供位掩码支持
Linux输入子系统提供的设备驱动层上报输入事件的函数:(在include/linux/input.h中)
void input_event(struct input_dev* dev, unsigned int type, unsigned int code, int value); //上报指定的type、code的输入事件 void input_report_key(struct input_dev *dev, unsigned int code, int value); //上报按键事件 void input_report_rel(struct input_dev *dev, unsigned int code, int value); //上报相对坐标事件 void input_report_abs(struct input_dev *dev, unsigned int code, int value); //上报绝对坐标事件
2、编写符合input系统框架的按键驱动程序
现在我们开始使用内核的输入子系统框架,将自己编写驱动程序融入其中,编写更通用的按键驱动程序。这里以JZ2440开发板上的4个按键作为输入子系统的按键,我们编写的驱动程序实现将开发板上的4个按键分别映射成键盘上不同键值,代表键盘上的字母L、S和Enter(回车)。这几个值是在include\linux\input.h中被定义的。
2.1 编写入口函数、出口函数,搭建好框架
2.1.1 分配一个input_dev结构体
2.1.2 设置input_dev结构体
先看以下Input_dev结构体的定义:
- evbit:用来设置该设备能产生哪类事件类型(在第1节有具体描述输入子系统所支持的所有事件类型),在此我们选择按键事件EV_KEY和EV_REP,代码如下:
- keybit:用来设置该设备能产生哪些按键值,在此我们设置本节开头说的键盘上的L、S、左shift、enter(为了可以在开发板上执行ls命令)。按键码定义在include\linux\input.h中,截取其中一小部分:
设置输入事件类型的哪一种按键的代码:
- relbit:表示能产生哪些相对位移事件, 例如滚轮
- absbit:表示能产生哪些绝对位移事件。
2.1.3 注册input_dev结构体
注册的功能其实就是将当前设备buttons_dev放到input_dev链表中去,然后和事件层的evdev_handler逐个比较(通过比较id和id.table[]),如果匹配,则会调用evdev_handler中的.connect函数,产生一个handle结构体(只含有.handler和.dev,一个指向设备,一个指向handler),将当前设备与handler建立起关联,将当前dev及其匹配的handler放到各自的.h_list中。代码如下:
2.1.4 硬件相关的操作
- 定义各按键描述结构体
- 初始化定时器(防抖动)
- 为每个按键注册中断
- 所以整体buttons_init函数的代码如下:
- 编写出口函数
2.2 编写按键的中断处理函数
2.3 编写定时器超时处理函数
当有按键数据产生时,调用input_event函数来上报事件,该函数会从input_dev链表中的.h_list找到对应的.handler,并调用里面的.event函数。
2.4 整体代码buttons.c
3、测试
3.1 利用hexdump
测试
3.2 如果当前没有启动QT,则按如下方法:
- 执行
cat /dev/tty1
- 开发板上按下:S2、S3、S4
- 可以看到:ls
或者
- 执行
exec 0</dev/tty1
- 可以使用开发板上的键盘来输入ls命令
3.3 如果已经启动了QT
- 先打开记事本
- 然后按键S2、S3、S4
- 可以看到记事本上显示ls
4、小结
- 打开输入设备后发生了什么?
- drivers/input/input.c:
- input_init > err = register_chrdev(INPUT_MAJOR, “input”, &input_fops);
- static const struct file_operations input_fops = {
.owner = THIS_MODULE,
.open = input_open_file,
};
- 怎么读按键?
- intput_open_file
- struct input_handler *handler = input_table[iminor(inode) >> 5];
- new_fops = fops_get(handler->fops) // =>&evdev_fops
- file->f_op = new_fops;
- err = new_fops->open(inode, file);
- APP: read > … >file->f_op->read
- input_table数组由谁构造?
- input_register_handler
- 如何注册input_handler?
- input_register_handler
- input_table[handler->minor >> 5] = handler; // 放入数组
- list_add_tail(&handler->node, &input_handler_list); // 放入链表
- // 对于每个input_dev,调用input_attach_handler
- list_for_each_entry(dev, &input_dev_list, node)
- input_attach_handler(dev, handler); // 根据input_handler的id_table判断能否支持这个input_dev
- 如何 注册输入设备:
- input_register_device
- list_add_tail(&dev->node, &input_dev_list); // 放入链表
- // 对于每一个input_handler,都调用input_attach_handler
list_for_each_entry(handler, &input_handler_list, node)
- input_attach_handler(dev, handler); // 根据input_handler的id_table判断能否支持这个input_dev
input_attach_handler
- id = input_match_device(handler->id_table, dev);
- error = handler->connect(handler, dev, id);
注册input_dev或input_handler时,会两两比较左边的input_dev和右边的input_handler,
根据input_handler的id_table判断这个input_handler能否支持这个input_dev,
如果能支持,则调用input_handler的connect函数建立"连接"
- 怎么建立连接?
- 分配一个input_handle结构体
- input_handle.dev = input_dev; // 指向左边的input_dev
input_handle.handler = input_handler; // 指向右边的input_handler - 注册:
input_handler->h_list = &input_handle;
inpu_dev->h_list = &input_handle;
evdev_connect
- evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL); // 分配一个input_handle
- // 设置
evdev->handle.dev = dev; // 指向左边的input_dev
evdev->handle.name = evdev->name;
evdev->handle.handler = handler; // 指向右边的input_handler
evdev->handle.private = evdev; - // 注册
error = input_register_handle(&evdev->handle);
- 怎么读按键?
- APP应用程序执行: read
- 。。。。
- evdev_read
- // 无数据并且是非阻塞方式打开,则立刻返回
if (client->head == client->tail && evdev->exist && (file->f_flags & O_NONBLOCK))
return -EAGAIN;
// 否则休眠
retval = wait_event_interruptible(evdev->wait,
client->head != client->tail || !evdev->exist);
- 谁来唤醒?
evdev_event
wake_up_interruptible(&evdev->wait); - evdev_event被谁调用?
猜:应该是硬件相关的代码,input_dev那层调用的
在设备的中断服务程序里,确定事件是什么,然后调用相应的input_handler的event处理函数
gpio_keys_isr
- // 上报事件
input_event(input, type, button->code, !!state);
input_sync(input); - input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value)
- struct input_handle *handle;
- list_for_each_entry(handle, &dev->h_list, d_node)
if (handle->open)
handle->handler->event(handle, type, code, value);