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

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


2010-01-16 (Sat)

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

しばらく間が空いてしまいましたが、一気に TVM を完成させてしまいましょう。今日のメインテーマは、ステート数の把握と immediate という用語の正しい理解です。これまで「即値」というトンデモ訳に悩まされてきた方々に、本エントリを捧げます。

クロックとステート、そしてプログラムカウンタ

メモリ回りの次は、いよいよ命令の実装ですが、その前に TVM (Tiny Virtual Machine) を駆動するクロック、そしてプログラムカウンタが必要です。これだけの準備が整い、初めて「何もしない (NOP: No OPeration)」命令を実装することができます。NOP のついでに「停まる」ための命令、SLEEP も実装します。なぜ、わざわざ停まる必要があるのか?その理由については、後で明らかになります。tvm3.c のソースは次の通り。

    1	//
    2	// t v m 3 . c
    3	//
    4	//   Tiny Virtual Machine 3, public domain
    5	//
    6	
    7	#include <stdint.h>	    // uint*_t
    8	#include <stdio.h>	    // sscanf(), fprintf(), stderr, stdout
    9	#include <unistd.h>	    // usleep()
   10	
   11	#define MAXMEM	32
   12	#define	CLOCK	500000	    // 500 msec/clock
   13	
   14	uint8_t mem[ MAXMEM ];
   15	uint8_t pc;
   16	int states;
   17	
   18	#define NOP	0
   19	#define SLEEP	1
   20	
   21	int setup_memory(int argc, char** argv) {
   22	    int i = 0, val, ret;
   23	
   24	    while (argc > 1) {
   25		ret = sscanf(argv[ i+1 ], "%02X", &val);
   26		if (ret == 0) {
   27		    fprintf(stderr, "Illegal instruction [%s].\n", argv[ i+1 ]);
   28		    return 1;
   29		}
   30		if (i >= MAXMEM) {
   31		    fprintf(stderr, "Memory overrun.\n");
   32		    return 1;
   33		}
   34		mem[ i++ ] = val;
   35		argc--;
   36	    }
   37	    return 0;
   38	}
   39	
   40	void dump_memory(void) {
   41	    int i;
   42	
   43	    fprintf(stderr, "\n");
   44	    fprintf(stderr, "+-------------------------------------------------+\n");
   45	    fprintf(stderr, "|  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |\n");
   46	    fprintf(stderr, "|-------------------------------------------------|\n");
   47	    fprintf(stderr, "|");
   48	    for (i = 0; i < 16; i++)
   49		fprintf(stderr, " %02X", mem[ i ]);
   50	    fprintf(stderr, " |\n");
   51	    fprintf(stderr, "+-------------------------------------------------+\n");
   52	    fprintf(stderr, "| 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |\n");
   53	    fprintf(stderr, "|-------------------------------------------------|\n");
   54	    fprintf(stderr, "|");
   55	    for (i = 16; i < MAXMEM; i++)
   56		fprintf(stderr, " %02X", mem[ i ]);
   57	    fprintf(stderr, " |\n");
   58	    fprintf(stderr, "+-------------------------------------------------+\n");
   59	    fprintf(stderr, "\n");
   60	}
   61	
   62	int fetch_memory() {
   63	    int val;
   64	
   65	    val = mem[ pc ];
   66	    states++;
   67	    pc = (pc + 1) & (MAXMEM - 1);
   68	    return val;
   69	}
   70	
   71	void initialize_led(void) {
   72	    fprintf(stdout, "\033[2J");	    // Clear screen
   73	    fprintf(stdout, "\033[1;1H");   // Move cursor to the home 
   74	    fprintf(stdout, "+-------------------------------+\n");
   75	    fprintf(stdout, "| 7   6   5   4   3   2   1   0 |\n");
   76	    fprintf(stdout, "|-------------------------------|\n");
   77	    fprintf(stdout, "|                               |\n");
   78	    fprintf(stdout, "+-------------------------------+\n");
   79	}
   80	
   81	void update_led(int val) {
   82	    int i, bit;
   83	
   84	    for (i = 0; i< 8; i++) {
   85		fprintf(stdout, "\033[4;%dH", 3+4*i);
   86		bit = 1 << (7 - i);
   87		fprintf(stdout, "%c", (val & bit) ? 'O' : '_');
   88	    }
   89	    fprintf(stdout, "\033[6;1H\n");
   90	}
   91	
   92	void vm_nop(void) {
   93	    fprintf(stderr, "NOP ");
   94	}
   95	
   96	void vm_sleep(void) {
   97	    fprintf(stderr, "SLEEP ");
   98	}
   99	
  100	int main(int argc, char** argv) {
  101	    int ret, exception = 0, oldpc, inst, total = 0, opecode, operand;
  102	
  103	    ret = setup_memory(argc, argv);
  104	    if (ret)
  105		return ret;
  106	    dump_memory();
  107	    initialize_led();
  108	    while (! exception) {
  109		update_led( pc );
  110		states = 0;
  111		oldpc = pc;
  112		inst = fetch_memory();
  113		opecode = (inst & 0xF0) >> 4;
  114		operand = inst & 0x0F;
  115		fprintf(stderr, "%02X: %02X ", oldpc, inst);
  116		switch (opecode) {
  117		    case NOP:
  118			vm_nop();
  119			break;
  120	
  121		    case SLEEP:
  122			vm_sleep();
  123			exception = 1;
  124			break;
  125	
  126		    default:
  127			fprintf(stderr, "Illegal instruction ");
  128			exception = 1;
  129			break;
  130		}
  131		total += states;
  132		fprintf(stderr, "(%d states)\n", total);
  133		usleep(states * CLOCK);
  134	    }
  135	    return 0;
  136	}

