实验17.实现键盘输入

已完成实验

已完成实验链接

简介

实验 16. 实现键盘输入

总结

    1.注册键盘中断,
    2.中断函数读取键盘端口,做按键判断,打印

主要代码

引导

省略

内核

kernel.s

; 文件: kernel.s
; 时间: 2024-07-22
; 来自: ccj
; 描述: 产生中断服务程序, 构建中断服务程序入口地址表

[bits 32]

; 相当于 #define ZERO (push 0)
%define ERROR_CODE nop
%define ZERO       push 0

extern put_str          ; 引入外部打印字符串函数
extern idt_table        ; 引入在interrupt.c中定义的中断处理函数

section .data
; 重要! 中断服务程序入口地址表 [intr01entry, intr1entry, ..., intr32entry]
global intr_entry_table
intr_entry_table:

;---宏定义 VECTOR 接收2个参数 begin---
%macro VECTOR 2
    ;---text节 start---
section .text
intr%1entry:            ; 中断入口

    %2                  ; 如果有错误码,错误码入栈

    ; 保存上下文环境
    push ds
    push es
    push fs
    push gs
    pushad              ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

    ; 如果是从片上进入的中断, 除了往从片上发送EOI外 ,还要往主片上发送EOI
    mov al,0x20         ; 0x20 = EOI
    out 0xa0,al         ; 向从片发送
    out 0x20,al         ; 向主片发送

    push %1                     ; 中断向量号入栈
    call [idt_table + %1 * 4]   ; 调用interrupt.c的中断处理函数表中向量号的中断处理函数
    jmp intr_exit               ; 中断执行完成
    ;---text节 end---

    ;---data节 begin---
section .data
   dd    intr%1entry    ; 中断入口地址
    ;---data节 end---
%endmacro
;---宏定义 VECTOR 接收2个参数 end---

;---退出中断 begin---
section .text
global intr_exit
intr_exit:
    add esp, 4          ; 中断号出栈

    ; 恢复上下文环境
    popad
    pop gs
    pop fs
    pop es
    pop ds

    add esp, 4          ; 错误码出栈
    iretd
;---退出中断 end---

;---创建中断处理函数 begin---
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
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    ;保留
;---创建中断处理函数 end---

interrupt.c

// 文件: interrupt.c
// 时间: 2024-07-22
// 来自: ccj
// 描述: 中断
//          硬件初始化
//          初始化中断门描述符表,绑定中断门描述符到kernel.s的中断服务函数
//          创建一个中断函数处理数组,让kernel.s的中断服务函数调用
//          开启或关闭中断机制

#include "interrupt.h"

#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"

// 可编程中断控制器是8259A
#define PIC_M_CTRL 0x20  // 主片控制端口 0x20
#define PIC_M_DATA 0x21  // 主片数据端口 0x21
#define PIC_S_CTRL 0xa0  // 从片控制端口 0xa0
#define PIC_S_DATA 0xa1  // 从片数据端口 0xa1

#define IDT_DESC_CNT 0x30  // 目前总共支持的中断数

#define EFLAGS_IF             0x00000200  // eflags寄存器中的if位为1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" : "=g"(EFLAG_VAR))

/// @brief 中断门描述符结构体
struct gate_desc {
    uint16_t func_offset_low_word;   // offset15-0
    uint16_t selector;               // 段选择子
    uint8_t dcount;                  // 忽略
    uint8_t attribute;               // P+EDL+S+TYPE
    uint16_t func_offset_high_word;  // pffset31-16
};

// 中断描述符表,每个表项叫做一个门描述符(gate descriptor) 门的含义是当中断发生时必须先通过这些门
static struct gate_desc idt[IDT_DESC_CNT];
// 声明引用定义在kernel.S中的中断处理函数入口数组
extern intr_handler intr_entry_table[IDT_DESC_CNT];
// 用于保存中断的名字
char* intr_name[IDT_DESC_CNT];
// 中断处理函数 kernel.S中的中断处理函数入口里调用的这里的函数
intr_handler idt_table[IDT_DESC_CNT];

/// @brief 可编程中断控制器8259A
/// @param
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);
    outb(PIC_S_DATA, 0xff);

    put_str("[int] init 8259A done\n");
}

/// @brief 把中断服务程序地址绑定到中断门描述符
/// @param p_gdesc
/// @param attr 属性
/// @param function 中断服务程序
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
    p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
    p_gdesc->selector = SELECTOR_K_CODE;
    p_gdesc->dcount = 0;
    p_gdesc->attribute = attr;
    p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}

/// @brief 把kernel.s的中断服务程序地址写入到中断描述符中
/// @param
static void idt_desc_init(void) {
    int i;
    for (i = 0; i < IDT_DESC_CNT; i++) {
        make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
    }
    /* 单独处理系统调用,系统调用对应的中断门dpl为3,
     * 中断处理程序为单独的syscall_handler */

    put_str("[int] init int_desc_table done\n");
}

