13 终于可以打字了——键盘驱动(上)

如果没有键盘,这篇文章大概是写不出来的,由此就可以看出键盘对于人机交互的重要影响。

首先是一个好消息,键盘在 PIC 里是有外设编号的(希望大家还记得 PIC,否则建议复习第10节),按我们的设定,编号为 33。并不是所有的外设都在 PIC 里自带编号,像网卡啊、声卡这些,都是没有自带编号的。

那么既然如此,新建 drivers/keyboard.c,我们来写一个最简单的键盘驱动:

代码 13-1 最简单的键盘驱动(drivers/keyboard.c)

#include "isr.h"
#include "keyboard.h"

void keyboard_handler(registers_t *regs)
{
    monitor_write("*");
}

void init_keyboard()
{
    register_interrupt_handler(IRQ1, keyboard_handler);
}

代码 13-2 声明(include/keyboard.h)

#ifndef _KEYBOARD_H_
#define _KEYBOARD_H_

void init_keyboard();

#endif

添加了新目录,照例放一下 Makefile:

代码 13-3 如今的 Makefile(Makefile)

OBJS = out/kernel.o out/common.o out/monitor.o out/main.o out/gdtidt.o out/nasmfunc.o out/isr.o out/interrupt.o \
     out/string.o out/timer.o out/memory.o out/mtask.o out/keyboard.o

out/%.o : kernel/%.c
    i686-elf-gcc -c -I include -O0 -fno-builtin -fno-stack-protector -o out/$*.o kernel/$*.c

out/%.o : kernel/%.asm
    nasm -f elf -o out/$*.o kernel/$*.asm

out/%.o : lib/%.c
    i686-elf-gcc -c -I include -O0 -fno-builtin -fno-stack-protector -o out/$*.o lib/$*.c

out/%.o : lib/%.asm
    nasm -f elf -o out/$*.o lib/$*.asm

out/%.o : drivers/%.c
    i686-elf-gcc -c -I include -O0 -fno-builtin -fno-stack-protector -o out/$*.o drivers/$*.c

out/%.o : drivers/%.asm
    nasm -f elf -o out/$*.o drivers/$*.asm

out/%.bin : boot/%.asm
    nasm -I boot/include -o out/$*.bin boot/$*.asm

out/kernel.bin : $(OBJS)
    i686-elf-ld -s -Ttext 0x100000 -o out/kernel.bin $(OBJS)

a.img : out/boot.bin out/loader.bin out/kernel.bin
    dd if=out/boot.bin of=a.img bs=512 count=1
    edimg imgin:a.img copy from:out/loader.bin to:@: copy from:out/kernel.bin to:@: imgout:a.img

run : a.img
    qemu-system-i386 -fda a.img

编译,运行,效果如下两图:

(图 13-1 按下第一个按键前)

(图 13-2 按下第一个按键后)

按下第一个按键后,出现了一个 *,这是非常好的现象。但是,我们发现,后续无论再怎么按键,都完全没有任何作用,屏幕上不再有新的星号出现。

这是为什么呢?查阅资料我们发现,这是键盘控制器(8042、8048)干的好事。当按键被按下时,键盘处理器将根据对应的键产生一个或多个对应的代码,我们称之为扫描码,这个或这些扫描码随即被依次写入到键盘控制器自带的缓冲区中。在写入完后,键盘控制器会立即发送一个中断信号。然而,如果内核在收到中断后不读出这个缓冲区里的扫描码,键盘就会卡死。

键盘控制器的缓冲区端口号为 0x60,我们只需要用 inb(0x60) 就可以读出键盘缓冲区中的扫描码。因此,修改 keyboard_handler,我们来看看读出的扫描码长什么样:

代码 13-4 新版键盘驱动(drivers/keyboard.c)

#include "isr.h"
#include "keyboard.h"

void keyboard_handler(registers_t *regs)
{
    monitor_write_hex(inb(0x60)); // 打印读出的扫描码
}

