Chinaunix首页 | 论坛 | 博客
  • 博客访问: 134458
  • 博文数量: 79
  • 博客积分: 30
  • 博客等级: 民兵
  • 技术积分: 435
  • 用 户 组: 普通用户
  • 注册时间: 2011-12-16 19:55
文章分类
文章存档

2015年(1)

2013年(1)

2012年(9)

2011年(68)

我的朋友

分类:

2011-12-21 17:11:31

原文地址:Intel8042芯片驱动分析 作者:xgr180

------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处: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的设备模型理解会更新的深刻了.
 
阅读(794) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~