TVM のような単純な仮想機械であれば、本来クロックなどは必要ないのですが、"ステート" という概念を理解して頂くために、敢えて実装しています。このステートが理解できなければ、次のテーマであるH8ボードにLED点灯プログラムを実装する際、ループの待ち時間が計算できないからです。今回、クロックをシミュレートするためにライブラリ関数である usleep を利用しています。その引数はμ秒単位ですから、12行のマクロ定義から usleep(CLOCK) は0.5秒の遅延を発生します。

15行では、8ビット長のプログラムカウンタ pc を定義しています。メモリの実装は現在32バイトですから、pc は本来5ビットで済むはずですが、Cコンパイラが「5ビットの演算」には対応していないため、8ビット長としています。このため、プログラムカウンタを進める際には、注意が必要です(後述)。

16行は、現在実行中の命令の総ステート数を格納するためのグローバル変数、states を定義しています。一般的に、命令を実行するためには「読み込み・解析・実行」の三段階を踏み、このうち最も時間がかかるステップがメモリアクセスを必要とする読み込みです(乗除算などを除く)。TVM では、メモリ参照を要するステップのみ、ステートを加算しました。

18,19行では、NOP/SLEEP 命令をマクロ定義しています。これから順を追って、命令が増えていきます。

62行からは、メモリ参照を担当する fetch_memory() 関数を定義しています。65行がその本体ですが、66行でステートの加算を行い、メモリの値 val を呼び出し元に返します。注意すべきは67行の内容であり、プログラムカウンタを加算後、その結果を AND 演算で5ビットに丸め込んでいます(上位ビットはゼロとする)。この処理を忘れるとプログラムはどうなるのか、結果を予見してみてください。

92行, 96行で新しい命令の処理を担当する vm_nop(), vm_sleep() 関数が定義されていますが、その内容はメッセージを表示するだけです。NOP は「何もしない」ため、これで良いのですが、それでは SLEEP の違いはどこに由来するのでしょうか?SLEEP が実行された場合は、123行で exception 変数が1にセットされ、命令実行ループを抜けるような仕組みになっています。

最後に main() 関数ですが、108行から while ループが組み込まれ、ループごとに

  • プログラムカウンタの内容を LED に反映
  • states 変数を初期化
  • fetch_memory() を実行
  • 命令語の上位4ビットを opecode 変数、下位4ビットを operand 変数に格納
  • opecode に基づき、命令を実行
  • TVM 起動時からの総ステート数 total をアップデート
  • states 変数の値に基づき usleep() を実行

以上を SLEEP 命令が実行されるまで、延々と続けます。

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

メモリの設定を行わず、デフォルトのオールゼロで TVM を稼働させると、アドレス0番地から順に NOP を0.5秒おきに実行し、アドレス31番地までいくと、次はアドレス0番地に戻ります(もうひとつのターミナル画面には、プログラムカウンタの内容が2進値で表示される)。これを wrap around と呼びますが、67行の丸め処理を忘れると、プログラムは本来存在しない32番地以降へ暴走してしまいます。

このようにプロセッサは明示的に停止、もしくは分岐を指示しない限り、機械的にプログラムカウンタを回し続けます。今回は、メモリがオールゼロに初期化されているため、NOP が連続実行されるだけですが、アドレス領域によってはプログラムの一部や、スタックの残骸、ベクターテーブル、データ領域などが、意図しない命令語に置き換わり、結果的にとんでもない処理が実行されてしまう可能性があります。このためにプロセッサには、必ず停止するための命令が用意されているのです(目覚める方法については、割り込みの知識が必要になるため割愛)。

 $ ./tvm3 0 0 0 10 > /dev/ttys001
 
 +-------------------------------------------------+
 |  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |
 |-------------------------------------------------|
 | 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 00 |
 +-------------------------------------------------+
 | 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |
 |-------------------------------------------------|
 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
 +-------------------------------------------------+
 
 00: 00 NOP (1 states)
 01: 00 NOP (2 states)
 02: 00 NOP (3 states)
 03: 10 SLEEP (4 states)
 
 $

