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 代码基严重影响,那是Zephania,E.Hull和Kristian Hogsberg写的东西。
更改
l
目录设置
输入驱动的命名规范应该是 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.h和random.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.conf的InputDevice段落指向的名字。PreInit 将被调用,当您的驱动被添加到server后。它会是每个设备调用一次,并允许您初始化用于保证驱动工作正常的内部结构。UnInit会在设备被移除server之外的时候被调用。清理现场。
module和refCount被server使用,不要碰它们。
对于我们的驱动,代码是这样的:
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循环之外。
初始化一个新设备
本节,我们将讨论在PreInit和UnInit中应该做什么。
PreInit在新设备加入Server时候被调用。这可能发生在Server启动的时候(device在config文件中有款项)或者在运行时(热插拔)。对于我们驱动开发人员来讲,这并无关紧要。只需要知道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);
}
更新:我们现在有了一个驱动,能够初始化,能够在启动的时候测试我们的设备了。
启动和停止设备
使用我们的设备的关键是在适当的时候启动和停止它。如果您的设备不做这些,那么在热插拔您的设备时可能导致灾难。
InputInfoRec的device_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 (!InitButtonClassDeviceStr uct(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 (!InitAbsoluteClassDeviceS truct(device))
return BadAlloc;
for (i = 0; i < pRandom->axes; i++) {
xf86InitValuatorAxisStruct(device, i, -1, -1, 1, 1, 1);
xf86InitValuatorDefaults(device, i);
}
return Success;
}
一般来说,驱动不需要进行指针加速。指针加速是在InitValuatorClassDeviceS
回到设备控制,我们继续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_ON和DEVICE_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来做相应工作了。
结束语
就是这样,您现在应该理解了一个驱动是如何工作的,并且能够编写您自己的驱动了。祝您好运。