void init_keyboard()
{
    register_interrupt_handler(IRQ1, keyboard_handler);
}

编译,运行,等待约半分钟后,效果如下:

(图 13-3 运行效果)

在本图中,依次按下了shift、a、a、shift、a、lctrl、lctrl、alt、win这几个键。我们发现,虽然一共只按了9个键,但产生了20个扫描码。这是因为,扫描码不是单独出现,而是成对出现的,按下时产生一组,松开时产生一组。

那么,我们怎么知道每个按键对应的是哪个扫描码呢?一个可实践的方法是,按照上面的顺序依次分别按下,观察屏幕上扫描码的变化。不过,这对写代码解析扫描码是没有帮助的。

还有另外一个办法,就是依次按下键盘上的每一个键,看看它对应的扫描码,然后记录到一个数组或者其他什么地方。不过,这样做实在太过耗时,前人栽树,后人乘凉,我们选择直接把这个数组抄下来:

代码 13-5 从扫描码到每一个键的对应关系(drivers/keymap.c)

#include "keyboard.h"
#include "common.h"

uint32_t keymap[NR_SCAN_CODES * MAP_COLS] = {
    0,            0,           0,
    ESC,          ESC,         0,
    '1',          '!',         0,
    '2',          '@',         0,
    '3',          '#',         0,
    '4',          '$',         0,
    '5',          '%',         0,
    '6',          '^',         0,
    '7',          '&',         0,
    '8',          '*',         0,
    '9',          '(',         0,
    '0',          ')',         0,
    '-',          '_',         0,
    '=',          '+',         0,
    BACKSPACE,    BACKSPACE,   0,
    TAB,          TAB,         0,
    'q',          'Q',         0,
    'w',          'W',         0,
    'e',          'E',         0,
    'r',          'R',         0,
    't',          'T',         0,
    'y',          'Y',         0,
    'u',          'U',         0,
    'i',          'I',         0,
    'o',          'O',         0,
    'p',          'P',         0,
    '[',          '{',         0,
    ']',          '}',         0,
    ENTER,        ENTER,       PAD_ENTER,
    CTRL_L,       CTRL_L,      CTRL_R,
    'a',          'A',         0,
    's',          'S',         0,
    'd',          'D',         0,
    'f',          'F',         0,
    'g',          'G',         0,
    'h',          'H',         0,
    'j',          'J',         0,
    'k',          'K',         0,
    'l',          'L',         0,
    ';',          ':',         0,
    '\'',         '"',         0,
    '`',          '~',         0,
    SHIFT_L,      SHIFT_L,     0,
    '\\',         '|',         0,
    'z',          'Z',         0,
    'x',          'X',         0,
    'c',          'C',         0,
    'v',          'V',         0,
    'b',          'B',         0,
    'n',          'N',         0,
    'm',          'M',         0,
    ',',          '<',         0,
    '.',          '>',         0,
    '/',          '?',         PAD_SLASH,
    SHIFT_R,      SHIFT_R,     0,
    '*',          '*',         0,
    ALT_L,        ALT_L,       ALT_R,
    ' ',          ' ',         0,
    CAPS_LOCK,    CAPS_LOCK,   0,
    F1,           F1,          0,
    F2,           F2,          0,
    F3,           F3,          0,
    F4,           F4,          0,
    F5,           F5,          0,
    F6,           F6,          0,
    F7,           F7,          0,
    F8,           F8,          0,
    F9,           F9,          0,
    F10,          F10,         0,
    NUM_LOCK,     NUM_LOCK,    0,
    SCROLL_LOCK,  SCROLL_LOCK, 0,
    PAD_HOME,     '7',         HOME,
    PAD_UP,       '8',         UP,
    PAD_PAGEUP,   '9',         PAGEUP,
    PAD_MINUS,    '-',         0,
    PAD_LEFT,     '4',         LEFT,
    PAD_MID,      '5',         0,
    PAD_RIGHT,    '6',         RIGHT,
    PAD_PLUS,     '+',         0,
    PAD_END,      '1',         END,
    PAD_DOWN,     '2',         DOWN,
    PAD_PAGEDOWN, '3',         PAGEDOWN,
    PAD_INS,      '0',         INSERT,
    PAD_DOT,      '.',         DELETE,
    0,            0,           0,
    0,            0,           0,
    0,            0,           0,
    F11,          F11,         0,
    F12,          F12,         0,
    0,            0,           0,
    0,            0,           0,
    0,            0,           GUI_L,
    0,            0,           GUI_R,
    0,            0,           APPS,
    0,            0,           0
};

