已完成实验
简介
实验 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); }
}