今回の実行例では、先頭の3バイトにゼロ、4バイト目に SLEEP 命令の機械語 0x10 を指定していますので、アドレス3番地で意図通り TVM は停止しました。この間、メモリアクセスは4回発生し、計4ステートが経過しています。なお、機械語として 0x01 を指定しないように注意しましょう。TVM のオペコードは上位4ビットですから、0x01 は NOP と認識されてしまいます。

レジスタ、そして LOAD/STORE 命令登場

TVM3 で示された通り、プログラム実行において最も時間がかかる処理は、メモリアクセスです。このため、プロセッサ内部にはメモリアクセスなしでデータを高速に処理できるレジスタが実装されています。次は、TVM にレジスタを搭載し、レジスタとメモリの間でデータを転送できる LOAD/STORE 命令を tvm4.c に組み込みましょう。

    1	//
    2	// t v m 4 . c
    3	//
    4	//   Tiny Virtual Machine 4, public domain
    5	//
    6	
    7	#include <stdint.h>	    // uint*_t
    8	#include <stdio.h>	    // sscanf(), fprintf(), stderr, stdout
    9	#include <unistd.h>	    // usleep()
   10	
   11	#define	CLOCK	500000	    // 500 msec/clock
   12	#define MAXMEM	32
   13	#define MAXREG	4
   14	
   15	uint8_t mem[ MAXMEM ];
   16	uint8_t pc;
   17	int states;
   18	uint8_t r[ MAXREG ];
   19	
   20	#define NOP	0
   21	#define SLEEP	1
   22	#define LOADI	2
   23	#define	STORE	3
   24	
   25	int setup_memory(int argc, char** argv) {
   26	    int i = 0, val, ret;
   27	
   28	    while (argc > 1) {
   29		ret = sscanf(argv[ i+1 ], "%02X", &val);
   30		if (ret == 0) {
   31		    fprintf(stderr, "Illegal instruction [%s].\n", argv[ i+1 ]);
   32		    return 1;
   33		}
   34		if (i >= MAXMEM) {
   35		    fprintf(stderr, "Memory overrun.\n");
   36		    return 1;
   37		}
   38		mem[ i++ ] = val;
   39		argc--;
   40	    }
   41	    return 0;
   42	}
   43	
   44	void dump_memory(void) {
   45	    int i;
   46	
   47	    fprintf(stderr, "\n");
   48	    fprintf(stderr, "+-------------------------------------------------+\n");
   49	    fprintf(stderr, "|  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |\n");
   50	    fprintf(stderr, "|-------------------------------------------------|\n");
   51	    fprintf(stderr, "|");
   52	    for (i = 0; i < 16; i++)
   53		fprintf(stderr, " %02X", mem[ i ]);
   54	    fprintf(stderr, " |\n");
   55	    fprintf(stderr, "+-------------------------------------------------+\n");
   56	    fprintf(stderr, "| 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |\n");
   57	    fprintf(stderr, "|-------------------------------------------------|\n");
   58	    fprintf(stderr, "|");
   59	    for (i = 16; i < MAXMEM; i++)
   60		fprintf(stderr, " %02X", mem[ i ]);
   61	    fprintf(stderr, " |\n");
   62	    fprintf(stderr, "+-------------------------------------------------+\n");
   63	    fprintf(stderr, "\n");
   64	}
   65	
   66	int fetch_memory() {
   67	    int val;
   68	
   69	    val = mem[ pc ];
   70	    states++;
   71	    pc = (pc + 1) & (MAXMEM - 1);
   72	    return val;
   73	}
   74	
   75	void dump_registers(void) {
   76	    int i;
   77	
   78	    fprintf(stderr, "\n");
   79	    fprintf(stderr, "+---------+\n");
   80	    for (i = 0; i < MAXREG; i++) 
   81		fprintf(stderr, "| R%d = %02X |\n", i, r[ i ]); 
   82	    fprintf(stderr, "+---------+\n");
   83	}
   84	
   85	void initialize_led(void) {
   86	    fprintf(stdout, "\033[2J");	    // Clear screen
   87	    fprintf(stdout, "\033[1;1H");   // Move cursor to the home 
   88	    fprintf(stdout, "+-------------------------------+\n");
   89	    fprintf(stdout, "| 7   6   5   4   3   2   1   0 |\n");
   90	    fprintf(stdout, "|-------------------------------|\n");
   91	    fprintf(stdout, "|                               |\n");
   92	    fprintf(stdout, "+-------------------------------+\n");
   93	}
   94	
   95	void update_led(int val) {
   96	    int i, bit;
   97	
   98	    for (i = 0; i< 8; i++) {
   99		fprintf(stdout, "\033[4;%dH", 3+4*i);
  100		bit = 1 << (7 - i);
  101		fprintf(stdout, "%c", (val & bit) ? 'O' : '_');
  102	    }
  103	    fprintf(stdout, "\033[6;1H\n");
  104	}
  105	
  106	void vm_nop(void) {
  107	    fprintf(stderr, "NOP ");
  108	}
  109	
  110	void vm_sleep(void) {
  111	    fprintf(stderr, "SLEEP ");
  112	}
  113	
  114	void vm_loadi(int operand) {
  115	    int dest, immed;
  116	
  117	    fprintf(stderr, "LOADI ");
  118	    dest = operand & (MAXREG - 1);
  119	    immed = fetch_memory();
  120	    r[ dest ] = immed;
  121	    fprintf(stderr, "R%d, $%02X ", dest, immed);
  122	}
  123	
  124	void vm_store(int operand) {
  125	    int dest, immed;
  126	
  127	    fprintf(stderr, "STORE ");
  128	    dest = operand & (MAXREG - 1);
  129	    immed = fetch_memory() & (MAXMEM - 1);
  130	    mem[ immed ] = r[ dest ];
  131	    fprintf(stderr, "R%d, [$%02X] <= $%02X ", dest, immed, r[ dest ]);
  132	}
  133	
  134	int main(int argc, char** argv) {
  135	    int ret, exception = 0, oldpc, inst, total = 0, opecode, operand;
  136	
  137	    ret = setup_memory(argc, argv);
  138	    if (ret)
  139		return ret;
  140	    dump_memory();
  141	    initialize_led();
  142	    while (! exception) {
  143		update_led(mem[ MAXMEM - 1 ]);
  144		states = 0;
  145		oldpc = pc;
  146		inst = fetch_memory();
  147		opecode = (inst & 0xF0) >> 4;
  148		operand = inst & 0x0F;
  149		fprintf(stderr, "%02X: %02X ", oldpc, inst);
  150		switch (opecode) {
  151		    case NOP:
  152			vm_nop();
  153			break;
  154	
  155		    case SLEEP:
  156			vm_sleep();
  157			exception = 1;
  158			break;
  159	
  160		    case LOADI:
  161			vm_loadi(operand);
  162			break;
  163	
  164		    case STORE:
  165			vm_store(operand);
  166			break;
  167	
  168		    default:
  169			fprintf(stderr, "Illegal instruction ");
  170			exception = 1;
  171			break;
  172		}
  173		total += states;
  174		fprintf(stderr, "(%d states)\n", total);
  175		usleep(states * CLOCK);
  176	    }
  177	    dump_registers();
  178	    dump_memory();
  179	    return 0;
  180	}

