手写简易操作系统(十七)--编写键盘驱动

前情提要

上一节我们实现了锁与信号量,这一节我们就可以实现键盘驱动了,访问键盘输入的数据也属于临界区资源,所以需要锁的存在。

一、键盘简介

之前的 ps/2 键盘使用的是中断驱动的,在当时,按下键盘就会触发中断,引导操作系统去处理这个按键行文。但是当今的usb键盘,使用的是轮询机制,cpu会定时访问键盘看有没有按下键盘。

我个人认为这是cpu技术的进步导致的,在之前,cpu的频率比较低,使用轮询可能会导致漏掉用户按键的行为。但是在今天,cpu的主频已经非常高了,处理一个按键行为就触发中断,这个开销太大了,而且轮询的频率也上来了,现在每秒访问几千次对电脑一点影响都没有,所以现在大多采用了轮询机制。

不过据说中断驱动的还是比较快,现在一些电竞主板还是支持ps/2的接口,这个未经论证。

1.1、键盘的通码与断码

键盘的状态要么是按下,要么是弹起,因此一个键便有两个编码,按键按下时的编码叫做通码,键盘上的触电接通了电路,使硬件产生了一个编码,故此通码叫makecode。按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为breakcode。

无论是按下键,或是松开键,当键的状态改变后,键盘中的8048芯片把按键对应的扫描码(通码或断码)发送到主板上的8042芯片,由8042处理后保存在自己的寄存器中,然后向8259A发送中断信号,这样处理器便去执行键盘中断处理程序,将8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。

1.2、键盘扫描码

键的扫描码是由键盘中的键盘编码器决定的,不同的编码方案便是不同的键盘扫描码,也就是说,相同的键在不同的编码方案下产生的通码和断码也是不同的。

根据不同的编码方案,键盘扫描码有三套,分别称为scan code set 1、scan code set 2、scan code set 3。

其中scan code set 1是XT键盘用的扫描码,这个历史就比较久远了。scan code set 2是AT键盘的扫描码,这个键盘和我们当今的键盘也不是很一样,但是已经比较接近了。scan code set 3是IBM PS/2系列高端计算机所用的键盘上,IBM蓝色巨人现在都凉了,这个键盘也就很少看到了。

第二套键盘扫描码几乎是目前所使用的键盘的标准,因此大多数键盘向8042发送的扫描码都是第二套扫描码。但是难免有别的键盘,所以才会出现8042这个芯片,这个芯片做一个中间层,为了兼容第一套键盘扫描码对应的中断处理程序,不管键盘用的是何种键盘扫描码,当键盘将扫描码发送到8042后,都由8042转换成第一套扫描码,我们再从8042中读取扫描码。

这里我们给出常用键位的扫描码(这里的扫描码就是通码,加0x80就是断码)