keymap 数组中的第一列代表正常按下对应的键,第二列代表按 shift 时对应的键,第三列代表同样的扫描码前面跟着 0xE0 时候对应的键。后续的解析中,这一段是有大用处的。

这一段数组中出现了非常多的宏,诸如 NR_SCAN_CODESMAP_COLSESC 等等。数组中的宏是每一个键的唯一标识,在保证唯一性的情况下,读者可以任意指定;而剩下的 NR_SCAN_CODESMAP_COLS 则分别为 0x7f3。需要注意的是,keymap 里的索引是按下时的扫描码,而非抬起时的扫描码,实际编程时需要留意一下。

在本教程中使用的一个 keyboard.h 的示例如下:

代码 13-6 各种宏的统一声明(include/keyboard.h)

#ifndef _KEYBOARD_H_
#define _KEYBOARD_H_

void init_keyboard();

#define NR_SCAN_CODES 0x80
#define MAP_COLS      3

#define FLAG_BREAK    0x0080
#define FLAG_EXT      0x0100
#define FLAG_SHIFT_L  0x0200
#define FLAG_SHIFT_R  0x0400
#define FLAG_CTRL_L   0x0800
#define FLAG_CTRL_R   0x1000
#define FLAG_ALT_L    0x2000
#define FLAG_ALT_R    0x4000
#define FLAG_PAD      0x8000

#define MASK_RAW      0x1ff

#define ESC           (0x01 + FLAG_EXT)
#define TAB           (0x02 + FLAG_EXT)
#define ENTER         (0x03 + FLAG_EXT)
#define BACKSPACE     (0x04 + FLAG_EXT)

#define GUI_L         (0x05 + FLAG_EXT)
#define GUI_R         (0x06 + FLAG_EXT)
#define APPS          (0x07 + FLAG_EXT)

#define SHIFT_L       (0x08 + FLAG_EXT)
#define SHIFT_R       (0x09 + FLAG_EXT)
#define CTRL_L        (0x0A + FLAG_EXT)
#define CTRL_R        (0x0B + FLAG_EXT)
#define ALT_L         (0x0C + FLAG_EXT)
#define ALT_R         (0x0D + FLAG_EXT)

#define CAPS_LOCK     (0x0E + FLAG_EXT)
#define NUM_LOCK      (0x0F + FLAG_EXT)
#define SCROLL_LOCK   (0x10 + FLAG_EXT)

#define F1            (0x11 + FLAG_EXT)
#define F2            (0x12 + FLAG_EXT)
#define F3            (0x13 + FLAG_EXT)
#define F4            (0x14 + FLAG_EXT)
#define F5            (0x15 + FLAG_EXT)
#define F6            (0x16 + FLAG_EXT)
#define F7            (0x17 + FLAG_EXT)
#define F8            (0x18 + FLAG_EXT)
#define F9            (0x19 + FLAG_EXT)
#define F10           (0x1A + FLAG_EXT)
#define F11           (0x1B + FLAG_EXT)
#define F12           (0x1C + FLAG_EXT)

