ゲーム「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 0000160PCプログラムカウンタ
1010 0001161BUSI/OバスIOのどちらかは自動選択
1010 0010162CSコールスタックPUSHあるいはPOPする
1010 0011163CSTコールスタックのトップPOPしない、RO
1010 0100164CSCコールスタックカウンタCSに積まれている個数、RO
1010 0101165PCNプログラムカウンタ + 4次の命令の場所、RO

OPCODE

OPCODEは8bitの命令であり、上位4bitで命令のカテゴリを示す。

カテゴリOPCODE
0x0?データ
0x1?ジャンプ
0x2?論理演算
0x3?算術演算
0xFF強制終了

以下、オペランド?をop?と略す(?は1から3まで)。 また、オペランド2およびオペランド3を16bitの即値として解釈したものをvalueと呼ぶ。

データ命令

16進10進ニーモニック内容補足
0x000MOVop1 ← op2
0x011MOVIop1 ← value
0x022LDop1 ← RAM[op2]
0x044STRAM[op1] ← op2

ジャンプ命令

16進10進ニーモニック内容補足
0x1016JEQZif op1 == 0: PC ← value
0x1117JNEZif op1 != 0: PC ← value
0x1218JPOSif op1 > 0: PC ← value
0x1319JNPOSif op1 0: PC ← value
0x1420JNEGif op1 < 0: PC ← value
0x1521JNNEGif op1 >= 0: PC ← value
0x1622CALLpush(PC + 4); PC ← value関数呼び出し
0x1723RETPC ← top(); pop()関数から復帰

論理演算命令

16進10進ニーモニック内容補足
0x2032ANDop1 ← op2 & op3
0x2133ORop1 ← op2 | op3
0x2234NOTop1 ← ~op2
0x2335XORop1 ← op2 ^ op3
0x2436SHLop1 ← op2 >> op3論理シフト
0x2537SHRop1 ← op2 << op3論理シフト
0x2638ROLop1 ← op2をop3回左にrotate
0x2739RORop1 ← op2をop3回右にrotate

算術演算命令

16進10進ニーモニック内容補足
0x3048ADDop1 ← op2 + op3
0x3149SUBop1 ← op2 - op3
0x3250MULop1 ← op2 * op3
0x3351MULUop1 ← (op2 * op3) >> 8上位bitの取得
0x3452DIVop1 ← op2 / op3
0x3553MODop1 ← op2 % op3
0x3654NEGop1 ← -op2
0x3755ASHRop1 ← op2 >>> op3算術シフト

実装

命令はOPCODE1バイトとOPERAND1バイトを解釈し、以下のようなデータを用意する。

  1. OPCODE
  2. 演算結果の格納場所
  3. 演算に使用する値1
  4. 演算に使用する値2
  5. value(OP2を上位、OP3を下位とした16bitの即値)

2, 3, 4は主にOP1, 2, 3を使用するが、以下の場合は別の値が指定される。

評価順状態格納場所演算値1演算値2その他
0(通常)(OP1)(OP2)(OP3)
1ST命令0OP1格納場所を無効
2ジャンプ命令PCOP1PCN
3CALL命令PCNをCSにPUSH
4RET命令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の場合を一度に指定したい場合があった。これは高級言語での書き方に慣れているためでもありそうだ。