Featured image of post 《操作系统真象还原》第十章(二) —— 键盘驱动程序的编写与输入系统

《操作系统真象还原》第十章(二) —— 键盘驱动程序的编写与输入系统

本文介绍了如何编写键盘驱动程序,以接收键盘输入并在屏幕上进行打印

本章节所有代码托管在miniOS_32

章节任务介绍

上一节中,我们介绍了操作系统的同步机制互斥锁的内容,并手动实现了互斥锁,同时实现了线程安全的屏幕打印。 至此,我们算是基本完成了操作系统的“输出”功能,但目前为止我们的输入仍旧依赖于程序,而不是用户操控的键盘 本节我们将正式完成操作系统的“输入”

任务简介

本节的主要任务有:

  1. 键盘驱动测试
  2. 编写键盘驱动程序
  3. 基于环形缓冲区的键盘驱动程序

前置知识

键盘输入原理简介

键盘编码介绍

  • 一个键的状态要么是按下,要么是弹起,因此一个键有两个编码,这两个编码统称扫描码,一个键的扫描码由通码和断码组成
  • 按键被按下时的编码叫通码,表示按键上的触点接通了内部电路,使硬件产生了一个码,故通码也称为makecode
  • 按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为breakcode
  • 断码=0x80+通码

以下是各个键的扫描码

image-20241227165419317

  • 无论是通码还是断码,它们基本都是一字节大小
  • 最高位也就是第7位的值决定按键的状态,最高位若值为 0,表示按键处于按下的状态,否则为1的话,表示按键弹起。
  • 有些按键的通码和断码都以0xe0开头,它们占2字节

8048芯片

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

image-20241227162816830

因此,8048是键盘上的芯片**,负责监控键盘哪个键被按下或者松开,并将扫描码发送给8042芯片

8042芯片

8042芯片负责接收来自键盘的扫描码,将它转换为标准的字符编码(如ASCII码),并保存在自己的寄存器中,然后向 8259A 发送中断信号,这样处理器便去执行键盘中断处理程序,将 8042处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理。

如下所示,8042共有4个8位寄存器,这4个寄存器共用2个端口

image-20241227170329350

8042是连接8048和处理器的桥梁,8042存在的目的是:为了处理器可以通过它控制8048的工作方式,然后让 8048的工作成果通过 8042回传给处理器。此时8042就相当于数据的缓冲区、中转站,根据数据被发送的方向,8042的作用分别是输入和输出。

image-20241227170531476

键盘中断处理程序测试

image-20241227170932654

本节我们将简单编写一个键盘驱动程序,用以测试键盘输入过程。我们首先梳理一下逻辑

  • 当键盘键入按键后,8048芯片就将扫描码发送给8042,然后8042触发中断信号,接着触发中断处理程序
  • 因此我们需要做的其实就是,编写键盘中断对应的中断处理程序,程序的逻辑就是读取8042接收到的扫描码,然后将其按照扫描码与键盘的对应关系显示在键盘上
  • 由于最终目的是要编写键盘中断处理程序,我们需要首先在中断描述符表中添加键盘中断的中断描述符
  • 要添加中断描述符,就要知道键盘中断对应的中断向量号

故而,我们的逻辑其实很简单

  • 添加键盘中断的中断向量号
  • 添加键盘中断处理程序
  • 构建中断描述符
  • 打开键盘中断

/kernel/kernel.S

