第5章 キーボードドライバを作る

前章で画面描画が行えるようになりました。

この章ではキーボードドライバを作成することで、シリアル通信ではない方法でキー入力を取得できるようにします。

この章では1章を通して「051_get_scancode」のサンプルを作成します。

5.1 キー入力を受け取るまでの流れ

キーボードのキー入力は、キーボードコントローラ(KBC)というICとやり取りすることで取得できます。KBCもレジスタを持っており、KBCのレジスタを参照することで、入力されたキーの情報や、キーボードに関する現在のステータスを確認できます。

KBCのレジスタはIOアドレス空間に対応付けられています。そのため、in命令/out命令でレジスタを読み書きします。

キー入力(スキャンコード)を取得する流れは、シリアル通信でデータ受信を行ったときと同様で、以下の通りです。

  1. ステータスレジスタ値を取得
  2. キー入力値が無ければ1.(プログラム先頭)へ戻る
  3. データレジスタからキー入力値を取得
  4. キー入力値からキー押した(Make)かキーを離した(Break)かを判別(希望の状態でなければ1.へ戻る)
  5. キー入力値からスキャンコードを取得
  6. 1.へ戻る

今回は、キーから指を離した(Break)時のスキャンコードを取得してみることにします。

5.2 ステータスレジスタ値を取得

それでは、実装してみます。

KBCのステータスレジスタはIOアドレス空間の0x0064に割り当てられている8ビットのレジスタです。

例によってin命令でレジスタ値を取得します。なお、今回のようにIOアドレスが8ビット(1バイト)に収まる場合、2バイトで書ける構文があります。

表5.1: 構文46: in: IOアドレス(1バイト)先からALへ値を読み出す

アセンブラin 即値(8bit), %al
機械語e4 即値(8bit)

今回の場合、以下のようになります。

表5.2: 例: in: IOアドレス(0x64)からALへ読み出す

アセンブラin $0x64,%al
機械語e4 64

なので、「1. ステータスレジスタ値を取得」の部分の機械語コードはリスト5.1の通りです。

リスト5.1: 051_get_scancode/sample.txt

# 1. ステータスレジスタ値を取得
00: e4 64       # in $0x64,%al

5.3 キー入力値が無ければ先頭へ戻る

ステータスレジスタの最下位ビット(ビット0)でKBCにキー入力値がある(=1)か無い(=0)かが分かります。

「フラグが立つまで待つ」系の処理で、コードブロックの構造としては「シリアル通信」の「受信待ち」の時と使う命令も同じです(リスト5.2)。

リスト5.2: 051_get_scancode/sample.txt

# 1. ステータスレジスタ値を取得
00: e4 64       # in $0x64,%al

# 追加(ここから)
# 2. キー入力値が無ければ先頭へ戻る
# (pauseしながら待つ)
02: 24 01       # and $0x01,%al
04: 75 04       # jne +6 (入力が有れば進む)
06: f3 90       # pause
08: eb f6       # jmp -0x08 (入力が無ければ戻る)
# 追加(ここまで)

and命令によるマスクでALレジスタの最下位ビットだけ抽出し、その結果に応じて以下の挙動となります。

  • andの結果が0 (ビット0がセットされていない)
    • ゼロフラグがセットされる
    • jneはスルーし、pauseした後、jmpでin命令まで戻る
  • andの結果が1 (ビット0がセットされている)
    • ゼロフラグがクリアされる
    • jneでこの命令から(この命令含む)6バイト先へジャンプ(このコードブロック直後)

5.4 データレジスタからキー入力値を取得

データレジスタのIOアドレスは0x60です。in命令で取得しALレジスタへ格納します。

sample.txtへ追記するとリスト5.3の通りです。

リスト5.3: 051_get_scancode/sample.txt

# 1. ステータスレジスタ値を取得
00: e4 64       # in $0x64,%al

# 2. キー入力値が無ければ先頭へ戻る
# (pauseしながら待つ)
02: 24 01       # and $0x01,%al
04: 75 04       # jne +6 (入力が有れば進む)
06: f3 90       # pause
08: eb f6       # jmp -0x08 (入力が無ければ戻る)

# 追加(ここから)
# 3. データレジスタからキー入力値を取得
0a: e4 60       # in $0x60,%al
# 追加(ここまで)

5.5 キー入力値がBreakならスキャンコードを抽出し、Makeなら先頭へ戻る

データレジスタの8ビットの内容は以下の通りです。

表5.3: データレジスタの内容

ビット内容
7Make(=0)かBreak(=1)か
0 - 6スキャンコード

Make時のスキャンコードなら、ビット7は0なので、単にデータレジスタから取得した8ビットをそのままスキャンコードとして使えば良いのですが、今回はBreak時のスキャンコードを取得したいので、ビット7がスキャンコードに含まれないように何らかの対処が必要です。

実装方針

今回は以下の2命令で実装してみます。

  1. sub命令: AL - 0x80 → AL
  2. jb命令 : 演算結果がマイナスなら先頭へジャンプ

1つ目のsub命令がミソです。ALのビット7がセットされていたか否かに応じて、以下の挙動になります。

  • ALのビット7が0 (Make)
    • 0x80の減算結果はマイナスの値になる
    • ALには負値の何らかの値が格納される(使わないので気にしない)
  • ALのビット7が1 (Break)
    • 0x80の減算結果はプラスの値になる
    • ALにはビット7のみ0にした値が格納される(スキャンコードのみ抽出)

