《Orange‘s 一个操作系统的实现》第七章

本文深入剖析了键盘输入的完整流程,从8042键盘控制器、扫描码到缓冲区管理,再到键盘中断处理。接着介绍了显示器中的TTY系统,包括TTY任务、键盘读取、屏幕输出以及中断处理。整个过程涉及键盘编码器、8042寄存器、键盘输入缓冲区、TTY任务框架和VGA寄存器的操作。此外,还展示了如何在Linux环境下处理键盘输入和屏幕显示,以及控制台切换和屏幕滚动等功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

键盘

目前主流键盘: AT、PS/2、USB。

键盘敲击的过程

在这里插入图片描述

在键盘中存在一枚叫做键盘编码器的芯片,它通常是 Intel 8048 以及兼容芯片,作用是监视键盘的输入,并把适当的数据传送给计算机。
在计算机主板中存在一个键盘控制器,作用是接受和解码来自键盘的数据,并与 8259A 以及软件等进行通信。
敲击键盘所产生的编码称为扫描码,共分成两类,分别是按下和弹起,每个 MakeCode 和 BreakCode 都对应一个扫描码。(Pause 键除外)。
扫描码共有三套:早期的 XT 键盘使用 Scan code set 1、现在默认 Scan code set 2、很少使用 Scan code set 3。
键盘敲击的过程:

  1. 8048 检测到有按键被按下或弹起。

  2. 8048 将相应的扫描码发送给 8042。

  3. 8042 会把它转换成相应的 Scan code set 1 扫描码,并且将其放入输入缓冲区中。

  4. 最后 8042 通知 8259A 产生中断(IRQ1)

    若此时按键又被按下,8042 将不再接收来自 8048 的扫描码,一直到缓冲区被清空为止。

8042 寄存器

作用: 从缓冲区中读取扫描码。

在这里插入图片描述

Scan code set 1 表:

键盘输入缓冲区

定义缓冲区结构:

// include/keyboard.h
typedef struct s_kb{
    char* p_head;           // 指向缓冲区中下一个空闲位置
    char* p_tail;           // 指向键盘任务应处理的字节
    int   count;            // 缓冲区中共有多少字节
    char  buf[KB_IN_BYTES]; // 缓冲区
} KB_INPUT;

具体过程

第一步:定义缓冲区结构,这步上文已完成。
第二步:键盘中断对应的中断处理函数。

// kernel/keyboard.c
// 键盘中断的处理函数
PUBLIC void keyboard_handler(int req) {
    u8 scan_code = in_byte(KB_DATA);

    if(kb_in.count < KB_IN_BYTES) { // 判断当前长度(数组容量)是否小于允许的最大值
        *(kb_in.p_head) = scan_code;
        kb_in.p_head++; // head 其实可以看成末尾了吧?
        if(kb_in.p_head == kb_in.buf + KB_IN_BYTES) // 判断 head 是否到达了数组末尾
            kb_in.p_head = kb_in.buf; // 那就重新开始
        kb_in.count++; // 元素个数,即长度 +1
    }
}

读缓冲区开始时关闭中断,到结束时才打开,因为 kb_in 作为一个整体,对其中的成员操作应该是一气呵成的。

第三步:初始化键盘中断。

// kernel/keyboard.c
PUBLIC void init_keyboard() {
    kb_in.count = 0;
    kb_in.p_head = kb_in.p_tail = kb_in.buf;

    shift_l = shift_r = 0;
    alt_l = alt_r = 0;
    ctrl_l = ctrl_r = 0;

    put_irq_handler(KEYBOARD_IRQ, keyboard_handler); // 设定键盘中断处理程序
    enable_irq(KEYBOARD_IRQ); // 开启键盘中断
}

第四步:新加一个任务,用于调用读取缓冲区的函数。

// kernel/tty.c
PUBLIC void task_tty() {
    while(1) keyboard_read();
}

第五步:编写读取缓冲区的函数。

PRIVATE KB_INPUT kb_in;

