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()¶
如果驱动程序必须重复轮询设备,因为它没有来自设备的中断,并且轮询过于昂贵而无法一直进行,或者如果设备使用有价值的资源(例如,中断),则可以使用打开和关闭回调来了解何时可以停止轮询或释放中断,以及何时必须恢复轮询或再次获取中断。为此,我们会将以下内容添加到我们的示例驱动程序中
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() 回调(为空)必须始终成功。
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)
报告给输入系统。有关代码的允许值(从 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 应在输入设备驱动程序注册输入设备之前设置。它是一个字符串,如“Generic button device”,包含设备的用户友好名称。
id* 字段包含设备的总线 ID(PCI、USB 等)、供应商 ID 和设备 ID。总线 ID 在 input.h 中定义。供应商和设备 ID 在 pci_ids.h、usb_ids.h 和类似的包含文件中定义。这些字段应由输入设备驱动程序在注册之前设置。
idtype 字段可用于输入设备驱动程序的特定信息。
id 和 name 字段可以通过 evdev 接口传递给用户空间。
1.8. keycode、keycodemax、keycodesize 字段¶
这三个字段应由具有密集键映射的输入设备使用。 keycode 是一个用于将扫描码映射到输入系统键码的数组。 keycode max 应包含数组的大小,keycode size 应包含其中每个条目的大小(以字节为单位)。
用户空间可以使用相应 evdev 接口上的 EVIOCGKEYCODE 和 EVIOCSKEYCODE ioctl 来查询和更改当前扫描码到键码的映射。当设备填充了所有 3 个上述字段时,驱动程序可以依赖于内核的默认实现来设置和查询键码映射。
1.9. dev->getkeycode() 和 dev->setkeycode()¶
getkeycode() 和 setkeycode() 回调允许驱动程序覆盖输入核心提供的默认键码/keycodesize/keycodemax 映射机制,并实现稀疏键码映射。
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 调用(尽管这不是规则),因此不得休眠,并且完成时间不得太长。
1.12. 轮询输入设备¶
通过将输入设备结构和回调传递给函数
int input_setup_polling(struct input_dev *dev,
void (*poll_fn)(struct input_dev *dev))
来设置输入轮询。在回调中,设备应使用常规的 input_report_* 函数和 input_sync,就像其他设备一样。
还有一个函数
void input_set_poll_interval(struct input_dev *dev, unsigned int interval)
用于配置设备将被轮询的间隔(以毫秒为单位)。