/// @brief 默认中断处理函数
/// @param vec_nr
static void general_intr_handler(uint8_t vec_nr) {
    // 0x2f是从片8259A上的最后一个irq引脚,保留
    // IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
    if (vec_nr == 0x27 || vec_nr == 0x2f) return;

    // 清空4行
    set_cursor(0);
    int cursor_pos = 0;
    while (cursor_pos < 320) {
        put_char(' ');
        cursor_pos++;
    }

    // 打印异常
    set_cursor(0);
    put_str("---excetion message begin---\n");
    set_cursor(84);
    put_str(intr_name[vec_nr]);
    if (vec_nr == 14) {
        int page_fault_vaddr = 0;
        asm("movl %%cr2, %0" : "=r"(page_fault_vaddr));
        put_str("\npage fault addr is ");
        put_int(page_fault_vaddr);
    }
    put_str("\n---excetion message end---\n");

    // 阻塞
    while (1) {}
}

/// @brief 给每一个中断一个中断名、默认的处理函数
/// @param
static void exception_init(void) {
    // 先初始化每一个中断
    int i;
    for (i = 0; i < IDT_DESC_CNT; i++) {
        idt_table[i] = general_intr_handler;
        intr_name[i] = "unknown";
    }

    // 给每一个中断命名
    intr_name[0] = "#DE Divide Error";
    intr_name[1] = "#DB Debug Exception";
    intr_name[2] = "NMI Interrupt";
    intr_name[3] = "#BP Breakpoint Exception";
    intr_name[4] = "#OF Overflow Exception";
    intr_name[5] = "#BR BOUND Range Exceeded Exception";
    intr_name[6] = "#UD Invalid Opcode Exception";
    intr_name[7] = "#NM Device Not Available Exception";
    intr_name[8] = "#DF Double Fault Exception";
    intr_name[9] = "Coprocessor Segment Overrun";
    intr_name[10] = "#TS Invalid TSS Exception";
    intr_name[11] = "#NP Segment Not Present";
    intr_name[12] = "#SS Stack Fault Exception";
    intr_name[13] = "#GP General Protection Exception";
    intr_name[14] = "#PF Page-Fault Exception";
    // intr_name[15] 第15项是intel保留项,未使用
    intr_name[16] = "#MF x87 FPU Floating-Point Error";
    intr_name[17] = "#AC Alignment Check Exception";
    intr_name[18] = "#MC Machine-Check Exception";
    intr_name[19] = "#XF SIMD Floating-Point Exception";
}

/// @brief 开启中断机制
/// @return 调用之前的状态
enum intr_status intr_enable() {
    enum intr_status old_status;
    if (INTR_ON == intr_get_status()) {
        old_status = INTR_ON;
        return old_status;
    } else {
        old_status = INTR_OFF;
        asm volatile("sti");  // 开中断,sti指令将IF位置1
        return old_status;
    }
}

/// @brief 关闭中断机制
/// @return 调用之前的状态
enum intr_status intr_disable() {
    enum intr_status old_status;
    if (INTR_ON == intr_get_status()) {
        old_status = INTR_ON;
        asm volatile("cli" : : : "memory");  // 关中断,cli指令将IF位置0
        return old_status;
    } else {
        old_status = INTR_OFF;
        return old_status;
    }
}

/// @brief 获取中断机制是否开启
enum intr_status intr_get_status() {
    uint32_t eflags = 0;
    GET_EFLAGS(eflags);
    return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}

/// @brief 开始或者关闭中断机制
enum intr_status intr_set_status(enum intr_status status) {
    return status & INTR_ON ? intr_enable() : intr_disable();
}

/// @brief 在中断处理程序数组第vector_no个元素中注册安装中断处理程序function
/// @param vector_no 中断号
/// @param function 中断处理程序
void register_handler(uint8_t vector_no, intr_handler function) { idt_table[vector_no] = function; }

/// @brief 完成有关中断的所有初始化工作
void idt_init() {
    put_str("[int] init start\n");

    idt_desc_init();   // 初始化中断描述符表
    exception_init();  // 定义每一个中断的中断名和中断处理函数
    pic_init();        // 初始化8259A

    // 加载中断描述符表 Load Interrupt Descriptor Table
    // lidt 2字节中断描述符偏移限制 4字节中断描述表的基地址
    // (sizeof(idt) - 1) 为 263
    uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
    asm volatile("lidt %0" : : "m"(idt_operand));

    put_str("[int] init done\n");
}

keyboard.c

// 文件: keyboard.c
// 时间: 2024-07-30
// 来自: ccj
// 描述: 键盘初始化。注册键盘中断,读取端口缓冲区,键码转换打印

#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"

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

// 部分控制字符
#define esc       '\033'
#define backspace '\b'
#define tab       '\t'
#define enter     '\r'
#define delete    '\177'

// ---键码定义 begin---
// 以下不可见字符一律定义为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

