键盘
目前主流键盘: AT、PS/2、USB。
键盘敲击的过程
在键盘中存在一枚叫做键盘编码器的芯片,它通常是 Intel 8048 以及兼容芯片,作用是监视键盘的输入,并把适当的数据传送给计算机。
在计算机主板中存在一个键盘控制器,作用是接受和解码来自键盘的数据,并与 8259A 以及软件等进行通信。
敲击键盘所产生的编码称为扫描码,共分成两类,分别是按下和弹起,每个 MakeCode 和 BreakCode 都对应一个扫描码。(Pause 键除外)。
扫描码共有三套:早期的 XT 键盘使用 Scan code set 1、现在默认 Scan code set 2、很少使用 Scan code set 3。
键盘敲击的过程:
-
8048 检测到有按键被按下或弹起。
-
8048 将相应的扫描码发送给 8042。
-
8042 会把它转换成相应的 Scan code set 1 扫描码,并且将其放入输入缓冲区中。
-
最后 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,并且处理它的事件、从键盘缓冲区读取数据、在屏幕显示字符等内容。
注意:
- 只有某个 TTY 对应的控制台是当前控制台时,才可以读取键盘缓冲区(因此图中为虚线)。
- TTY 可以处理很多事情,这里只做显示一项。
- 显示器和键盘是所有 TTY 共有的,因此画在了外面。
轮询到的 TTY 的任务:
- 处理输入 —— 判断 TTY 对应的控制台是否为当前控制台,若是则从键盘缓冲区读取数据。
- 处理输出 —— 在屏幕中显示字符,该步骤不需要判断 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]);
}
图解
代码中大概的执行就是这样。