18行で、8ビット長のレジスタ4つ(R0, R1, R2, R3)を r[] 配列中に定義しています。22行の LOADI は "LOAD Immediate" の略ですが、これは定数をレジスタにロードするための命令です。例えば、LOADI R2, 0x23 という命令を実行すると、定数 0x23 がR2レジスタにロードされます。この 0x23 は、いわゆる「即値」ですが、なぜ英語では "immediate" と表記されるのか、その理由は vm_loadi() 関数のソースを読み進めると明らかになります。

LOADI に対して、STORE はレジスタの内容をメモリへ転送する命令です。例えば、STORE R1, [0x23] は、R1レジスタの内容をメモリ 0x23 番地へ転送します。それでは、ソースを見てみましょう。

即値という命名が不幸を生んでいる

vm_loadi() 関数の118行では、命令語の下位4ビットから、定数をロードする先のレジスタ番号を dest 変数に取り出します。この値が、ゼロであれば R0 レジスタ、1であれば R1 レジスタ、2であれば R2 レジスタ、3であれば R3 レジスタを意味します。最も重要な点は119行であり、ここで命令語の直後に続く定数値を immed 変数に読み込み、ついで120行で該当する r[] 配列に格納しています。定数を読み込む際に、fetch_memory() 関数を呼び出し、states 変数が加算されている点に注意しましょう。

このように、命令語の直後の値を定数として扱うことを専門用語で "immediate addressing" と言いますが、その定義を英語で表現した箇所を名著 "Digital Computer Electronics, McGraw-Hill, 1993, 絶版" から引用します。

 Immediate addressing tells the microprocessor that the data it needs will be coming immediately after the op code.

明快に言い切られている通り、immediate とは、オペコード(正確には命令語)の直後に続いているデータのことなのです。日本語であれば、"後続値" と表現した方がより的確でしょう。ところが、国語力のない誰かがこれを「即値」と訳出してしまった。この時点から、悲劇がはじまったと言って良いでしょう。続く著者達が、盲目的に即値を使ったものだから、悲劇は拡大生産され続けています。

「すなわちの値」こんなおかしな日本語はありません。技術用語で、意味不明瞭の日本語に出会ったならば、必ず英語表記に立ち戻らなければなりません。あやふやな言葉や文章の影には、必ず著者の誤解や無理解が隠れています。大切なことは、日頃から自分の国語力を磨き、これを信じることです。たとえ相手が教科書やリファレンス書であっても、奴隷的態度は捨てなければなりません。

memory mapped I/O