按键扫描码按键扫描码按键扫描码
Esc0x01F10x3BF20x3C
F30x3DF40x3EF50x3F
F60x40F70x41F80x42
F90x43F100x44F110x57
F120x58PrintSc0x37ScrollLk0x46
Pause/Brk0x45`0x2910x02
20x0330x0440x05
50x0660x0770x08
80x0990x0A00x0B
-0x0C=0x0DBackspace0x0E
Tab0x0FQ0x10W0x11
E0x12R0x13T0x14
Y0x15U0x16I0x17
O0x18P0x19[0x1A
]0x1B|0x2BCapsLock0x3A
A0x1ES0x1FD0x20
F0x21G0x22H0x23
J0x24K0x25L0x26
;0x270x28Enter0x1C
Shift左0x2AZ0x2CX0x2D
C0x2EV0x2FB0x30
N0x31M0x32,0x33
.0x34/0x35Shift右0x36
Ctrl左0x1DWin左0xE0Alt左0x38
Space0x39Alt右0xE038Win右0xE0
Menu0xE0Ctrl右0xE01D

问:为什么会有通码和断码,通码不就够了嘛

**答:**如果按一个组合键的话,比如ctrl+a,是先按下ctrl,再按a,再松开ctrl,再松开a。如果没有断码,我们无法判断ctrl是否松开。

1.3、键盘的芯片

和键盘相关的芯片只有8042和8048,它们都是独立的处理器,都有自己的寄存器和内存。Intel 8048芯片或兼容芯片位于键盘中,它是键盘编码器,Intel 8042芯片或兼容芯片被集成在主板上的南桥芯片中,它是键盘控制器,也就是键盘的IO接口,因此它是8048的代理,也是前面所得到的处理器和键盘的“中间层”。我们只需要学习8042就够了

他的端口如下

寄存器端口读写
Output Buffer(输出缓冲区)0x60
Input Buffer(输入缓冲区)0x60
Status Register(状态寄存器)0x64
Control Register(控制寄存器)0x64

状态寄存器8位宽度的寄存器,只读,反映8048和8042的内部工作状态。各位意义如下。

(1)位0:置1时表示输出缓冲区寄存器已满,处理器通过in指令读取后该位自动置0。
(2)位1:置1时表示输入缓冲区寄存器已满,8042将值读取后该位自动置0。
(3)位2:系统标志位,最初加电时为0,自检通过后置为1。
(4)位3:置1时,表示输入缓冲区中的内容是命令,置0时,输入缓冲区中的内容是普通数据。
(5)位4:置1时表示键盘启用,置0时表示键盘禁用。
(6)位5:置1时表示发送超时。
(7)位6:置1时表示接收超时。
(8)位7:来自8048的数据在奇偶校验时出错。

8位宽度的寄存器,只写,用于写入命令控制字。每个位都可以设置一种工作方式,意义如下。

(1)位0:置1时启用键盘中断。
(2)位1:置1时启用鼠标中断。
(3)位2:设置状态寄存器的位2。
(4)位3:置1时,状态寄存器的位4无效。
(5)位4:置1时禁止键盘。
(6)位5:置1时禁止鼠标。
(7)位6:将第二套键盘扫描码转换为第一套键盘扫描码。
(8)位7:保留位,默认为0。

二、环形队列

键盘中断的数据是放在队列中的,等待其他线程的读取。如果我们之前做过关于软件相关的工作,很容易理解这个概念,就是buffer,缓冲区。因为我们是一直在输入的,所以这里设计成了环形队列。

我们看一下环形队列的数据结构

#define bufsize 256

/* 环形队列 */
struct ioqueue {
    // 生产者消费者问题
    struct lock lock;
    // 生产者,缓冲区不满时就继续往里面放数据
    struct task_struct* producer;
    // 消费者,缓冲区不空时就继续从往里面拿数据
    struct task_struct* consumer;
    char buf[bufsize];			// 缓冲区大小
    int32_t head;			    // 队首,数据往队首处写入
    int32_t tail;			    // 队尾,数据从队尾处读出
};

这个就很明朗了。一个生产者一个消费者,生产者向buf中添加数据,消费者从buf中取出数据,为了防止buf中的数据出错,生产者和消费者同时只能有一个可以访问到buf。如果buf中数据满了,生产者就不能放了,此时阻塞生产者,如果buf中数据为空,消费者就不能拿了,此时阻塞消费者。

我们看一下具体的实现

/* 初始化io队列ioq */
void ioqueue_init(struct ioqueue* ioq) {
    lock_init(&ioq->lock);                 // 初始化io队列的锁
    ioq->producer = ioq->consumer = NULL;  // 生产者和消费者置空
    ioq->head = ioq->tail = 0;             // 队列的首尾指针指向缓冲区数组第0个位置
}

/* 返回pos在缓冲区中的下一个位置值 */
static inline int32_t next_pos(int32_t pos) {
    return (pos + 1) % bufsize;
}

/* 判断队列是否已满 */
bool ioq_full(struct ioqueue* ioq) {
    return next_pos(ioq->head) == ioq->tail;
}

/* 判断队列是否已空 */
bool ioq_empty(struct ioqueue* ioq) {
    return ioq->head == ioq->tail;
}

/* 使当前生产者或消费者在此缓冲区上等待 */
static void ioq_wait(struct task_struct** waiter) {
    // 二级指针不为空,指向的pcb指针地址为空
    ASSERT(*waiter == NULL && waiter != NULL);
    *waiter = running_thread();
    thread_block(TASK_BLOCKED);
}

/* 唤醒waiter */
static void wakeup(struct task_struct** waiter) {
    // 二级指针指向不为空
    ASSERT(*waiter != NULL);
    thread_unblock(*waiter);
    *waiter = NULL;
}

/* 消费者从ioq队列中获取一个字符 */
char ioq_getchar(struct ioqueue* ioq) {
    // 若缓冲区(队列)为空,把消费者ioq->consumer记为当前线程自己,等待生产者唤醒
    while (ioq_empty(ioq)) {
        lock_acquire(&ioq->lock);
        ioq_wait(&ioq->consumer);
        lock_release(&ioq->lock);
    }

    char byte = ioq->buf[ioq->tail];	  // 从缓冲区中取出
    ioq->tail = next_pos(ioq->tail);	  // 把读游标移到下一位置

    if (ioq->producer != NULL) {
        wakeup(&ioq->producer);		      // 唤醒生产者
    }

    return byte;
}

/* 生产者往ioq队列中写入一个字符byte */
void ioq_putchar(struct ioqueue* ioq, char byte) {
    // 若缓冲区(队列)已经满了,把生产者ioq->producer记为自己,等待消费者线程唤醒自己
    while (ioq_full(ioq)) {
        lock_acquire(&ioq->lock);
        ioq_wait(&ioq->producer);
        lock_release(&ioq->lock);
    }
    ioq->buf[ioq->head] = byte;      // 把字节放入缓冲区中
    ioq->head = next_pos(ioq->head); // 把写游标移到下一位置

    if (ioq->consumer != NULL) {
        wakeup(&ioq->consumer);      // 唤醒消费者
    }
}

我们看一下后面两个函数,waitwakeup,这两个函数,这两个函数传入的是一个pcb指针的地址,所以这里是一个二级指针。所以无论是阻塞还是解除阻塞都是取这个二级指针的地址,也就得到了pcb指针。这里对于不熟悉指针的人来说可能会有点扰。

三、键盘驱动

#define KBD_BUF_PORT 0x60	 // 键盘buffer寄存器端口号为0x60

/* 用转义字符定义部分控制字符 */
#define esc		    '\033'	 // 八进制表示字符,也可以用十六进制'\x1b'
#define backspace	'\b'
#define tab		    '\t'
#define enter		'\r'
#define delete		'\177'	 // 八进制表示字符,十六进制为'\x7f'

/* 以上不可见字符一律定义为0 */
#define char_invisible	0
#define ctrl_l_char	    char_invisible
#define ctrl_r_char	    char_invisible
#define shift_l_char    char_invisible
#define shift_r_char	char_invisible
#define alt_l_char	    char_invisible
#define alt_r_char	    char_invisible
#define caps_lock_char	char_invisible

/* 定义控制字符的通码和断码 */
#define shift_l_make	0x2a
#define shift_r_make 	0x36 
#define alt_l_make   	0x38
#define alt_r_make   	0xe038
#define alt_r_break   	0xe0b8
#define ctrl_l_make  	0x1d
#define ctrl_r_make  	0xe01d
#define ctrl_r_break 	0xe09d
#define caps_lock_make 	0x3a

struct ioqueue kbd_buf;	   // 定义键盘缓冲区

/* 定义以下变量记录相应键是否按下的状态,
 * ext_scancode用于记录makecode是否以0xe0开头 */
static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;

/* 以通码make_code为索引的二维数组 */
static char keymap[][2] = {
    /* 扫描码   未与shift组合  与shift组合*/
    /* ---------------------------------- */
    /* 0x00 */	{0,	0},
    /* 0x01 */	{esc,	esc},
    /* 0x02 */	{'1',	'!'},
    /* 0x03 */	{'2',	'@'},
    /* 0x04 */	{'3',	'#'},
    /* 0x05 */	{'4',	'$'},
    /* 0x06 */	{'5',	'%'},
    /* 0x07 */	{'6',	'^'},
    /* 0x08 */	{'7',	'&'},
    /* 0x09 */	{'8',	'*'},
    /* 0x0A */	{'9',	'('},
    /* 0x0B */	{'0',	')'},
    /* 0x0C */	{'-',	'_'},
    /* 0x0D */	{'=',	'+'},
    /* 0x0E */	{backspace, backspace},
    /* 0x0F */	{tab,	tab},
    /* 0x10 */	{'q',	'Q'},
    /* 0x11 */	{'w',	'W'},
    /* 0x12 */	{'e',	'E'},
    /* 0x13 */	{'r',	'R'},
    /* 0x14 */	{'t',	'T'},
    /* 0x15 */	{'y',	'Y'},
    /* 0x16 */	{'u',	'U'},
    /* 0x17 */	{'i',	'I'},
    /* 0x18 */	{'o',	'O'},
    /* 0x19 */	{'p',	'P'},
    /* 0x1A */	{'[',	'{'},
    /* 0x1B */	{']',	'}'},
    /* 0x1C */	{enter,  enter},
    /* 0x1D */	{ctrl_l_char, ctrl_l_char},
    /* 0x1E */	{'a',	'A'},
    /* 0x1F */	{'s',	'S'},
    /* 0x20 */	{'d',	'D'},
    /* 0x21 */	{'f',	'F'},
    /* 0x22 */	{'g',	'G'},
    /* 0x23 */	{'h',	'H'},
    /* 0x24 */	{'j',	'J'},
    /* 0x25 */	{'k',	'K'},
    /* 0x26 */	{'l',	'L'},
    /* 0x27 */	{';',	':'},
    /* 0x28 */	{'\'',	'"'},
    /* 0x29 */	{'`',	'~'},
    /* 0x2A */	{shift_l_char, shift_l_char},
    /* 0x2B */	{'\\',	'|'},
    /* 0x2C */	{'z',	'Z'},
    /* 0x2D */	{'x',	'X'},
    /* 0x2E */	{'c',	'C'},
    /* 0x2F */	{'v',	'V'},
    /* 0x30 */	{'b',	'B'},
    /* 0x31 */	{'n',	'N'},
    /* 0x32 */	{'m',	'M'},
    /* 0x33 */	{',',	'<'},
    /* 0x34 */	{'.',	'>'},
    /* 0x35 */	{'/',	'?'},
    /* 0x36	*/	{shift_r_char, shift_r_char},
    /* 0x37 */	{'*',	'*'},
    /* 0x38 */	{alt_l_char, alt_l_char},
    /* 0x39 */	{' ',	' '},
    /* 0x3A */	{caps_lock_char, caps_lock_char}
    /*其它按键暂不处理*/
};