#define PRINTSCREEN   (0x1D + FLAG_EXT)
#define PAUSEBREAK    (0x1E + FLAG_EXT)
#define INSERT        (0x1F + FLAG_EXT)
#define DELETE        (0x20 + FLAG_EXT)
#define HOME          (0x21 + FLAG_EXT)
#define END           (0x22 + FLAG_EXT)
#define PAGEUP        (0x23 + FLAG_EXT)
#define PAGEDOWN      (0x24 + FLAG_EXT)
#define UP            (0x25 + FLAG_EXT)
#define DOWN          (0x26 + FLAG_EXT)
#define LEFT          (0x27 + FLAG_EXT)
#define RIGHT         (0x28 + FLAG_EXT)

#define POWER         (0x29 + FLAG_EXT)
#define SLEEP         (0x2A + FLAG_EXT)
#define WAKE          (0x2B + FLAG_EXT)

#define PAD_SLASH     (0x2C + FLAG_EXT)
#define PAD_STAR      (0x2D + FLAG_EXT)
#define PAD_MINUS     (0x2E + FLAG_EXT)
#define PAD_PLUS      (0x2F + FLAG_EXT)
#define PAD_ENTER     (0x30 + FLAG_EXT)
#define PAD_DOT       (0x31 + FLAG_EXT)
#define PAD_0         (0x32 + FLAG_EXT)
#define PAD_1         (0x33 + FLAG_EXT)
#define PAD_2         (0x34 + FLAG_EXT)
#define PAD_3         (0x35 + FLAG_EXT)
#define PAD_4         (0x36 + FLAG_EXT)
#define PAD_5         (0x37 + FLAG_EXT)
#define PAD_6         (0x38 + FLAG_EXT)
#define PAD_7         (0x39 + FLAG_EXT)
#define PAD_8         (0x3A + FLAG_EXT)
#define PAD_9         (0x3B + FLAG_EXT)

#define PAD_UP        PAD_8
#define PAD_DOWN      PAD_2
#define PAD_LEFT      PAD_4
#define PAD_RIGHT     PAD_6
#define PAD_HOME      PAD_7
#define PAD_END       PAD_1
#define PAD_PAGEUP    PAD_9
#define PAD_PAGEDOWN  PAD_3
#define PAD_INS       PAD_0
#define PAD_MID       PAD_5
#define PAD_DEL       PAD_DOT

#define KB_DATA  0x60
#define KB_CMD   0x64
#define LED_CODE 0xED
#define KB_ACK   0xFA

#endif

这里给所有在 keymap 数组中用到的宏统一加了 FLAG_EXT,这是为了后期处理时可以分辨可打印的字符对应的键(如asdf)与不可打印的字符对应的键(如F11、F12)。

MakefileOBJS 中追加一个 keymap.o,由于我们尚未开始解析扫描码,所以这一部分没有变化。

接下来,为了存储获得到的扫描码,我们来做一个存储扫描码用的数据结构。显然,先按下的键需要先被处理,所以我们选择做一个队列。

基本数据结构相关的代码就不解说了,下面是一个没那么经典的环形队列的实现:

代码 13-7 FIFO队列的实现(lib/fifo.c)

#include "fifo.h"

void fifo_init(fifo_t *fifo, int size, uint32_t *buf)
{
    fifo->size = size;
    fifo->buf = buf;
    fifo->free = size;
    fifo->flags = 0;
    fifo->p = 0;
    fifo->q = 0;
}

int fifo_put(fifo_t *fifo, uint32_t data)
{
    if (fifo->free == 0) {
        fifo->flags |= FIFO_FLAGS_OVERRUN;
        return -1;
    }
    fifo->buf[fifo->p] = data;
    fifo->p++;
    if (fifo->p == fifo->size) fifo->p = 0;
    fifo->free--;
    return 0;
}