話を続けます。124行からの vm_store() 関数は、転送方向が vm_loadi() 関数の逆になりますが、後続値を定数ではなく、アドレスとして解釈する点がことなります。

main() 関数の中では、143行においてメモリの最終31番地の内容を LED に反映するように変更しました。すなわち、31番地に 0xFF を格納するとすべての LED が点灯しますし、0xAA を格納すると1ビットおきに点灯することになります。このように、メモリ空間の一部を通して外部制御を可能にする方式を memory mapped I/O と呼びます。x86 などはメモリ空間とは別に、専用の I/O 空間を持っていますが、H8マイコンは memory mapped I/O 方式で外部を制御します。

TVM4 ではレジスタを実装しましたので、75行から dump_register() 関数を用意し、SLEEP 命令で TVM を停止させた後に、全レジスタの内容を表示するようにしました。

それでは、LOADI/STORE 命令を使って、31番地に 0xAA を格納するプログラムを作成してみましょう。具体的には、次のような手順を踏みます(レジスタにはR1を使うこととする)。

  • R1レジスタに定数 0xAA をロードする
  • R1レジスタの内容をメモリの 0x1F 番地にストアする
  • SLEEP する

次に、この内容を TVM アセンブリ言語に書き換えてみましょう。

 LOADI R1, 0xAA
 STORE R1, [0x1F]
 SLEEP

それでは、このアセンブリソースを機械語にハンドアセンブルします。LOADI 命令のオペコードが2、レジスタはR1なのでオペランドは1、よって機械語は 0x21 となります。これに後続値として 0xAA が続きます。

 21 AA    LOADI R1, 0xAA

次に、STORE 命令のオペコードは3、レジスタはR1でオペランド1、よって機械語は 0x31、後続値がアドレス31番地なので 0x1F。

 31 1F    STORE R1, [0x1F]

最後に、SLEEP 命令の 0x10 を付加してプログラムは完成です。実行してみましょう。

 $ ./tvm4 21 aa 31 1f 10 > /dev/ttys001
 
 +-------------------------------------------------+
 |  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |
 |-------------------------------------------------|
 | 21 AA 31 1F 10 00 00 00 00 00 00 00 00 00 00 00 |
 +-------------------------------------------------+
 | 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |
 |-------------------------------------------------|
 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
 +-------------------------------------------------+
 
 00: 21 LOADI R1, $AA (2 states)
 02: 31 STORE R1, [$1F] <= $AA (4 states)
 04: 10 SLEEP (5 states)
 
 +---------+
 | R0 = 00 |
 | R1 = AA |
 | R2 = 00 |
 | R3 = 00 |
 +---------+
 
 +-------------------------------------------------+
 |  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |
 |-------------------------------------------------|
 | 21 AA 31 1F 10 00 00 00 00 00 00 00 00 00 00 00 |
 +-------------------------------------------------+
 | 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |
 |-------------------------------------------------|
 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 AA |
 +-------------------------------------------------+
 
 $

プログラムは意図通りに動作し、メモリ31番地に 0xAA が格納されていることが分かります。R1レジスタの内容も、きちんと 0xAA に設定されています。また、LOADI, STORE 命令においてそれぞれ2ステートが消費されている点に注目して下さい。後続値を読み込むために、それぞれ1ステート余分にかかっています。

他のレジスタを変更したり、メモリの様々な箇所を書き換えるプログラムに挑戦してみるのも良いでしょう。

演算と条件分岐

レジスタとメモリの操作ができるようになると、次に欲しいのは演算処理と繰り返しです。TVM5 では、減算を担当する DEC と、分岐を担当する JMP 命令を追加しました。DEC はオペランドで指定したレジスタの内容を1減算し、結果がゼロになるとゼロフラグを1に設定します。結果が非ゼロの場合は、ゼロフラグはゼロになります。JMP 命令には、オペランドによって通常の無条件分岐 JMP (オペランド0)と、ゼロフラグが立っていない時だけ条件分岐を行う JNZ (オペランド1)の2種類があります。NZ は Non Zero の略であり、DEC 演算の結果が非ゼロの時だけ、分岐を行うことになります。