// 定义控制字符的通码和断码
// 通码 按下时产生的键码
// 断码 释放时产生的键码
// alt_r, ctrl_r 检测到是断码,其余的键检测到是通码
#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

/// @brief 以通码为索引的二维数组
// {未与shift组合, 与shift组合}
static char keymap[][2] = {
    {0, 0},
    {esc, esc},
    {'1', '!'},
    {'2', '@'},
    {'3', '#'},
    {'4', '$'},
    {'5', '%'},
    {'6', '^'},
    {'7', '&'},
    {'8', '*'},
    {'9', '('},
    {'0', ')'},
    {'-', '_'},
    {'=', '+'},
    {backspace, backspace},  // 0x0E
    {tab, tab},
    {'q', 'Q'},
    {'w', 'W'},
    {'e', 'E'},
    {'r', 'R'},
    {'t', 'T'},
    {'y', 'Y'},
    {'u', 'U'},
    {'i', 'I'},
    {'o', 'O'},
    {'p', 'P'},
    {'[', '{'},  // 0x1A
    {']', '}'},  // 0x1b
    {enter, enter},
    {ctrl_l_char, ctrl_l_char},
    {'a', 'A'},
    {'s', 'S'},
    {'d', 'D'},
    {'f', 'F'},
    {'g', 'G'},
    {'h', 'H'},
    {'j', 'J'},
    {'k', 'K'},
    {'l', 'L'},
    {';', ':'},   // 0x27
    {'\'', '"'},  // 0x28
    {'`', '~'},   // 0x29
    {shift_l_char, shift_l_char},
    {'\\', '|'},  // 0x2b
    {'z', 'Z'},
    {'x', 'X'},
    {'c', 'C'},
    {'v', 'V'},
    {'b', 'B'},
    {'n', 'N'},
    {'m', 'M'},
    {',', '<'},  // 0x33
    {'.', '>'},  // 0x34
    {'/', '?'},  // 0x35
    {shift_r_char, shift_r_char},
    {'*', '*'},
    {alt_l_char, alt_l_char},
    {' ', ' '},
    {caps_lock_char, caps_lock_char}
    // 其它按键暂不处理
};
// ---键码定义 end---

static bool ctrl_status;
static bool shift_status;
static bool alt_status;
static bool caps_lock_status;
static bool ext_scancode;  // 用于记录通码是否以0xe0开头

/// @brief 键盘中断函数
/// @param
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;

    // 读取1字节,如果是e0开头,那么直接返回,再次读取1字节
    uint16_t scancode = inb(KBD_BUF_PORT);
    if (scancode == 0xe0) {
        ext_scancode = true;  // 打开e0标记
        return;
    }
    // 如果上一次e0,那么整合上一次的1字节和读取的1字节
    if (ext_scancode) {
        scancode = ((0xe000) | scancode);
        ext_scancode = false;  // 关闭e0标记
    }

    // 判断读取值是否是断码, 如果是断码,那么就是特殊按键
    bool break_code;
    break_code = ((scancode & 0x0080) != 0);
    if (break_code) {
        // ctrl_r, alt_r的通码和断码两字节
        uint16_t make_code = (scancode &= 0xff7f);  // 得到其通码

        // 设置状态
        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;
        }
        // 由于caps_lock不是弹起后关闭,所以需要单独处理

        return;
    } else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) ||
               (scancode == ctrl_r_make)) {
        // 判断是否与shift组合,用来在一维数组中索引对应的字符
        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; }
        } 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;
            }
        }

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

        // 0不处理
        if (cur_char) {
            put_char(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键,当再次按下时则状态取反,
             * 即:已经开启时,再按下同样的键是关闭。关闭时按下表示开启。*/
            caps_lock_status = !caps_lock_status;
        }
    } else {
        put_str("unknown key\n");
    }

    return;
}

/// @brief 键盘初始化
void keyboard_init() {
    put_str("[keyboard] init start\n");

    register_handler(0x21, intr_keyboard_handler);

    put_str("[keyboard] init done\n");
}

init.c

// 文件: init.c
// 时间: 2024-07-22
// 来自: ccj
// 描述: 内核所有初始化操作

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "keyboard.h"

/// @brief 内核所有初始化
void init_all() {
    put_str("init all\n");

    idt_init();       // 初始化中断
    timer_init();     // 调快时钟、注册时钟中断来调度线程
    mem_init();       // 初始化内存管理系统
    thread_init();    // 初始化线程
    console_init();   // 控制台初始化最好放在开中断之前
    keyboard_init();  // 键盘初始化
}

main.c

// 文件: main.c
// 时间: 2024-07-19
// 来自: ccj
// 描述: 内核从此处开始

#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"

void k_thread(void*);

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

    init_all();

    intr_enable();  // 打开中断,使时钟中断起作用
    while (1) {};
    return 0;
}

void k_thread(void* arg) {
    char* para = arg;
    while (1) { console_put_str(para); }
}

在这里插入图片描述

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值