/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {

    /* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */
    bool ctrl_down_last = ctrl_status;
    bool shift_down_last = shift_status;
    bool caps_lock_last = caps_lock_status;

    uint16_t scancode = inb(KBD_BUF_PORT);

    // 若扫描码是e0开头的, 结束此次中断处理函数,等待下一个扫描码进来
    if (scancode == 0xe0) {
        ext_scancode = true;    // 打开e0标记
        return;
    }

    // 如果上次是以0xe0开头,将扫描码合并
    if (ext_scancode) {
        scancode = ((0xe000) | scancode);
        ext_scancode = false;   // 关闭e0标记
    }

    // 若是断码(按键弹起时产生的扫描码)
    if ((scancode & 0x0080) != 0) {
        // 获得相应的通码
        uint16_t make_code = (scancode &= 0xff7f);
        // 若是任意以下三个键弹起了,将状态置为false
        if (make_code == ctrl_l_make || make_code == ctrl_r_make) {
            ctrl_status = false;
        }
        else if (make_code == shift_l_make || make_code == shift_r_make) {
            shift_status = false;
        }
        else if (make_code == alt_l_make || make_code == alt_r_make) {
            alt_status = false;
        }
        // 若是其他非控制键位,不需要处理,那些键位我们只需要知道通码
        return;

    }
    // 若是通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code
    else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)) {
        // keymap的二维索引
        bool shift = false;
        // 按下的键不是字母
        if ((scancode < 0x0e) || (scancode == 0x29) || \
            (scancode == 0x1a) || (scancode == 0x1b) || \
            (scancode == 0x2b) || (scancode == 0x27) || \
            (scancode == 0x28) || (scancode == 0x33) || \
            (scancode == 0x34) || (scancode == 0x35)) {  
            if (shift_down_last) {
                shift = true;
            }
        }
        // 如果按下的键是字母,需要和CapsLock配合
        else {
            if (shift_down_last && caps_lock_last) {      // 如果shift和capslock同时按下
                shift = false;
            }
            else if (shift_down_last || caps_lock_last) { // 如果shift和capslock任意被按下
                shift = true;
            }
            else {
                shift = false;
            }
        }

        // 将扫描码的高字节置0,主要是针对高字节是e0的扫描码.
        uint8_t index = (scancode &= 0x00ff);
        // 在数组中找到对应的字符
        char cur_char = keymap[index][shift];

        // 如果cur_char不为0,也就是ascii码为除'\0'外的字符就加入键盘缓冲区中
        if (cur_char) {
            // 如果ctrl按下,且输入的字符为‘l’或者‘u’,那就保存为 cur_char-‘a’,主要是‘a’前面26位没啥用
            if ((ctrl_down_last && cur_char == 'l') || (ctrl_down_last && cur_char == 'u')) {
                cur_char -= 'a';
            }

            // 如果缓冲区未满,就将其加入缓冲区
            if (!ioq_full(&kbd_buf)) {
                ioq_putchar(&kbd_buf, cur_char);
            }
            return;
        }

        // 记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键
        if (scancode == ctrl_l_make || scancode == ctrl_r_make) {
            ctrl_status = true;
        }
        else if (scancode == shift_l_make || scancode == shift_r_make) {
            shift_status = true;
        }
        else if (scancode == alt_l_make || scancode == alt_r_make) {
            alt_status = true;
        }
        // 这里注意,大写的锁定键是取反
        else if (scancode == caps_lock_make) {
            caps_lock_status = !caps_lock_status;
        }
    }
    else {
        put_str("unknown key\n");
    }
}

/* 键盘初始化 */
void keyboard_init() {
    put_str("keyboard init start\n");
    ioqueue_init(&kbd_buf);
    register_handler(0x21, intr_keyboard_handler);
    put_str("keyboard init done\n");
}

键盘驱动就稍显复杂一点,主要是涉及到了shiftctrlaltcaplock这些个控制键,这些键位是否按下所表示的通码断码是不一样的。这里就是处理字符,相信大家看代码就可以看明白。

四、仿真

我们创建一个线程,键盘输入什么,打印什么

image-20240325171327961

结束语

本节我们编写了键盘驱动以及其使用的环形队列数据结构。下一节我们将实现一个用户进程,即特权级为3的进程。

老规矩,代码地址为 https://github.com/lyajpunov/os

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LyaJpunov

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值