起動するまでの長い道のり D言語編(2) それとなくシリアル・ポートの巻

 D言語の関数を呼び出すことに成功した。これからどんどんD言語で機能を追加していく。
 OSを作っている以上、外部デバイスをいじれないとつまらない。画面表示や何かに動いているという証が欲しい。今回は、割合制御が簡単なシリアル・ポートに文字列を出力させてみる。シリアル・ポートの出力ならqemuからも簡単に見られたりする。

今回のソース

IO関数の用意

 シリアル・ポートはIOポートを叩くことによって制御する。IOポートはアセンブラの命令で叩くことができる。が、D言語の世界に既に移行してしまったので、できればD言語の関数でIOポートを叩けるようにしたい。今後もIOを操作する機会は多いだろうから、アセンブラ命令を単純にラップした関数を用意しておく。

 実はD言語の標準ライブラリ(Phobos)には、IOポートを叩くための関数が用意されている。しかし、OSを作るに当たって標準ライブラリはそのままでは使えない。特にIOを叩く関数は諸事情により使えない。よって、独自にIO操作用の関数を用意する。

module outlandish.os.io;

/// IO読み込み。
ubyte inp(uint port_address) {
    ubyte result;
    asm {
        mov DX, port_address;
        in AL, DX;
        mov result, AL;
    }
    return result;
}

/// IO書き込み。
ubyte outp(uint port_address, ubyte value) {
    asm {
        mov DX, port_address;
        mov AL, value;
        out DX, AL;
    }
    return value;
}

 多少省略したが、大体こんな感じだ。他に2バイト読み書き用のinpw・outpw、4バイト用のinpl・outplがある。関数の仕様は標準ライブラリに合わせた。
 基本的にはアセンブラのin命令・out命令をラップしているだけだ。ただしD言語のインライン・アセンブラではmov DX←port_addressというように、コピー元とコピー先が逆順になっているので注意。

シリアル・ポートを叩く

 先述のIO関数を用いてシリアル・ポートを操作できる。どうすれば出来るかという仕様に関しては以下のサイトが詳しい。

http://community.osdev.info/index.php?%28serial%29PC16550

 で、startup.dに処理を追加する。

/// 起動直後の処理。
extern(C) void startup() {
    const COM1 = 0x300;
    
    // ボーレートを19200bpsに設定。
    outp(COM1 + 0xfb, 0b1000_0000);
    outp(COM1 + 0xf9, 0x00);
    outp(COM1 + 0xf8, 0x06);
    
    // データビット数を8に設定。
    outp(COM1 + 0xfb, 0b0000_0011);
    
    // 割り込み・RTS・DTSを有効にする。
    outp(COM1 + 0xfc, 0b0000_1011);
    outp(COM1 + 0xf9, 0x00);
    
    // 転送完了まで待つ。
    void waitTransmit() {
        while(!(inp(COM1 + 0xfd) & 0b0100_0000)) {}
    }
    
    // 1バイト転送する。
    void transmitData(ubyte b) {
        outp(COM1 + 0xf8, b);
        waitTransmit();
    }
    
    // 文字列を転送する。
    void transmitString(char[] s) {
        foreach(c; s) {
            transmitData(cast(ubyte) c);
        }
    }
    
    // 文字列を表示。
    transmitString("Hello,World!\r\n");
    
    // 止まる。
    asm {hlt;}
}

 waitTransmit・transmitData・transmitStringはそれぞれローカル関数だ。D言語C言語とかと違って関数内にも関数を定義できる。
 それと、foreachという構文も出てくる。これはfor文の簡易版みたいな奴で、配列sを先頭から順に読んでいくという処理になる。配列の各要素はcに代入される。JavaとかC++やっている人は「cの型指定がないけど、別の場所で定義されてる変数?」と思うかもしれない。いや、cはれっきとしたローカル変数だ。D言語ではここの型指定は要らなかったりする(しても良い)。コンパイラがcharだと判断してくれる。
 多分きっとみんな読み流していると思うけど、関数冒頭の「const COM1 = 0x300;」でも型指定が省略されている。この場合もコンパイラが勝手に判断してuint型にしてくれる。
 IOポートに読み書きしている内容とかは大体コメントに書いておいたけど、詳しくは参考URLを見て欲しい。面倒だし。

ビルド・実行

 で、Makefileにio.dを追加する。

# Makefile 16行目
OBJS = \
	ipl.o\
	setup.o\
	outlandish/os/startup.o\
	outlandish/os/io.o\
# Makefile 61行目
outlandish/os/io.o: outlandish/os/io.d

 そしてmakeして実行する。qemuの画面でSHIFT+CTRL+3を押すと、シリアル・ポートの出力が見られる。そこで画面に「Hello,World!」と出ていたら成功だ。

次回予告

 ここまでは結構あっけなく出来たと思う。問題はここからなのだ……。
 次回はさらにD言語らしく構造体・enum・クラス・テンプレートを駆使して行こうと思う。
 その時、標準ライブラリという巨大な壁にぶつかることを、僕たちはまだ知る由もなかった……。