PRIVATE	int	code_with_E0 = 0;
PRIVATE	int	shift_l;	    /* l shift state */
PRIVATE	int	shift_r;	    /* r shift state */
PRIVATE	int	alt_l;		    /* l alt state	 */
PRIVATE	int	alt_r;		    /* r left state	 */
PRIVATE	int	ctrl_l;		    /* l ctrl state	 */
PRIVATE	int	ctrl_r;		    /* l ctrl state	 */
PRIVATE	int	caps_lock;	    /* Caps Lock	 */
PRIVATE	int	num_lock;	    /* Num Lock	     */
PRIVATE	int	scroll_lock;	/* Scroll Lock	 */
PRIVATE	int	column;

PRIVATE u8 get_byte_from_kbuf();

PUBLIC void keyboard_read() {
    u8 scan_code;
    char output[2];
    int make;

    u32 key = 0; // 表示一个键。例如 Home 被按下,则 Key 的值为 keyboard.h 中宏 HOME 的值
    u32* keyrow; // 指向 keymap[] 的某一行

    memset(output, 0, 2); // 【TIPS】我这里不加这个 会乱码,书中是注释掉了...

    if(kb_in.count > 0) { // 判断缓冲区中是否有扫描码
        code_with_E0 = 0;
        scan_code = get_byte_from_kbuf();

        // 下面开始解析扫描码
        if(scan_code == 0xE1) {
            int i;
            u8 pausebrk_scode[] = {0xE1, 0x1D, 0x45, 0xE1, 0x9D, 0xC5};
            int is_pausebreak = 1;
            for(i = 1; i < 6; i++) {
                if(get_byte_from_kbuf() != pausebrk_scode[i]) {
                    is_pausebreak = 0;
                    break;
                }
            }
            if(is_pausebreak) key = PAUSEBREAK;
        } else if(scan_code == 0xE0) {
            scan_code = get_byte_from_kbuf();
            // PrintScreen 被按下
            if(scan_code == 0x2A && get_byte_from_kbuf() == 0xE0 && get_byte_from_kbuf() == 0x37) {
                key = PRINTSCREEN;
                make = 1;
            }
            // PrintScreen 被释放
            if(scan_code == 0xB7 && get_byte_from_kbuf() == 0xE0 && get_byte_from_kbuf() == 0xAA) {
                key = PRINTSCREEN;
                make = 0;
            }
            // 不是 PrintScreen,此时 scan_code 为 0xE0 紧跟的那个值
            if(key == 0) code_with_E0 = 1;
        }
        if((key != PAUSEBREAK) && (key != PRINTSCREEN)) {
            // 0: Break Code; 1: Make Code
            make = (scan_code & FLAG_BREAK ? FALSE : TRUE);
            // 定位到 keymap 中的行
            keyrow = &keymap[(scan_code & 0x7F) * MAP_COLS];

            column = 0;

            if(shift_l || shift_r) column = 1;
            if(code_with_E0) {
                column = 2;
                code_with_E0 = 0;
            }

            key = keyrow[column];

            switch(key) {
                case SHIFT_L:
                    shift_l = make;
                    key = 0;
                    break;
                case SHIFT_R:
                    shift_r = make;
                    key = 0;
                    break;
                case CTRL_L:
                    ctrl_l = make;
                    key = 0;
                    break;
                case CTRL_R:
                    ctrl_r = make;
                    key = 0;
                    break;
                case ALT_L:
                    alt_l = make;
                    key = 0;
                    break;
                case ALT_R:
                    alt_r = make;
                    key = 0;
                    break;
                default:
                    break;
            }

            if(make) {
                key |= shift_l ? FLAG_SHIFT_L : 0;
                key |= shift_r ? FLAG_SHIFT_R : 0;
                key |= ctrl_l ? FLAG_CTRL_L : 0;
                key |= ctrl_r ? FLAG_CTRL_R : 0;
                key |= alt_l ? FLAG_ALT_L : 0;
                key |= alt_r ? FLAG_ALT_R : 0;

                in_process(key);
            }
        }
    }
}

// 从缓冲区中读取下一个字节
PRIVATE u8 get_byte_from_kbuf() {
    u8 scan_code;

    while(kb_in.count <= 0); // 等待下一个字节到来

    disable_int();
    scan_code = *(kb_in.p_tail);
    kb_in.p_tail++;
    if(kb_in.p_tail == kb_in.buf + KB_IN_BYTES) 
        kb_in.p_tail = kb_in.buf;
    kb_in.count--;
    enable_int();

    return scan_code;
}

