Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール
しばらく間が空いてしまいましたが、一気に 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 ループが組み込まれ、ループごとに
以上を 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 と認識されてしまいます。
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 とは、オペコード(正確には命令語)の直後に続いているデータのことなのです。日本語であれば、"後続値" と表現した方がより的確でしょう。ところが、国語力のない誰かがこれを「即値」と訳出してしまった。この時点から、悲劇がはじまったと言って良いでしょう。続く著者達が、盲目的に即値を使ったものだから、悲劇は拡大生産され続けています。
「すなわちの値」こんなおかしな日本語はありません。技術用語で、意味不明瞭の日本語に出会ったならば、必ず英語表記に立ち戻らなければなりません。あやふやな言葉や文章の影には、必ず著者の誤解や無理解が隠れています。大切なことは、日頃から自分の国語力を磨き、これを信じることです。たとえ相手が教科書やリファレンス書であっても、奴隷的態度は捨てなければなりません。
話を続けます。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を使うこととする)。
次に、この内容を 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 を点滅できるようになりました。必要となる処理を書き出してみましょう。
大まかにはこれだけですが、細部をつめていかなければなりません。メモリへの転送にはレジスタを介する必要がありますから、定数 0xAA はR0レジスタに格納し、定数 0x55 はR1レジスタに格納しておきましょう。問題は、どうやって「1秒待つ」かということです。要するに時間稼ぎをすれば良い訳ですが、このために DEC 演算と JNZ 条件分岐を使います。方法は次の通り。
しばしば、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マイコンボードで稼働するLED点滅プログラムを作成し、Pythonによるシリアル通信でプログラムを送り込み、これを実行します。