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

前情提要

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

一、键盘简介

之前的 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就是断码)

按键 扫描码 按键 扫描码 按键 扫描码
Esc 0x01 F1 0x3B F2 0x3C
F3 0x3D F4 0x3E F5 0x3F
F6 0x40 F7 0x41 F8 0x42
F9 0x43 F10 0x44 F11 0x57
F12 0x58 PrintSc 0x37 ScrollLk 0x46
Pause/Brk 0x45 ` 0x29 1 0x02
2 0x03 3 0x04 4 0x05
5 0x06 6 0x07 7 0x08
8 0x09 9 0x0A 0 0x0B
- 0x0C = 0x0D Backspace 0x0E
Tab 0x0F Q 0x10 W 0x11
E 0x12 R 0x13 T 0x14
Y 0x15 U 0x16 I 0x17
O 0x18 P 0x19 [ 0x1A
] 0x1B | 0x2B CapsLock 0x3A
A 0x1E S 0x1F D 0x20
F 0x21 G 0x22 H 0x23
J 0x24 K 0x25 L 0x26
; 0x27 ' 0x28 Enter 0x1C
Shift左 0x2A Z 0x2C X 0x2D
C 0x2E V 0x2F B 0x30
N 0x31 M 0x32 , 0x33
. 0x34 / 0x35 Shift右 0x36
Ctrl左 0x1D Win左 0xE0 Alt左 0x38
Space 0x39 Alt右 0xE038 Win右 0xE0
Menu 0xE0 Ctrl右 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,缓冲区。因为我们是一直在输入的,所以这里设计成了环形队列。

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

c 复制代码
#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中数据为空,消费者就不能拿了,此时阻塞消费者。

我们看一下具体的实现

c 复制代码
/* 初始化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指针。这里对于不熟悉指针的人来说可能会有点扰。

三、键盘驱动

c 复制代码
#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这些个控制键,这些键位是否按下所表示的通码断码是不一样的。这里就是处理字符,相信大家看代码就可以看明白。

四、仿真

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

结束语

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

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

相关推荐
yutian06063 小时前
Keil MDK下载程序后MCU自动重启设置
单片机·嵌入式硬件·keil
析木不会编程5 小时前
【小白51单片机专用教程】protues仿真独立按键控制LED
单片机·嵌入式硬件·51单片机
ThreeYear_s7 小时前
基于FPGA 的4位密码锁 矩阵键盘 数码管显示 报警仿真
fpga开发·矩阵·计算机外设
枯无穷肉9 小时前
stm32制作CAN适配器4--WinUsb的使用
stm32·单片机·嵌入式硬件
不过四级不改名67710 小时前
基于HAL库的stm32的can收发实验
stm32·单片机·嵌入式硬件
嵌入式大圣10 小时前
单片机UDP数据透传
单片机·嵌入式硬件·udp
云山工作室11 小时前
基于单片机的视力保护及身姿矫正器设计(论文+源码)
stm32·单片机·嵌入式硬件·毕业设计·毕设
嵌入式-老费11 小时前
基于海思soc的智能产品开发(mcu读保护的设置)
单片机·嵌入式硬件
不知火猪11 小时前
最新雷蛇鼠标键盘驱动Razer Synapse 4(雷云) 下载与安装
计算机外设·驱动·雷云·雷蛇驱动
qq_3975623112 小时前
MPU6050 , 设置内部低通滤波器,对于输出数据的影响。(简单实验)
单片机