系列目录

键盘输入

这个 kernel 系列项目到这里已经完成了所有基本功能的搭建了,最后两篇算是拓展完善,我们将会加入键盘功能,并在此基础上实现一个 shell 命令行界面。

键盘码

键盘的详细原理和实现比较冗长无聊,这里也不想浪费时间解释,感兴趣的话你可以在网上找资料研究。本篇会尽可能地简单化处理,把很多底层细节省略掉,只关注核心实现原理。

一般来说键盘上你按一个键,然后松开,它会产生两个电信号:

  • 按下键产生的那个信号叫通码(make code),就是接通的意思;
  • 松开键产生的那个信号叫断码(break code),就是断开的意思;

通码和断码都称为扫描码(scan code),对于键盘来说是一视同仁的,就是发送一个信号给主机而已,操作系统接收到这一系列信号之后,则需要将它们翻译成对应的输出字符。一个键有通、断两个码是必要的,比如在用户界面上你可以决定是按下键就打印出字符,还是一定要按下并松开才打印字符,这在用户感受上是不一样的;再例如某些组合键,Shift + a,系统连续接收到 Shfit 的通码和 a 的通码,才会翻译成一个 A 的通码,中间不可以有 Shfit 的断码,否则表示 Shift 已经松开。

中断触发

键盘的信号是通过中断来触发的,中断号 33,因此我们首先为它注册中断 handler:

register_interrupt_handler(IRQ1_INT_NUM,
                           &keyboard_interrupt_handler);

keyboard_interrupt_handler 函数里,会从端口 0x60 读取输入的 scan code,然后将它加入到缓冲区暂存。这里我们用到了一个环形缓冲区(ring buffer),它是一个容量有限的队列,键盘中断 handler 不断将新输入的 scan code 加入到这个缓冲区尾部,而消费者则从缓冲区头部不断地读取消费 scan code 并翻译成字符。

消费阻塞等待

scan code 缓冲队列消费者是函数 read_keyboard_char_impl,它的核心逻辑在函数 process_scancode,它的功能是将读入的 scan code 翻译为字符。不过它的实现细节不必深究,十分枯燥冗长,就是对着 scan code 码表翻译而已。

int32 read_keyboard_char_impl() {
  if (queue.size == 0) {
    return -1;
  }

  int32 augchar = process_scancode((int)dequeue());
  while (!(KH_HASDATA(augchar) && KH_ISMAKE(augchar))) {
    if (queue.size == 0) {
      return -1;
    }
    augchar = process_scancode((int)dequeue());
  }
  return KH_GETCHAR(augchar);
}

如果缓冲区是空的,或者当前的 scan code 不足以翻译成一个有效的字符(例如只读到一个 Shfit 的通码),那么它不会返回有效字符。注意它的退出判断条件:

(KH_HASDATA(augchar) && KH_ISMAKE(augchar))

即翻译出来的是一个有效字符并且是一个通码,就视作是一个合法的按键输入,需要返回给上层做反馈。

read_char 系统调用

我们从顶向下来看用户如何获取键盘输入的字符。键盘输入的处理是在 kernel 里的,作为 user 层,需要使用系统调用来获取键盘输入。我们定义一个新的系统调用 read_char,具体实现是 read_keyboard_char 函数,它调用的正是上面的 read_keyboard_char_impl 函数。如果当前无有效字符能被翻译出来,它会阻塞当前线程,那么用户端看来就是程序会卡在这里,等待键盘输入。

如果有新的键盘中断进来,表示有新 scan code,那么 kernel 会唤醒阻塞在 read_keyboard_char 里的等待线程,让它继续消费 scan code 的缓冲区队列,尝试继续翻译有效字符出来。

可以用下面的测试程序,你会在屏幕上得到一个类似 shell 命令行,或者文本编辑器里的按键反馈输出字符的效果:

void test_read_char() {
    while (1) {
        int8 c = read_char();
        printf("%c", c);
    }
}
03-05 23:58