/ «2010-01-04 (Mon) ^ 2010-01-09 (Sat)» ?
   西田 亙の本:GNU 開発ツール -- hello.c から a.out が誕生するまで --

Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール


2010-01-05 (Tue)

[Thoughts][Hardware] 新春 機械語入門講座・その3

今日のテーマは「疑似 LED 発光機能」の搭載です。出来上がってみるとお馬鹿な機能なのですが、UNIX の面白さを垣間見させてくれる、良いテーマではないかと私は思います。

懐かしのエスケープシーケンス

"エスケープシーケンス" この言葉を聞いて、思わず涙腺がゆるむのは間違いなくパソコン通信世代でしょう。当時のネットワークは、モデムと呼ばれる装置を介した RS-232C 通信が主流でした。画面は 80桁 x 25行の固定、表示はASCIIと漢字の混在、いわゆる CUI (Character-based User Interface)と呼ばれる古典的なインターフェースです。このためMS-DOSが提供する標準APIには、スクロール機能は含まれておらず、過ぎ去った画面を再表示させる処理は、個々人のプログラマーの腕に任されていました。この際に大活躍した仕組みが、ANSIエスケープシーケンスと呼ばれる画面制御コードです。

エスケープシーケンスで画面を消去せよ!

今日は、既に死語と化した "エスケープシーケンス" を若い方々にも体験して頂きましょう。まずは、UNIX 環境のシェル上でふたつのウィンドウを用意します。雰囲気を出すためにどちらも 80x25 のサイズにしてみましょう。下記は、Mac OS X のターミナルを使って再現した画面です。
Mac OS X での実行例2画面
tty コマンドで、それぞれのターミナル名を表示させていますが、私の場合は "ttys000, ttys001" になっています。以後は、皆さんのターミナル名に置き換えて実験を行ってください。

エスケープシーケンスとは、その名の通り ASCII 制御コード(ASCII code 0x00-0x1F)中に存在する Escape code (ESC: ASCII code 0x1B) および左カギ括弧 "[" の2文字(CSI: Control Sequence Introducer)で始まる画面制御文字列です。通信ソフトは、受信したメッセージ中に連続した ESC と "[" が含まれていると、そこから画面制御文字列が始まると判断し、特殊処理に入る訳です。

 while (ch = readch()) {
     switch (ch) {
         case ESC:
             if (prefetch() == '[')
                 エスケープ処理
             ・・・
         default:
             メッセージ出力
     }
 }

大雑把な擬似コードで示すと、このような感じになります。パソコン通信時代は、HTML も存在しませんでしたので、テキスト中に含まれていない制御コードに様々な役目を持たせていたのです。主として MS-DOS 上で大活躍した ANSI エスケープシーケンスですが、その仕様は DEC 社の端末 VT-100 シリーズサブセットとなっています。

tvm2.c ではこの中から、ふたつのエスケープシーケンスを使います。まず最初は画面制御の基本となる、画面消去を試してみましょう。UNIX 環境であれば clear コマンドが用意されていますが、今回はエスケープシーケンスを用いて画面を消去してみます。

先程用意したふたつのウィンドウの左側でコマンドを入力します。ESC 制御コードをコマンドプロンプトから入力するために、printf コマンドを利用します。古来、制御コードの入力には "バックスラッシュの直後に3桁の8進数を指定する" 方法が一般的でした。ESC の ASCII コードは10進数で27ですから、8進数で表記すると 033 となります。

一覧表から分かる通り、画面消去の制御コードは ESC "[2J" になっていますから

 $ printf "\033[2J"

と入力しますと、使用中のウィンドウの画面がクリアされます。ただし、プロンプト位置はそのままである点に注意してください。専門用語で表現すると、画面は消去されるが「カーソル位置は不変」なのです(MS-DOS のドライバはカーソル位置もホームに変更する)。この意味をより深く理解するために、今度はふたつのウィンドウを用いて実験します。

左側の端末から、リダイレクションを用いて右側の端末へ制御文字列を送りましょう。

 $ printf "\033[2J" > /dev/ttys001

/dev/ の直後にターミナル名を続ければ、目的の端末に printf コマンドの標準出力を流し込むことが可能になります。ここが UNIX 環境の素晴らしさですね。すると、確かに右側の端末画面は消去され、Mac OS X のターミナルでは白抜きになったカーソル位置がそのまま残っていることが観察できます。

つまり、「カーソル位置は不変」である訳です。この事実をさらに次のコマンドで確認します。

 $ printf "Hello" > /dev/ttys001

先程白抜きになっていたカーソル位置に "Hello" が表示され、白抜きカーソルはその直後へ移動しました。カーソルの意味が実感できたことと思います。

カーソル位置の制御

さて、カーソル位置さえ自在に制御できれば、画面上の好きなところにメッセージを表示できる訳ですが、このカーソル位置制御のために用意されたエスケープシーケンスが、ESC + "[row;colH" です。少々分かりづらいのですが、row に行番号、col に桁番号を10進数で指定します。座標系は、画面の左上隅が (1,1) になり、右方向・下方向に向かって増加します。原点は (0,0) ではないので、注意してください。

例えば、画面12行目の40桁目に Hello を表示するためには

 $ printf "\033[12;40HHello" > /dev/ttys001

と入力します。このふたつのテクニックさえ分かれば、printf 関数を使い自在に画面をプログラムすることが可能になります。興味のある方は、残りのエスケープシーケンスにも挑戦してみてください。

LED が点灯した!