それでは、tvm5.c の実装です。

    1	//
    2	// t v m 5 . c
    3	//
    4	//   Tiny Virtual Machine 5, public domain
    5	//
    6	
    7	#include <stdint.h>	    // uint*_t
    8	#include <stdio.h>	    // sscanf(), fprintf(), stderr, stdout
    9	#include <unistd.h>	    // usleep()
   10	
   11	#define	CLOCK	50000	    // 50 msec/clock
   12	#define MAXMEM	32
   13	#define MAXREG	4
   14	
   15	uint8_t mem[ MAXMEM ];
   16	uint8_t pc;
   17	int states;
   18	uint8_t r[ MAXREG ];
   19	uint8_t zflag;
   20	
   21	#define NOP	0
   22	#define SLEEP	1
   23	#define LOADI	2
   24	#define	STORE	3
   25	#define DEC	4
   26	#define JMP	5
   27	
   28	int setup_memory(int argc, char** argv) {
   29	    int i = 0, val, ret;
   30	
   31	    while (argc > 1) {
   32		ret = sscanf(argv[ i+1 ], "%02X", &val);
   33		if (ret == 0) {
   34		    fprintf(stderr, "Illegal instruction [%s].\n", argv[ i+1 ]);
   35		    return 1;
   36		}
   37		if (i >= MAXMEM) {
   38		    fprintf(stderr, "Memory overrun.\n");
   39		    return 1;
   40		}
   41		mem[ i++ ] = val;
   42		argc--;
   43	    }
   44	    return 0;
   45	}
   46	
   47	void dump_memory(void) {
   48	    int i;
   49	
   50	    fprintf(stderr, "\n");
   51	    fprintf(stderr, "+-------------------------------------------------+\n");
   52	    fprintf(stderr, "|  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |\n");
   53	    fprintf(stderr, "|-------------------------------------------------|\n");
   54	    fprintf(stderr, "|");
   55	    for (i = 0; i < 16; i++)
   56		fprintf(stderr, " %02X", mem[ i ]);
   57	    fprintf(stderr, " |\n");
   58	    fprintf(stderr, "+-------------------------------------------------+\n");
   59	    fprintf(stderr, "| 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |\n");
   60	    fprintf(stderr, "|-------------------------------------------------|\n");
   61	    fprintf(stderr, "|");
   62	    for (i = 16; i < MAXMEM; i++)
   63		fprintf(stderr, " %02X", mem[ i ]);
   64	    fprintf(stderr, " |\n");
   65	    fprintf(stderr, "+-------------------------------------------------+\n");
   66	    fprintf(stderr, "\n");
   67	}
   68	
   69	int fetch_memory() {
   70	    int val;
   71	
   72	    val = mem[ pc ];
   73	    states++;
   74	    pc = (pc + 1) & (MAXMEM - 1);
   75	    return val;
   76	}
   77	
   78	void dump_registers(void) {
   79	    int i;
   80	
   81	    fprintf(stderr, "\n");
   82	    fprintf(stderr, "+---------+\n");
   83	    for (i = 0; i < MAXREG; i++) 
   84		fprintf(stderr, "| R%d = %02X |\n", i, r[ i ]); 
   85	    fprintf(stderr, "+---------+\n");
   86	}
   87	
   88	void initialize_led(void) {
   89	    fprintf(stdout, "\033[2J");	    // Clear screen
   90	    fprintf(stdout, "\033[1;1H");   // Move cursor to the home 
   91	    fprintf(stdout, "+-------------------------------+\n");
   92	    fprintf(stdout, "| 7   6   5   4   3   2   1   0 |\n");
   93	    fprintf(stdout, "|-------------------------------|\n");
   94	    fprintf(stdout, "|                               |\n");
   95	    fprintf(stdout, "+-------------------------------+\n");
   96	}
   97	
   98	void update_led(int val) {
   99	    int i, bit;
  100	
  101	    for (i = 0; i< 8; i++) {
  102		fprintf(stdout, "\033[4;%dH", 3+4*i);
  103		bit = 1 << (7 - i);
  104		fprintf(stdout, "%c", (val & bit) ? 'O' : '_');
  105	    }
  106	    fprintf(stdout, "\033[6;1H\n");
  107	}
  108	
  109	void vm_nop(void) {
  110	    fprintf(stderr, "NOP ");
  111	}
  112	
  113	void vm_sleep(void) {
  114	    fprintf(stderr, "SLEEP ");
  115	}
  116	
  117	void vm_loadi(int operand) {
  118	    int dest, immed;
  119	
  120	    fprintf(stderr, "LOADI ");
  121	    dest = operand & (MAXREG - 1);
  122	    immed = fetch_memory();
  123	    r[ dest ] = immed;
  124	    fprintf(stderr, "R%d, $%02X ", dest, immed);
  125	}
  126	
  127	void vm_store(int operand) {
  128	    int dest, immed;
  129	
  130	    fprintf(stderr, "STORE ");
  131	    dest = operand & (MAXREG - 1);
  132	    immed = fetch_memory() & (MAXMEM - 1);
  133	    mem[ immed ] = r[ dest ];
  134	    fprintf(stderr, "R%d, [$%02X] <= $%02X ", dest, immed, r[ dest ]);
  135	}
  136	
  137	void vm_dec(int operand) {
  138	    int dest;
  139	
  140	    fprintf(stderr, "DEC ");
  141	    dest = operand & (MAXREG - 1);
  142	    if (r[ dest ] == 0)
  143		r[ dest ] = 0xFF;
  144	    else
  145		r[ dest ]--;
  146	    zflag = (r[ dest ] == 0) ? 1 : 0;
  147	    fprintf(stderr, "R%d => $%02X ", dest, r[ dest ]);
  148	}
  149	
  150	void vm_jmp(int operand) {
  151	    int type, immed;
  152	
  153	    type = operand & 1;
  154	    if (type == 0)
  155		fprintf(stderr, "JMP ");
  156	    else
  157		fprintf(stderr, "JNZ ");
  158	    immed = fetch_memory() & (MAXMEM - 1);
  159	    if (type == 0 || zflag == 0)
  160		pc = immed;
  161	    fprintf(stderr, "$%02X ", immed);
  162	}
  163	
  164	int main(int argc, char** argv) {
  165	    int ret, exception = 0, oldpc, inst, total = 0, opecode, operand;
  166	
  167	    ret = setup_memory(argc, argv);
  168	    if (ret)
  169		return ret;
  170	    dump_memory();
  171	    initialize_led();
  172	    while (! exception) {
  173		update_led(mem[ MAXMEM - 1 ]);
  174		states = 0;
  175		oldpc = pc;
  176		inst = fetch_memory();
  177		opecode = (inst & 0xF0) >> 4;
  178		operand = inst & 0x0F;
  179		fprintf(stderr, "%02X: %02X ", oldpc, inst);
  180		switch (opecode) {
  181		    case NOP:
  182			vm_nop();
  183			break;
  184	
  185		    case SLEEP:
  186			vm_sleep();
  187			exception = 1;
  188			break;
  189	
  190		    case LOADI:
  191			vm_loadi(operand);
  192			break;
  193	
  194		    case STORE:
  195			vm_store(operand);
  196			break;
  197	
  198		    case DEC:
  199			vm_dec(operand);
  200			break;
  201	
  202		    case JMP:
  203			vm_jmp(operand);
  204			break;
  205	
  206		    default:
  207			fprintf(stderr, "Illegal instruction ");
  208			exception = 1;
  209			break;
  210		}
  211		total += states;
  212		fprintf(stderr, "(%d states)\n", total);
  213		usleep(states * CLOCK);
  214	    }
  215	    dump_registers();
  216	    dump_memory();
  217	    return 0;
  218	}

