一:前言:
在分析intel8042芯片驱动的时候,对其中断处理的后续流程还没有讨论完.在本章以键盘通道为索引讲述intel8042的后续处理,这部份内容实际上是独立的键盘驱动部份,多种型号的键盘都有自己的驱动程序,但原理都是一样的,都承接着intel8042芯片的后续处理.所以在这里为了讨论的方便,将其以单独小节的方式给出分析.
下面以基于2.6.25kernel的atkbd.c键盘驱动为例进行分析.
二:intel8042中断处理回顾
记得在intel8042的i8042_probe()处理的中断处理中,注册了几个serio port.以kbd通道为例,再次将其列出.
static int __devinit i8042_probe (void)
{
……
i8042_setup_kbd ()
……
i8042_register_ports()
……
}
在i8042_setup_kbd () -> i8042_create_kbd_port()中:
static int __devinit i8042_create_kbd_port(void)
{
struct serio *serio;
struct i8042_port *port = &i8042_ports[I8042_KBD_PORT_NO];
serio = kzalloc(sizeof(struct serio), GFP_KERNEL);
if (!serio)
return -ENOMEM;
serio->id.type = i8042_direct ? SERIO_8042 : SERIO_8042_XL;
serio->write = i8042_dumbkbd ? NULL : i8042_kbd_write;
serio->start = i8042_start;
serio->stop = i8042_stop;
serio->port_data = port;
serio->dev.parent = &i8042_platform_device->dev;
strlcpy(serio->name, "i8042 KBD port", sizeof(serio->name));
strlcpy(serio->phys, I8042_KBD_PHYS_DESC, sizeof(serio->phys));
port->serio = serio;
port->irq = I8042_KBD_IRQ;
return 0;
}
初始化了一个serio结构.
在i8042_register_ports()中:
static void __devinit i8042_register_ports(void)
{
……
serio_register_port(i8042_ports[i].serio);
……
}
将这个serio注册到了总线.
根据我们之前分析的serio总线的相关知识.注册serio port的时候会产生serio port与serio driver的匹配事件
那serio driver在什么地方呢? 这就是今天要讨论的键盘驱动了.
三:键盘驱动入口
在atkbd.c中,module的入口函数为:
static int __init atkbd_init(void)
{
dmi_check_system(atkbd_dmi_quirk_table);
return serio_register_driver(&atkbd_drv);
}
在这个初始化函数里,注册了一个serio driver.即atkbd_drv.这就是上在讨论的serio driver的由来了.它的结构如下:
static struct serio_driver atkbd_drv = {
.driver = {
.name = "atkbd",
},
.description = DRIVER_DESC,
.id_table = atkbd_serio_ids,
.interrupt = atkbd_interrupt,
.connect = atkbd_connect,
.reconnect = atkbd_reconnect,
.disconnect = atkbd_disconnect,
.cleanup = atkbd_cleanup,
};
先来看一下它的id_table成员.这个成员决定着serio port与serio driver是否匹配成功.如下:
static struct serio_device_id atkbd_serio_ids[] = {
{
.type = SERIO_8042,
.proto = SERIO_ANY,
.id = SERIO_ANY,
.extra = SERIO_ANY,
},
{
.type = SERIO_8042_XL,
.proto = SERIO_ANY,
.id = SERIO_ANY,
.extra = SERIO_ANY,
},
{
.type = SERIO_RS232,
.proto = SERIO_PS2SER,
.id = SERIO_ANY,
.extra = SERIO_ANY,
},
{ 0 }
};
由此看出,所有类型为SERIO_8042, SERIO_8042_XL, SERIO_RS232的serio port都适用于此驱动.回顾前面分析的i8042_create_kbd_port()函数中,对serio的id成员赋值如下:
serio->id.type = i8042_direct ? SERIO_8042 : SERIO_8042_XL;
也就是说,由i8042产生的serio类型为SERIO_8042或者 SERIO_8042_XL.都是适用这个驱动的.
就这样,我们找到了由intel8042产生serio port对应的驱动了.
四:serio drver的connect函数
在之前分析的serio总线中得知.如果serio port与驱动匹配成功,就会调用驱动的connect函数,在我们分析的这个键盘驱动里,这个成员函数对应是atkbd_connect().代码如下:
static int atkbd_connect(struct serio *serio, struct serio_driver *drv)
{
struct atkbd *atkbd;
struct input_dev *dev;
int err = -ENOMEM;
atkbd = kzalloc(sizeof(struct atkbd), GFP_KERNEL);
dev = input_allocate_device();
if (!atkbd || !dev)
goto fail1;
atkbd->dev = dev;
ps2_init(&atkbd->ps2dev, serio);
INIT_DELAYED_WORK(&atkbd->event_work, atkbd_event_work);
mutex_init(&atkbd->event_mutex);
switch (serio->id.type) {
case SERIO_8042_XL:
atkbd->translated = 1;
case SERIO_8042:
if (serio->write)
atkbd->write = 1;
break;
}
atkbd->softraw = atkbd_softraw;
atkbd->softrepeat = atkbd_softrepeat;
atkbd->scroll = atkbd_scroll;
if (atkbd->softrepeat)
atkbd->softraw = 1;
serio_set_drvdata(serio, atkbd);
err = serio_open(serio, drv);
if (err)
goto fail2;
if (atkbd->write) {
if (atkbd_probe(atkbd)) {
err = -ENODEV;
goto fail3;
}
atkbd->set = atkbd_select_set(atkbd, atkbd_set, atkbd_extra);
atkbd_activate(atkbd);
} else {
atkbd->set = 2;
atkbd->id = 0xab00;
}
atkbd_set_keycode_table(atkbd);
atkbd_set_device_attrs(atkbd);
err = sysfs_create_group(&serio->dev.kobj, &atkbd_attribute_group);
if (err)
goto fail3;
atkbd_enable(atkbd);
err = input_register_device(atkbd->dev);
if (err)
goto fail4;
return 0;
fail4: sysfs_remove_group(&serio->dev.kobj, &atkbd_attribute_group);
fail3: serio_close(serio);
fail2: serio_set_drvdata(serio, NULL);
fail1: input_free_device(dev);
kfree(atkbd);
return err;
}
这段代码第一次看的时候可能觉得比较恐怖,很多陌生的接口,其实,这里面处理的事情并不多,首先它调用atkbd_set_keycode_table()根据设备的类型选择一套扫描码.什么叫扫描码?扫描码就是指根据从intel8042中读取到的数据转换为我们所用的字符的过程.例如,在第一套扫描码中,数字1的扫描码就是0x2.具体的知识请查阅相关资料,在这里不做详细分析,值得注意的是.,在这里.键盘的类型通常为SERIO_8042_XL型,因为在intel8042驱动中,默认是不支持i8042_direct.的
然后申请一个input_dev. Input_dev是输出子系统的一个概念.所谓输出子系统,是处理I/O设备与上层应用的一个中间层,它接收下层驱动的相关事件(例如键盘按键,鼠标移动等)发送到上层.这个子系统我们下一节再给出详细的分析,在这里只要知道它的概念就可以了.
接下来,调用input_register_device()将input_dev注册到了输入子系统,注意在这之前还会调用atkbd_set_device_attrs()设置iput_dev的相关属性,在这一节里,这并不是我们所分析的重点.在此不详细讨论.
五:键盘的中断处理
在intel8042接收到中断之后,会调用驱动的interrupt接口来处理数据,在这里,这个接口为atkbd_interrupt().代码如下:
static irqreturn_t atkbd_interrupt(struct serio *serio, unsigned char data,
unsigned int flags)
{
struct atkbd *atkbd = serio_get_drvdata(serio);
struct input_dev *dev = atkbd->dev;
unsigned int code = data;
int scroll = 0, hscroll = 0, click = -1;
int value;
unsigned char keycode;
#ifdef ATKBD_DEBUG
printk(KERN_DEBUG "atkbd.c: Received %02x flags %02x\n", data, flags);
#endif
//不是在x86中的情况.
#if !defined(__i386__) && !defined (__x86_64__)
if ((flags & (SERIO_FRAME | SERIO_PARITY)) && (~flags & SERIO_TIMEOUT) && !atkbd->resend && atkbd->write) {
printk(KERN_WARNING "atkbd.c: frame/parity error: %02x\n", flags);
serio_write(serio, ATKBD_CMD_RESEND);
atkbd->resend = 1;
goto out;
}
if (!flags && data == ATKBD_RET_ACK)
atkbd->resend = 0;
#endif
//intel 8042的应答消息处理
if (unlikely(atkbd->ps2dev.flags & PS2_FLAG_ACK))
if (ps2_handle_ack(&atkbd->ps2dev, data))
goto out;
//命令正在写入,等待写完
if (unlikely(atkbd->ps2dev.flags & PS2_FLAG_CMD))
if (ps2_handle_response(&atkbd->ps2dev, data))
goto out;
//如果键盘没有被启用,退出
if (!atkbd->enabled)
goto out;
input_event(dev, EV_MSC, MSC_RAW, code);
//对一些断开码,控制码的处理
if (atkbd->translated) {
if (atkbd->emul || atkbd_need_xlate(atkbd->xl_bit, code)) {
atkbd->release = code >> 7;
code &= 0x7f;
}
if (!atkbd->emul)
atkbd_calculate_xl_bit(atkbd, data);
}
switch (code) {
case ATKBD_RET_BAT:
atkbd->enabled = 0;
serio_reconnect(atkbd->ps2dev.serio);
goto out;
case ATKBD_RET_EMUL0:
atkbd->emul = 1;
goto out;
case ATKBD_RET_EMUL1:
atkbd->emul = 2;
goto out;
case ATKBD_RET_RELEASE:
atkbd->release = 1;
goto out;
case ATKBD_RET_ACK:
case ATKBD_RET_NAK:
if (printk_ratelimit())
printk(KERN_WARNING "atkbd.c: Spurious %s on %s. "
"Some program might be trying access hardware directly.\n",
data == ATKBD_RET_ACK ? "ACK" : "NAK", serio->phys);
goto out;
case ATKBD_RET_ERR:
atkbd->err_count++;
#ifdef ATKBD_DEBUG
printk(KERN_DEBUG "atkbd.c: Keyboard on %s reports too many keys pressed.\n", serio->phys);
#endif
goto out;
}
//将接收到的数据通过扫描码转换成字符码
code = atkbd_compat_scancode(atkbd, code);
if (atkbd->emul && --atkbd->emul)
goto out;
keycode = atkbd->keycode[code];
if (keycode != ATKBD_KEY_NULL)
input_event(dev, EV_MSC, MSC_SCAN, code);
switch (keycode) {
case ATKBD_KEY_NULL:
break;
case ATKBD_KEY_UNKNOWN:
printk(KERN_WARNING
"atkbd.c: Unknown key %s (%s set %d, code %#x on %s).\n",
atkbd->release ? "released" : "pressed",
atkbd->translated ? "translated" : "raw",
atkbd->set, code, serio->phys);
printk(KERN_WARNING
"atkbd.c: Use 'setkeycodes %s%02x ' to make it known.\n",
code & 0x80 ? "e0" : "", code & 0x7f);
input_sync(dev);
break;
case ATKBD_SCR_1:
scroll = 1 - atkbd->release * 2;
break;
case ATKBD_SCR_2:
scroll = 2 - atkbd->release * 4;
break;
case ATKBD_SCR_4:
scroll = 4 - atkbd->release * 8;
break;
case ATKBD_SCR_8:
scroll = 8 - atkbd->release * 16;
break;
case ATKBD_SCR_CLICK:
click = !atkbd->release;
break;
case ATKBD_SCR_LEFT:
hscroll = -1;
break;
case ATKBD_SCR_RIGHT:
hscroll = 1;
break;
default:
if (atkbd->release) {
value = 0;
atkbd->last = 0;
} else if (!atkbd->softrepeat && test_bit(keycode, dev->key)) {
/* Workaround Toshiba laptop multiple keypress */
value = time_before(jiffies, atkbd->time) && atkbd->last == code ? 1 : 2;
} else {
value = 1;
atkbd->last = code;
atkbd->time = jiffies + msecs_to_jiffies(dev->rep[REP_DELAY]) / 2;
}
//上报一个按键事件
input_event(dev, EV_KEY, keycode, value);
//sync事件,表明当前事件已经完了
input_sync(dev);
//按键松开了,上报一个松开的按键消息
if (value && test_bit(code, atkbd->force_release_mask)) {
input_report_key(dev, keycode, 0);
input_sync(dev);
}
}
if (atkbd->scroll) {
if (click != -1)
input_report_key(dev, BTN_MIDDLE, click);
input_report_rel(dev, REL_WHEEL, scroll);
input_report_rel(dev, REL_HWHEEL, hscroll);
input_sync(dev);
}
atkbd->release = 0;
out:
return IRQ_HANDLED;
}
在这个中断处理程序里,涉及到了按键断开码,控制码,按键应答等处理.查阅intel8042的相关资料理解这部份并不难.在中断处理程序中,将接收到的按键扫描码转换之后,调用input_event()产生一个按键事件,将其上报给上层的input_handler.
六:小结
简而言之,键盘的驱动流程就是这样的,当驱动检测到设备之后,注册一个input device.然后在中断处理中,将接收到的扫描码转换之后,再给上层上报一个事件。在这一小节里,对具体的键盘驱动讲述较少。只因为这部份的东西部份有很多详尽的资料描述。在此不再赘述.