こうすることで、「スキャンコード部分の抽出」と「最上位ビットの状態確認」を同時に行うことができます。

比較結果が「小さい」ならジャンプ(命令26「jb」)

なお、「jb」命令は初出なのでここで紹介します。

条件付きジャンプ命令の一種で、「Jump if Below」の略です。フラグレジスタとしてはキャリーフラグがセットされているとジャンプします。

構文は以下の通りです。

表5.4: 構文47: jb: 比較結果が小さいならジャンプ

アセンブラjb <ラベルあるいは".">
機械語72 fe+rel_addr

キャリーフラグに応じてジャンプする命令でありながら「Jump if Below」という名前なのは、cmp命令が内部的には減算によって比較を行っていることに由来していると思いますが、キャリーフラグはオーバーフローした場合にもセットされるので、add命令等で桁溢れが起きてキャリーフラグがセットされた場合も、jb命令はもちろんジャンプします。

実装

sample.txtへ追記するとリスト5.4の通りです。

リスト5.4: 051_get_scancode/sample.txt

# 1. ステータスレジスタ値を取得
00: e4 64       # in $0x64,%al

# 2. キー入力値が無ければ先頭へ戻る
# (pauseしながら待つ)
02: 24 01       # and $0x01,%al
04: 75 04       # jne +6 (入力が有れば進む)
06: f3 90       # pause
08: eb f6       # jmp -0x08 (入力が無ければ戻る)

# 3. データレジスタからキー入力値を取得
0a: e4 60       # in $0x60,%al

# 追加(ここから)
# 4. キー入力値がBreakならスキャンコードを抽出し、Makeなら先頭へ戻る
0c: 2c 80       # sub $0x80,%al
0e: 72 f0       # jb -0x0e (Makeなら先頭へ戻る)
# 追加(ここまで)

jb命令2バイト目の相対アドレス表記はその他のjmp命令と同じで、自分自身の命令位置が0xfeです。今回の場合、sub命令含め、jbより上に14バイトあるので、相対アドレス表記としては0xf0になります。

5.6 スキャンコードをレジスタCLへコピーし、先頭へ戻る

特に意味はないのですが、一応、アウトプット代わりとして、取得したスキャンコードをCLレジスタへコピーした上で、先頭へ戻ります(リスト5.5)。

リスト5.5: 051_get_scancode/sample.txt

# 1. ステータスレジスタ値を取得
00: e4 64       # in $0x64,%al

# 2. キー入力値が無ければ先頭へ戻る
# (pauseしながら待つ)
02: 24 01       # and $0x01,%al
04: 75 04       # jne +6 (入力が有れば進む)
06: f3 90       # pause
08: eb f6       # jmp -0x08 (入力が無ければ戻る)

# 3. データレジスタからキー入力値を取得
0a: e4 60       # in $0x60,%al

# 4. キー入力値がBreakならスキャンコードを抽出し、Makeなら先頭へ戻る
0c: 2c 80       # sub $0x80,%al
0e: 72 f0       # jb -0x0e (Makeなら先頭へ戻る)

# 追加(ここから)
# 5. スキャンコードをレジスタCLへコピー
10: 88 c1       # mov %al,%cl

# 6. 先頭へ戻る
12: eb ec       # jmp -0x12
# 追加(ここまで)

5.7 動作確認

QEMUのGUI画面でキー入力をしながらQEMUモニターでCLレジスタの値を確認したいので、動作確認の際は、以下のようにQEMUモニターだけ、実行したシェル上で起動するようにするとやりやすいです。そして、別途立ち上がったGUIのウィンドウの方でキー入力(Make&Break)を行います。

$ ../tools/run_qemu ../fs -monitor stdio
vvfat ../fs chs 1024,16,63
QEMU 2.8.1 monitor - type 'help' for more information
(qemu) info registers
RAX=000000000011001c RBX=00000000bef6ab18 RCX=0000000000000000 RDX=0000000000000000
# ↑起動直後、CLは0x00
...
# '1'キーをMake&Break
(qemu) info registers
RAX=000000000011001c RBX=00000000bef6ab18 RCX=0000000000000002 RDX=0000000000000000
# ↑CLにはスキャンコード0x02が格納された
...
# '2'キーをMake&Break
(qemu) info registers
RAX=000000000011001c RBX=00000000bef6ab18 RCX=0000000000000003 RDX=0000000000000000
# ↑CLにはスキャンコード0x03が格納された
...
# '3'キーをMake&Break
(qemu) info registers
RAX=000000000011001c RBX=00000000bef6ab18 RCX=0000000000000004 RDX=0000000000000000
# ↑CLにはスキャンコード0x04が格納された
...
# 'a'キーをMake&Break
(qemu) info registers
RAX=000000000011001c RBX=00000000bef6ab18 RCX=000000000000001e RDX=0000000000000000
# ↑CLにはスキャンコード0x1eが格納された
...

スキャンコードはASCIIコードとは別で、スキャンコードセットには大きく3つの種類があります。スキャンコードについては例えば以下のページ等が詳しいです。