------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处:http://ericxiao.cublog.cn/
------------------------------------------
一:intel8042芯片概述
Intel8024是intel公司的一款键盘控制器芯片,它为x86系统中的标准配置.虽然名为键盘控制器,但是鼠标也是由其控制的.
分配给键盘控制器的I/O端口有四个,分别是0x60~0x64.在大部分情况中,只会使用到0x60和0x64.其余0x61~0x64的存在主要是为了兼容XT.可以将0x64看做是状态寄存器.0x60看成是数据寄存器.有时在给键盘控制器下指令的时候,这两个端口都要用到.两者配合来达到下指令与参数的目的.
在微机原理中学过,键盘通常使用IRQ1.鼠标通常使用IRQ12.其它IRQ1和IRQ12都是连接在键盘控制器上的.对应intel8042的两个端口.
Intel8024的指令类型:
1:取得控制器状态.通过读取0x64中的值
2:键盘控制命令:将指令写入0x60中.如果该指令带有参数,也写入到0x60中.通过在20ms内.键盘会给一个应答(0xfa)
3:键盘控制器命令:用来控制键盘控制器.将指令写入0x64.将参数写入0x60中
有关更详细的说明请参阅有关intel 8042芯片的相关手册.
二:架构概述
先来看硬件的连接层次.如下图所示:
红线部份标识的serio总线在硬件上是不存在的.这是Linux内核为了简化串行的输入输出设备添加一个中间层.
如上层所示.串行键盘鼠标等外设都是连接在i8042控制器上的.串行I/O设备的输入输出数据以及设备的控制都是通过i8042进行的.结合我们之前分析过的serio总线代码.serio总线在这里正好为i8042和外设之间提供了一个通信层, 串行外设的驱动也是基于serio结构的.
当i8042检测到一个外部设备,它生成一个serio device.然后将其注册到serio总线.这时就会去匹配注册到serio总线上的serio driver.
当一个中断到来时,i8042会检测serio device设备是否已经关联到驱动,如果已经关联了,直接调用驱动中的中断处理函数,如果没有.手动使device去匹配serio driver.
由此可以看出.这种架构模型极大的简化了驱动的设计,它会串行驱动设计者提供了一个统一的接口.
在接下来的驱动代码分析中,会涉及到我们之前分析过的platform.serio等.
三:intel8042驱动分析
以下的代码分析是基于linux kernel 2.6.25 intel8042的驱动位于linux-2.6.25/drivers/input/serio/ i8042.c
驱动对应的初始化入口如下:
static int __init i8042_init(void)
{
int err;
dbg_init();
err = i8042_platform_init();
if (err)
return err;
err = i8042_controller_check();
if (err)
goto err_platform_exit;
err = platform_driver_register(&i8042_driver);
if (err)
goto err_platform_exit;
i8042_platform_device = platform_device_alloc("i8042", -1);
if (!i8042_platform_device) {
err = -ENOMEM;
goto err_unregister_driver;
}
err = platform_device_add(i8042_platform_device);
if (err)
goto err_free_device;
panic_blink = i8042_panic_blink;
return 0;
err_free_device:
platform_device_put(i8042_platform_device);
err_unregister_driver:
platform_driver_unregister(&i8042_driver);
err_platform_exit:
i8042_platform_exit();
return err;
}
在初始化入口里,调用i8042_platform_init()来进行i8024驱动的一些初始化.然后调用i8042_controller_check()来检查8042芯片是否正常.如果一切正常,注意一个platform总线的驱动和设备.关于platform总线,参考<< linux设备模型之platform总线>>
挨个分析初始化入口里所调用的子函数.
i8042_platform_init()用来进行一系统的初始化,代码如下:
static int __init i8042_platform_init(void)
{
int retval;
/*
* On ix86 platforms touching the i8042 data register region can do really
* bad things. Because of this the region is always reserved on ix86 boxes.
*
* if (!request_region(I8042_DATA_REG, 16, "i8042"))
* return -EBUSY;
*/
//键盘通道所对应的IRQ
i8042_kbd_irq = I8042_MAP_IRQ(1);
//鼠标通道所对应的IRQ
i8042_aux_irq = I8042_MAP_IRQ(12);
//PNP选择编译部份
retval = i8042_pnp_init();
if (retval)
return retval;
#if defined(__ia64__)
i8042_reset = 1;
#endif
//DMI选择编译部份
#if defined(__i386__) || defined(__x86_64__)
if (dmi_check_system(i8042_dmi_noloop_table))
i8042_noloop = 1;
if (dmi_check_system(i8042_dmi_nomux_table))
i8042_nomux = 1;
#endif
#ifdef CONFIG_X86
if (dmi_check_system(i8042_dmi_dritek_table))
i8042_dritek = 1;
#endif /* CONFIG_X86 */
return retval;
}
在这里,主要指定了键盘接口和鼠标接口的IRQ.其它部份为选择编译部份,忽略.
i8042_controller_check()用来检查8042是否正常,代码如下:
static int i8042_controller_check(void)
{
if (i8042_flush() == I8042_BUFFER_SIZE) {
printk(KERN_ERR "i8042.c: No controller found.\n");
return -ENODEV;
}
return 0;
}
当i8042_flush()返回I8042_BUFFER_SIZE的时候,会提示末找到8042控制器.看这个函数的名称是刷新什么东西.转进去看看
static int i8042_flush(void)
{
unsigned long flags;
unsigned char data, str;
int i = 0;
spin_lock_irqsave(&i8042_lock, flags);
while (((str = i8042_read_status()) & I8042_STR_OBF) && (i < I8042_BUFFER_SIZE)) {
udelay(50);
data = i8042_read_data();
i++;
dbg("%02x <- i8042 (flush, %s)", data,
str & I8042_STR_AUXDATA ? "aux" : "kbd");
}
spin_unlock_irqrestore(&i8042_lock, flags);
return i;
}
从代码中可以看出.读取8042状态寄存器的值,如果返回值含有I8042_STR_OBF位.则延迟之后再读取,这样一直尝试I8042_BUFFER_SIZE次.如果读出来的值一直都包含I8042_STR_OBF位的话,那i8042_controller_check()就会返回错误了.
I8042_STR_OBF定义如下:
#define I8042_STR_OBF 0x01
对应为寄存器的第1位,
对于0x64第1位的定义为:如果此位被置,将表示数据端口0x60有数据.这样,对上面代码的逻辑含义就很好理解了:
如果数据端口一直的数据,就说明此时可能出现了异常
然后在初始化函数里,注册了一个platform 的驱动i8042_driver,如下:
static struct platform_driver i8042_driver = {
.driver = {
.name = "i8042",
.owner = THIS_MODULE,
},
.probe = i8042_probe,
.remove = __devexit_p(i8042_remove),
.shutdown = i8042_shutdown,
#ifdef CONFIG_PM
.suspend = i8042_suspend,
.resume = i8042_resume,
#endif
};
紧接着,又注册了一个名为i8042的platform设备,根据之前研究的platform总线的知识,可得知,进行设备与驱动匹配的时候,首先会判断设备和驱动的name是否相同.在这里是相同的.接着会调用platform driver的probe接口,在这里,这个接口对应为:
i8042_probe().代码分段如下
static int __devinit i8042_probe(struct platform_device *dev)
{
int error;
error = i8042_controller_selftest();
if (error)
return error;
i8042_controller_selftest()用于键盘控控制器的自检.控制器自检的指令为0xaa.若自检成功,返回0x55
error = i8042_controller_init();
if (error)
return error;
进行键盘控制器的初始化。
if (!i8042_noaux) {
error = i8042_setup_aux();
if (error && error != -ENODEV && error != -EBUSY)
goto out_fail;
}
if (!i8042_nokbd) {
error = i8042_setup_kbd();
if (error)
goto out_fail;
}
建立两个通道。一个是aux.为鼠标使用。另一个是kbd.为键盘使用
#ifdef CONFIG_X86
if (i8042_dritek) {
char param = 0x90;
error = i8042_command(¶m, 0x1059);
if (error)
goto out_fail;
}
#endif
/*
* Ok, everything is ready, let's register all serio ports
*/
i8042_register_ports();
注册端口。端口是在建立通道的时候建立的。
return 0;
out_fail:
i8042_free_aux_ports(); /* in case KBD failed but AUX not */
i8042_free_irqs();
i8042_controller_reset();
return error;
}:
I8042设置键盘和鼠标接口的过程非常复杂.我们在下面分段对这两个过程进行讲解.在讲解之前,我们先对i8042_register_ports()有一个了解.代码如下:
static void __devinit i8042_register_ports(void)
{
int i;
for (i = 0; i < I8042_NUM_PORTS; i++) {
if (i8042_ports[i].serio) {
printk(KERN_INFO "serio: %s at %#lx,%#lx irq %d\n",
i8042_ports[i].serio->name,
(unsigned long) I8042_DATA_REG,
(unsigned long) I8042_COMMAND_REG,
i8042_ports[i].irq);
serio_register_port(i8042_ports[i].serio);
}
}
}
显然,这段代码是对i8042_ports[]数组中被初始化的项进行处理,调用serio_register_port()将其注册到虚拟总线..
i8042_setup_aux()用来设置鼠标通道.代码如下:
static int __devinit i8042_setup_aux(void)
{
int (*aux_enable)(void);
int error;
int i;
if (i8042_check_aux())
return -ENODEV;
i8042_check_aux()检查8042是否支持aux通道
if (i8042_nomux || i8042_check_mux()) {
error = i8042_create_aux_port(-1);
if (error)
goto err_free_ports;
aux_enable = i8042_enable_aux_port;
} else {
for (i = 0; i < I8042_NUM_MUX_PORTS; i++) {
error = i8042_create_aux_port(i);
if (error)
goto err_free_ports;
}
aux_enable = i8042_enable_mux_ports;
}
一般情况下,流程会转入elsa中.即会通过i8042_create_aux_port()在i8042_ports[]数组中进行相关项的初始化.
error = request_irq(I8042_AUX_IRQ, i8042_interrupt, IRQF_SHARED,
"i8042", i8042_platform_device);
if (error)
goto err_free_ports;
if (aux_enable())
goto err_free_irq;
i8042_aux_irq_registered = 1;
return 0;
为鼠标对应的IRQ设置中断处理例程.并在8042中启用aux
err_free_irq:
free_irq(I8042_AUX_IRQ, i8042_platform_device);
err_free_ports:
i8042_free_aux_ports();
return error;
}
i8042_create_aux_port()代码如下:
static int __devinit i8042_create_aux_port(int idx)
{
struct serio *serio;
int port_no = idx < 0 ? I8042_AUX_PORT_NO : I8042_MUX_PORT_NO + idx;
struct i8042_port *port = &i8042_ports[port_no];
serio = kzalloc(sizeof(struct serio), GFP_KERNEL);
if (!serio)
return -ENOMEM;
serio->id.type = SERIO_8042;
serio->write = i8042_aux_write;
serio->start = i8042_start;
serio->stop = i8042_stop;
serio->port_data = port;
serio->dev.parent = &i8042_platform_device->dev;
if (idx < 0) {
strlcpy(serio->name, "i8042 AUX port", sizeof(serio->name));
strlcpy(serio->phys, I8042_AUX_PHYS_DESC, sizeof(serio->phys));
} else {
snprintf(serio->name, sizeof(serio->name), "i8042 AUX%d port", idx);
snprintf(serio->phys, sizeof(serio->phys), I8042_MUX_PHYS_DESC, idx + 1);
}
port->serio = serio;
port->mux = idx;
port->irq = I8042_AUX_IRQ;
return 0;
}
就是通过这个接口为aux通道在i8042_ports[]中初始化相关项.
Kdb通道的处理也差不多.代码如下:
static int __devinit i8042_setup_kbd(void)
{
int error;
error = i8042_create_kbd_port();
if (error)
return error;
error = request_irq(I8042_KBD_IRQ, i8042_interrupt, IRQF_SHARED,
"i8042", i8042_platform_device);
if (error)
goto err_free_port;
error = i8042_enable_kbd_port();
if (error)
goto err_free_irq;
i8042_kbd_irq_registered = 1;
return 0;
err_free_irq:
free_irq(I8042_KBD_IRQ, i8042_platform_device);
err_free_port:
i8042_free_kbd_port();
return error;
}
它先调用i8042_create_kbd_port()在i8042_ports[]初始化关于kdb通道的相关项.然后为键盘IRQ注册中断处理例程,最后在8042芯片中开通此通道.
来流览下i8042_create_kdb_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;
}
最后在调用i8042_register_ports()在serio总线中注册端口的时候,就会将设备与serio中驱动关联起来了.
来看下8024为键盘IRQ和鼠标IRQ注册的两个中断处理例程:
static irqreturn_t i8042_interrupt(int irq, void *dev_id)
{
struct i8042_port *port;
unsigned long flags;
unsigned char str, data;
unsigned int dfl;
unsigned int port_no;
int ret = 1;
spin_lock_irqsave(&i8042_lock, flags);
str = i8042_read_status();
//如果有输出缓存中有数据,0位会被置为1
if (unlikely(~str & I8042_STR_OBF)) {
spin_unlock_irqrestore(&i8042_lock, flags);
if (irq) dbg("Interrupt %d, without any data", irq);
ret = 0;
goto out;
}
data = i8042_read_data();
spin_unlock_irqrestore(&i8042_lock, flags);
if (i8042_mux_present && (str & I8042_STR_AUXDATA)) {
static unsigned long last_transmit;
static unsigned char last_str;
dfl = 0;
if (str & I8042_STR_MUXERR) {
dbg("MUX error, status is %02x, data is %02x", str, data);
/*
* When MUXERR condition is signalled the data register can only contain
* 0xfd, 0xfe or 0xff if implementation follows the spec. Unfortunately
* it is not always the case. Some KBCs also report 0xfc when there is
* nothing connected to the port while others sometimes get confused which
* port the data came from and signal error leaving the data intact. They
* _do not_ revert to legacy mode (actually I've never seen KBC reverting
* to legacy mode yet, when we see one we'll add proper handling).
* Anyway, we process 0xfc, 0xfd, 0xfe and 0xff as timeouts, and for the
* rest assume that the data came from the same serio last byte
* was transmitted (if transmission happened not too long ago).
*/
switch (data) {
default:
if (time_before(jiffies, last_transmit + HZ/10)) {
str = last_str;
break;
}
/* fall through - report timeout */
case 0xfc:
case 0xfd:
case 0xfe: dfl = SERIO_TIMEOUT; data = 0xfe; break;
case 0xff: dfl = SERIO_PARITY; data = 0xfe; break;
}
}
port_no = I8042_MUX_PORT_NO + ((str >> 6) & 3);
last_str = str;
last_transmit = jiffies;
} else {
dfl = ((str & I8042_STR_PARITY) ? SERIO_PARITY : 0) |
((str & I8042_STR_TIMEOUT) ? SERIO_TIMEOUT : 0);
port_no = (str & I8042_STR_AUXDATA) ?
I8042_AUX_PORT_NO : I8042_KBD_PORT_NO;
}
port = &i8042_ports[port_no];
dbg("%02x <- i8042 (interrupt, %d, %d%s%s)",
data, port_no, irq,
dfl & SERIO_PARITY ? ", bad parity" : "",
dfl & SERIO_TIMEOUT ? ", timeout" : "");
if (unlikely(i8042_suppress_kbd_ack))
if (port_no == I8042_KBD_PORT_NO &&
(data == 0xfa || data == 0xfe)) {
i8042_suppress_kbd_ack--;
goto out;
}
if (likely(port->exists))
serio_interrupt(port->serio, data, dfl);
out:
return IRQ_RETVAL(ret);
}
在这个处理例程里,它会判断是键盘的IRQ还是鼠标的IRQ.然后转向serio_interrupt().在serio总线中我们分析过这个接口.如果serio设备没有被驱动绑定,则重新扫描一下驱动,否则,调用驱动的interrupt处理函数.
四:小结
在本节里,简单的分析了i8042芯片驱动的代码.本节中所涉及到的控制器驱动架构对其它的总线设备也是类似的,如pci,usb等.相信经过这一节的分析过后,我们对linux的设备模型理解会更新的深刻了.