X Input 驱动开发教程

http://blog.sina.com.cn/s/blog_5375a6c60100ngq1.html
X Input
驱动教程

(本文为本人翻译自http://www.x.org/wiki/Development/Documentation/XorgInputHOWTO,引用时无需申明译者,但请务必指明原始英文文档位置)

这是关于如何为X开发输入驱动的教材. 读者必须具备C知识, 具备 X server 开发经验更好但并非必要.

如果您计划发明一个新设备, 你很可能需要一个在Linux内核中匹配的驱动. 本教程不涉及这些内容. 提醒一下,如果你想写一个新输入驱动为“通用”设备(例如触摸屏,鼠标,键盘等),请罢手。时间最好用来写一个内核驱动,然后您的设备将会自动识别。本教程是为那些试图理解输入驱动,或者有特殊设备不能通过evdev接口来使用的人的。

本教程将解释“random”驱动,这是一个针对点输入设备的驱动,读取/dev/random文件。这个是一个没用的驱动,它的作用只是要告诉您编写驱动的路径,而不是为您的设备提供一个实际的驱动。这个驱动能做到所有事情,只是让指针沿着X坐标轴随机移动。

全部代码可以从git://people.freedesktop.org/~whot/xf86-input-random下载. X server和大多数驱动通过git版本管理工具保存。如果在你开始开发一个新驱动之前,熟悉一下git将是一个好主意。建议您把您自己的驱动发到一个git版本库中,特别是如果你希望把它和X server打包到一起的时候。

起源

本文档最早是Peter Hutterer编写,用来告诉他自己怎样写输入驱动。

本教程被evdev 代码基严重影响,那是ZephaniaE.HullKristian Hogsberg写的东西。

更改

l      2009-10-26:使用xf86SetStrOption 替代xf86CheckStrOption, 更新链接至git 存储库(即使现在存储库是MIA权利)

目录设置

输入驱动的命名规范应该是 xf86-input-设备名。我们先建立目录,然后提供最基本的文件。初始的目录树应该看起来是这样的:

xf86-input-random/COPYING
                  Makefile.am
                  configure.ac
                  autogen.sh
                  man/Makefile.am
                  man/random.man
                  src/Makefile.am
                  src/random.h
                  src/random.c

我们不想从无到有的写automake文件,所以我们从其它驱动复制了这些文件,并且把里面的驱动名用我们自己的替换。evdev驱动将是复制的一个很好的来源,因为它们被很积极的维护着。

如果你没有完整的X源程序树,又试图编译输入驱动程序,转而采用你的分发版的开发库。为了让automake脚本正常运转,你可能需要安装xorg-util-macros。这些宏提供了DRIVER_MAN_SUFFIX变量和PACKAGE_VERSION_*变量。

我们的源程序random.hrandom.c将成为我们驱动的实现。random.man将会成为您的man文件,本文不讨论怎样写man文件。

顺便说一下,现在最好初始化一下您的git库。

一些基础知识

X驱动必须要处理三个不同的实体:模块,驱动和设备。模块是多个驱动的容器(实际上,我们的xf86-input-random不是一个真正的驱动,而是模块)。驱动是用来负责设置设备的。选择哪个驱动,是由您的X server的配置决定的。一旦设备被设置,它有责任把事件发送给服务器。换句话说,模块被装载一次。驱动是每当xorg.conf的段落中参考到的时候就会被装载。设备是在驱动被装载的时候创建的。其它是设备的问题。

现在让我们继续构建。

模块信息

我们的模块需要提供一些基础信息给X server。这些信息是在XF86ModuleData(xf86Module.h)中。

typedef pointer (*ModuleSetupProc)(pointer, pointer, int *, int *);
typedef void (*ModuleTearDownProc)(pointer);
 
typedef struct {
    XF86ModuleVersionInfo *     vers;
    ModuleSetupProc             setup;
    ModuleTearDownProc          teardown;
} XF86ModuleData;

这个结构体的第一个字段是驱动的版本信息。我不想过多解释它,只是从其它驱动复制过来,然后改了一下名字。请记住这些信息必须是可用的,在你的模块函数被动用之前,所以把它作为你的文件的全局静态内容。

ModuleSetupProc在模块被装载的时候调用。所有模块相关的数据初始化需要在这里进行。

ModuleTearDownProc将在模块被卸载时调用,所有我们模块分配的东西都要在这里被释放。

通过这些信息,我们可以编写最初的几行代码(版权头部和包含文件的内容就略过了,请参考全部源程序)

static XF86ModuleVersionInfo RandomVersionRec =
{
    "random",
    MODULEVENDORSTRING,
    MODINFOSTRING1,
    MODINFOSTRING2,
    XORG_VERSION_CURRENT,
    PACKAGE_VERSION_MAJOR, PACKAGE_VERSION_MINOR,
    PACKAGE_VERSION_PATCHLEVEL,
    ABI_CLASS_XINPUT,
    ABI_XINPUT_VERSION,
    MOD_CLASS_XINPUT,
    {0, 0, 0, 0}
};
 
_X_EXPORT XF86ModuleData randomModuleData =
{
    &RandomVersionRec,
    RandomPlug,
    RandomUnplug
};
 
static void
RandomUnplug(pointer p)
{
}
 
static pointer
RandomPlug(pointer        module,
           pointer        options,
           int            *errmaj,
           int            *errmin)
{
    xf86AddInputDriver(&RANDOM, module, 0);
    return module;
}

所有在VersionInfo中的定义会被我们的automake脚本替换。

驱动信息

在前面的列表中,你能够看到对xf86AddInputDriver()的调用。这个调用告诉X关于新的可用驱动的信息。如前所述,模块可以包含多个驱动。

我们这里用到的RANDOM变量包含用于初始化我们的驱动的信息。它是InputDriverRec类型的。

typedef struct _InputDriverRec {
    int                     driverVersion;
    char *                  driverName;
    void                    (*Identify)(int flags);
    struct _LocalDeviceRec *(*PreInit)(struct _InputDriverRec *drv,
                                       IDevPtr dev, int flags);
    void                    (*UnInit)(struct _InputDriverRec *drv,
                                      struct _LocalDeviceRec *pInfo,
                                      int flags);
    pointer                 module;
    int                     refCount;
} InputDriverRec, *InputDriverPtr;

其中一些字段是很重要的,其它的就不那么重要了。Indentify从来不被输入驱动调用,所以没有X.org输入驱动使用它,所有的驱动使用驱动版本都是1.

其它驱动更总要一些。driverName需要被您的驱动名字替换。它并不需要同模块同名。驱动名是你需要在xorg.confInputDevice段落指向的名字。PreInit 将被调用,当您的驱动被添加到server后。它会是每个设备调用一次,并允许您初始化用于保证驱动工作正常的内部结构。UnInit会在设备被移除server之外的时候被调用。清理现场。

modulerefCountserver使用,不要碰它们。

对于我们的驱动,代码是这样的:

static InputInfoPtr
RandomPreInit(InputDriverPtr  drv,
              IDevPtr         dev,
              int             flags)
{
    return NULL;
}
 
static void
RandomUnInit(InputDriverPtr       drv,
             InputInfoPtr         pInfo,
             int                  flags)
{
}
 
_X_EXPORT InputDriverRec RANDOM = {
    1,
    "random",
    NULL,
    RandomPreInit,
    RandomUnInit,
    NULL,
    0
};

通过这些代码,我们的驱动已经能够编译并被调用。但是,它不做任何事,除了浪费一些CPU循环之外。

初始化一个新设备

本节,我们将讨论在PreInitUnInit中应该做什么。

PreInit在新设备加入Server时候被调用。这可能发生在Server启动的时候(deviceconfig文件中有款项)或者在运行时(热插拔)。对于我们驱动开发人员来讲,这并无关紧要。只需要知道PreInit对于用到我们驱动的每个设备都会被调用。

PreInit应该做以下事情:

  • 一个InputInfoRec应该被分配。这个结构包含了关于我们的设备的重要信息。
  • 配置选项需要被分析。
  • 设备应该被测试可用性和可访问性。
  • 内部数据应该被初始化,如果驱动需要。

 

现在看看我给出的代码:

static InputInfoPtr RandomPreInit(InputDriverPtr  drv,
                                  IDevPtr         dev,
                                  int             flags)
{
    InputInfoPtr        pInfo;
    RandomDevicePtr     pRandom;
 
   if (!(pInfo = xf86AllocateInput(drv, 0)))
        return NULL;
 
    pRandom = xcalloc(1, sizeof(RandomDeviceRec));
    if (!pRandom) {
        pInfo->private = NULL;
        xf86DeleteInput(pInfo, 0);
        return NULL;
    }
 
    pInfo->private = pRandom;

首先,我们让server分配信息,接着为我们的信息分配RandomDeviceRec,把它连接到设备的私有字段。RandomDeviceRec(请看代码)是是一个结构,我们用来为我们的驱动保存设备内部内容。我们的驱动很简单,它不包含很多内容。如果您的驱动更复杂,那么可能您要在这里保存更多的内容。

如果发生错误,PreInit应该清理,并返回NULL

    pInfo->name = xstrdup(dev->identifier);
    pInfo->flags = 0;
    pInfo->type_name = XI_MOUSE; 
    pInfo->conf_idev = dev;
    pInfo->read_input = RandomReadInput; 
    pInfo->switch_mode = NULL; 
    pInfo->device_control = RandomControl; 

对我们来讲,重要的是:

  • read_input:将会在新数据可用时被调用
  • switch_mode:将会在新的client请求切换设备绝对和相对模式时被调用。(我们的随机设备不支持)
  • device_control:将会在设备打开或关闭时候被调用。

现在我们要处理一些选项:

    
    pRandom->device = xf86SetStrOption(dev->commonOptions,
                                         "Device",
                                         "/dev/random");
 
    xf86Msg(X_INFO, "%s: Using device %s.\n", pInfo->name, pRandom->device);
 
    
    xf86CollectInputOptions(pInfo, NULL, NULL);
    xf86ProcessCommonOptions(pInfo, pInfo->options);

那么首先,我们检查驱动特定选项。如果Device选项被设置,我们分离它(如果不,我们就使用缺省提供的)。同样的接口对于获取其它数据类型时可用。xf86Set{Int|Real|Str|Bool}OPtion(),返回给我们所需要的值(包含一个选项缺省提供的)。使用xf86SetStrOption标记这个选项是使用的,并把它打印到日志文件。

在我们完成对特定选项的处理之后,我们就可以告诉server来处理通用选项(例如SendCoreEvents)。

现在我们应该测试设备是否可用,是否可访问。

    
    SYSCALL(pInfo->fd = open(pRandom->device, O_RDWR | O_NONBLOCK));
    if (pInfo->fd == -1)
    {
        xf86Msg(X_ERROR, "%s: failed to open %s.",
                pInfo->name, pRandom->device);
        pInfo->private = NULL;
        xfree(pRandom);
        xf86DeleteInput(pInfo, 0);
        return NULL;
    }
 
    
 
    close(pInfo->fd);
    pInfo->fd = -1;

我们使用SYSCALL来确保我们的调用是不会被中断的。一旦设备被打开,你可能需要进行一些额外的小心的工作(看一下evdev)来获取设备信息。最后我们再次关闭文件,毕竟只是要测试一下它是否能行。

让我们设置一些标志然后结束。注意如果我们不设置XI86_CONFIGURED标志,server会以为有任务失败,会清理我们的设备。

    pInfo->flags |= XI86_OPEN_ON_INIT;
    pInfo->flags |= XI86_CONFIGURED;
 
    return pInfo;
}

UnInit没什么特别让人激动的内容,就不过多介绍了。

static void RandomUnInit(InputDriverPtr drv,
                         InputInfoPtr   pInfo,
                         int            flags)
{
    RandomDevicePtr       pRandom = pInfo->private;
 
    if (pRandom->device)
    {
        xfree(pRandom->device);
        pRandom->device = NULL;
    }
 
    xfree(pRandom);
    xf86DeleteInput(pInfo, 0);
}

更新:我们现在有了一个驱动,能够初始化,能够在启动的时候测试我们的设备了。

启动和停止设备

使用我们的设备的关键是在适当的时候启动和停止它。如果您的设备不做这些,那么在热插拔您的设备时可能导致灾难。

InputInfoRecdevice_control字段是用来提示设备状态变化的。它接受两个参数,第一个是设备,第二个是设备应该做的事情(DEVICE_INIT,DEVICE_ON,DEVICE_OFF,DEVICE_CLOSE中的某个)。

继续,这里是代码:

static int RandomControl(DeviceIntPtr    device,
                         int             what)
{
    InputInfoPtr  pInfo = device->public.devicePrivate;
   RandomDevicePtr pRandom = pInfo->private;
 
    switch(what)
    {
        case DEVICE_INIT:
            _random_init_buttons(device);
            _random_init_axes(device);
            break;

DEVICE_INIT在设备被加到server时候调用。server希望设备能够分配所有必要的信息来指出它是什么设备。

这里是我们用来初始化设备的代码,它硬编码成两个按键。您的设备应该包含更多的选项来切换按键映射,但是我们不在这里聒噪了。

static int
_random_init_buttons(DeviceIntPtr device)
{
    InputInfoPtr        pInfo = device->public.devicePrivate;
    CARD8               *map;
    int                 i;
    int                 ret = Success;
    const int           num_buttons = 2;
 
    map = xcalloc(num_buttons, sizeof(CARD8));
 
    for (i = 0; i < num_buttons; i++)
        map[i] = i;
 
    if (!InitButtonClassDeviceStruct(device, num_buttons, map)) {
            xf86Msg(X_ERROR, "%s: Failed to register buttons.\n", pInfo->name);
            ret = BadAlloc;
    }
 
    xfree(map);
    return ret;
}

同时,这里是初始化设备的代码,硬编码为两个轴。并且,我们还把它硬编码为Relative模式。

static int
_random_init_axes(DeviceIntPtr device)
{
    InputInfoPtr        pInfo = device->public.devicePrivate;
    int                 i;
    const int           num_axes = 2;
 
    if (!InitValuatorClassDeviceStruct(device,
                num_axes,
                GetMotionHistory,
                GetMotionHistorySize(),
                0))
        return BadAlloc;
 
    pInfo->dev->valuator->mode = Relative;
    if (!InitAbsoluteClassDeviceStruct(device))
           return BadAlloc;
 
    for (i = 0; i < pRandom->axes; i++) {
            xf86InitValuatorAxisStruct(device, i, -1, -1, 1, 1, 1);
            xf86InitValuatorDefaults(device, i);
    }
 
    return Success;
}

一般来说,驱动不需要进行指针加速。指针加速是在InitValuatorClassDeviceStruct过程中被初始化的,用户设置加速是在驱动调用xf86InitValuatorDefaults被调用的。但是当然它依赖于您的要求,可以用一个小API,在《指针加速》一文讨论。

回到设备控制,我们继续switch语句。

收到DEVICE_ON信号,我们现在应该打开设备并开始发送事件。

            
            case DEVICE_ON:
                xf86Msg(X_INFO, "%s: On.\n", pInfo->name);
                if (device->public.on)
                        break;
 
                SYSCALL(pInfo->fd = open(pRandom->device, O_RDONLY | O_NONBLOCK));
               if (pInfo->fd < 0)
                {
                    xf86Msg(X_ERROR, "%s: cannot open device.\n", pInfo->name);
                    return BadRequest;
                }
 
                xf86FlushInput(pInfo->fd);
                xf86AddEnabledDevice(pInfo);
                device->public.on = TRUE;
                break;

xf86AddEnabledDevice(0将会把我们的哦设备的fd加入到SIGIO处理句柄中。当SIGIO发生时,我们的read_input将被调用。同时,当然,我们需要标记设备被打开,这样Server知道我们是在这里做我们自己的工作。

DEVICE_OFF应该关闭设备,DEVICE_CLOSE释放留下的所有东西...

           case DEVICE_OFF:
                xf86Msg(X_INFO, "%s: Off.\n", pInfo->name);
                if (!device->public.on)
                    break;
                xf86RemoveEnabledDevice(pInfo);
                close(pInfo->fd);
                pInfo->fd = -1;
                device->public.on = FALSE;
                break;
          case DEVICE_CLOSE:
                
                break;
    }
    return Success;
}

注意客户端可能导致DEVICE_ONDEVICE_OFF被重复调用。不管怎样DEVICE_CLOSE指示设备被移除,并不应该被再次打开。

没错,这是基本的。所有我们现在需要做的是读取数据,发送事件。

读取输入

我们知道我们的read_input将在数据可用时被调用。所以我们要做的如下:

static void RandomReadInput(InputInfoPtr pInfo)
{
    char                  data;
 
    while(xf86WaitForInput(pInfo->fd, 0) > 0)
    {
        read(pInfo->fd, &data, 1);
 
        xf86PostMotionEvent(pInfo->dev,
                            0, 
                            0, 
                            1, 
                            data);
    }
}

那么每次我们被中断了,我们从设备读取一个字节,并传递它,作为一个移动时间,附带一个x值。指针会不规律的沿着x坐标移动(你可能需要移动其它鼠标,这样/dev/random会产生足够的数据)。

现在当然你需要进行错误检查,或者其它更复杂的事件产生。用于发送事件的DDX接口是:

xf86PostMotionEvent(); xf86PostButtonEvent(); xf86PostKeyboardEvent(); xf86PostProximityEvent(); xf86PostKeyEvent();

(针对有估值的按键事件)

您能在xf86Xinput.h文件中找到这些定义。

当你发送事件时,server关心XInput时间,并把所有事件放入事件队列。在之后的某个时间点,server会把它取出,并进行处理。如果你在短时间内输送了太多事件,队列可能溢出,事件将会丢失。这对于输入驱动来讲,并不是一个问题,除非server实在实在忙于绘制。构建一个/dev/urandom驱动可能会导致server溢出。

大图

让我们复习一下。我们有了模块。模块可以提供多个驱动,每个有各自的能力。我们有了驱动,驱动依靠模块,并且负责设置设备和同设备进行交谈。

我们有了设备。每个设备应该互相独立,并且他们能够有不同的read_input/device_control/其它处理过程。设备的read_input将在数据可用时被调用。设备的device_control将会被调用,当设备是活跃/无效时。

一个设备需要传递事件给DDX,并且从那时起,轮到server来做相应工作了。

结束语

就是这样,您现在应该理解了一个驱动是如何工作的,并且能够编写您自己的驱动了。祝您好运。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值