現在進行形で作成中の自作三進CPU、愛称を Libra the Processor としました。(以下省略して Libra と書きます)
名前の由来は天秤座の英名 Libra です。平衡三進数は英語では Balanced Ternary といいますが、balance という単語は天秤という意味も持ちます。
このページでは実装の回路的な話はあまりせずに、プログラムを描いたりするうえで把握したい情報をまとめたいと思います。
アーキテクチャ
まずは Libra のアーキテクチャを簡単に説明します。Libra は大きく分けて以下の4つのユニットから構成されます。
- ステートユニット
- レジスタユニット
- ALUユニット
- メモリユニット
そしてすべてのユニットが一つの大きなバスに接続される形となっています。なお、すべてをバスに接続する形を採用したのは、それぞれのユニットを基板を分けて作ったときに接続しやすいようにという実装上の理由です。
次にそれぞれのユニットに含まれるものについて説明します。
ステートユニット
ステートユニットはCPUの状態を管理するためのユニットです。Libra はマルチサイクルプロセッサなので、各クロックにおいて命令のどの段階を実行しているかを state が表します。具体的な状態遷移についてはここでは省略します。
PC (プログラムカウンタ)は実行する命令のアドレスを表し、SP (スタックポインタ)はスタックトップのアドレスを表します。どちらも幅は5trit、初期値は-121 (0t#####)です。
sign は演算命令を行った際にその結果の符号が記憶されるレジスタです。条件付きジャンプに使用します。幅は1tritです。
レジスタユニット
Libra は3つの汎用レジスタを持ちます。Imm は即値命令を実行する際にメモリから読みだした即値を格納するレジスタで、自由にアクセスすることはできません。
A, B, C の3つのレジスタのうち、Aレジスタは特殊なレジスタで基本的にどの演算もオペランドの片方はAレジスタとなります。アキュムレータに近いですが、書き込み先はB, Cも選べるので微妙なところです。後述する ISA も併せて確認してください。
レジスタはすべて5tritです。
ALUユニット
ALUユニットは計算を行うユニットです。(ALUのUはUnitなのでALUユニットって表記は気持ち悪いですね。)記憶素子は持ちません。
ALUが実行できる演算は以下のとおりです。(すべて5trit幅)
- ADD
- SUB (NOT + ADD)
- NOT
- AND
- OR
- Shift Left (1trit)
- Shift Right (1trit)
Shift Left の LST, Shift Right の MST には0が補充されます。
メモリユニット
Libra は命令メモリ、データメモリ、スタックと3つのメモリを持っています。アドレス空間はそれぞれ独立しています。
アドレス幅、データ幅ともに5tritです。
データメモリの121(0t11111)番地はMMIOとして使用する予定です。(変更の可能性あり)
ISA
Libra 用の命令セットを紹介します。ISAについてはあまり練れていないので、Libra the Processor 1号機が動いた後には改良することも考えています。
4~0の欄は機械語のコードです。どの命令も1ワードで表現しています。即値を使う命令は命令本体と即値で2ワードとなっています。
r, d は A, B, C のレジスタを表し、機械語ではA: 1, B: 0, C: #に対応します。
形式はアセンブリの引数を表しています。
命令 | 説明 | sign変更 | 命令長 | サイクル数 | 4 | 3 | 2 | 1 | 0 | 形式 |
---|---|---|---|---|---|---|---|---|---|---|
AND | d = A AND r | 〇 | 1 | 3 | 1 | r | 1 | 1 | d | DR |
OR | d = A OR r | 〇 | 1 | 3 | 1 | r | 1 | # | d | DR |
ADD | d = A + r | 〇 | 1 | 3 | 1 | r | 0 | 1 | d | DR |
MOV | d = r | 〇 | 1 | 3 | 1 | r | 0 | 0 | d | DR |
SUB | d = A – r | 〇 | 1 | 3 | 1 | r | 0 | # | d | DR |
LD | d = MEM[r] | 1 | 3 | 1 | r | # | 1 | d | DR | |
CMP | A – r | 〇 | 1 | 3 | 1 | r | # | 0 | – | R |
ST | MEM[r] = A | 1 | 3 | 1 | r | # | # | – | R | |
SL | d = A << 1 | 〇 | 1 | 3 | 0 | 1 | 1 | – | d | D |
NOT | d = NOT A | 〇 | 1 | 3 | 0 | 1 | 0 | – | d | D |
SR | d = A >> 1 | 〇 | 1 | 3 | 0 | 1 | # | – | d | D |
ANDI | d = A AND imm | 〇 | 2 | 5 | 0 | 0 | 1 | 1 | d | DI |
ORI | d = A OR imm | 〇 | 2 | 5 | 0 | 0 | 1 | # | d | DI |
ADDI | d = A + imm | 〇 | 2 | 5 | 0 | 0 | 0 | 1 | d | DI |
MOVI | d = imm | 〇 | 2 | 5 | 0 | 0 | 0 | 0 | d | DI |
LDI | d = MEM[imm] | 2 | 5 | 0 | 0 | # | 1 | d | DI | |
CMPI | A – imm | 〇 | 2 | 5 | 0 | 0 | # | 0 | – | I |
STI | MEM[imm] = A | 2 | 5 | 0 | 0 | # | # | – | I | |
PUSH | STACK[SP] = A; SP++ | 1 | 5 | 0 | # | 1 | – | – | N | |
POP | SP–; d = STACK[SP] | 1 | 4 | 0 | # | # | – | d | D | |
J | PC = imm | 2 | 5 | # | – | 1 | 1 | – | L | |
JP | if (sign == 1) PC = imm | 2 | 5 | # | 1 | # | 1 | – | L | |
JZ | if (sign == 0) PC = imm | 2 | 5 | # | 0 | # | 1 | – | L | |
JN | if (sign == #) PC = imm | 2 | 5 | # | # | # | 1 | – | L | |
CALL | PUSH(PC+1); PC = imm | 2 | 7 | # | – | 1 | 0 | – | L | |
RET | PC = POP(); | 1 | 4 | # | – | # | 0 | – | N | |
HALT | プロセッサ停止 | 1 | 3 | # | # | # | # | # | N |
特徴、注意点としては以下のような点が挙げられるでしょうか。
- Aレジスタをオペランドの片方として使う
- 書き込み先は自由に選べる
- ST, PUSHはAレジスタからしか保存できない
- 分岐はCMP + JZ (JP/JN) で行う
- スタックへのアクセスはPUSH/POP, CALL/RET で行う。特にSPは直接触れない
- MOVでもsignが変わってしまう(設計ミス)
- CMPは引き算の結果の符号だけ使うが、オーバーフロー、アンダーフローしたときも下位5tritの値で符号を判断するので注意(設計ミス)
まだ私自身もこの命令セットでプログラムをほとんど書いたことがないので使い勝手が分かりません…
アセンブリ
アセンブリのサンプルとして適当に書いたプログラムを載せておきます。R形式だけ例がありませんが、まあ分かるでしょってことで。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 100から3ずつ増加させて出力を10回繰り返すプログラム MOVI A, 10 // DI MOVI B, 3 MOVI C, 100 L1: PUSH // N MOV A, C // DR ADD A, B STI 0t11111 // I MOV C, A POP A // D ADDI A, -1 JZ L2 // L J L1 L2: HALT |
記述の特徴としては以下の点があります。
- コメントは //
- 即値は10進法または0tのプレフィックスをつけた平衡三進法で記述
- 即値の範囲は-121~121 (0t##### ~ 0t11111)
- N形式は引数なし
- L形式はラベル名を引数にとる
ツール
Libra the Processor を他の人にも触ってほしいという気持ちから、ツールをウェブで動かせるように作ったのでぜひ遊んでみてください。