19行でゼロフラグを格納する zflag 変数を定義しています。25,26行で DEC, JMP 命令を追加し、その本体は137,150行で定義されています。vm_dec() 関数では、現在のレジスタの値がゼロの時は 0xFF に wrap around させている点に注意してください(処理系によっては不要な場合もある)。減算結果がゼロであれば zflag 変数が1、それ以外の場合は0にセットされます。

vm_jmp() 関数では、オペランドの値によって分岐形式を JMP (type = 0), JNZ (type = 1)に切り替えます。158行で分岐先を読み込み、続く159行でプログラムカウンタを変更します。分岐とは、プログラムカウンタを書き換えることなのです。

長い道のりでしたが、以上で TVM5 はやっと LED を点滅できるようになりました。必要となる処理を書き出してみましょう。

  • メモリ31番地に 0xAA を書き込む
  • 1秒待つ
  • メモリ31番地に 0x55 を書き込む
  • 1秒待つ
  • 最初から繰り返す

大まかにはこれだけですが、細部をつめていかなければなりません。メモリへの転送にはレジスタを介する必要がありますから、定数 0xAA はR0レジスタに格納し、定数 0x55 はR1レジスタに格納しておきましょう。問題は、どうやって「1秒待つ」かということです。要するに時間稼ぎをすれば良い訳ですが、このために DEC 演算と JNZ 条件分岐を使います。方法は次の通り。

  1. R2レジスタにある定数(wait)を設定する
  2. R2レジスタを減算する
  3. ゼロになっていなければ2から繰り返す

しばしば、3の条件分岐先を1に設定してしまうので注意しましょう(どうなるでしょうか?)。R2の値を wait といいますが、これをいくつにすれば1秒になるでしょうか。TVM5 のクロック周期は11行で 50m秒 (周波数 20Hz)に設定されています。上記3命令の中で繰り返し実行されるのは、2・3ですから、このふたつの命令のステート数が分かれば、求める wait は決定できます。DEC 演算が1ステート、JNZ 命令が2ステートですから、このループは1回りごとに3ステート、すなわち 150m秒を消費します。wait を6に設定すれば、150x6 = 900m秒となります。ループ外の処理(R2レジスタの初期化)で 100m秒消費しますので、両者で丁度1秒です。

道のりが見えましたので、早速コーディングです。まずは、アセンブリソースから。

 LOADI R0, 0xAA
 LOADI R1, 0x55
 STORE R0, [0x1F]
 LOADI R2, 6
 DEC R2
 JNZ ○○○
 STORE R1, [0x1F]
 LOADI R2, 6
 DEC R2
 JNZ ○○○