首先在intr_entry_table中添加键盘中断的中断处理程序入口,键盘中断的中断号是0x21,为方便后续代码编写,以下添加了所有的中断号

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
VECTOR 0x20,ZERO	;时钟中断对应的入口
VECTOR 0x21,ZERO	;键盘中断对应的入口
VECTOR 0x22,ZERO	;级联用的
VECTOR 0x23,ZERO	;串口2对应的入口
VECTOR 0x24,ZERO	;串口1对应的入口
VECTOR 0x25,ZERO	;并口2对应的入口
VECTOR 0x26,ZERO	;软盘对应的入口
VECTOR 0x27,ZERO	;并口1对应的入口
VECTOR 0x28,ZERO	;实时时钟对应的入口
VECTOR 0x29,ZERO	;重定向
VECTOR 0x2a,ZERO	;保留
VECTOR 0x2b,ZERO	;保留
VECTOR 0x2c,ZERO	;ps/2鼠标
VECTOR 0x2d,ZERO	;fpu浮点单元异常
VECTOR 0x2e,ZERO	;硬盘
VECTOR 0x2f,ZERO	;保留

/kernel/interrupt.c

修改中断描述符的总数量,原来只有33个中断描述符

1
#define IDT_DESC_CNT 0x30

然后打开键盘中断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*初始化可编程中断控制器8259A*/
static void pic_init(void)
{
    /* 初始化主片 */
    outb(PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
    outb(PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
    outb(PIC_M_DATA, 0x04); // ICW3: IR2接从片.
    outb(PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI

    /* 初始化从片 */
    outb(PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
    outb(PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
    outb(PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
    outb(PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI

    /* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
    // outb(PIC_M_DATA, 0xfe);
    // outb(PIC_S_DATA, 0xff);

    /* 测试键盘,只打开键盘中断,其它全部关闭 */
    outb(PIC_M_DATA, 0xfd); // 键盘中断在主片ir1引脚上,所以将这个引脚置0,就打开了
    outb(PIC_S_DATA, 0xff);
    put_str("pic_init done\n");
}

接下来编写键盘驱动程序

/device/keyboard.h

1
2
3
4
#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
void keyboard_init(void); 
#endif

/device/keyboard.c

下述键盘驱动程序代码只做测试用,无论键盘的哪个按键被按下或者松开,都会只显示字符k,并未对键盘按键的情况做处理,后续我们再修改键盘驱动程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"

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

/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {
   put_char('k');
//每次必须要从8042读走键盘8048传递过来的数据,否则8042不会接收后续8048传递过来的数据
   inb(KBD_BUF_PORT);
   return;
}

/* 键盘初始化 */
void keyboard_init() {
   put_str("keyboard init start\n");
   register_handler(0x21, intr_keyboard_handler);       //注册键盘中断处理函数
   put_str("keyboard init done\n");
}

添加键盘中断初始化

/kernel/init.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/*负责初始化所有模块 */
void init_all()
{
    put_str("init_all\n");
    idt_init();      // 初始化中断
    mem_init();      // 初始化内存管理系统
    thread_init();   // 初始化线程相关结构
    timer_init();    // 时钟中断初始化
    console_init();  // 终端初始化
    keyboard_init(); // 键盘中断初始化
}

修改main.c测试键盘中断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
void thread_work_a(void *arg);
void thread_work_b(void *arg);

int main(void)
{
    put_str("I am kernel\n");
    init_all();

    // thread_start("thread_work_a", 31, thread_work_a, "pthread_A ");
    // thread_start("thread_work_b", 8, thread_work_b, "pthread_B ");

    /*打开中断,主要是打开时钟中断,以让时间片轮转调度生效*/
    intr_enable();
    while (1);
    // {
    //     console_put_str("Main ");
    // }
    return 0;
}

/* 线程执行函数 */
void thread_work_a(void *arg)
{
    char *para = (char *)arg;
    while (1)
    {
        console_put_str(para);
    }
}
/* 线程执行函数 */
void thread_work_b(void *arg)
{
    char *para = (char *)arg;
    while (1)
    {
        console_put_str(para);
    }
}

编译运行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
mkdir -p bin
#编译mbr
nasm -o $(pwd)/bin/mbr -I $(pwd)/boot/include/ $(pwd)/boot/mbr.S
dd if=$(pwd)/bin/mbr of=~/bochs/hd60M.img bs=512 count=1 conv=notrunc

#编译loader
nasm -o $(pwd)/bin/loader -I $(pwd)/boot/include/ $(pwd)/boot/loader.S
dd if=$(pwd)/bin/loader of=~/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc

#编译print函数
nasm -f elf32 -o $(pwd)/bin/print.o $(pwd)/lib/kernel/print.S
# 编译kernel
nasm -f elf32 -o $(pwd)/bin/kernel.o $(pwd)/kernel/kernel.S
# 编译switch
nasm -f elf32 -o $(pwd)/bin/switch.o $(pwd)/thread/switch.S

#编译main文件
gcc-4.4 -o $(pwd)/bin/main.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/device/ -I $(pwd)/thread/ $(pwd)/kernel/main.c
#编译interrupt文件
gcc-4.4 -o $(pwd)/bin/interrupt.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/interrupt.c
#编译init文件
gcc-4.4 -o $(pwd)/bin/init.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ -I $(pwd)/device/ $(pwd)/kernel/init.c
# 编译debug文件
gcc-4.4 -o $(pwd)/bin/debug.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/debug.c
# 编译string文件
gcc-4.4 -o $(pwd)/bin/string.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/string.c
# 编译bitmap文件
gcc-4.4 -o $(pwd)/bin/bitmap.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/bitmap.c
# 编译memory文件
gcc-4.4 -o $(pwd)/bin/memory.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/memory.c
# 编译thread文件
gcc-4.4 -o $(pwd)/bin/thread.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/thread/thread.c
# 编译list文件
gcc-4.4 -o $(pwd)/bin/list.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/list.c
# 编译timer文件
gcc-4.4 -o $(pwd)/bin/timer.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/timer.c
# 编译sync文件
gcc-4.4 -o $(pwd)/bin/sync.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/thread/sync.c
# 编译console文件
gcc-4.4 -o $(pwd)/bin/console.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/console.c
# 编译keyboard文件
gcc-4.4 -o $(pwd)/bin/keyboard.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/keyboard.c

#将main函数与print函数进行链接
ld -m elf_i386 -Ttext 0xc0001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o $(pwd)/bin/kernel.o  $(pwd)/bin/init.o  $(pwd)/bin/thread.o $(pwd)/bin/switch.o $(pwd)/bin/list.o $(pwd)/bin/sync.o $(pwd)/bin/keyboard.o $(pwd)/bin/console.o $(pwd)/bin/timer.o $(pwd)/bin/interrupt.o $(pwd)/bin/memory.o $(pwd)/bin/bitmap.o  $(pwd)/bin/print.o  $(pwd)/bin/string.o $(pwd)/bin/debug.o

#将内核文件写入磁盘,loader程序会将其加载到内存运行
dd if=$(pwd)/bin/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9

#rm -rf bin/*

结果如下所示,我们按下了空格键字母q键,由于按下和松开都会触发中断,因此每个按键会显示两次字符k,故共有四个字符k

image-20241227172427014

编写键盘驱动程序

上一小节,我们测试了键盘驱动程序的流程,在这一小节,我们修改键盘驱动程序,以实现当按键被按下时,屏幕上会显示对应的字符

/device/keyboard.c

数据准备与定义

按照扫描码表格定义每个扫描码对应的按键情况

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/*
二维数组,用于记录从0x00到0x3a通码对应的按键的两种情况
(如0x02,不加shift表示1,加了shift表示!)的ascii码值,如果没有,则用ascii0替代
*/
char keymap[][2] = {
    /* 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}};

其中不可见字符以及控制字符的显式定义宏为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#define esc '\033' // esc 和 delete都没有\转义字符这种形式,用8进制代替
#define delete '\0177'
#define enter '\r'
#define tab '\t'
#define backspace '\b'

// 功能性 不可见字符均设置为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

接下来定义控制字符的通码和断码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/// 定义控制字符的通码和断码
#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

由于控制字符常常和其余键作为组合键使用,因此需要记录控制字符的状态

1
2
3
4
5
int ctrl_status = 0;      // 用于记录是否按下ctrl键
int shift_status = 0;     // 用于记录是否按下shift
int alt_status = 0;       // 用于记录是否按下alt键
int caps_lock_status = 0; // 用于记录是否按下大写锁定
int ext_scancode = 0;     // 用于记录是否是扩展码

键盘驱动程序逻辑

image-20241227181642887

首先从8042芯片中读取扫描码,需要注意的是,8042每次只接受一个字节的扫描码,但是有些按键的扫描码是两个字节,因此会触发两次中断,并向8042依次发送这两个字节的数据

因此,需要根据第一次接受到的扫描码是否是0xe0,如果是,说明该按键的扫描码是由两个字节组成的,需要再次接受一个字节的扫描码,然后才能拼接出完整的两个字节的扫描码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    // 从0x60端口读入一个字
    uint16_t scancode = inb(KBD_BUF_PORT);
    // 如果传入是0xe0,说明是处理两字节按键的扫描码,那么就应该立即退出去取出下一个字节
    if (scancode == 0xe0)
    {
        ext_scancode = 1;
        return;
    }
    // 如果能进入这个if,那么ext_scancode==1,说明上次传入的是两字节按键扫描码的第一个字节
    if (ext_scancode)
    {
        scancode |= (0xe000); // 合并扫描码,这样两字节的按键的扫描码就得到了完整取出
        ext_scancode = 0;
    }

接受到扫描码后需要判断是断码还是通码,然后分别进行处理,而由以上我们知道

断码=0x80+通码

因此有

1
2
    // 断码=通码+0x80,如果是断码,那么&出来结果!=0,那么break_code值为1
    int break_code = ((scancode & 0x0080) != 0);

如果是断码,说明松开了按键

  • 如果松开的按键是字母键,则不进行处理
  • 如果松开的是控制按键,则清除对应控制按键的状态(因为控制按键在按下的时候我们会置状态位,因此松开的时候需要清除)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    // 如果是断码,就要判断是否是控制按键的断码
    // 如果是,就要将表示他们按下的标志清零,如果不是,就不处理。最后都要退出程序
    if (break_code)
    {
        // 将扫描码(现在是断码)还原成通码
        uint16_t make_code = (scancode &= 0xff7f);
        if (make_code == ctrl_l_make || make_code == ctrl_r_make)
            ctrl_status = 0;
        if (make_code == shift_l_make || make_code == shift_r_make)
            shift_status = 0;
        if (make_code == alt_l_make || make_code == alt_r_make)
            alt_status = 0;
        return;
    }

以下是通码对应的处理逻辑

  • 判断按键是否是控制键(ctrl、alt、shift、大写锁定键):如果是,说明用户可能在使用组合键,因此首先记录该控制按键的状态是被按下了,然后返回接受下一个中断的按键(这里我们并没有实现具体的组合键处理情况)
  • 判断按键是否是特殊两个字母的键(和shift可以组合使用的键):如果是,则判断shift按键的状态是否被按下,如果被按下就打印转换的字符,如果shift状态没有被按下,就直接打印对应字符即可
  • 判断正常字母按键:正常字母按键可能会和shift或者大写锁定键组合使用,但只有一个会起作用,但无论是哪个起作用,都将shift状态位置为1,表示接下来该字母输出的是大写
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
    // 如果是通码,首先保证我们只处理这些数组中定义了的键,以及右alt和ctrl。
    else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make))
    {
        // 是否开启shift转换标志
        int shift = 0;
        // 将扫描码留下低字节,这就是在数组中对应的索引
        uint8_t index = (scancode & 0x00ff);
        if (scancode == ctrl_l_make || scancode == ctrl_r_make)
        {
            ctrl_status = 1;
            return;
        }
        if (scancode == shift_l_make || scancode == shift_r_make)
        {
            shift_status = 1;
            return;
        }
        if (scancode == alt_l_make || scancode == alt_r_make)
        {
            alt_status = 1;
            return;
        }
        if (scancode == caps_lock_make) // 大写锁定键是按一次,然后取反
        {
            caps_lock_status = !caps_lock_status;
            return;
        }
        if ((scancode < 0x0e) || (scancode == 0x29) || (scancode == 0x1a) ||
            (scancode == 0x1b) || (scancode == 0x2b) || (scancode == 0x27) ||
            (scancode == 0x28) || (scancode == 0x33) || (scancode == 0x34) || (scancode == 0x35))
        {
            /*代表两个字母的键 0x0e 数字'0'~'9',字符'-',字符'='
                           0x29 字符'`'
                           0x1a 字符'['
                           0x1b 字符']'
                           0x2b 字符'\\'
                           0x27 字符';'
                           0x28 字符'\''
                           0x33 字符','
                           0x34 字符'.'
                           0x35 字符'/'
            */
            if (shift_status) // 如果同时按下了shift键
                shift = true;
        }
        else
        {
            // 默认字母键
            if (shift_status + caps_lock_status == 1)
                shift = 1; // shift和大写锁定,那么判断是否按下了一个,而且不能是同时按下,那么就能确定是要开启shift
        }
        put_char(keymap[index][shift]); // 打印字符

        return;
    }

编译运行

如下是运行的结果,我在键盘输入的是“nihao hello world

可以看到程序正常显示了我的按键情况

image-20241227175640643

环形输入缓冲区

到现在,我们的键盘驱动仅能够输出咱们所键入的按键,这还没有什么实际用途。

在键盘上操作是为了与系统进行交互,交互的过程一般是键入各种shell 命令,然后shel 解析并执行。

shell 命令是由多个字符组成的,并且要以回车键结束,因此咱们在键入命令的过程中,必须要找个缓冲区把已键入的信息存起来,当凑成完整的命令名时再一并由其他模块处理。

本节咱们要构建这个缓冲区

  • 环形缓冲区本质上是用数组进行表示,并使用模运算实现区域的回绕
  • 当缓冲区满时,要阻塞生产者继续向缓冲区写入字符
  • 当缓冲区空时,要阻塞消费者取字符

以下是具体代码,实现较为简单,不再赘述细节

/device/ioqueue.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#ifndef __DEVICE_IOQUEUE_H
#define __DEVICE_IOQUEUE_H

#include "stdint.h"
#include "thread.h"
#include "sync.h"

// 定义缓冲区大小
#define bufsize 64
/*环形队列*/
struct ioqueue
{
    struct lock lock;
    /* 生产者,缓冲区不满时就继续往里面放数据,
     * 否则就睡眠,此项记录哪个生产者在此缓冲区上睡眠。*/
    struct task_struct *producer;
    /* 消费者,缓冲区不空时就继续从往里面拿数据,
     * 否则就睡眠,此项记录哪个消费者在此缓冲区上睡眠。*/
    struct task_struct *consumer;

    // 缓冲区大小
    char buf[bufsize];
    // 队首,数据往队首处写入
    int32_t head;
    // 队尾,数据从队尾处读出
    int32_t tail;
};
void ioqueue_init(struct ioqueue *ioq);
bool ioq_full(struct ioqueue *ioq);
bool ioq_empty(struct ioqueue *ioq);
char ioq_getchar(struct ioqueue *ioq);
void ioq_putchar(struct ioqueue *ioq, char byte);

#endif

/device/ioqueue.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include "ioqueue.h"
#include "interrupt.h"
#include "global.h"
#include "debug.h"

/*初始化io队列*/
void ioqueue_init(struct ioqueue *ioq)
{
    lock_init(&ioq->lock);
    ioq->producer = ioq->consumer = NULL;
    ioq->head = ioq->tail = 0;
}
static int32_t next_pos(int32_t pos)
{
    return (pos + 1) % bufsize;
}
/*判断队列是否已满*/
bool ioq_full(struct ioqueue *ioq)
{
    ASSERT(intr_get_status() == INTR_OFF);
    return next_pos(ioq->head) == ioq->tail;
}
/*判断队列是否为空*/
bool ioq_empty(struct ioqueue *ioq)
{
    ASSERT(intr_get_status() == INTR_OFF);
    return ioq->head == ioq->tail;
}
/*
使当前生产者或消费者在此缓冲区上等待
传入参数是ioq->producer或者ioq->consumer
*/
static void ioq_wait(struct task_struct **waiter)
{
    ASSERT(*waiter == NULL && waiter != NULL);
    // *waiter = running_thread();
    thread_block(TASK_BLOCKED);
}
/* 唤醒waiter */
static void ioq_wakeup(struct task_struct **waiter)
{
    ASSERT(*waiter != NULL);
    thread_unblock(*waiter);
    *waiter = NULL;
}
/* 消费者从ioq队列中获取一个字符 */
char ioq_getchar(struct ioqueue *ioq)
{
    ASSERT(intr_get_status() == INTR_OFF);
    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)
        ioq_wakeup(&ioq->producer);
    return byte;
}
/* 生产者往ioq队列中写入一个字符byte */
void ioq_putchar(struct ioqueue *ioq, char byte)
{
    ASSERT(intr_get_status() == INTR_OFF);
    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)
        ioq_wakeup(&ioq->consumer);
}

接下来我们需要进行测试

  • 生产者自然是键盘驱动程序
  • 为了模拟消费者,我们在main函数中添加两个子线程,两个线程都用于从缓冲区中取字符
  • 由于有两个线程取字符,因此每次按下键盘后,字符可能由不同的线程接收并显示在屏幕,我们在代码中显示每次显示的字符是由哪个线程打印的

之前为了测试键盘中断,我们关闭了时钟中断,仅打开了键盘中断,而此时由于要使用子线程,因此我们需要开启时钟中断

/kernel/interrupt.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*初始化可编程中断控制器8259A*/
static void pic_init(void)
{
    /* 初始化主片 */
    outb(PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
    outb(PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
    outb(PIC_M_DATA, 0x04); // ICW3: IR2接从片.
    outb(PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI

    /* 初始化从片 */
    outb(PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
    outb(PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
    outb(PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
    outb(PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI

    /* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
    // outb(PIC_M_DATA, 0xfe);
    // outb(PIC_S_DATA, 0xff);

    /* 测试键盘,只打开键盘中断,其它全部关闭 */
    // outb(PIC_M_DATA, 0xfd); // 键盘中断在主片ir1引脚上,所以将这个引脚置0,就打开了
    // outb(PIC_S_DATA, 0xff);

    // 同时打开时钟中断与键盘中断
    outb(PIC_M_DATA, 0xfc);
    outb(PIC_S_DATA, 0xff);
    put_str("pic_init done\n");
}

/kernel/main.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"

/* 临时为测试添加 */
#include "ioqueue.h"
#include "keyboard.h"
void thread_work_a(void *arg);
void thread_work_b(void *arg);

int main(void)
{
    put_str("I am kernel\n");
    init_all();

    thread_start("consumer_a", 31, thread_work_a, "consumer_A:");
    thread_start("consumer_b", 8, thread_work_b, "consumer_B:");

    /*打开中断,主要是打开时钟中断,以让时间片轮转调度生效*/
    intr_enable();
    while (1)
        ;
    return 0;
}

/* 线程执行函数 */
void thread_work_a(void *arg)
{
    char *para = (char *)arg;
    while (1)
    {
        enum intr_status old_status = intr_disable();
        if (!ioq_empty(&kbd_buf))
        {
            console_put_str(para);
            char byte = ioq_getchar(&kbd_buf);
            console_put_char(byte);
            console_put_str("   ");
        }
        intr_set_status(old_status);
    }
}
/* 线程执行函数 */
void thread_work_b(void *arg)
{
    char *para = (char *)arg;
    while (1)
    {
        enum intr_status old_status = intr_disable();
        if (!ioq_empty(&kbd_buf))
        {
            console_put_str(para);
            char byte = ioq_getchar(&kbd_buf);
            console_put_char(byte);
            console_put_str("   ");
        }
        intr_set_status(old_status);
    }
}

编译运行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
mkdir -p bin
#编译mbr
nasm -o $(pwd)/bin/mbr -I $(pwd)/boot/include/ $(pwd)/boot/mbr.S
dd if=$(pwd)/bin/mbr of=~/bochs/hd60M.img bs=512 count=1 conv=notrunc

#编译loader
nasm -o $(pwd)/bin/loader -I $(pwd)/boot/include/ $(pwd)/boot/loader.S
dd if=$(pwd)/bin/loader of=~/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc

#编译print函数
nasm -f elf32 -o $(pwd)/bin/print.o $(pwd)/lib/kernel/print.S
# 编译kernel
nasm -f elf32 -o $(pwd)/bin/kernel.o $(pwd)/kernel/kernel.S
# 编译switch
nasm -f elf32 -o $(pwd)/bin/switch.o $(pwd)/thread/switch.S

#编译main文件
gcc-4.4 -o $(pwd)/bin/main.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/device/ -I $(pwd)/thread/ $(pwd)/kernel/main.c
#编译interrupt文件
gcc-4.4 -o $(pwd)/bin/interrupt.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/interrupt.c
#编译init文件
gcc-4.4 -o $(pwd)/bin/init.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ -I $(pwd)/device/ $(pwd)/kernel/init.c
# 编译debug文件
gcc-4.4 -o $(pwd)/bin/debug.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/debug.c
# 编译string文件
gcc-4.4 -o $(pwd)/bin/string.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/string.c
# 编译bitmap文件
gcc-4.4 -o $(pwd)/bin/bitmap.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/bitmap.c
# 编译memory文件
gcc-4.4 -o $(pwd)/bin/memory.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/memory.c
# 编译thread文件
gcc-4.4 -o $(pwd)/bin/thread.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/thread/thread.c
# 编译list文件
gcc-4.4 -o $(pwd)/bin/list.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/list.c
# 编译timer文件
gcc-4.4 -o $(pwd)/bin/timer.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/timer.c
# 编译sync文件
gcc-4.4 -o $(pwd)/bin/sync.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/thread/sync.c
# 编译console文件
gcc-4.4 -o $(pwd)/bin/console.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/console.c
# 编译keyboard文件
gcc-4.4 -o $(pwd)/bin/keyboard.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/keyboard.c
# 编译ioqueue文件
gcc-4.4 -o $(pwd)/bin/ioqueue.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/ioqueue.c

#将main函数与print函数进行链接
ld -m elf_i386 -Ttext 0xc0001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o $(pwd)/bin/kernel.o  $(pwd)/bin/init.o  $(pwd)/bin/thread.o $(pwd)/bin/switch.o $(pwd)/bin/list.o $(pwd)/bin/sync.o $(pwd)/bin/console.o $(pwd)/bin/keyboard.o $(pwd)/bin/timer.o $(pwd)/bin/ioqueue.o $(pwd)/bin/interrupt.o $(pwd)/bin/memory.o $(pwd)/bin/bitmap.o  $(pwd)/bin/print.o  $(pwd)/bin/string.o $(pwd)/bin/debug.o

#将内核文件写入磁盘,loader程序会将其加载到内存运行
dd if=$(pwd)/bin/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9

#rm -rf bin/*

以下是运行结果

image-20241227182447119

网站已运行
发表了20篇文章 · 总计 138,209字