从 keymap[] 中取出字符时进行了“与”操作,原因有二,其一,若当前扫描码是 Break Code,“与” 后变为 Make Code。其二,避免越界,因为 keymap[] 的大小为 0x80。

第五步:disable_int、enable_int

; ========================================================================
; 关闭中断  void disable_int();
; ========================================================================
disable_int:
	cli
	ret

; ========================================================================
; 开启中断  void enable_int();
; ========================================================================
enable_int:
	sti
	ret

第六步:编写显示函数。

// kernel/tty.c
PUBLIC void in_process(u32 key) {
    // Too long...
}

显示器

TTY

TTY 是 Linux 和 Unix 中的一个子系统,通过 TTY 驱动程序在内核级别实现流程管理、编辑和会话管理。实际上,每当启动终端模拟器或使用系统中的任何类型的 shell 时,它都会与被称为伪 TTY 或 PTY 的虚拟 TTY 进行交互。

可以在大多数发行版上使用以下键盘快捷键来获取TTY屏幕:

  • CTRL + ALT + F1 – 锁定屏幕

  • CTRL + ALT + F2 – 桌面环境

  • CTRL + ALT + F3 – TTY3

  • CTRL + ALT + F4 – TTY4

  • CTRL + ALT + F5 – TT5

  • CTRL + ALT + F6 – TTY6

一般总共最多可以访问六个TTY,前两个快捷方式指向发行版的锁定屏幕和桌面环境。

它门都共用一个键盘,只是输出的结果显示在不同的屏幕上,同一台显示器实现多个屏幕就需要控制各个屏幕在显存中的位置。

我们默认屏幕为 80 × 25 80 \times 25 80×25 的文本模式,该模式显存大小为 32KB,占用范围 0xB8000 ~ 0xBFFFF,每 2 字节代表一个字符,其中低 8 位为 ASCII 码,高 8 位位字符属性。一个屏幕可以显示 25 行,每行 80 个字符。

一个屏幕映射到显存中所占的空间大小为: 80 × 25 × 2 = 4000  字节 80 \times 25 \times 2 = 4000 \space 字节 80×25×2=4000 字节 ,一个屏幕 4KB,所以显存中共可以放 8 个屏幕。

VGA 寄存器

在这里插入图片描述

在这里插入图片描述

操作方式:

out 端口号, idx
out 端口号, new_value
; 这里 CTR Controller Registers 端口号是 Address Register = 0x3D4, Data Registers = 0x3D5

TTY 任务

简化版的 TTY 任务

在这里插入图片描述

在 TTY 任务中循环每个 TTY,并且处理它的事件、从键盘缓冲区读取数据、在屏幕显示字符等内容。
注意:

  1. 只有某个 TTY 对应的控制台是当前控制台时,才可以读取键盘缓冲区(因此图中为虚线)。
  2. TTY 可以处理很多事情,这里只做显示一项。
  3. 显示器和键盘是所有 TTY 共有的,因此画在了外面。

轮询到的 TTY 的任务:

  1. 处理输入 —— 判断 TTY 对应的控制台是否为当前控制台,若是则从键盘缓冲区读取数据。
  2. 处理输出 —— 在屏幕中显示字符,该步骤不需要判断 TTY 对应的控制台是否为当前控制台。

TTY 任务框架

第一步:定义结构。

// include/tty.h
#define TTY_IN_BYTES	256	// TTY 输入缓冲区最大容量

struct s_console;

typedef struct s_tty {
    u32  in_buf[TTY_IN_BYTES];  // TTY 输入缓冲区
    u32* p_inbuf_head;          // 指向缓冲区中的下一个空闲位置
    u32* p_inbuf_tail;          // 指向键盘任务应处理的键值
    int  inbuf_count;           // 缓冲区中已经填充了多少

    struct s_console* p_console;
} TTY;
// include/console.h
typedef struct s_console {
    unsigned int current_start_addr; // 当前显示到了什么位置
    unsigned int original_addr;      // 当前控制台对应的显存位置
    unsigned int v_mem_limit;        // 当前控制台所占用的显存大小
    unsigned int cursor;             // 当前光标位置
} CONSOLE;

#define SCR_UP	1	/* scroll forward */
#define SCR_DN	-1	/* scroll backward */

#define SCREEN_SIZE		(80 * 25)
#define SCREEN_WIDTH	80

