代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
为了方便获取结果,我们将这些复杂的硬件控制指令封装成一个过程,每次只把对硬件的操作需道的过程便是驱动程序。
这里我们要编写键盘中断处理程序,也是目前第一个硬件驱动程序。 /device/keyboard.c
#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"
#define KBD_BUF_PORT 0x60 //键盘buffer寄存器端口号为0x60
/*用转义字符定义部分控制字符*/
#define esc '\033' //8进制表示字符,也可以用16进制'\x1b'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177' //8进制表示字符,也可以用16进制'\x7f'
/*以上不可见字符一律定义为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
/*定义控制字符的通码和断码*/
#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
/*定义一下变量记录相应键是否按下的状态,ext_scancode用于记录makecode是否以0xe0开头*/
static bool ctrl_status,shift_status,alt_status,caps_lock_status,ext_scancode;
/*以通码make_code为索引的二维数组*/
static char keymap[][2] = {
/*扫描码未与shift组合*/
/*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}
/*其他按键暂不处理*/
};
/*键盘中断处理程序*/
static void intr_keyboard_handler(void){
/*必须要读取输出缓冲区寄存器,否则8042不在继续响应键盘中断*/
/*这次中断发送前的上一次中断,一下任意三个键是否有按下*/
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool caps_lock_last = caps_lock_status;
bool break_code;
uint16_t scancode = inb(KBD_BUF_PORT);
/*若扫描码scancode是e0开头的,表示此键的按下将产生多个扫描码,
所以马上结束此次中断处理函数,等待下一个扫描码进来*/
if(scancode == 0xe0){
ext_scancode = true; //打开e0标志
return; //表示后面还有扫描码,返回
}
/*如果上次是以0xe0开头的,将扫描码合并*/
if(ext_scancode){
scancode = ((0xe000) | scancode);
ext_scancode = false;
}
/*判断扫描码是通码还是断码*/
break_code = ((scancode & 0x0080) != 0); //获取break_code
//若是断码break_code(按键弹起时产生的扫描码)
if(break_code){
/*由于ctrl_r和alt_r的make_code和break_code都是2字节,所以可以用下面的方法取make_code,多字节的扫描码暂不处理*/
uint16_t make_code = (scancode &= 0xff7f); //通码与断码的区别在于第8位是0还是1
//取得其make_code(按键按下时产生的扫描码)
/*若是任意下面三个键弹起,将其状态置为false*/
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;
}
/*若为通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code*/
else if((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)){
bool shift = false; //判断是否与shift组合,用来在一维数组中索引对应的字符
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键
shift = true;
}else{ //默认为字母键
if(shift_down_last && caps_lock_last){ //如果shift和capslock同时按下
shift = false;
}else if(shift_down_last || caps_lock_last){
shift = true;
}else{
shift = false;
}
}
uint8_t index = (scancode &= 0x00ff); //将扫描码的高字节置零,主要针对高字节是e0的扫描码
char cur_char = keymap[index][shift]; //在字节中找到对应的字符
/*只处理ASCII码不为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("unkown key\n");
}
return;
}
/*键盘初始化*/
void keyboard_init(){
put_str("keyboard init start\n");
register_handler(0x21,intr_keyboard_handler);
put_str("keyboard init done\n");
}代码解释:
KBD_BUF_PORT为0x60,这是 8042 输入和输出缓冲区寄存器的端口。
#define KBD_BUF_PORT 0x60 //键盘buffer寄存器端口号为0x60
下面是预先定义的按键扫描码,用宏定义的控制键的通码(make)及断码(break),它们将在扫描码数组中用到。
这是定义了控制键的 ASCI 码。
/*用转义字符定义部分控制字符*/ #define esc '\033' //8进制表示字符,也可以用16进制'\x1b' #define backspace '\b' #define tab '\t' #define enter '\r' #define delete '\177' //8进制表示字符,也可以用16进制'\x7f'
这是定义的是操作控制键的“ASCI 码”。
/*以上不可见字符一律定义为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
这是定义的是操作控制键的扫描码,用于组合键中的判断。 比如按下了shift时再按字母键,就表示输入的是大写字符。
/*定义控制字符的通码和断码*/ #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
操作控制键与其他键配合时是先被按下的,每次在接收一个按键时,需要查看上一次是否有按下相关的操作控制键,所以咱们得记录操作控制键在之前是否被按下了,也就是将操作控制键的当前状态记录在某个全局变量中。
/*定义一下变量记录相应键是否按下的状态,ext_scancode用于记录makecode是否以0xe0开头*/ static bool ctrl_status,shift_status,alt_status,caps_lock_status,ext_scancode;
二维数组keymap主要是定义了与shift组合时的字符效果。数组范围是0~0x3A,这是目前所支持的主键盘区的按键范围。
一维数组中的第0个元素是某个按键未与shift键组合时对应的字符ASCII码值,第1个元素是某个按键与shift键组合时对应的字符ASCI码值。比如 a的通码为0x1e,比如按下a键,若之前未下shift键,应该处理为小写字符’a’,所以keymap[0x1e][0]等于’a’,否则若已经按下了shift键,应该处理为大写字符’A’,所以keymap[0x1e][1]等于A’。没有通码为0的键,因此数组第0个一维数组keymap[0][],为其定义两个0值。
下面来看那些函数:
键盘中断处理程序始终是每次处理一个字节,所以当扫描码中是多字节时,或者有组合键时,咱们要定义额外的全局变量来记录它们曾经被按下过。ctrl_status、shift_status 和 caps_lock_status 是分别记录<ctrl>、<shift>和<caps lock>三个键的状态,值为true表示按下,值为false表示弹起。每次这 三个键被按下或被弹起时,都将记录在这些变量中,对这三个键的处理是在intr_keyboard_handler中 我们只定义了ctrl_status,shift_status,alt_status和caps_lock_status这4个控制键,其中用于组合键的只有前三个,因此,咱们最多支持<ctrl>+<shift>+<alt>三个控制键形式的组合键。
函数intr_keyboard_handler在程序开头定义了三个布尔变量ctrl_down_last、 shift_down_last和caps_lock_last,分别从那三个全局变量中获取这三个键曾经是否被按下并且尚未松开。
uint16_t scancode = inb(KBD_BUF_PORT)从端口 KBD_BUF_PORT 获取扫描码,开始处理。目前只支持主键盘区上的键,因此只存在一个0xe0作为扫描码前缀的情况。我们这里用变量ext_scancode 作为0xe0扩展扫描码的标记。if(scancode == 0xe0)只要发现扫描码为0xe0,就表示此键的扫描码多于一个字节,后面还有扫描码,因此将ext_scancode标记置为true后执行return返回。if(ext_scancode)通过ext_scancode标记判断上一次是否收到了0xe0,如果是,将此次接收到的扫描码与0xe0合并为完整的扫描码(此时为通码或断码)用于后续处理,并且将ext_scancode置为false,关闭扩展扫描码标记。
通过break_code= ((scancode & 0x0080) !=0)来判断扫描码是否为断码。断码的第8位为1,所以 用扫描码scancode和0x0080进行位与操作,此时bread_code的值为true或false。判断若为断码,就进入断码的处理代码块中。接下来我们要判断此次按键(扫描码)对应的字符是什么。因为我们的扫描码对应的字符定义在二维数组keymap中,通码是此数组的索引,所以此时接收的若为断码,为了检索数组keymap,我们还是要将其还原为通码。
第一套键盘扫描码通码和断码的区别就是扫描码第 8 位的值,断码的第 8 位为 1,通码的第 8 位为 0。因此,在第 137 行,将扫描码 scancode(此时它为断码)与0xff7f 进行位与运算,抹去第8位的1,这样就获得了其通码,并将其存储到变量make_code中。
然后判断此通码是否为 ctrl、shift、alt。一般情况下这三个键在键盘上都是左右各一个,所以无论按下哪个都表示按下了同一功能的控制键,因此无论弹起哪个也都表示弹起了同一功能的控制键。
“if (make_code=ctrl_1_make || make_code=ctrl_r_make)”,拿通码 make_code分别与左ctrl键的通码ctrl_l_make和右ctrl键的通码ctrl_r_make比较,若满足其一,便将 ctrl_status 置为 false,表示 ctrl 键此时被松开弹起了。注意是置为 false,虽然我们是用通码在判断,但现在处于处理断码的代码块中,此代码块就是判断是哪个键被弹起了。前面咱们分析过组合键的弹起顺序了,一般是先松开控制键,再松开字符键,所以这三个键的状态变量 ctrl_status、shift_status 和 alt_status 并不是本次使用,是供下次判断组合键用的,本次只是记录是否松开了它们。
下次在进入键盘中断时,在intr_keyboard_handler的开头通过ctrl_down_last = ctrl_status获取上一次ctrl 键是否处于弹起的状态(也就是没有按下它)。对于alt和shift的处理也是一样,最后结束断码的处理,通过return返回。
大致如此,不再阐述。
对应/device/keyboard.h
#ifndef __DEVICE_KEYBOARD_H #define __DEVICE_KEYBOARD_H #include "stdint.h" //函数声明 /*键盘初始化*/ void keyboard_init(); #endif
结果如下:
正确打印”Hello world! Hello Dreams!”

参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