一度目の1秒ループでR2レジスタの値はゼロになっているため、再度R2の内容を6にセットしている点に注意してください。JNZ 命令の分岐先は、この時点ではアドレスが決定できないため空白としてあります。これからアセンブルを行い、アドレスが順次決まったところで JNZ の分岐先を書き込みます。

 00: 20 AA    LOADI R0, 0xAA
 02: 21 55    LOADI R1, 0x55
 04: 30 1F    STORE R0, [0x1F]
 06: 22 06    LOADI R2, 6
 08: 42       DEC R2

今回のプログラムはアドレスも必要になるため、左端にアドレスも併記しています。この結果から、最初のループの JNZ の分岐先は8番地であることが分かります。アセンブルを続けましょう。

 09: 51 08    JNZ 8
 0B: 31 1F    STORE R1, [0x1F]
 0D: 22 06    LOADI R2, 6
 0F: 42       DEC R2

よって、2回目の JNZ の分岐先は15番地となります。最後は、テストのためわざと分岐せず、SLEEP で終了させます。

 10: 51 0F    JNZ 0x0F
 12: 10       SLEEP

以上で機械語プログラムは完成しました。

 $ ./tvm5 20 aa 21 55 30 1f 22 06 42 51 08 31 1f 22 06 42 51 0f 10 > /dev/ttys001
 
 +-------------------------------------------------+
 |  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |
 |-------------------------------------------------|
 | 20 AA 21 55 30 1F 22 06 42 51 08 31 1F 22 06 42 |
 +-------------------------------------------------+
 | 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |
 |-------------------------------------------------|
 | 51 0F 10 00 00 00 00 00 00 00 00 00 00 00 00 00 |
 +-------------------------------------------------+
 
 00: 20 LOADI R0, $AA (2 states)
 02: 21 LOADI R1, $55 (4 states)
 04: 30 STORE R0, [$1F] <= $AA (6 states)
 06: 22 LOADI R2, $06 (8 states)
 08: 42 DEC R2 => $05 (9 states)
 09: 51 JNZ $08 (11 states)
 08: 42 DEC R2 => $04 (12 states)
 09: 51 JNZ $08 (14 states)
 08: 42 DEC R2 => $03 (15 states)
 09: 51 JNZ $08 (17 states)
 08: 42 DEC R2 => $02 (18 states)
 09: 51 JNZ $08 (20 states)
 08: 42 DEC R2 => $01 (21 states)
 09: 51 JNZ $08 (23 states)
 08: 42 DEC R2 => $00 (24 states)
 09: 51 JNZ $08 (26 states)
 0B: 31 STORE R1, [$1F] <= $55 (28 states)
 0D: 22 LOADI R2, $06 (30 states)
 0F: 42 DEC R2 => $05 (31 states)
 10: 51 JNZ $0F (33 states)
 0F: 42 DEC R2 => $04 (34 states)
 10: 51 JNZ $0F (36 states)
 0F: 42 DEC R2 => $03 (37 states)
 10: 51 JNZ $0F (39 states)
 0F: 42 DEC R2 => $02 (40 states)
 10: 51 JNZ $0F (42 states)
 0F: 42 DEC R2 => $01 (43 states)
 10: 51 JNZ $0F (45 states)
 0F: 42 DEC R2 => $00 (46 states)
 10: 51 JNZ $0F (48 states)
 12: 10 SLEEP (49 states)
 
 +---------+
 | R0 = AA |
 | R1 = 55 |
 | R2 = 00 |
 | R3 = 00 |
 +---------+
 
 +-------------------------------------------------+
 |  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F |
 |-------------------------------------------------|
 | 20 AA 21 55 30 1F 22 06 42 51 08 31 1F 22 06 42 |
 +-------------------------------------------------+
 | 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F |
 |-------------------------------------------------|
 | 51 0F 10 00 00 00 00 00 00 00 00 00 00 00 00 55 |
 +-------------------------------------------------+
 
 $

1秒間 0xAA を表示した後、1秒間 0x55 を表示して停止します。総ステート数は49ステートですから、2.5秒近くかかっていますが、これはレジスタの初期化や31番地への転送などで余計な時間がかかっているためです。

最終尾の SLEEP 命令を4番地への無条件分岐に書き換えれば、LED の点灯を永遠に続けます。

 ./tvm5 20 aa 21 55 30 1f 22 06 42 51 08 31 1f 22 06 42 51 0f 50 04 > /dev/ttys001

次のプログラムは、制御ループを外側にもうひとつ増やし、合計で5回LED点を繰り返して終了します。皆さんも、紙と鉛筆でハンドアセンブルに挑戦してみてください。

 ./tvm5 20 aa 21 55 22 05 30 1f 23 06 43 51 0a 31 1f 23 06 43 51 11 42 51 06 10 > /dev/ttys001

H8マイコンボードに挑戦!

ここまでの基礎知識が揃えば、データシートといえども恐れるに足りません。一部分であれ、きちんと理解できるようになっています。ホンマ?

ホンマです。最終回は、秋月H8マイコンボードで稼働するLED点滅プログラムを作成し、Pythonによるシリアル通信でプログラムを送り込み、これを実行します。