翻译并不是一字一句的翻译,或者有的专业术语还会保持原样,尽可能准确的表达原文的意思。水平有限,敬请谅解。
第7章:Input Drivers
内核的输入子系统的创建是为了统一分散的驱动程序处理的不同的类输入设备的,如键盘,鼠标,轨迹球,操纵杆,滚轮,触摸屏,加速度计,和平板电脑。输入子系统带来以下优点:
1.统一处理物理上不同但是功能上类似的输入设备,所有的鼠标,PS/2,USB,蓝牙,都被类似的对待;
2.一个简单的事件接口用来上报输入的数据到用户空间中,你的驱动不需要创建/dev下的节点以及访问的方法,相反,可以简单的调用input 的api函数来上报鼠标的移动,按键的按下,触摸事件到用户空间,应用如x Windows 可以无缝的工作在输入子系统导出的事件接口。
3.提取了输入驱动的相同点产生了一个抽象的概念使得变得一致,例如,输入子系统提供了一个收集被称作serio的低阶驱动,例如串口,按键控制器。
图片7.1阐明了输入子系统是如何运作的,输入子系统有两类的驱动工作,event(事件) 驱动以及device(设备)驱动,event驱动是面向应用层,然而,device 驱动是面向低阶输入设备。鼠标事件发生器即mousedev是前者的例子,而PS/2驱动是后者,这两个事件的驱动程序和设备驱动程序可以利用一种有效的,无缺陷,可重复使用的核心,对于输入子系统来讲就是心脏。
Figure 7.1. The input subsystem.
因为所有的event 驱动是通用的,所以一般是是实现device 驱动而不是event驱动,你的device 驱动可以选择一个合适的事件驱动通过输入子系统核心与用户空间建立联系。
Input Event Drivers
输入子系统导出的接口可以被许多图形界面系统所认识,事件驱动程序提供了一个独立于硬件的抽象去跟输入设备交流,类似于frame buffer的接口代表着一种机制与显示设备通信,event驱动和frame buffer驱动使得用户图形的接口与实际的硬件多样的变化分离出来了。The Evdev Interface
struct input_event { struct timeval time; /* Timestamp */ __u16 type; /* Event Type */ __u16 code; /* Event Code */ __s32 value; /* Event Value */ };为了学习如何使用evdev,让我们一起实现一个虚拟的mouce 的device 驱动。
Device Example: Virtual Mouse
Virtual Mouse 的驱动是这样工作的,应用程序(coord.c)仿真鼠标移动并且分发坐标信息给virtual mouse的驱动程序(vms.c)通过sysfs节点,/sys/devices/platform/vms/coordinates,virtual mouse的驱动输送这些数据流通过evdev,图7.2是详细说明
Listing 7.1. Application to Simulate Mouse Movements (coord.c)
#include <fcntl.h> int main(int argc, char *argv[]) { int sim_fd; int x, y; char buffer[10]; /* Open the sysfs coordinate node */ sim_fd = open("/sys/devices/platform/vms/coordinates", O_RDWR); if (sim_fd < 0) { perror("Couldn't open vms coordinate file\n"); exit(-1); } while (1) { /* Generate random relative coordinates */ x = random()%20; y = random()%20; if (x%2) x = -x; if (y%2) y = -y; /* Convey simulated coordinates to the virtual mouse driver */ sprintf(buffer, "%d %d %d", x, y, 0); write(sim_fd, buffer, strlen(buffer)); fsync(sim_fd); sleep(1); } close(sim_fd); }
Listing 7.2. Input Driver for the Virtual Mouse (vms.c)
#include <linux/fs.h> #include <asm/uaccess.h> #include <linux/pci.h> #include <linux/input.h> #include <linux/platform_device.h> struct input_dev *vms_input_dev; /* Representation of an input device */ static struct platform_device *vms_dev; /* Device structure */ /* Sysfs method to input simulated coordinates to the virtual mouse driver */ static ssize_t write_vms(struct device *dev, struct device_attribute *attr, const char *buffer, size_t count) { int x,y; sscanf(buffer, "%d%d", &x, &y); /* Report relative coordinates via the event interface */ input_report_rel(vms_input_dev, REL_X, x); input_report_rel(vms_input_dev, REL_Y, y); input_sync(vms_input_dev); return count; } /* Attach the sysfs write method */ DEVICE_ATTR(coordinates, 0644, NULL, write_vms); /* Attribute Descriptor */ static struct attribute *vms_attrs[] = { &dev_attr_coordinates.attr, NULL }; /* Attribute group */ static struct attribute_group vms_attr_group = { .attrs = vms_attrs, }; /* Driver Initialization */ int __init vms_init(void) { /* Register a platform device */ vms_dev = platform_device_register_simple("vms", -1, NULL, 0); if (IS_ERR(vms_dev)) { PTR_ERR(vms_dev); printk("vms_init: error\n"); } /* Create a sysfs node to read simulated coordinates */ sysfs_create_group(&vms_dev->dev.kobj, &vms_attr_group); /* Allocate an input device data structure */ vms_input_dev = input_allocate_device(); if (!vms_input_dev) { printk("Bad input_alloc_device()\n"); } /* Announce that the virtual mouse will generate relative coordinates */ set_bit(EV_REL, vms_input_dev->evbit); set_bit(REL_X, vms_input_dev->relbit); set_bit(REL_Y, vms_input_dev->relbit); /* Register with the input subsystem */ input_register_device(vms_input_dev); printk("Virtual Mouse Driver Initialized.\n"); return 0; } /* Driver Exit */ void vms_cleanup(void) { /* Unregister from the input subsystem */ input_unregister_device(vms_input_dev); /* Cleanup sysfs node */ sysfs_remove_group(&vms_dev->dev.kobj, &vms_attr_group); /* Unregister driver */ platform_device_unregister(vms_dev); return; } module_init(vms_init); module_exit(vms_cleanup);让我们走近去看一下listing 7.2,初始化过程中,vms的驱动注册自己为input设备驱动,首先申请了一个input_dev 的结构体使用api函数 input_allocate_device():
然后宣布虚拟鼠标生成相对的事件
set_bit(EV_REL, vms_input_dev->evbit); /* Event Type is EV_REL */接下来,它声明虚拟鼠标产生的事件编码
set_bit(REL_X, vms_input_dev->relbit); /* Relative 'X' movement */ set_bit(REL_Y, vms_input_dev->relbit); /* Relative 'Y' movement */如果你的虚拟鼠标也能生成按钮点击,您需要添加这个vms_init():
set_bit(EV_KEY, vms_input_dev->evbit); /* Event Type is EV_KEY */ set_bit(BTN_0, vms_input_dev->keybit); /* Event Code is BTN_0 */最后,注册:
input_register_device(vms_input_dev);write_vms()是sysfs的store()方法连接到/sys/devices/platform/vms/coordinates,当coord.c写入一个X / Y到这个文件,write_vms()做以下事情:
input_report_rel(vms_input_dev, REL_X, x); input_report_rel(vms_input_dev, REL_Y, y); input_sync(vms_input_dev);第一句产生一个 REL_X的事件相对于X方向上的,第二句产生一个REL_的事件相对于Y方向上的,input_sync()表明事件完成,因此input子系统收集到两个事件之后转化为简单的evdev的数据包发送到/dev/input/eventX,X是分配给vms_driver的接口编号,应用读取这个文件时会收到event的数据包以input_event格式 ,看到光标在屏幕上能够随着上报的值动:
bash> gpm -m /dev/input/eventX -t evdev
ADS7846触摸控制器驱动和加速度计的驱动,也是使用evdev的。
More Event Interfaces
static struct input_handler my_event_handler = { .event = mydev_event, /* Handle event reports sent by input device drivers that use this event driver's services */ .fops = &mydev_fops, /* Methods to manage /dev/input/mydev */ .minor = MYDEV_MINOR_BASE, /* Minor number of /dev/input/mydev */ .name = "mydev", /* Event driver name */ .id_table = mydev_ids, /* This event driver can handle requests from these IDs */ .connect = mydev_connect, /* Invoked if there is an ID match */ .disconnect = mydev_disconnect, /* Called when the driver unregisters */ }; /* Driver Initialization */ static int __init mydev_init(void) { /* ... */ input_register_handler(&my_event_handler); /* ... */ return 0; }看实现mousedev( drivers/input/mousedev.c)一个完整的示例
Input Device Drivers
Serio
serio层提供了库程序去访问老旧的输入设备例如i8042兼容的键盘控制器以及串口,PS/2键盘和鼠标是前者,而串行的触摸控制器连接的是后者,为了与serio提供的服务的硬件通信,例如发送命令给一个PS/2的鼠标,注册规定的程序使用serio的函数 serio_register_driver().Keyboards
键盘是来自PS/2,USB,Bluetooth, Infrared(红外)的产物,每一种类型都有各自特点的驱动程序,但是使用相同的键盘事件驱动,保证对于他们的使用者拥有一致的接口,按键事件驱动,仍然有与其他的事件驱动不同的地方,它传递数据到另一个内核子系统(tty layer)而不是通过/dev 节点。PC Keyboards
PC键盘使用i8042兼容的控制器,桌面电脑一般有专有的控制器,但是笔记本仅仅是通用意义上的嵌入式控制器,当你的键按下是,会反生以下的事情:-
bash> showkey -s kb mode was UNICODE [ if you are trying this under X, it might not work since the X server is also reading /dev/console ] press any key (program terminates 10s after last keypress)... ... 0x1e 0x9e
A push of the "a" key
-
bash> showkey ... keycode 30 press
A push of the "a" key keycode 30 release
Release of the "a" key
static void fn_boot_it(struct vc_data *vc, struct pt_regs *regs) { + set_vc_kbd_led(kbd, VC_CAPSLOCK); + set_vc_kbd_led(kbd, VC_NUMLOCK); - ctrl_alt_del(); }5.对于通用的按键,导出的键值发送到相关的虚拟终端上和N_TTY线路规程上,这个由drivers/char/keyboard.c完成
/* Add the keycode to flip buffer */ tty_insert_flip_char(tty, keycode, 0); /* Schedule */ con_schedule_flip(tty);N_TTY线路规程处理从键盘输入来的数据,与虚拟控制台呼应,使用户空间应用程序读取字符通过连接到虚拟终端上的 /dev/ttyX 节点。
USB and Bluetooth Keyboards
USB规范相关人机接口设备规定了用来通信的USB键盘,鼠标,其他的输入外设的协议。在linux中,这个通过usbhid client 驱动来实现,这个驱动对USB HID 类(0x03)负责的,Usbhid的寄存器自己作为了输入设备驱动,其符合输入子系统的api,上报连接在HID合适的事件。
为了理解USB键盘的代码路径,查看7.3改变左半部分的硬件相关的,替换在Input Hardware方框键盘控制器为USB控制器,输入设备用usbhid驱动替换。
对于蓝牙键盘来说,替换图7.3中的键盘控制器为蓝牙芯片组,serio使用蓝牙核心层户,输入设备使用蓝牙hidp驱动替换。
USB和蓝牙在11章中讨论。
Mice
鼠标,类似于键盘,有不同的功能以及有不同的接口选项,让我们看看常见的。PS/2 Mice
鼠标产生相对于x,y轴的相对坐标,有一个或者多个按键,一些有滚轮,输入设备驱动依赖于serio层与底层硬件通信,输入事件针对于鼠标是 mousedev,通过via/dev/input/mice向用户层传递消息。Device Example: Roller Mouse
感受一个真实的鼠标设备驱动程序,让我们回过来看第4章讨论的滚轮,“奠定基础,”到通用PS / 2鼠标的变化。该“鼠标滚轮”,在Y轴产生一维运动。顺时针和逆时针分别产生正,负相对Y坐标,同时按下滚轮导致左键鼠标事件。鼠标滚轮因而非常适用于诸如智能手机,掌上电脑和音乐播放器等设备浏览菜单。Listing 7.3. The Roller Mouse Driver
Code View:
+ #include <linux/input.h> + #include <linux/interrupt.h> + /* Device structure */ + struct { + /* ... */ + struct input_dev dev; + } roller_mouse; + static int __init + roller_mouse_init(void) + { + /* Allocate input device structure */ + roller_mouse->dev = input_allocate_device(); + + /* Can generate a click and a relative movement */ + roller_mouse->dev->evbit[0] = BIT(EV_KEY) | BIT(EV_REL); + /* Can move only in the Y-axis */ + roller_mouse->dev->relbit[0] = BIT(REL_Y); + + /* My click should be construed as the left button + press of a mouse */ + roller_mouse->dev->keybit[LONG(BTN_MOUSE)] = BIT(BTN_LEFT); + roller_mouse->dev->name = "roll"; + + /* For entries in /sys/class/input/inputX/id/ */ + roller_mouse->dev->id.bustype = ROLLER_BUS; + roller_mouse->dev->id.vendor = ROLLER_VENDOR; + roller_mouse->dev->id.product = ROLLER_PROD; + roller_mouse->dev->id.version = ROLLER_VER; + /* Register with the input subsystem */ + input_register_device(roller_mouse->dev); +} /* Global variables */ - spinlock_t roller_lock = SPIN_LOCK_UNLOCKED; - static DECLARE_WAIT_QUEUE_HEAD(roller_poll); /* The Roller Interrupt Handler */ static irqreturn_t roller_interrupt(int irq, void *dev_id) { int i, PA_t, PA_delta_t, movement = 0; /* Get the waveforms from bits 0, 1 and 2 of Port D as shown in Figure 7.1 */ PA_t = PORTD & 0x07; /* Wait until the state of the pins change. (Add some timeout to the loop) */ for (i=0; (PA_t==PA_delta_t); i++){ PA_delta_t = PORTD & 0x07; } movement = determine_movement(PA_t, PA_delta_t); - spin_lock(&roller_lock); - - /* Store the wheel movement in a buffer for - later access by the read()/poll() entry points */ - store_movements(movement); - - spin_unlock(&roller_lock); - - /* Wake up the poll entry point that might have - gone to sleep, waiting for a wheel movement */ - wake_up_interruptible(&roller_poll); - + if (movement == CLOCKWISE) { + input_report_rel(roller_mouse->dev, REL_Y, 1); + } else if (movement == ANTICLOCKWISE) { + input_report_rel(roller_mouse->dev, REL_Y, -1); + } else if (movement == KEYPRESSED) { + input_report_key(roller_mouse->dev, BTN_LEFT, 1); + } + input_sync(roller_mouse->dev); return IRQ_HANDLED; } |
Trackpoints
一个指点杆是自带的PS/2式键盘集成在一些笔记本电脑的定位设备。该装置包括位于定位在所述空格键下键和鼠标按钮之间的操纵杆。一个指点杆基本上可用作鼠标,让您可以使用PS/2鼠标驱动程序操作。Touchpads
触摸板是一个鼠标式定点设备上的笔记本电脑中常见的。不同于传统的鼠标,触摸板不具有移动部件。它可以产生鼠标兼容相对坐标,但通常在产生绝对坐标一个更强大的模式使用的操作系统。在绝对模式中使用的通信协议是类似于PS / 2鼠标协议,但与它不兼容。-
PS/2 鼠标驱动,drivers/input/mouse/psmouse-base.c,实例化一个psmouse_protocol结构体,信息包括鼠标协议(触摸板协议也包括)
-
psmouse结构体,定义在drivers/input/mouse/psmouse.h中,将PS/2的协议都绑定到一起。
-
synaptics_init()填充psmouse结构的相关协议功能的地址。
-
协议处理函数synaptics_process_byte(),填充synaptics_init(),被从中断上下文调用时,Serio的感应鼠标移动。如果你展开synaptics_process_byte(),你会看到触控板走势,报经mousedev用户应用程序。
Listing 7.4. PS/2 Mouse Protocol Driver for the Synaptics Touchpad
drivers/input/mouse/psmouse-base.c: /* List of supported PS/2 mouse protocols */ static struct psmouse_protocol psmouse_protocols[] = { { .type = PSMOUSE_PS2, /* The bare PS/2 handler */ .name = "PS/2", .alias = "bare", .maxproto = 1, .detect = ps2bare_detect, }, /* ... */ { .type = PSMOUSE_SYNAPTICS, /* Synaptics TouchPad Protocol */ .name = "SynPS/2", .alias = "synaptics", .detect = synaptics_detect, /* Is the protocol detected? */ .init = synaptics_init, /* Initialize Protocol Handler */ }, /* ... */ } drivers/input/mouse/psmouse.h: /* The structure that ties various mouse protocols together */ struct psmouse { struct input_dev *dev; /* The input device */ /* ... */ /* Protocol Methods */ psmouse_ret_t (*protocol_handler) (struct psmouse *psmouse, struct pt_regs *regs); void (*set_rate)(struct psmouse *psmouse, unsigned int rate); void (*set_resolution) (struct psmouse *psmouse, unsigned int resolution); int (*reconnect)(struct psmouse *psmouse); void (*disconnect)(struct psmouse *psmouse); /* ... */ }; drivers/input/mouse/synaptics.c: /* init() method of the Synaptics protocol */ int synaptics_init(struct psmouse *psmouse) { struct synaptics_data *priv; psmouse->private = priv = kmalloc(sizeof(struct synaptics_data), GFP_KERNEL); /* ... */ /* This is called in interrupt context when mouse movement is sensed */ psmouse->protocol_handler = synaptics_process_byte; /* More protocol methods */ psmouse->set_rate = synaptics_set_rate; psmouse->disconnect = synaptics_disconnect; psmouse->reconnect = synaptics_reconnect; /* ... */ } drivers/input/mouse/synaptics.c: /* If you unfold synaptics_process_byte() and look at synaptics_process_packet(), you can see the input events being reported to user applications via mousedev */ static void synaptics_process_packet(struct psmouse *psmouse) { /* ... */ if (hw.z > 0) { /* Absolute X Coordinate */ input_report_abs(dev, ABS_X, hw.x); /* Absolute Y Coordinate */ input_report_abs(dev, ABS_Y, YMAX_NOMINAL + YMIN_NOMINAL - hw.y); } /* Absolute Z Coordinate */ input_report_abs(dev, ABS_PRESSURE, hw.z); /* ... */ /* Left TouchPad button */ input_report_key(dev, BTN_LEFT, hw.left); /* Right TouchPad button */ input_report_key(dev, BTN_RIGHT, hw.right); /* ... */ }
USB and Bluetooth Mice
USB鼠标由相同的输入驱动器(USBHID)驱动USB键盘处理。同样,实现蓝牙键盘支持HIDP驱动程序还需要兼容蓝牙鼠标。Touch Controllers
在第6章中,我们实现了一个针对于串行控制器设备驱动程序以被称为N_TCH线路规程形式实现。输入子系统提供了更简单的方式实现了这个程序,重塑有限状态机N_TCH作为输入设备驱动程序需要改变以下:Accelerometers
bash> od –x /dev/input/event3
0000000 a94d 4599 1f19 0007 0003 0000 ffed ffff
...
Output Events
一些输入设备驱动还处理输出事件,键盘的驱动还可以唤醒 CAPSLOCK的led灯,以及PC上的喇叭,仔细看一下后者,在初始化期间,喇叭的驱动声明了它的输出使能通过设置适当的evbits以及注册了一个回调方法去处理输出的事件:drivers/input/misc/pcspkr.c: static int __devinit pcspkr_probe(struct platform_device *dev) { /* ... */ /* Capability Bits */ pcspkr_dev->evbit[0] = BIT(EV_SND); pcspkr_dev->sndbit[0] = BIT(SND_BELL) | BIT(SND_TONE); /* The Callback routine */ pcspkr_dev->event = pcspkr_event; err = input_register_device(pcspkr_dev); /* ... */ } /* The callback routine */ static int pcspkr_event(struct input_dev *dev, unsigned int type, unsigned int code, int value) { /* ... */ /* I/O programming to sound a beep */ outb_p(inb_p(0x61) | 3, 0x61); /* set command for counter 2, 2 byte write */ outb_p(0xB6, 0x43); /* select desired HZ */ outb_p(count & 0xff, 0x42); outb((count >> 8) & 0xff, 0x42); /* ... */ }使喇叭响,键盘的事件的驱动产生一个声响事件(EV_SND)如下:
input_event(handle->dev, EV_SND, /* Type */ SND_TONE, /* Code */ hz /* Value */);
这个触发执行回调方法, pcspkr_event(),你将会听到喇叭响。
Debugging
你可以使用evbug模块作为调试诊断,会打印出( type, code, value)对应的输入子系统产生的事件的记录。Figure 7.4. Evbug output.
Code View:
/* Touchpad Movement */ evbug.c Event. Dev: isa0060/serio1/input0: Type: 3, Code: 28, Value: 0 evbug.c Event. Dev: isa0060/serio1/input0: Type: 1, Code: 325, Value: 0 evbug.c Event. Dev: isa0060/serio1/input0: Type: 0, Code: 0, Value: 0 /* Trackpoint Movement */ evbug.c Event. Dev: synaptics-pt/serio0/input0: Type: 2, Code: 0, Value: -1 evbug.c Event. Dev: synaptics-pt/serio0/input0: Type: 2, Code: 1, Value: -2 evbug.c Event. Dev: synaptics-pt/serio0/input0: Type: 0, Code: 0, Value: 0 /* USB Mouse Movement */ evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 2, Code: 1, Value: -1 evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 0, Code: 0, Value: 0 evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 2, Code: 0, Value: 1 evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 0, Code: 0, Value: 0 /* PS/2 Keyboard keypress 'a' */ evbug.c Event. Dev: isa0060/serio0/input0: Type: 4, Code: 4, Value: 30 evbug.c Event. Dev: isa0060/serio0/input0: Type: 1, Code: 30, Value: 0 evbug.c Event. Dev: isa0060/serio0/input0: Type: 0, Code: 0, Value: 0 /* USB keyboard keypress 'a' */ evbug.c Event. Dev: usb-0000:00:1d.1-1/input0: Type: 1, Code: 30, Value: 1 evbug.c Event. Dev: usb-0000:00:1d.1-1/input0: Type: 0, Code: 0, Value: 0 evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 1, Code: 30, Value: 0 evbug.c Event. Dev: usb-0000:00:1d.1-2/input0: Type: 0, Code: 0, Value: 0 |
input_event(dev, EV_SYN, SYN_REPORT, 0);翻译为( type, code, value)是( 0x0, 0x0, 0x0)完成每个输入事件。
Looking at the Sources
大部分的驱动是在 drivers/input/的目录中,而键盘事件驱动在drivers/char/keyboard.c,因为它连接到了虚拟终端而且不在设备节点/dev/input/下。Data Structure | Location | Description |
---|---|---|
input_event | include/linux/input.h | Each event packet produced by evdev has this format. |
input_dev | include/linux/input.h | Representation of an input device. |
input_handler | include/linux/serial_core.h | Contains the entry points supported by an event driver. |
psmouse_protocol | drivers/input/mouse/psmouse-base.c | Information about a supported PS/2 mouse protocol driver. |
psmouse | drivers/input/mouse/psmouse.h | Methods supported by a PS/2 mouse driver. |