#define DEFAULT_CHAR_COLOR	0x07	/* 0000 0111 黑底白字 */

第二步:定义相关宏与变量。

// include/tty.h
#define TTY_IN_BYTES	256	// TTY 输入缓冲区最大容量
// include/console.h
#define SCR_UP	1	/* scroll forward */
#define SCR_DN	-1	/* scroll backward */

#define SCREEN_SIZE		(80 * 25)
#define SCREEN_WIDTH	80

#define DEFAULT_CHAR_COLOR	0x07	/* 0000 0111 黑底白字 */
// include/const.h
#define NR_CONSOLES 3 // 控制台个数
// kernel/global.c
PUBLIC TTY tty_table[NR_CONSOLES];
PUBLIC CONSOLE console_table[NR_CONSOLES];
// kernel/tty.c
#define TTY_FIRST (tty_table)
#define TTY_END (tty_table + NR_CONSOLES)
/* VGA */
#define	CRTC_ADDR_REG	0x3D4	/* CRT Controller Registers - Addr Register */
#define	CRTC_DATA_REG	0x3D5	/* CRT Controller Registers - Data Register */
#define	START_ADDR_H	0xC	    /* reg index of video mem start addr (MSB) */
#define	START_ADDR_L	0xD	    /* reg index of video mem start addr (LSB) */
#define	CURSOR_H	    0xE	    /* reg index of cursor position (MSB) */
#define	CURSOR_L	    0xF	    /* reg index of cursor position (LSB) */

#define	V_MEM_BASE	    0xB8000	/* base of color video memory */
#define	V_MEM_SIZE	    0x8000	/* 32K: B8000H -> BFFFFH */

第二步:编写 TTY 任务,轮询每个 TTY。

PUBLIC void task_tty() {
    TTY* p_tty;
    init_keyboard();
    for(p_tty = TTY_FIRST; p_tty < TTY_END; p_tty++) { // 初始化所有 TTY
        init_tty(p_tty);
    }
    select_console(0); // 当前是哪个控制台
    while(1) {
        for(p_tty = TTY_FIRST; p_tty < TTY_END; p_tty++) { // 对每个 TTY 进行读写操作
            tty_do_read(p_tty);
            tty_do_write(p_tty);
        }
    }
}

注意:task_tty() 指的是 TTY 进程,不是说每个 TTY 的任务(要执行的工作),该函数的作用是轮询每个 TTY,对每个 TTY 都进程初始化,以及完成对键盘缓冲区的读取和字符显示,这些才是 TTY 的工作。

第三步:init_tty()、tty_do_read()、tty_do_write()、in_process()

PUBLIC void init_tty(TTY* p_tty) {
    // 初始化缓冲区
    p_tty -> inbuf_count = 0;
    p_tty -> p_inbuf_head = p_tty -> p_inbuf_tail = p_tty -> in_buf;

    // 初始化屏幕
    init_screen(p_tty);
}
PRIVATE void tty_do_write(TTY* p_tty) {
    if(p_tty -> inbuf_count) { // 若缓冲区中有数据,则进行写入
        char ch = *(p_tty -> p_inbuf_tail);
        p_tty -> p_inbuf_tail++;
        if(p_tty -> p_inbuf_tail == p_tty -> in_buf + TTY_IN_BYTES)
            p_tty -> p_inbuf_tail = p_tty -> in_buf;
        p_tty -> inbuf_count--;
        out_char(p_tty -> p_console, ch);
    }
}
PRIVATE void tty_do_read(TTY* p_tty) {
    if(is_current_console(p_tty -> p_console)) // 判断当前TTY的控制台是不是当前控制台
        keyboard_read(p_tty); // 是,则读取缓冲区数据
}
PUBLIC void in_process(TTY* p_tty, u32 key) {
    char output[2] = {'\0', '\0'};
    if(!(key & FLAG_EXT)) {
        // 当前 TTY 的缓冲区长度超过 TTY 允许的最大容量
        if(p_tty -> inbuf_count >= TTY_IN_BYTES) return;
        *(p_tty -> p_inbuf_head) = key;
        p_tty -> p_inbuf_head++;

        if(p_tty -> p_inbuf_head == p_tty -> in_buf + TTY_IN_BYTES)
            p_tty -> p_inbuf_head = p_tty -> in_buf;

        p_tty -> inbuf_count++;
    } else {
        int raw_code = key & MASK_RAW;
        switch(raw_code) {
            case UP:
                if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R))
                    scroll_screen(p_tty -> p_console, SCR_DN);
                break;
            case DOWN:
                if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R))
                    scroll_screen(p_tty -> p_console, SCR_UP);
                break;
            case F1:
		    case F2:
		    case F3:
		    case F4:
		    case F5:
		    case F6:
		    case F7:
		    case F8:
		    case F9:
		    case F10:
		    case F11:
		    case F12:
		    	/* Alt + F1~F12 */
		    	if ((key & FLAG_ALT_L) || (key & FLAG_ALT_R)) {
		    		select_console(raw_code - F1);
		    	}
		    	break;
            default:
                break;
        }
    }
}

