LINUX设备驱动之键盘驱动

    技术2025-09-20  125

    Eric Fang  2010-01-23

    --------------------------------------------------------------

    本站分析linux内核源码,版本号为2.6.32.3

    转载请注明出处:http://ericfang.cublog.cn/

    --------------------------------------------------------------

    在前面的分析中我们看到了linux设备驱动程序的分层结构,每一层可能有不同型的下层,每一层的驱动程序都服务于它的上下层,也就是为上层和下层提供相应的程序接口,我们今天要分析的键盘驱动就为它上面的输入子系统和下面的串行I/O等提供服务,我们编写设备驱动程序就是要编写这些相应的接口函数。

     

    键盘按接口类型分有ps/2键盘、at键盘、usb键盘、r232键盘等,drivers/input/keyboard目录下都是有关键盘的驱动程序,本文将分析AT PS/2键盘驱动,主机通过intel8042芯片连接到intel8048键盘芯片,如果你对intel8042驱动还不熟悉,就请先阅读本站相关文章。

     

    drivers/input/keyboard目录下的atkbd.c,连接到intel8042芯片的所有键盘都将使用这个文件所描述的驱动程序,我们将一步一步解开这个日常和我们手指接触最密切的设备是怎样工作的。

     

    一.模块的初始化

    static int __init atkbd_init(void)

    {

           dmi_check_system(atkbd_dmi_quirk_table);

     

           return serio_register_driver(&atkbd_drv);

    }

    调用serio_register_driver(&atkbd_drv)注册一个串行I/O驱动atkbd_drv,看下atkbd_drv结构:

    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,

    };

    我们在前面分析中已经知道:在注册驱动时会进行设备驱动的匹配,调用总线上的matchprobe函数,我们再次粘出serio总线match函数最终匹配部分的代码:

    static int serio_match_port(const struct serio_device_id *ids, struct serio *serio)

    {

           while (ids->type || ids->proto) {

                  if ((ids->type == SERIO_ANY || ids->type == serio->id.type) &&

                      (ids->proto == SERIO_ANY || ids->proto == serio->id.proto) &&

                      (ids->extra == SERIO_ANY || ids->extra == serio->id.extra) &&

                      (ids->id == SERIO_ANY || ids->id == serio->id.id))

                         return 1;

                  ids++;

           }

           return 0;

    }

    这里如果驱动的id_table中包含了设备的id字段中的信息,则会匹配成功。

    回想一下我们在注册键盘鼠标通道时是这样设置其内嵌的serioid字段的type属性:

    serio->id.type         = i8042_direct ? SERIO_8042 : SERIO_8042_XL;

    再看看tkbd_drvid_table赋值为atkbd_serio_ids

    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 }

    };

    很明显i8042serio设备的id字段和atkbd_serio_ids[0]相匹配。

    match匹配成功后,接着调用总线上的probe函数,我们在串行I/O的分析中知道串行总线的probe函数最终会回调用驱动的connect()函数,这里atkbd_drv驱动的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_reset_state(atkbd);

                  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;

    }

    这个函数创建一个input_dev设备dev和一个atkbd设备atkbdatkbd内嵌的input_devdevinput_dev是输入子系统的设备,我们这里先记住就好,后续再分析输入子系统,接着进行一系列的初始化和设置,包括键盘扫描码等,扫描码就是将intel8042中读取到的数据转换上层(输入子系统)所能识别的编码。

    然后创建dev的相关属性,激活atkbd,注册dev设备到输入子系统。

     

    注册驱动时会去匹配总线上所有已注册了设备,在i8042模块的初始化过程中分别为鼠标和键盘注册了相关的serio设备,而这些设备和atkbd_drv都是相匹配的,所以每个键盘鼠标设备在输入子系统都有他们对应的input_dev设备。

    初始化部分分析完了。

     

    二.键盘驱动的中断处理

    Serio总线的中断处理最终回调对应设备的驱动的中断处理程序,对于通过i8042键盘接口连接的键盘鼠标设备在Serio总线上的驱动为atkbd_drv,其中断处理函数为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;

    获得serio对应的atkbdinput_dev

           unsigned int code = data;

           int scroll = 0, hscroll = 0, click = -1;

           int value;

           unsigned short keycode;

     

    #ifdef ATKBD_DEBUG

           printk(KERN_DEBUG "atkbd.c: Received %02x flags %02x/n", data, flags);

    #endif

     

    #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;

           }

    不是在x86的情况,这里说明一下:

    根据输入标志flags判断是不是data出错了要求设备重发一次,serio_write函数:

    static inline int serio_write(struct serio *serio, unsigned char data)

    {

           if (serio->write)

                  return serio->write(serio, data);

           else

                  return -1;

    }

    调用serio设备的write函数,记得我们在注册键盘鼠标通道时为它内嵌的seriowrite赋值如下:

    serio->write           = i8042_dumbkbd ? NULL : i8042_kbd_write;

    i8042_kbd_write函数如下:

    static int i8042_kbd_write(struct serio *port, unsigned char c)

    {

           unsigned long flags;

           int retval = 0;

     

           spin_lock_irqsave(&i8042_lock, flags);

     

           if (!(retval = i8042_wait_write())) {

                  dbg("%02x -> i8042 (kbd-data)", c);

                  i8042_write_data(c);

           }

     

           spin_unlock_irqrestore(&i8042_lock, flags);

     

           return retval;

    }

    也就是往i8042发送数据,这里发送的数据是ATKBD_CMD_RESENDi8042收到这个数据后会要求相应的设备重发一次上次发送的数据。

    调用serio_write函数后会把atkbd->resend设为1,如果下一次收到的数据还是出错会判断这个值,如果已经重发过了就不会再要求重发。

    接着atkbd_interrupt中的代码:

           if (!flags && data == ATKBD_RET_ACK)

                  atkbd->resend = 0;

    #endif

    如果数据没有出错,也不是i8042的反馈信号数据,则清atkbd->resend

           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;

    如果datai8042ps2dev的应答信号,则做相应的处理,唤醒在ps2dev->wait上等待的进程,然后跳出中断例程。

           if (!atkbd->enabled)

                  goto out;

    如果atkbd没有激活就退出。

           input_event(dev, EV_MSC, MSC_RAW, code);

    上班包一个事件给输入子系统。

           if (atkbd_platform_scancode_fixup)

                  code = atkbd_platform_scancode_fixup(atkbd, code);

    如果atkbd_platform_scancode_fixup函数被定义则调用它,这个函数是针对某些特殊的平台而设计的,其初始化在模块初始化时调用dmi_check_system(atkbd_dmi_quirk_table)里边。

           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;

    要求设备重新连接驱动,最终会调用驱动atkbd_drvreconnect接口函数,即atkbd_reconnect

                  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);

    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 <keycode>' 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);

    向输入子系统上报按键事件。

                         input_sync(dev);

    向输入子系统上报sync事件。

                         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;

    }

     

    由于这里涉及到输入子系统的内容,等我们分析到输入子系统可以再回头来看这段程序。

     

    三.与驱动相关的一些其他操作

    atkbd_drv设备驱动结构中还定于的reconnectdisconnectcleanup函数,这些函数会在什么地方调用呢?

    我们在串行I/O的分析中知道对serio设备的操作会产生一些事件, SERIO_RECONNECT_PORTSERIO_RECONNECT_CHAINSERIO_ATTACH_DRIVER类型的事件的相应的处理就会去调用serio设备对应的设备的驱动的reconnectdisconnect函数,在系统休眠时会调用设备驱动的cleanup函数,系统唤醒时会产生SERIO_RECONNECT_CHAIN事件,另外,我们在文件系统中对相应的serio设备的某些属性写操作时也会产生serio事件,serio_bus总线结构的dev_attrsdrv_attrs属性定义了这些属性的相关操作。详细的内容请自行到代码中去分析。

     

    键盘驱动部分内容就分析到这里,由于这部分内容涉及到输入子系统,后面分析输入子系统可以再回头看这部分的内容,以加深理解。

    最新回复(0)