文字を出力することはできたので、次はキーボードの文字入力を取得してみます。本書ではキーボードコントローラ(KBC)を使用してキー入力の取得を行います。
この章を通して作成するソースコードはサンプルディレクトリの"030_keyinput_polling"に格納しています。また、文中では前章最後の"022_font"のサンプルへ機能拡張・機能追加を行っていく形で説明していきます。
加えて、この章で実装するKBC関連の機能の構成は図3.1の通りです。
図3.1: この章で作る機能の構成
kbc.cとkbc.hがKBCのデバイスドライバに当たるものです。また、x86.cとx86.hはx86 CPU依存の処理をまとめたアーキテクチャ依存部です。x86.cとx86.hへはこの章以降も適宜関数を追加していきます。
キーボードコントローラは32ビット(そしてそれ以前)のアーキテクチャから存在するコントロールICで、その名の通りキーボードの制御を行ってくれます。昔はIntelの8042(i8042)というICがKBCとしてマザーボードに搭載されていましたが、集積度が上がり、現在は互換の機能がひとつのSoC*1内に入っています。
[*1] System on Chipの略。一つのチップの中に複数のICに相当する機能が集積されている。
どんな制御ICでもそうですが、KBCもレジスタ経由で操作します。各レジスタにはアドレスが割り当てられており、特定のアドレスへ読み書きすることで制御ICのレジスタへアクセスできます。ただし、Intelの場合、周辺の制御ICのレジスタは通常のメモリとは別のアドレス空間であり、io命令(in命令とout命令)を使用してアクセスします。
KBCには現在の状態を取得するためのステータスレジスタと、入力されたキーのデータ(キーコード)を取得するためのデータレジスタがあります。
KBCのステータスレジスタとデータレジスタのIOアドレスと読み出せる値の意味は表3.1の通りです。
表3.1: KBCのレジスタのIOアドレスと読み出せる値の意味
IOアドレス | レジスタ名と読み出した値の意味 |
---|---|
0x0060 | データレジスタ |
bit7 : 押下状態(make=0 / brake=1) | |
bit6-0: キーコード | |
0x0064 | ステータスレジスタ |
bit7-4: 予約 | |
bit3 : KBCコマンド/パラメータ書込フラグ | |
bit2 : 予約 | |
bit1 : IBF(入力バッファフル) | |
bit0 : OBF(出力バッファフル) |
表3.1の2つのレジスタのビットフィールドの中で、ステータスレジスタ(0x0064)のbit0と、データレジスタ(0x0060)の全ビットを使用します。
ステータスレジスタのOBFとIBFは、少しわかりにくいのですが、KBC自身から見た入力/出力であるため、KBCというICに対して外部に存在するx86 CPUはKBCの出力バッファからデータを読み出す事になります。そのため、KBCのOBF(出力バッファフル)を確認してデータ読み出しを行います。
データレジスタの押下状態(bit7)は、指でキーを押している状態を"make"、キーから指を離した状態を"brake"と呼んでいます。そのため、キーを押しっぱなしにしている間はデータレジスタのbit7は0(make)で読み出せ、キーから指を離すと一度だけbit7が1(brake)のデータがデータレジスタから読み出せます。
in命令はアセンブラで記述しなければなりませんが、本書では極力C言語で記述するためにインラインアセンブラを使用してみます。
まず、in命令を使用したIOアドレスのレジスタ読み出しはアセンブラでリスト3.1のように記述します。
リスト3.1: in命令を使用したステータスレジスタ読み出し例(アセンブラ)
movw $0x0064, %dx inb %dx, %al
リスト3.1について、アセンブラではmovbやinbといった命令に当たる部分を「オペコード」、続く"$0x0064, %dx"等を「オペランド」と呼びます。
オペコードにはmovやinといった命令に'w'や'b'といった一文字が付いていますが、これはデータサイズを示しています。'w'は2バイト、'b'は1バイトで、他にも'q'(8バイト)、'l'(4バイト)があります。
リスト3.1では、ステータスレジスタのアドレス(0x0064)をmov命令でdxレジスタへ格納し、in命令でdxレジスタが指すIOアドレスの値(KBCステータスレジスタの値)をalレジスタへ格納します。
リスト3.1をインラインアセンブラで記述するとリスト3.2の様になります。
リスト3.2: インラインアセンブラでのステータスレジスタ読み出し例
unsigned short ioaddr = 0x0064; unsigned char value; asm volatile ("inb %[ioaddr], %[value]" : [value]"=a"(value) : [ioaddr]"d"(ioaddr));
リスト3.2について、"asm"がインラインアセンブラであることを示す指定です。"asm"というシンボルを別の用途で使用したい場合等は"__asm__"と書くこともできます(そのように書いているコードも多いです)。そして、コンパイラの最適化の影響を受けない様、"volatile"を付けています。
リスト3.2の"inb %[ioaddr], %[value]"が元のアセンブラ(リスト3.1)の"inb %dx, %al"に当たる箇所です。なんとなく分かるかと思いますが、変数"ioaddr"のIOアドレスにあるレジスタの値を変数"value"へ読み出しています。
ちゃんと説明すると、インラインアセンブラ内の要素はコロン(:)区切りでリスト3.3のように並んでいます*2。
[*2] 入力オペランドの後ろにもう一つコロンで区切る事で「使用するレジスタのリスト」を指定することもできますが、本書では使用しないため特に説明しません。
リスト3.3: インラインアセンブラの構成要素
asm volatile (アセンブラテンプレート : 出力オペランド : 入力オペランド)
そして、出力オペランドと入力オペランドのフォーマットはリスト3.4の通りです(入力オペランドと出力オペランドの違いは'='が付くかどうかだけです)。
リスト3.4: 入力/出力オペランドのフォーマット
■ 出力オペランド =[アセンブラから参照する名前]"使用するレジスタ等の指定"(変数名) ■ 入力オペランド [アセンブラから参照する名前]"使用するレジスタ等の指定"(変数名)
リスト3.4の"使用するレジスタ等の指定"には、in命令の仕様の都合上、入力オペランドには"d"のレジスタを、出力オペランドには"a"のレジスタを指定しています。
x86の汎用レジスタには、32ビットではEAXやEBX、64ビットではRAXやRBXのように頭に'E'、あるいは'R'が付き、最後に'X'で終わる名前のレジスタがいくつかあります。
この様な命名には歴史的な経緯があります。そもそも、初めはAやBといった8ビットのレジスタがあり、CPUの16ビットへの進化に併せて8ビットから16ビット拡張され"extended"の意味で'X'を付け、16ビットレジスタはAXレジスタ、BXレジスタといった名前となりました。その後、32ビットへのさらなる拡張に伴い、同様に"extended"の意味で'E'が頭に付けられ32ビットレジスタはEAX、EBXといった名前となりましたが、64ビットでは流石に"extended"から文字を取るのに限界を感じたのかRAX、RBXという名前になっています*3*4。
[*3] 書籍"自作エミュレータで学ぶx86アーキテクチャ: コンピュータが動く仕組みを徹底理解!"より
[*4] 64ビットモードでは汎用レジスタ自体も16個へ増えており、増えた分は"R8"〜"R15"という名前になっています。そのため、RAX等の'R'は単に"Register"を指すものと思われます。
同じアルファベットに対して'E'や'R'、'X'が付いたレジスタはアクセスするサイズが違うだけで同じレジスタを指していて、RAXの下位32ビットをEAXでアクセスでき、EAXの下位16ビットをAXでアクセスできます。AXの上位と下位8ビットはそれぞれ"AH"・"AL"という名前でアクセスできます('A'というレジスタ名は無くなりました)*5。
[*5] R8〜R15の様な64ビットレジスタはR8D〜R15Dという名前で下位32ビットアクセスできます。(16ビット単位/8ビット単位のレジスタ名はありません)
それでは、キーが入力されたかどうかをソフトウェア的にステータスレジスタを監視する方法(ポーリング)で実装してみます。(ハードウェアによる割り込みを使用する方法は次章で紹介します。)
まずは、「指定したIOアドレスのレジスタから値を取得する」処理をC言語から汎用的に呼べるように関数化します。本書では、インラインアセンブラを使用して"io_read"という関数を用意することにします。x86 CPUに依存した関数群は"x86.c"というソースファイルにまとめることにし、対応して"x86.h"というヘッダファイルも用意します(リスト3.5、リスト3.6)。
リスト3.5: 030_keyinput_polling/include/x86.h
#ifndef _X86_H_ #define _X86_H_ inline unsigned char io_read(unsigned short addr); #endif
リスト3.6: 030_keyinput_polling/x86.c
#include <x86.h> inline unsigned char io_read(unsigned short addr) { unsigned char value; asm volatile ("inb %[addr], %[value]" : [value]"=a"(value) : [addr]"d"(addr)); return value; }
それでは、io_read関数を使用して、データレジスタの取得を行う"get_kbc_data"関数と、get_kbc_data関数を使用してキーコードの取得を行う"get_keycode"関数を作成します。KBCに依存した処理のため"kbc.c"で定義することにします。内容はリスト3.7の通りです。
リスト3.7: 030_keyinput_polling/kbc.c(get_kbc_data()とget_keycode())
#define KBC_DATA_ADDR 0x0060 #define KBC_DATA_BIT_IS_BRAKE 0x80 #define KBC_STATUS_ADDR 0x0064 #define KBC_STATUS_BIT_OBF 0x01 unsigned char get_kbc_data(void) { /* ステータスレジスタのOBFがセットされるまで待つ */ while (!(io_read(KBC_STATUS_ADDR) & KBC_STATUS_BIT_OBF)); return io_read(KBC_DATA_ADDR); } unsigned char get_keycode(void) { unsigned char keycode; /* make状態(brakeビットがセットされていない状態)まで待つ */ while ((keycode = get_kbc_data()) & KBC_DATA_BIT_IS_BRAKE); return keycode; }
ポーリング(ソフトウェア的な待機処理)を行っているのが正にリスト3.7のget_kbc_data関数とget_keycode関数それぞれのwhileです。
また、リスト3.7のget_keycode関数ではmake状態のキーコードを待ちます。brake状態のキーコードを待つようにしたい場合はwhile内の条件を反転させてください。
キーコードを取得できたので、キーコードをASCIIへ変換し、画面へ表示してみます。キーコードをASCIIへ変換する配列"keymap"と、キー入力1文字を取得する"getc"関数、そして各種ヘッダのincludeを"kbc.c"へ追加します(リスト3.8)。
リスト3.8: 030_keyinput_polling/kbc.c
#include <kbc.h> /* 追加 */ #include <x86.h> /* 追加 */ #define KBC_DATA_ADDR 0x0060 #define KBC_DATA_BIT_IS_BRAKE 0x80 #define KBC_STATUS_ADDR 0x0064 #define KBC_STATUS_BIT_OBF 0x01 /* 追加(ここから) */ const char keymap[] = { 0x00, ASCII_ESC, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '^', ASCII_BS, ASCII_HT, 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '@', '[', '\n', 0x00, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ':', 0x00, 0x00, ']', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 0x00, '*', 0x00, ' ', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, '7', '8', '9', '-', '4', '5', '6', '+', '1', '2', '3', '0', '.', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, '_', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, '\\', 0x00, 0x00 }; /* 追加(ここまで) */ /* ・・・ 省略 ・・・ */ /* 追加(ここから) */ char getc(void) { return keymap[get_keycode()]; } /* 追加(ここまで) */
そして、kbc.cのgetc関数と、keymap配列で使用しているASCII_ESC等のASCII制御文字は外部からも参照できるよう"kbc.h"に宣言と定義を用意します(リスト3.9)。
リスト3.9: 030_keyinput_polling/include/kbc.h
#ifndef _KBC_H_ #define _KBC_H_ #define ASCII_ESC 0x1b #define ASCII_BS 0x08 #define ASCII_HT 0x09 char getc(void); #endif
ここまでで、キー入力取得に必要な道具は揃いました。ここではmain.cを改造して、キー入力取得のサンプルとしてはよくある「エコーバック*6」を作ってみます。改造後のmain.cはリスト3.10の通りです。
[*6] 入力された文字をそのまま画面へ表示するプログラム
リスト3.10: 030_keyinput_polling/main.c
#include <fb.h> #include <fbcon.h> #include <kbc.h> /* 追加 */ void start_kernel(void *_t __attribute__ ((unused)), struct framebuffer *fb, void *_fs_start __attribute__ ((unused))) { fb_init(fb); set_fg(255, 255, 255); set_bg(0, 70, 250); clear_screen(); /* 変更箇所(ここから) */ while (1) { char c = getc(); if (('a' <= c) && (c <= 'z')) c = c - 'a' + 'A'; else if (c == '\n') putc('\r'); putc(c); } /* 変更箇所(ここまで) */ }
現状のフォントの都合上、アルファベットは大文字しか表示できないので、リスト3.10では入力された文字がアルファベットの場合、大文字に変換しています。また、Enterを押下すると'\n'(LF)を受け取るので、'\n'取得時は追加で'\r'(CR)も出力しています。
最後に今回追加したソースファイルをMakefileへ反映します(リスト3.11)。
リスト3.11: 030_keyinput_polling/Makefile
TARGET = kernel.bin CFLAGS = -Wall -Wextra -nostdinc -nostdlib -fno-builtin -fno-common -Iinclude LDFLAGS = -Map kernel.map -s -x -T kernel.ld $(TARGET): main.o fbcon.o fb.o font.o kbc.o x86.o # kbc.oとx86.oを追加 ld $(LDFLAGS) -o $@ $+ # ・・・ 省略 ・・・
ここまででソースコードの追加・変更は終わりです。実行すると、図3.2の様に入力した文字が画面に表示されます。
図3.2: 030_keyinput_pollingの実行結果