第四步:

// 向指定控制台输出字符
PUBLIC void out_char(CONSOLE* p_con, char ch) {
    u8* p_vmem = (u8*) (V_MEM_BASE + p_con -> cursor * 2);

    *p_vmem++ = ch;
    *p_vmem++ = DEFAULT_CHAR_COLOR;
    p_con -> cursor++;

    set_cursor(p_con -> cursor);
}
// 设置光标位置
PRIVATE void set_cursor(unsigned int position) {
    disable_int();
    out_byte(CRTC_ADDR_REG, CURSOR_H);
    out_byte(CRTC_DATA_REG, (position >> 8) & 0xFF);
    out_byte(CRTC_ADDR_REG, CURSOR_L);
    out_byte(CRTC_DATA_REG, position & 0xFF);
    enable_int();
}
// 设置屏幕的显示位置
PRIVATE void set_video_start_addr(u32 addr) {
    disable_int();
    out_byte(CRTC_ADDR_REG, START_ADDR_H);
    out_byte(CRTC_DATA_REG, (addr >> 8) & 0xFF);
    out_byte(CRTC_ADDR_REG, START_ADDR_L);
    out_byte(CRTC_DATA_REG, addr & 0xFF);
    enable_int();
}
// 切换控制台
PUBLIC void select_console(int nr_console) {
    if((nr_console < 0) || (nr_console >= NR_CONSOLES)) return;
    
    nr_current_console = nr_console;

    set_cursor(console_table[nr_console].cursor);
    set_video_start_addr(console_table[nr_console].current_start_addr);
}
// 往控制台输出字符
PUBLIC void init_screen(TTY* p_tty) {
    int nr_tty = p_tty - tty_table;
    p_tty -> p_console = console_table + nr_tty;
    
    int v_mem_size = V_MEM_SIZE >> 1; // 显存大小

    int con_v_mem_size = v_mem_size / NR_CONSOLES;
    p_tty -> p_console -> original_addr = nr_tty * con_v_mem_size;
    p_tty -> p_console -> v_mem_limit = con_v_mem_size;
    p_tty -> p_console -> current_start_addr = p_tty -> p_console -> original_addr;

    // 默认光标位置在最开始处
    p_tty -> p_console -> cursor = p_tty -> p_console -> original_addr;
    
    if(nr_tty == 0) {
        // 第一个控制台沿用原来的光标位置
        p_tty -> p_console -> cursor = disp_pos / 2;
        disp_pos = 0;
    } else {
        out_char(p_tty -> p_console, nr_tty + '0');
        out_char(p_tty -> p_console, '#');
    }

    set_cursor(p_tty -> p_console -> cursor);
}
// 滚动屏幕
PUBLIC void scroll_screen(CONSOLE* p_con, int direction) {
    if(direction == SCR_UP) {
        if(p_con -> current_start_addr > p_con -> original_addr) {
            p_con -> current_start_addr -= SCREEN_WIDTH;
        }
    } else if(direction == SCR_DN) {
        if(p_con -> current_start_addr + SCREEN_SIZE < 
           p_con -> original_addr + p_con -> v_mem_limit) {
           p_con -> current_start_addr += SCREEN_WIDTH;
        }
    } else {
        // ...
    }

    set_video_start_addr(p_con -> current_start_addr);
    set_cursor(p_con -> cursor);
}
// 判断是否为当前控制台
PUBLIC int is_current_console(CONSOLE* p_con) {
    return (p_con == &console_table[nr_current_console]);
}

图解

在这里插入图片描述

代码中大概的执行就是这样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值