ゲーム「Turing Complete」で作成した自作アーキテクチャの命令フォーマットの仕様をメモしておく。
CPUアーキテクチャを学ぶパズルゲーム「Turing Complete」はSteamにて配信中。
命令フォーマット
命令は8bitが4つの3オペランド命令セットである。
Byte 0 | Byte 1 | Byte 2 | Byte 3
───────────┼──────────────┼──────────────┼──────────────
[ OPCODE ] | [ OPERAND1 ] | [ OPERAND2 ] | [ OPERAND3 ]
OPERAND
OPERANDは8bitの命令であり、以下のように定まる。 -64から127の範囲を即値として扱い、他に汎用レジスタ、特殊レジスタを指定する。
| bitパターン | 内容 | 補足 |
|---|---|---|
0??? ???? | 即値 | 0から127 |
100? ???? | 汎用レジスタ | R0-R7 |
101? ???? | 特殊レジスタ | 下記詳細 |
11?? ???? | 即値 | -1から-64 |
特殊レジスタ
特殊レジスタは以下の通りである。
| bitパターン | 数値 | ニーモニック | 内容 | 補足 |
|---|---|---|---|---|
1010 0000 | 160 | PC | プログラムカウンタ | |
1010 0001 | 161 | BUS | I/Oバス | IOのどちらかは自動選択 |
1010 0010 | 162 | CS | コールスタック | PUSHあるいはPOPする |
1010 0011 | 163 | CST | コールスタックのトップ | POPしない、RO |
1010 0100 | 164 | CSC | コールスタックカウンタ | CSに積まれている個数、RO |
1010 0101 | 165 | PCN | プログラムカウンタ + 4 | 次の命令の場所、RO |
OPCODE
OPCODEは8bitの命令であり、上位4bitで命令のカテゴリを示す。
| カテゴリ | OPCODE |
|---|---|
0x0? | データ |
0x1? | ジャンプ |
0x2? | 論理演算 |
0x3? | 算術演算 |
0xFF | 強制終了 |
以下、オペランド?をop?と略す(?は1から3まで)。 また、オペランド2およびオペランド3を16bitの即値として解釈したものをvalueと呼ぶ。
データ命令
| 16進 | 10進 | ニーモニック | 内容 | 補足 |
|---|---|---|---|---|
0x00 | 0 | MOV | op1 ← op2 | |
0x01 | 1 | MOVI | op1 ← value | |
0x02 | 2 | LD | op1 ← RAM[op2] | |
0x04 | 4 | ST | RAM[op1] ← op2 |
ジャンプ命令
| 16進 | 10進 | ニーモニック | 内容 | 補足 |
|---|---|---|---|---|
0x10 | 16 | JEQZ | if op1 == 0: PC ← value | |
0x11 | 17 | JNEZ | if op1 != 0: PC ← value | |
0x12 | 18 | JPOS | if op1 > 0: PC ← value | |
0x13 | 19 | JNPOS | if op1 ⇐ 0: PC ← value | |
0x14 | 20 | JNEG | if op1 < 0: PC ← value | |
0x15 | 21 | JNNEG | if op1 >= 0: PC ← value | |
0x16 | 22 | CALL | push(PC + 4); PC ← value | 関数呼び出し |
0x17 | 23 | RET | PC ← top(); pop() | 関数から復帰 |
論理演算命令
| 16進 | 10進 | ニーモニック | 内容 | 補足 |
|---|---|---|---|---|
0x20 | 32 | AND | op1 ← op2 & op3 | |
0x21 | 33 | OR | op1 ← op2 | op3 | |
0x22 | 34 | NOT | op1 ← ~op2 | |
0x23 | 35 | XOR | op1 ← op2 ^ op3 | |
0x24 | 36 | SHL | op1 ← op2 >> op3 | 論理シフト |
0x25 | 37 | SHR | op1 ← op2 << op3 | 論理シフト |
0x26 | 38 | ROL | op1 ← op2をop3回左にrotate | |
0x27 | 39 | ROR | op1 ← op2をop3回右にrotate |
算術演算命令
| 16進 | 10進 | ニーモニック | 内容 | 補足 |
|---|---|---|---|---|
0x30 | 48 | ADD | op1 ← op2 + op3 | |
0x31 | 49 | SUB | op1 ← op2 - op3 | |
0x32 | 50 | MUL | op1 ← op2 * op3 | |
0x33 | 51 | MULU | op1 ← (op2 * op3) >> 8 | 上位bitの取得 |
0x34 | 52 | DIV | op1 ← op2 / op3 | |
0x35 | 53 | MOD | op1 ← op2 % op3 | |
0x36 | 54 | NEG | op1 ← -op2 | |
0x37 | 55 | ASHR | op1 ← op2 >>> op3 | 算術シフト |
実装
命令はOPCODE1バイトとOPERAND1バイトを解釈し、以下のようなデータを用意する。
- OPCODE
- 演算結果の格納場所
- 演算に使用する値1
- 演算に使用する値2
- value(OP2を上位、OP3を下位とした16bitの即値)
2, 3, 4は主にOP1, 2, 3を使用するが、以下の場合は別の値が指定される。
| 評価順 | 状態 | 格納場所 | 演算値1 | 演算値2 | その他 |
|---|---|---|---|---|---|
| 0 | (通常) | (OP1) | (OP2) | (OP3) | |
| 1 | ST命令 | 0 | OP1 | 格納場所を無効 | |
| 2 | ジャンプ命令 | PC | OP1 | PCN | |
| 3 | CALL命令 | PCNをCSにPUSH | |||
| 4 | RET命令 | CS(POPする) | |||
| ジャンプ命令はop1の状態に応じて、valueもしくは演算値2がPCに格納される。CALL命令は必ずvalueが、RET命令はCS(POPする)が選択され、PCに格納される。 |
使用しての感想
アセンブリ言語を書いてみると、MOVとMOVIが冗長な気がした。これは、使用する際に8bit以上のデータを扱う機会が少なく、MOVIでしかカバーできない16bitの値をあまり使わなかったためである。また、データを8bitとして扱いたい場合、256でANDを取ろうとすると即値が対応していない(-1と解釈される)ため、一度レジスタに256を入れてから使用するという手間がかかった。
関数呼び出しについて、The Nand Gameの方ではPCだけでなくレジスタの値までコールスタックにプッシュする実装になっていた(これは回路で実装するというよりは関数呼び出しで賄っている側面もあるが)。レジスタの数を8に制限しているため、たびたび使用するレジスタの数を工夫して減らすことが必要となった。
命令として用意しなかったが、+=など四則演算の演算される数と結果の代入先が同じものや、条件分岐でTRUEの場合とFALSEの場合を一度に指定したい場合があった。これは高級言語での書き方に慣れているためでもありそうだ。