int fifo_get(fifo_t *fifo)
{
    int data;
    if (fifo->free == fifo->size) return -1;
    data = fifo->buf[fifo->q];
    fifo->q++;
    if (fifo->q == fifo->size) fifo->q = 0;
    fifo->free++;
    return data;
}

int fifo_status(fifo_t *fifo)
{
    return fifo->size - fifo->free;
}

代码 13-8 声明(include/fifo.h)

#ifndef _FIFO_H_
#define _FIFO_H_

#include "common.h"

typedef struct FIFO {
    uint32_t *buf;
    int p, q, size, free, flags;
} fifo_t;

#define FIFO_FLAGS_OVERRUN 1

void fifo_init(fifo_t *fifo, int size, uint32_t *buf);
int fifo_put(fifo_t *fifo, uint32_t data);
int fifo_get(fifo_t *fifo);
int fifo_status(fifo_t *fifo);

#endif

MakefileOBJS 中追加 out/fifo.o,编译运行,效果仍应不变,因为这个队列我们也还没开始用。

具体实践中如何使用这样一个队列呢?我们先来到 keyboard.c,创建一个存储扫描码用的 keyfifo

代码 13-9 创建 keyfifo(drivers/keyboard.c)

#include "isr.h"
#include "keyboard.h"
#include "fifo.h"

fifo_t keyfifo;
uint32_t keybuf[32];
extern uint32_t keymap[];

void keyboard_handler(registers_t *regs)
{
    monitor_write_hex(inb(0x60));
}

void init_keyboard()
{
    fifo_init(&keyfifo, 32, keybuf);
    register_interrupt_handler(IRQ1, keyboard_handler);
}

然后在 keyboard_handler 中,我们存储扫描码到 keyfifo

代码 13-10 存入扫描码(drivers/keyboard.c)

static uint8_t get_scancode()
{
    uint8_t scancode;
    asm("cli");
    scancode = fifo_get(&keyfifo);
    asm("sti");
    return scancode;
}

static void keyboard_read()
{
    if (fifo_status(&keyfifo) > 0) {
        uint8_t scancode = get_scancode();
        monitor_write_hex(scancode);
    }
}

void keyboard_handler(registers_t *regs)
{
    fifo_put(&keyfifo, inb(KB_DATA));
    keyboard_read();
}

我们同时还新建了 keyboard_readget_scancode 两个函数,未来我们对键盘数据的处理将主要在 keyboard_read 当中进行。

编译,运行,效果仍应不变,因为我们还没有开始处理扫描码。事不过三,我们马上就开始处理工作。

代码 13-11 初步处理扫描码(drivers/keyboard.c)

static void keyboard_read()
{
    uint8_t scancode;
    int make;
    if (fifo_status(&keyfifo) > 0) {
        scancode = get_scancode();
        if (scancode == 0xE1) {
            // 特殊开头,暂不做处理
        } else if (scancode == 0xE0) {
            // 特殊开头,暂不做处理
        } else {
            make = (scancode & FLAG_BREAK ? true : false);
            if (make) {
                char key = keymap[(scancode & 0x7f) * MAP_COLS];
                monitor_put(key);
            }
        }
    }
}

FLAG_BREAK 在之前的 keyboard.h 中已有定义,是 0x80。多启动几次按几个键会发现,除了一部分产生多个扫描码的键以外,每次按下的扫描码比抬起的扫描码少 0x80。因此只需要探测 0x80 是否存在,就可以确定现在的这个键是被按下还是被抬起,选择一个处理即可。

这就是 else 中第一行的作用,这里选择的是被抬起时进行判断。scancode & 0x7f 可以取得对应的被按下时的扫描码,从而作为 keymap 的索引获得对应的键。

好了,现在编译运行,等待 task_c 输出完成后,输入 abc123,效果如下:

(图 13-4 成功输出)

我们看到了后面的 abc123,说明我们的键盘驱动已经初步完成。本节的篇幅已经够长了,下一节我们将继续写键盘驱动,做出一个基本的处理框架。