I/Oポートを使ってカーソルを動かす

前回はVRAMを操作し文字の表示を行ったが,ビデオコントローラーには直接命令を送ることはしなかった。ビデオコントローラーへ直接命令を送ることで,表示モードの変更や,カーソルの表示・移動などを行うことが出来る。
今回は,ビデオコントローラーが提供する機能の中で,興味の対象であるカーソルの操作について解説する。

PCに接続されているデバイスに命令を送るには?

CPUは,メモリ空間と呼ばれるアドレスが割り振られたメモリ境域の読み書きをすることが出来る。

メモリ空間について

メモリ空間には,メインメモリ空間とI/Oメモリ空間の二種類がある。

私たちが普段のデスクトップアプリケーションの開発で意識しているのは,メインメモリ空間のアドレスである。そのアドレスを指定することで,メインメモリーに保存されているデータを読み書きできることを,私たちは良く知っている(アセンブラC言語のポインタを用いて,アドレスを指定したことがあればの話だが…)。

普段はメインメモリ以外のデバイスを直接操作する機会が無いため,意識することは無かったが,実はデバイスにもアドレスを割り振られたI/Oメモリ空間が存在し,そのアドレスを指定することで,デバイスの操作が可能である。

I/Oポートを使う

バイスの操作には,I/Oメモリ空間をI/Oポートを使って読み書きする。

C言語にはI/Oポートを操作する命令が無いため,アセンブラを使用する。アセンブラには次の二つの命令が用意されている:

  • out アドレス, データ
    • 指定したアドレスにデータを書き込む(デバイスへ送る)。
  • in データ,アドレス
    • 指定したアドレスのデータを読む(デバイスから受け取る)。

ここで,アドレスのサイズは二バイトで,データのサイズはプログラマがデバイスに合わせて適当に変更する。

インラインアセンブラ

アセンブラファイルを新しく作り,C言語とのインターフェースを整えるのは少々面倒である。しかし,GCCインラインアセンブラ機能を使うことで,C言語の中にアセンブラを埋め込み,C言語アセンブラ間のデータのやり取りを簡単に記述できる。

具体的には,I/Oポートへ1バイトのデータを送る関数outbは,インラインアセンブラを用いて次のように書く:

void outb(unsigned short port, unsigned char val) {
  __asm__ __volatile__ ("out dx, al" : : "d" (port), "a" (val));
}

ビデオコントローラーの操作

ビデオコントローラーのアドレス(正確には今回使うのはCRTコントローラーのアドレス)は0x3D4,0x3D5なので,このアドレスを指定すればいい。
カーソルを移動する関数は次のように書ける:

void set_cur(unsigned short cur_pos) {
  outb(0x3D4, 15);
  outb(0x3D5, cur_pos);
  outb(0x3D4, 14);
  outb(0x3D5, cur_pos >> 8);
}

カーソルの位置は画面上の文字数に対応していて,一番左上に移動したい場合は0,一番右下に移動したい場合は25*80=2000を指定すればいい。カーソル位置をビデオコントローラーに伝えるには,このデバイスは一度に1バイトのデータしかやり取りできないので,二回に分けでカーソル位置を伝える。outb(0x3D4, 15)でカーソル位置の下位1バイトを送ることを伝え,outb(0x3D5, cur_pos)でカーソル位置のデータの下位1バイトを送り,そのあとoutb(0x3D4, 14)で上位1バイトを送ることを伝え同様に残りのデータを送ることで,カーソルの位置を変更できる。

kernel.c

表示した文字数をカウントして,カーソルを移動してみる:

void outb(unsigned short port, unsigned char val) {
  __asm__ __volatile__ ("out dx, al" : : "d" (port), "a" (val));
}

void set_cur(unsigned short cur_pos) {
  outb(0x3D4, 15);
  outb(0x3D5, cur_pos);
  outb(0x3D4, 14);
  outb(0x3D5, cur_pos >> 8);
}

void main() {
  unsigned short *video = (unsigned short *)0xb8000;
  unsigned char  *str   = "hello,world";
  unsigned char  c;
  unsigned short count;

  count=0;
  while(c = *str++) {
    *video++ = (0x0E << 8) | c;
    count++;
  }

  set_cur(count);

  while(1)
    ;
}

次のように表示した文字列の後にカーソル(_)が表示されていれば正常に動作している。BIOSがブート時に表示した文字が残っているので,カーソルは水色になっている。: