2.4 mouse_handler 的实现
在内核的 driver/input/mousedev.c 中内核已经实现了一个专门处理鼠标事件的 mousedev_handler 。这个 handler 占用的次设备号是 32~63 。虽然有 32 个次设备号可用,但是只有前 31 个可被用户所用,系统保留了最后一个,所以最多连接 31 个鼠标设备。
mousedev_handler 对应 dev/input/mouseX(X 是数字 ) 和 dev/input/mice 。其中前者是具体设备的对应节点, mice 是所有鼠标共享的,次设备号 63 就是被 mice 占用的。每个鼠标设备的事件都可以从 mice 和各自的设备节点读到。没有特殊原因,用户应该总是打开 mice ,因为即使没有的鼠标设备也不会打开失败,这对于热拔插的 USB 鼠标很有用。
2.4.1 支持的设备种类
mousedev_handler 支持多种类鼠标设备,它的匹配规则如 程序清单 2 .7 所示。
程序清单 2 . 7 mousedev_ids
/* driver/input/mousedev.c */
static const struct input_device_id mousedev_ids[] = {
{
.flags = INPUT_DEVICE_ID_MATCH_EVBIT | ⑴
INPUT_DEVICE_ID_MATCH_KEYBIT |
INPUT_DEVICE_ID_MATCH_RELBIT,
.evbit = { BIT_MASK(EV_KEY) | BIT_MASK(EV_REL) },
.keybit = { [BIT_WORD(BTN_LEFT)] = BIT_MASK(BTN_LEFT) },
.relbit = { BIT_MASK(REL_X) | BIT_MASK(REL_Y) },
}, /* A mouse like device, at least one button,
two relative axes */
{
.flags = INPUT_DEVICE_ID_MATCH_EVBIT | ⑵
INPUT_DEVICE_ID_MATCH_RELBIT,
.evbit = { BIT_MASK(EV_KEY) | BIT_MASK(EV_REL) },
.relbit = { BIT_MASK(REL_WHEEL) },
}, /* A separate scrollwheel */
{
.flags = INPUT_DEVICE_ID_MATCH_EVBIT | ⑶
INPUT_DEVICE_ID_MATCH_KEYBIT |
INPUT_DEVICE_ID_MATCH_ABSBIT,
.evbit = { BIT_MASK(EV_KEY) | BIT_MASK(EV_ABS) },
.keybit = { [BIT_WORD(BTN_TOUCH)] = BIT_MASK(BTN_TOUCH) },
.absbit = { BIT_MASK(ABS_X) | BIT_MASK(ABS_Y) },
}, /* A tablet like device, at least touch detection,
two absolute axes */
{
.flags = INPUT_DEVICE_ID_MATCH_EVBIT | ⑷
INPUT_DEVICE_ID_MATCH_KEYBIT |
INPUT_DEVICE_ID_MATCH_ABSBIT,
.evbit = { BIT_MASK(EV_KEY) | BIT_MASK(EV_ABS) },
.keybit = { [BIT_WORD(BTN_TOOL_FINGER)] =
BIT_MASK(BTN_TOOL_FINGER) },
.absbit = { BIT_MASK(ABS_X) | BIT_MASK(ABS_Y) |
BIT_MASK(ABS_PRESSURE) |
BIT_MASK(ABS_TOOL_WIDTH) },
}, /* A touchpad */
{
.flags = INPUT_DEVICE_ID_MATCH_EVBIT | ⑸
INPUT_DEVICE_ID_MATCH_KEYBIT |
INPUT_DEVICE_ID_MATCH_ABSBIT,
.evbit = { BIT_MASK(EV_KEY) | BIT_MASK(EV_ABS) },
.keybit = { [BIT_WORD(BTN_LEFT)] = BIT_MASK(BTN_LEFT) },
.absbit = { BIT_MASK(ABS_X) | BIT_MASK(ABS_Y) },
}, /* Mouse-like device with absolute X and Y but ordinary
clicks, like hp ILO2 High Performance mouse */
{ }, /* Terminating entry */
};
各种设备的特点如下:
⑴ 支持按键和相对坐标。可以匹配只有相对坐标和按键的设备。
⑵ 支持按键和滚轮。可以匹配拥有一个滚轮和有若干按键的设备。
⑶ 支持按键和绝对坐标。可以匹配的设备至少支持触按和两个绝对坐标,比如触摸板。
⑷ 支持按键和绝对坐标。可以匹配支持触摸、两个绝对坐标轴、按压和多点触摸的设备。
⑸ 支持按键和绝对坐标。可以匹配支持左键、两个绝对坐标轴的设备。
以上匹配规则只是能够匹配的设备的子集,能够支持几乎所有的类鼠标设备。 mousedev_handler 将事件分为两大类进行处理,一类是鼠标可以产生的,另一类就触摸板产生的(点击击触摸板和在触摸板上移动)。而从用户空间看来, mousedev_handler 的行为就是一只鼠标,无法分辨有没有触摸板。
2.4.2 鼠标的通信协议
用户程序跟 dev/input/mice 通信就仿佛同一只真正的 PS2 鼠标通信,读取到的数据包模拟了 ps2 鼠标的数据格式。当然, mousedev_handler 并没有实现所有的功能。
现在的鼠标往往支持不同的通信协议,分别是标准 PS2 、 Microsoft Intellimouse 和 Intellimouse Extensions ,其中后面两个分别是前一个的扩展。它们的数据包的格式也各不相同。
读取标准 PS2 鼠标得到的数据格式如 图 2 .1 所示。
图 2 . 1 标准 PS2 数据包格式
由上图可以看出,标准 PS2 鼠标仅仅支持三个按键和两个方向的坐标,不支持滚轮。 Microsoft Intellimouse 加入了对滚轮的支持,数据包变为 4 个字节。如 图 2 .2 所示。
图 2 . 2 Microsoft Intellimouse 数据包格式
但是现在的鼠标上往往有一些其他的按键可用,为了进一步拓展鼠标的功能,又发展出了 Intellimouse Extensions 。如 图 2 .3 所示。
图 2 . 3 Intellimouse Extensions 数据包格式
Intellimouse Extensions 可以支持的鼠标最多允许多达五个按键。
2.4.3 mousedev_handler 初始化
mousedev_handler 的初始化非常简单,如 程序清单 2 .8 所示。
程序清单 2 . 8 mousedev_handler 初始化函数
/* driver/input/mousedev.c */
static int __init mousedev_init(void)
{
int error;
mousedev_mix = mousedev_create(NULL, &mousedev_handler, MOUSEDEV_MIX); ⑴
if (IS_ERR(mousedev_mix))
return PTR_ERR(mousedev_mix);
error = input_register_handler(&mousedev_handler); ⑵
if (error) {
mousedev_destroy(mousedev_mix);
return error;
}
printk(KERN_INFO "mice: PS/2 mouse device common for all mice/n");
return 0;
}
这个函数做了两件事情:
⑴ 首先创建一个 mousedev 结构体并用全局指针 mousedev_mix 指向它。 mousedev_create 第一个参数为“ NULL ”意味着这个 mousedev 不和任何 input_dev 相关,它对应我们上文提到的 /dev/input/mice 。
⑵ 将 mousedev_handler 注册进输入子系统。
2.4.4 用户打开鼠标设备
用户空间可以打开鼠标设备的设备节点。
打开设备节点时,首先创建一个 mousedev_client 并添加到对应的 mousedev 的链表当中。 mousedev_client 中包含一个大小为 16 的环形 fifo 缓冲,缓冲可以存储 16 个同步时传入的数据包(这个数据包的格式不是 PS2 数据包,只能被 mousedev 识别),每个数据包只能被读一次,未能及时读取的数据包将被直接覆盖并不会有任何提示。同时 mousedev_client 还包含一个大小为 6 字节的数据缓冲 ps2[6] ,每次用户读取数据时,最旧的数据包被组织成合适的格式(参考 2.4.2 节的内容)存储在 ps2 中传给用户。
每个设备可以被多个线程同时打开,每个线程都能得到相同的数据。对同一个设备而言,每打开一次就会新建一个 client 。
2.4.5 用户写入 PS2 命令
鼠标的设备节点 mouseX 和 mice 支持用户空间写入符合 PS2 标准的命令。利用这些命令可以实现对鼠标的控制和信息的读取。
打开鼠标设备之后使用标准 PS2 协议,打开同一设备的不同线程之间的协议互不影响。如果想使用 Microsoft Intellimouse 协议进行通信,需要连续写入 { 0xf3, 200, 0xf3, 100, 0xf3, 80 } 这几个字节,任何时候写入都可以;同样写入 { 0xf3, 200, 0xf3, 200, 0xf3, 80 } 将会使用 Intellimouse Extensions 协议。用户读取的鼠标移动和按键的信息是通过以上三种协议的数据包格式传给用户空间的。
下面要介绍的命令,会首先填充 1 字节的 0xfa(ACK) 到 ps2[] 头部,接下来才存放有意义的数据到 ps2[] 。
写入 0xeb 将会使对应的 mousedev_client 的一个数据包的数据转化为对应的格式存储在该 client 的 ps2[] 中,第一个字节是 0xfa(ACK) 。
写入 0xf2 将会把当前协议对应的 ID 存储在 ps2[1] 当中,标准 PS2 对应 0 , Microsoft Intellimouse 协议对应 0x3 , Intellimouse Extensions 协议对应 0x4 。
写入 0xe9 将会紧挨着 0xfa(ACK) 存放三个字节的内容,包含缩放比例、分辨率、采样速率等信息(具体请参考 PS2 协议)。
写入 0xff 将会复位 client 的设置,返回标准 PS2 的通信模式。
上面介绍的命令的执行结果放在对应的 client 的缓冲之中,写入命令之后调用 read 函数就可以返回结果。需要强调的是如果写入上面没有提到的命令或无意义的数据,则仅仅能够读到 ACK 字节,用户无法通过往 mouseX 或者 mice 写入数据模拟鼠标移动,但是可以通过往鼠标设备对应的 eventX 节点中写入数据模拟鼠标移动。
2.4.6 用户读取鼠标设备的数据
用户空间打开一个鼠标设备节点之后,可以调用 read 函数读取数据。如果是非阻塞调用,没有数据时会阻塞,否则立即返回。读取一次,最多能够读取的数据长度是 client 中 ps2[] 拥有的有效数据长度,短于有效数据长度将会只读取前面的部分,剩下的数据作废。
如果之前写入了命令,那么紧接着调用 read 函数将会读取执行的结果,其中第一字节是 0xfa(ACK) 。如果之前没有写入命令,就会将 client 中现有的最旧的数据包转化为当前协议的格式返回给用户。读取长度长于一个数据包则只返回一个数据包的长度,短于数据包的长度则只返回靠前的部分。如果没有可读的数据包,则阻塞或者直接返回。
2.4.7 建立连接
当一个鼠标类 input_dev 和 mousedev_handler 匹配之后会建立连接,连接的过程会创建一个 mousedev 结构体,其中的 handle 成员连接 input_dev 和 mousedev_handler 。
2.4.8 消息处理
mousedev_handler 处理消息的函数如所示。
程序清单 2 . 9 mousedev_event
static void mousedev_event(struct input_handle *handle,
unsigned int type, unsigned int code, int value)
{
struct mousedev *mousedev = handle->private;
switch (type) {
case EV_ABS:
/* Ignore joysticks */
if (test_bit(BTN_TRIGGER, handle->dev->keybit))
return;
if (test_bit(BTN_TOOL_FINGER, handle->dev->keybit))
mousedev_touchpad_event(handle->dev, ⑴
mousedev, code, value);
else
mousedev_abs_event(handle->dev, mousedev, code, value); ⑵
break;
case EV_REL:
mousedev_rel_event(mousedev, code, value); ⑶
break;
case EV_KEY:
if (value != 2) {
if (code == BTN_TOUCH &&
test_bit(BTN_TOOL_FINGER, handle->dev->keybit))
mousedev_touchpad_touch(mousedev, value); ⑷
else
mousedev_key_event(mousedev, code, value); ⑸
}
break;
case EV_SYN:
if (code == SYN_REPORT) {
if (mousedev->touch) {
mousedev->pkt_count++;
fx(0) = fx(1);
fy(0) = fy(1);
}
mousedev_notify_readers(mousedev, &mousedev->packet); ⑹
mousedev_notify_readers(mousedev_mix, &mousedev->packet); ⑺
mousedev->packet.dx = mousedev->packet.dy = mousedev->packet.dz = 0;
mousedev->packet.abs_event = 0;
}
break;
}
}
各句的解释如下:
⑴ 如果是触摸板类设备,则调用 mousedev_touchpad_event 。
⑵ 否则调用 mousedev_abs_event 作为一般的绝对坐标轴事件。
⑶ 处理相对坐标轴事件。
⑷ 如果是触摸板类设备,则作为触摸板的点击处理,调用 mousedev_touchpad_touch 。
⑸ 否则 mousedev_key_event 作为一般鼠标的按键处理。
⑹ 接收到同步事件则将同步消息发送给对应的 mousedev ,将目前的设备状态(按键和坐标的状态)打包传入每个 client 的缓冲区。
⑺ 无论哪个设备同步,同时将当前设备的鼠标状态同步给 mousedev_mix ,这样所有打开 /dev/input/mice 的线程就能收到数据了。
这里我们看到在鼠标类设备中同步事件的重要性。如果一直不进行同步的话,用户空间就会一直读不到有效的数据。每同步一次,只要自上次同步以来有了相对位置的改变或者有按键和点击触摸板的事件发生就会生成一个新的数据包放在 client 的缓冲当中。
之所以要区分触摸设备和非触摸设备,是因为触摸类设备发送的是绝对坐标,需要将每次的绝对坐标和之前的状态比较转换为相对坐标的值。而鼠标发送的绝对事件会被当做是对光标的重定位,直接把光标移到相应的位置。
2.4.9 设备驱动应该如何编写
上面已经基本分析了 mousedev_handler 的实现。下面总结如何编写鼠标类设备的设备驱动。
1. 鼠标设备驱动的编写
这里假设我们有一个光电鼠标,除了左键、右键、中键还有两个扩展键。支持持滚轮朝两个方向滑动。假设我们已经定义了一个 struct input_dev 变量 mice_dev ,其他的初始化同 1.2 节的内容类似,下面只写不同的部分。如所 程序清单 2 .10 示。
程序清单 2 . 10 鼠标设备驱动实例
static unsigned int mice_keycode[] = {BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_SIDE, BTN_EXTRA};
mice_dev.keycode = mice_keycode;
mice_dev.keycodesize = sizeof(unsigned int);
mice_dev.keycodemax = ARRAY_SIZE(mice_keycode);
set_bit(EV_KEY, mice_dev.evbit); /* 使设备支持按键事件 */
set_bit(EV_REL, mice_dev.evbit); /* 使设备支持相对坐标事件 */
set_bit(BTN_LEFT, mice_dev.keybit); /* 依次使设备支持五个按键 */
set_bit(BTN_RIGHT, mice_dev.keybit);
set_bit(BTN_MIDDLE, mice_dev.keybit);
set_bit(BTN_SIDE, mice_dev.keybit);
set_bit(BTN_EXTRA, mice_dev.keybit);
set_bit(REL_X, mice_dev.relbit); /* 支持 X 轴相对坐标 */
set_bit(REL_Y, mice_dev.relbit); /* 支持 Y 轴相对坐标 */
set_bit(REL_WHEEL, mice_dev.relbit); /* 支持滚轮的相对坐标 */
报告事件发生的时候调用 input_report_key 或者 input_report_rel 函数。其它的注意事项参考 1.2 节。每次报告了事件之后都要及时调用 input_sync 进行同步。
2. 触摸板设备驱动的编写
现在我们有一个触摸板,点触之后会感应到点触和点触的位置。能够计算出点击的绝对坐标。下面仅仅示范需要特殊注意的地方。同样假设已经声明了一个 struct input_dev 变量 tp_dev ,初始化过程如 程序清单 2 .11 所示。
程序清单 2 . 11 触摸板设备驱动
unsigned int tp_keycode[] = {BTN_TOUCH, BTN_TOOL_FINGER};
tp_dev.keycode = tp_keycode;
tp_dev.keysize = sizeof(unsigned int);
tp_dev.keymax = ARRAY_SIZE(tp_keycode);
set_bit(EV_KEY, mice_dev.evbit); /* 使设备支持按键事件 */
set_bit(EV_ABS, mice_dev.evbit); /* 使设备支持绝对坐标事件 */
set_bit(BTN_TOUCH, tp_dev.keybit); /* 使设备支持两个按键 */
set_bit(BTN_TOOL_FINGER, tp_dev.keybit); /* 这个设置仅仅表明输入设备种类 */
input_set_abs_params(&tp_dev, ABS_X, 0, 100, 4, 0); /* 设置坐标轴的信息 */
input_set_abs_params(&tp_dev, ABS_Y, 0, 100, 4, 0);
设置坐标轴的信息的用法可以参考 1.3.2 节。
点击的事件应该用 input_report_key( &tp_dev, BTN_TOUCH, 1) 的形式。并不需要报告 BTN_TOOL_FINGER 按键,事实上它不对应任何按键,仅仅让 handler 确定是什么类别的输入工具。 BTN_TOOL_FINGER 代表“手指(触摸输入)”,相应的还有 BTN_TOOL_PEN 等其它类别的输入设备。目前 mousedev_handler 仅仅支持 BTN_TOOL_FINGER