1. 创建输入设备驱动程序¶
1.1. 最简单的例子¶
这里有一个非常简单的输入设备驱动程序示例。该设备只有一个按钮,并且可以通过 I/O 端口 BUTTON_PORT 访问该按钮。按下或释放按钮时,会发生 BUTTON_IRQ。驱动程序可能看起来像这样
#include <linux/input.h>
#include <linux/module.h>
#include <linux/init.h>
#include <asm/irq.h>
#include <asm/io.h>
static struct input_dev *button_dev;
static irqreturn_t button_interrupt(int irq, void *dummy)
{
input_report_key(button_dev, BTN_0, inb(BUTTON_PORT) & 1);
input_sync(button_dev);
return IRQ_HANDLED;
}
static int __init button_init(void)
{
int error;
if (request_irq(BUTTON_IRQ, button_interrupt, 0, "button", NULL)) {
printk(KERN_ERR "button.c: Can't allocate irq %d\n", button_irq);
return -EBUSY;
}
button_dev = input_allocate_device();
if (!button_dev) {
printk(KERN_ERR "button.c: Not enough memory\n");
error = -ENOMEM;
goto err_free_irq;
}
button_dev->evbit[0] = BIT_MASK(EV_KEY);
button_dev->keybit[BIT_WORD(BTN_0)] = BIT_MASK(BTN_0);
error = input_register_device(button_dev);
if (error) {
printk(KERN_ERR "button.c: Failed to register device\n");
goto err_free_dev;
}
return 0;
err_free_dev:
input_free_device(button_dev);
err_free_irq:
free_irq(BUTTON_IRQ, button_interrupt);
return error;
}
static void __exit button_exit(void)
{
input_unregister_device(button_dev);
free_irq(BUTTON_IRQ, button_interrupt);
}
module_init(button_init);
module_exit(button_exit);
1.2. 示例的作用¶
首先,它必须包含 <linux/input.h> 文件,该文件与输入子系统接口。这提供了所有需要的定义。
在 _init 函数中,该函数在模块加载或启动内核时调用,它会获取所需的资源(它还应检查设备是否存在)。
然后,它使用 input_allocate_device()
分配一个新的输入设备结构,并设置输入位域。这样,设备驱动程序会告诉输入系统的其他部分它是什么 - 此输入设备可以生成或接受哪些事件。我们的示例设备只能生成 EV_KEY 类型事件,并且只能从中生成 BTN_0 事件代码。因此,我们只设置这两个位。我们也可以使用
set_bit(EV_KEY, button_dev->evbit);
set_bit(BTN_0, button_dev->keybit);
但是,对于多个位,第一种方法往往会更短。
然后,示例驱动程序通过调用注册输入设备结构
input_register_device(button_dev);
这会将 button_dev 结构添加到输入驱动程序的链接列表中,并调用设备处理程序模块的 _connect 函数,以告知它们新的输入设备已出现。input_register_device()
可能会休眠,因此不得从中断或持有自旋锁的情况下调用。
在使用中,驱动程序唯一使用的函数是
button_interrupt()
在每次按钮中断时,它会检查按钮的状态,并通过
input_report_key()
调用输入系统。无需检查中断例程是否向输入系统报告两个相同的值事件(例如,按下,按下),因为 input_report_* 函数会自行检查。
然后是
input_sync()
调用,告诉那些接收事件的人我们已发送完整的报告。这在单按钮情况下似乎并不重要,但对于鼠标移动而言非常重要,例如,您不希望 X 和 Y 值被单独解释,因为这会导致不同的移动。
1.3. dev->open() 和 dev->close()¶
如果驱动程序必须重复轮询设备,因为它没有来自设备的中断,并且轮询的成本太高而无法一直进行,或者如果设备使用了宝贵的资源(例如中断),它可以利用 open 和 close 回调来了解何时可以停止轮询或释放中断,以及何时必须恢复轮询或再次获取中断。为此,我们会将此添加到我们的示例驱动程序中
static int button_open(struct input_dev *dev)
{
if (request_irq(BUTTON_IRQ, button_interrupt, 0, "button", NULL)) {
printk(KERN_ERR "button.c: Can't allocate irq %d\n", button_irq);
return -EBUSY;
}
return 0;
}
static void button_close(struct input_dev *dev)
{
free_irq(IRQ_AMIGA_VERTB, button_interrupt);
}
static int __init button_init(void)
{
...
button_dev->open = button_open;
button_dev->close = button_close;
...
}
请注意,输入核心会跟踪设备的用户数量,并确保仅当第一个用户连接到设备时才调用 dev->open(),并且仅当最后一个用户断开连接时才调用 dev->close()。对这两个回调的调用是串行化的。
如果成功,open() 回调应返回 0,如果失败,则返回任何非零值。close() 回调(void)必须始终成功。
1.4. 禁止输入设备¶
禁止设备意味着忽略来自设备的输入事件。因此,它涉及维护与输入处理程序的关系 - 无论是已有的关系,还是在设备处于禁止状态时要建立的关系。
如果禁止某个设备,则没有输入处理程序会接收来自该设备的事件。
通过在禁止和取消禁止操作中分别调用设备的 close()(如果有用户)和 open()(如果有用户),进一步利用了没有人想要来自该设备的事件的事实。实际上,close() 的含义是停止向输入核心提供事件,而 open() 的含义是开始向输入核心提供事件。
在禁止时调用设备的 close() 方法(如果有用户)允许驱动程序节省电力。可以直接关闭设备电源,也可以释放驱动程序在使用运行时 PM 时在 open() 中获得的运行时 PM 参考。
禁止和取消禁止与输入处理程序打开和关闭设备是正交的。在任何处理程序与设备成功匹配之前,用户空间可能希望提前禁止设备。
禁止和取消禁止与设备是否为唤醒源也是正交的。当系统处于睡眠状态时,而不是当系统运行时,作为唤醒源会起作用。驱动程序应如何编程其在禁止、睡眠和作为唤醒源之间的交互取决于驱动程序本身。
以网络设备的类比为例 - 关闭网络接口并不意味着应该不可能通过此接口在 LAN 上唤醒系统。因此,即使被禁止,也可能存在应被视为唤醒源的输入驱动程序。实际上,在许多 I2C 输入设备中,它们的中断被声明为唤醒中断,并且其处理发生在驱动程序的核心中,而驱动程序的核心不知道特定于输入的禁止(也不应该知道)。包含多个接口的复合设备可以按每个接口禁止,例如,禁止一个接口不应影响设备作为唤醒源的能力。
如果一个设备在被禁止时被视为唤醒源,则在对其 suspend() 进行编程时必须特别小心,因为它可能需要调用设备的 open()。根据 close() 对相关设备的含义,在进入睡眠状态之前不调用 open() 可能会导致无法提供任何唤醒事件。设备无论如何都会进入睡眠状态。
1.5. 基本事件类型¶
最简单的事件类型是 EV_KEY,用于键和按钮。它通过以下方式报告给输入系统
input_report_key(struct input_dev *dev, int code, int value)
有关 code 的允许值(从 0 到 KEY_MAX),请参见 uapi/linux/input-event-codes.h。Value 被解释为真值,即任何非零值都表示按下键,零值表示释放键。仅当值与之前不同时,输入代码才会生成事件。
除了 EV_KEY 之外,还有两个更基本的事件类型:EV_REL 和 EV_ABS。它们用于设备提供的相对值和绝对值。相对值例如可以是鼠标在 X 轴上的移动。鼠标将其报告为与最后位置的相对差异,因为它没有任何可用于工作的绝对坐标系。绝对事件主要用于操纵杆和数字化仪 - 在绝对坐标系中工作的设备。
使设备报告 EV_REL 按钮与 EV_KEY 一样简单;只需设置相应的位并调用
input_report_rel(struct input_dev *dev, int code, int value)
函数。仅针对非零值生成事件。
但是,EV_ABS 需要一点特殊的注意。在调用 input_register_device 之前,您必须为设备具有的每个绝对轴在 input_dev 结构中填写其他字段。如果我们的按钮设备也有 ABS_X 轴
button_dev.absmin[ABS_X] = 0;
button_dev.absmax[ABS_X] = 255;
button_dev.absfuzz[ABS_X] = 4;
button_dev.absflat[ABS_X] = 8;
或者,您可以只说
input_set_abs_params(button_dev, ABS_X, 0, 255, 4, 8);
此设置适用于操纵杆 X 轴,最小值为 0,最大值为 255(操纵杆必须能够达到此值,如果有时报告更多值也没有问题,但它必须始终能够达到最小值和最大值),数据中的噪声高达 +- 4,并且中心平坦位置的大小为 8。
如果您不需要 absfuzz 和 absflat,可以将它们设置为零,这意味着该物体是精确的,并且始终返回到精确的中心位置(如果有任何)。
1.6. BITS_TO_LONGS()、BIT_WORD()、BIT_MASK()¶
bitops.h 中的这三个宏可以帮助进行一些位域计算
BITS_TO_LONGS(x) - returns the length of a bitfield array in longs for
x bits
BIT_WORD(x) - returns the index in the array in longs for bit x
BIT_MASK(x) - returns the index in a long for bit x
1.7. id* 和 name 字段¶
在输入设备驱动程序注册输入设备之前,应设置 dev->name。它是一个类似“通用按钮设备”的字符串,其中包含设备的易于理解的名称。
id* 字段包含设备的总线 ID(PCI、USB...)、供应商 ID 和设备 ID。总线 ID 在 input.h 中定义。供应商 ID 和设备 ID 在 pci_ids.h、usb_ids.h 和类似的包含文件中定义。这些字段应由输入设备驱动程序在注册之前设置。
idtype 字段可用于输入设备驱动程序的特定信息。
id 和 name 字段可以通过 evdev 接口传递给用户空间。
1.8. keycode、keycodemax、keycodesize 字段¶
这三个字段应由具有密集键映射的输入设备使用。keycode 是一个用于将扫描码映射到输入系统键代码的数组。keycode max 应包含数组的大小,keycodesize 应包含其中每个条目的大小(以字节为单位)。
用户空间可以使用相应 evdev 接口上的 EVIOCGKEYCODE 和 EVIOCSKEYCODE ioctl 查询和更改当前扫描码到键代码的映射。当设备具有所有 3 个上述字段填充时,驱动程序可以依赖于内核设置和查询键代码映射的默认实现。
1.9. dev->getkeycode() 和 dev->setkeycode()¶
getkeycode() 和 setkeycode() 回调允许驱动程序覆盖输入核心提供的默认键代码/键码大小/键码最大映射机制,并实现稀疏的键代码映射。
1.10. 按键自动重复¶
...很简单。它由 input.c 模块处理。硬件自动重复不使用,因为它在许多设备中不存在,即使存在,有时也会损坏(在键盘上:东芝笔记本电脑)。要为您的设备启用自动重复,只需在 dev->evbit 中设置 EV_REP。所有内容都将由输入系统处理。
1.11. 其他事件类型,处理输出事件¶
目前为止,其他的事件类型有
EV_LED - 用于键盘 LED 灯。
EV_SND - 用于键盘蜂鸣。
它们与例如按键事件非常相似,但它们的方向相反 - 从系统到输入设备驱动程序。如果您的输入设备驱动程序可以处理这些事件,它必须在 evbit 中设置相应的位,并且还要设置回调例程
button_dev->event = button_event;
int button_event(struct input_dev *dev, unsigned int type,
unsigned int code, int value)
{
if (type == EV_SND && code == SND_BELL) {
outb(value, BUTTON_BELL);
return 0;
}
return -1;
}
这个回调例程可以从中断或 BH(虽然这不是一个规则)中调用,因此不能睡眠,并且完成的时间不能太长。