写在前面:自制操作系统Gos 第三章第三篇:主要内容是操作键盘
键盘输入原理
键盘是个独立的设备,其内存有个叫键盘编码器的芯片。其作用是:每当键盘上发生案件操作,他就向键盘控制器报告哪个键被按下,哪个按键弹起了。
而键盘控制器并不在键盘内部,它在主机内部的芯片。所以当某个键被按下时,键盘编码器就把这个键对应的数值发送给键盘控制器。
而这个数值就是被称之为键盘扫描码,其对照表如下:
我们可以看到在扫描码中有两个码:Make Code(通码)、Break Code(断码)。其中通码就是按下去的时候产生的码,而断码是按键弹开的时候产生的码,断码和通码的关系是:断码=0x80+通码。那么对于断码和通码我们就可以这样理解了:其都是一字节大小,最高位也就是第七位的值决定按键的状态,如果最高位为0,那么其实就表示按键处于按下状态;否则为1的话,则表示按键弹起。
组合键的实现
上面阐述的都是单个键的识别,而对于成熟的操作系统来说,往往是支持组合键的。比如说,Windows下我们常用的ctrl+c
和ctrl+v
组合键是如何实现的?
那么我们就拿ctrl+c
这个组合键来介绍一下这个过程:
- 首先我们是先按下ctrl这个键,通过查第一套键盘扫描表可以知道,这个时候键盘编码器会持续向键盘控制器发送扫描码
0x1D
,这个时候键盘控制器将这个放到自己的输出缓冲区寄存器。之后键盘控制器会触发硬件中断,键盘驱动会从缓冲区获得扫描码0x1D
,之后在某个全局变量中记录ctrl已经被按下。 - 现在我们是按下ctrl不放手的状态,这个时候会持续发送扫描码
0x1D
,还是会执行第一步的过程,所以我们需要增加逻辑判断,全局变量ctrl_status
此时应该判断,如果已经被按下,就忽略此次中断。 - 之后,c键被按下,这个时候就会收到扫描码
0x2E
,我们这个时候ctrl_status
已经表示ctrl键被按下了,那么这个时候,我们就能判断用户按下的其实就是ctrl+c
这个组合键了。之后执行相应的逻辑处理就可以了。
操纵键盘控制器
键盘控制器,其实大多指Intel的8042芯片,其被集成在主板上的南桥芯片中。本质上,其是键盘的代言人,所以和键盘交互可以理解为和键盘控制器交互。
键盘控制器有4个8位寄存器,它们就是我们和键盘控制器交互的窗口:
寄存器 | 端口 | 作用 |
---|---|---|
输出缓冲区 | 0x60 | 读 |
输入缓冲区 | 0x60 | 写 |
状态寄存器 | 0x64 | 读 |
控制寄存器 | 0x64 | 写 |
可以看到,四个寄存器共用两个端口,这是因为在不同的场景下,有不同的含义:
- 当我们把数据发送到键盘控制器的时候,这个时候
0x60
端口就是输入缓冲区,这个时候使用我们的outb
函数将数据写入0x60
端口 - 当我们接收来自键盘编码器的数据的时候,
0x60
端口的作用就是输出缓冲区,这个时候我们应该用inb
函数从0x60
端口读取数据
对于状态寄存器和控制寄存器来说,其每一位都有不同的意义,首先是状态控制器,我们先看一下其结构:
然后是控制寄存器,如果说状态控制器更多的是表示和键盘交互的状态的话,那么控制寄存器更多的表示当前键盘的控制权限:
键盘初始化
键盘初始化的动作很简单,对应键盘的中断是0x21
,我们直接设置中断处理函数intr_keyboard_handler
和中断号0x21
的映射关系就完成了初始化。
/*
* @brief 键盘驱动初始化
*/
void keyboard_init()
{
put_str("keyboard init start!\n");
ioqueue_init(&keyboard_buff); //定义键盘缓冲区,这样可以连续处理多个字符
register_handler(0x21, intr_keyboard_handler);
put_str("keyboard init done!\n");
}
键盘驱动
每当发生一次键盘中断,就会触发键盘中断函数,也就是键盘驱动。首先我们定义了三个全局变量来表示组合键ctrl
、shift
、alt
和capslock
这四个键是否被按下,由于只有两种状态,所以我们可以直接选择布尔类型作为变量类型。同时,我们知道使用组合键的时候,按键是会一直被触发的,所以我们还需要一个变量ext_scancode
来表示当前是否是组合键状态,等待下一个有效字符的输入。
// * @brief 定义以下变量记录相应键是否按下的状态,用于组合键
static bool ctrl_status; //ctrl
static bool shift_status; //shift
static bool alt_status; //alt
static bool caps_lock_status; //capslock
static bool ext_scancode; //0xe0标记,代表是持续按下按键,会产生多个扫描码
所以,在键盘驱动中,我们一开始要做的其是就是先获取组合键的状态。之后我们开始从缓冲区中获取此次输入的字符,放到scancode
中,如果发现此次输入的字符是0xe0
,那么其是表示此次按下的键的扫描码多于一个字符,后面还有扫描码,所以我们直接返回就可以;如果不是的话,我们还需要判断此时ext_scancode
,因为其是上一次是否收到0xe0
的标记,如果其为true,那么代表这次收到的字符和上一次收到的字符应该合并成一个完整的字符。
之后,我们需要判断此次字符是断码还是通码,判断之后就可以进入具体的字符处理模块了,这里的规则和平时的输入都是一样的,比如说ctrl+8 = *
字符等等,之后,我们识别出了这些字符就可以把它们放到系统内核的环形缓冲区中,详情可以见代码:
/*
* @brief 键盘中断程序
*/
static void intr_keyboard_handler(void)
{
//检测上次中断发生前ctrl、shift以及capslock是否被按下
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool capslock_down_last = caps_lock_status;
bool break_code; //断码标记,断码我们有特殊的处理方式
//获取上一次中断发生的字符
uint16_t scancode = inb(KEYBOARD_BUFF_PORT);
if (scancode == 0xe0)
{
//持续按下,会有后续按键
ext_scancode = true;
return;
}
if (ext_scancode)
{
scancode = ((0xe000) | scancode); //获取scancode的完整版本,即加上0xe0前缀
ext_scancode = false; //关闭0xe0标记
}
break_code = ((scancode & 0x0080) != 0); //判断是否是断码
if (break_code)
{
//断码处理模块,做的工作主要是组合键状态改变的记录
uint16_t make_code = (scancode &= 0xff7f); //得到按键按下时的扫描码
if (make_code == ctrl_l_make || make_code == ctrl_r_make)
{
//如果时ctrl,那么代表ctrl键已经被松开了
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;
}
return;
}
else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make))
{
//通码处理,如果时范围在00~3b、alt_r_make、ctrl_r_make
bool shift = false; //shift效果标记
//扫描码转换为字符
//先判断是否是二义性字符有0~9、[\;/,.=-'等等
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 && capslock_down_last)
{
//shift 和 capslock同时按下
shift = false;
}
else if (shift_down_last || capslock_down_last)
{
//任意按下其中之一
shift = true;
}
else
{
//都没按下
shift = false;
}
}
uint8_t index = (scancode &= 0x00ff); //得到下标,0xe0开头的会去除前缀
char current_char = keymap[index][shift];
//这里处理组合键ctrl+l 或者ctrl+u
if ((ctrl_down_last && current_char == 'l') || (ctrl_down_last && current_char == 'u'))
{
current_char -= 'a';
}
if (current_char)
{
//只处理ascii不为0的键
if (!ioqueue_is_full(&keyboard_buff))
{
ioqueue_putchar(&keyboard_buff, current_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 char!\n");
}
}
参考文献
[1] 操作系统真相还原