それでは前回の tvm1.c に「疑似 LED 点灯機能」を追加した tvm2.c を作成してみましょう。

    1	//
    2	// t v m 2 . c
    3	//
    4	//   Tiny Virtual Machine 2, public domain
    5	//
    6	
    7	#include <stdint.h>	    // uint*_t
    8	#include <stdio.h>	    // sscanf(), fprintf(), stderr, stdout, fflush()
    9	
   10	#define MAXMEM	32
   11	
   12	uint8_t mem[ MAXMEM ];
   13	
   14	int setup_memory(int argc, char** argv) {
   15	    int i = 0, val, ret;
   16	
   17	    while (argc > 1) {
   18		ret = sscanf(argv[ i+1 ], "%x", &val);
   19		if (ret == 0) {
   20		    fprintf(stderr, "Illegal hexadecimal data [%s].\n", argv[ i+1 ]);
   21		    return 1;
   22		}
   23		if (i >= MAXMEM) {
   24		    fprintf(stderr, "Memory overrun.\n");
   25		    return 1;
   26		}
   27		mem[ i++ ] = val & 0xFF;
   28		argc--;
   29	    }
   30	    return 0;
   31	}
   32	
   33	void dump_memory(void) {
   34	    int i;
   35	
   36	    fprintf(stderr, "\n");
   37	    fprintf(stderr, "+-------------------------------------------------+\n");
   38	    fprintf(stderr, "|  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |\n");
   39	    fprintf(stderr, "|-------------------------------------------------|\n");
   40	    fprintf(stderr, "|");
   41	    for (i = 0; i < 16; i++)
   42		fprintf(stderr, " %02X", mem[ i ]);
   43	    fprintf(stderr, " |\n");
   44	    fprintf(stderr, "+-------------------------------------------------+\n");
   45	    fprintf(stderr, "| 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |\n");
   46	    fprintf(stderr, "|-------------------------------------------------|\n");
   47	    fprintf(stderr, "|");
   48	    for (i = 16; i < MAXMEM; i++)
   49		fprintf(stderr, " %02X", mem[ i ]);
   50	    fprintf(stderr, " |\n");
   51	    fprintf(stderr, "+-------------------------------------------------+\n");
   52	    fprintf(stderr, "\n");
   53	}
   54	
   55	void initialize_led(void) {
   56	    fprintf(stdout, "\033[2J");	    // Clear screen
   57	    fprintf(stdout, "\033[1;1H");   // Move cursor to the home 
   58	    fprintf(stdout, "+-------------------------------+\n");
   59	    fprintf(stdout, "| 7   6   5   4   3   2   1   0 |\n");
   60	    fprintf(stdout, "|-------------------------------|\n");
   61	    fprintf(stdout, "|                               |\n");
   62	    fprintf(stdout, "+-------------------------------+\n");
   63	}
   64	
   65	void update_led(int val) {
   66	    int i, bit;
   67	
   68	    for (i = 0; i< 8; i++) {
   69		fprintf(stdout, "\033[4;%dH", 3+4*i);
   70		bit = 1 << (7 - i);
   71		fprintf(stdout, "%c", (val & bit) ? 'O' : '_');
   72	    }
   73	    fprintf(stdout, "\033[6;1H");
   74	    fflush(stdout);
   75	}
   76	
   77	
   78	int main(int argc, char** argv) {
   79	    int ret;
   80	
   81	    ret = setup_memory(argc, argv);
   82	    if (ret)
   83		return ret;
   84	    dump_memory();
   85	    initialize_led();
   86	    update_led(mem[0]);
   87	    return 0;
   88	}
   89	

追加したものは、55行からの initialize_led() 関数と65行からの update_led() 関数、そして main() 関数内部での呼び出しです。85行で初期化処理を行い、86行でメモリのゼロ番地の内容をバイナリで LED 表示させています。

initialize_led() 関数ですが、56行で画面消去を行い、57行でカーソル位置を home position に移動しています。ここで、fprintf() 関数の出力先が stderr ではなく、stdout に変更されている点に注意してください。

update_led() 関数は、既に描画されているフレームの中に、指定された8ビット値を2進数に変換し、ビットが立っていれば大文字のO、ビットが立っていなければアンダースコアを表示します。この時、各ビットの表示位置を計算し、カーソルを制御しています。ビットパターン全体を再表示する方が簡単ですが、画面書き替えの際のちらつきを抑えるために、必要最低限の箇所のみアップデートします。74行で出力バッファをフラッシュしていますが、これは画面を確実に書き換えるためです。それでは実行してみましょう。

 $ gcc -Wall -o tvm2 tvm2.c
 $ ./tvm2 > /dev/ttys001

このように、シェルコマンドから標準出力を右ターミナルにリダイレクションすることで、プログラム中で stdout に出力されたメッセージは /dev/ttys001 に表示され、stderr に出力されたメッセージは自分自身である /dev/ttys000 に表示されるようになります。ちなみに、BSD 系の UNIX 基本ツールのソースを読むと、fprintf() 関数を用いて、意図的に stdout, stderr が使い分けられており、勉強になります。

コマンド引数を指定しない場合( ./tvm2 > /dev/ttys001 )、メモリはゼロに初期化されていますので、右ターミナルには次のような LED 表示が現れます。

 +-------------------------------+
 | 7   6   5   4   3   2   1   0 |
 |-------------------------------|
 | _   _   _   _   _   _   _   _ |
 +-------------------------------+

ビットが全てオフのため、8つのアンダースコアが並ぶはずです。次は、0xAA をゼロ番地に書き込んでみましょう( ./tvm2 aa > /dev/ttys001 )。

 +-------------------------------+
 | 7   6   5   4   3   2   1   0 |
 |-------------------------------|
 | O   _   O   _   O   _   O   _ |
 +-------------------------------+

最上位ビットから、1ビットおきに「LED が ON」になりました。0x55 や 0xFF など、様々なビットパターンをお楽しみください。

次回はいよいよ、TVM に命令を実装します。