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(虽然这不是一个规则)中调用,因此不能睡眠,并且完成的时间不能太长。