This page looks best with JavaScript enabled

Turing Completeで8bit CPUを作ってみた。

 ·   ·  ☕ 12 min read

Steamで配信されるTuring Completeという電子回路のシミュレーションゲームをやってみました。
実際のハードウェアに触らずにCPUをさくっと作れて面白かったので、ゲームの概要と作ってみたCPUの構成についてまとめてみたいと思います。

  1. ざっくり説明
  2. 感想
  3. 作ったCPUの説明

ざっくり説明

おおまかに2つのモードがあるようです。
ひとつは、問題が出されてステップアップしていくモード(play campaign)、もう一つは、自分で自由に回路を作れるモード(sandbox)です。

ステップアップしていくモードでは、NANDといった基礎的な部品からスタートしてCPUなどの複雑な回路を作成していきます。
たとえば、最初の方は「NANDだけを使ってANDやORを実装してみよう」といった問題が出され、クリアすると作成できた部品ANDやORを次のステップから使えるようになります。
そこから、加算器やマルチプレクサなどを作成していき、やがてはCPUに到達するわけです。

Turing Completeで全加算器を作ってみた図

自分は今のところ、8bit CPU(下の画像のWORKING COMPUTERの箇所)を作り、そのあと機械語命令でプログラミングするステージまで到達しましたが、まだステージが残されています。CPUをもう少し進化させるんでしょうか。

Level Map

感想

何より、ハードウェアに触らずに手を動かしながらCPUの仕組み、ソフトとハードの境目を知ることができるところがすばらしいなぁと思います。

実は以前、CPUの創りかたを読んだ際に、こうしたソフトウェアがあったらいいなぁと思っていました。。

「とても勉強になったんだけど、うーん、読んだだけではまだ仕組みが馴染んだ感じがしないなぁ。何か手を動かしたい。自分で実際にCPUを作ってみたいなぁ。けど、電子工作をしたことの無い自分がいきなりハードに触るのはちょっと遠回りすぎるし、デバッグも辛そうだから何かシミュレーションできるソフトウェアが無いかなぁ。」

そのとき私はCircuit Simulator Appletというブラウザで電子回路をシミュレートできるソフトウェアに出会いました。
そして先の書籍に登場するTD4(とりあえず動作するだけの4bitCPU)を作ってみたのです。

https://github.com/qawatake/cpu/tree/main/td4

Circuit Simulator Appletで作ってみた4bit CPU

ただ、Circuit Simulator Appletにはもう一歩なところがありました。それは「回路が複雑になると動作が重くなってしまう」点です。CPUのbit数を増やしたり、部品を増やしたりするとカクカクしてしまうのです。つまり、4bit CPUくらいがこのソフトで作れる限界だったようなのです。

一方、今回のTuring Completeは8bit CPUを作っても、大量に部品を使ってもサクサク軽快に動作します。どうやら64bitの部品なども用意されているようですから、もっとずっと複雑で現実的なCPUを作ったりもできるのでしょう。

ただ、Circuit Simulator AppletがTuring Completeより優れたところもあります。それは、部品がもう少しハード寄り、別の言い方をすると部品がそれほど抽象化されていないところです。
たとえば、Turing Completeでは、delayed lineやswitch、クロックが所与となります。対して、Circuit Simulator AppletではNANDを使ってフリップフロップを作ったり、ダイオードを使ってswitchを作ったり、コンデンサや抵抗を使ってクロックを作ったりできます。

というわけで、結局Turing CompleteとCircuit Simulator Appletは使い分けをしたらいいのかもしれないですね。たとえば、回路の中でもより低いレイヤはCircuit Simulator Appletで遊んでみて、そのあたりで作ってみた部品を所与のものとしてTuring Completeで複雑な回路を作ってみるといった使い方ですね。

作ったCPUの説明

ここからはTuring Completeを使って作ってみた8bit CPUの仕組みを説明してみます。自分用メモなので説明は飛ばしめですし、設計が理想的になっているわけではありません。
ちなみに、Turing Completeには解説動画(英語)があるのでそっちを参照されてもいいかもしれません。

回路図の全体

最初から用意されている部品

  • レジスタ0番-5番
  • 8bit出力
  • 8bit入力
  • 機械語命令が記録されたROM(8bit × 8bit): 1命令8bitで命令のアドレスもまた8bitです。

最初から用意されている部品

サポートする命令

以下の4種類の命令がサポートされています。ちなみに、これはTuring Completeの問題ですでに設定されていました。なので、オリジナルではありません。

  1. Immediate(即値)
  2. Compute
  3. Copy
  4. Condition

1. Immediate

上位2bitが00の場合、Immediate命令となります。下位6bitの値が0番レジスタに書きこまれます。

2. Compute

上位2bitが01の場合、Compute命令となります。1番レジスタと2番レジスタに演算を適用し3番レジスタに書き込みます。
さらに下位3bitによって演算の種類が決まります。

  1. 000: OR
  2. 001: NAND
  3. 010: NOR
  4. 011: AND
  5. 100: ADD
  6. 101: SUB

3. Copy

上位2bitが10の場合、Copy命令となります。下位3番〜5番bitでコピー元、下位0番〜2番bitでコピー先を表します。例えば、機械語命令が10010011であれば、2番レジスタから3番レジスタにコピーすることになります。

ただし、レジスタは0番から5番までしかないことと、コピー元110は8bit入力をコピー先110は8bit出力を表すことに注意が必要です。

4. Condition

上位2bitが11の場合、Condition命令となります。この命令は3番レジスタを評価し、真であった場合に、0番レジスタの値をプログラムカウンタに書き込みます。
評価に用いる条件式は機械語命令の下位3bitで決まります。

  1. 000: Never
  2. 001: = 0
  3. 010: < 0
  4. 011: <= 0
  5. 100: Always
  6. 101: != 0
  7. 110: >= 0
  8. 111: > 0

CPUの構成部品

自分の作ったCPUは概ね以下の部品に分けられます。

  1. 書き込みを制御する部品
  2. 出力を決める部品
  3. プログラムカウンタを制御する部品

それぞれ見ていきます。

書き込みを制御する部品

書き込みを制御する部品の回路図

この部品は、命令を入力として、更新すべきレジスタに対応する端子がonになるように出力します。
また、レジスタはSaveの端子がonの場合にのみ、入力を受けつけて内部の状態を更新します。
ですので、この部品とレジスタを結びつけることで、あるレジスタ更新すべき命令が読まれたときだけそのレジスタの書き込みを許可できるようになります。

例えば、レジスタ1を更新すべきなのは、Copy命令でありかつCopy先がレジスタ1にになっているときなので、命令が10xxx001の場合のみ1番の出力がonになります。

また、レジスタ0を更新すべきなのは、Copy命令に加えて即値を受け取る場合なので、命令が10xxx00000xxxxxxの場合のみ0番の出力がonになります。

出力を決める部品

出力を決める部品の回路図

ここでは「出力」という言葉をだいぶアバウトに使っているので、具体的にしたいと思います。

まず、「何に使うための出力なのか」をはっきりさせます。この部品は以下の用途で使用されます。

  • レジスタに書き込む
  • 8bit出力に書き込む
  • プログラムカウンタに書き込む

この3つの用途はひとつの命令で同時に必要になることはありません。
ですので、1命令で1用途のために出力すればいいわけです。

では、次に「何を出力するのか」をはっきりさせます。この部品は以下の値を出力します。

  • 即値
  • レジスタの値に算術論理演算を行った値
  • レジスタの値
  • レジスタ0の値(なぜ他と分けているかは後ほど説明します。)

つまり、ここで取り上げている「出力を決める部品」というのは、レジスタ or 8bit出力or プログラムカウンタに書き込むために、即値 or レジスタの値 or 算術論理演算を行った値のを出力する部品ということになります。
ただ、わかりやすさ?のために出力の用途を書きましたが、実際に使われる用途を決めるのは、あくまで、先に説明した「書き込みを制御する部品」やこの後説明する「プログラムカウンタを制御する部品」であることに注意が必要です。

続いて、出力を決めるための仕組みを見ていきます。部品の中でも↓のDECと4つのswitchがつながっている部品(ここでは門番と読んでみます。)に着目してみます。

門番の回路図

DECは入力された命令が4パターンのうちどれかを、4つの端子を使って出力してくれる部品です。
0番がImmediate、1番がALU、2番がCopy、3番がConditionに相当しています。その端子がそれぞれ、switchとつながっているわけです。
つまり、門番は、4種類の個別出力のうち、どれを最終的な出力として利用するかを命令のパターンに応じて切り分ける仕事をしてくれていることになります。

で、その4種類の個別出力はなにかというと、先にあげた4つなわけです。上から順に見ていきます。

1つ目はImmediate(即値)です。8bitの機械語のうち下位6bitが即値となるので、上位2bitを捨てて出力が行われています。例えば、命令が11111111であれば、00111111を出力します。
ここで、上位2bitが00ではなく11であっても気にしないことに注意してください。
命令のパターンを見るのは先に述べた門の仕事です。

2つ目はレジスタの値に算術論理演算を行った値です。COMPUTE命令はレジスタ1とレジスタ2の値に対して演算を実行するため、ALUの入力にはそれらを利用しています。
ALUは命令の下位3bitから演算の種類を特定し、入力として受け取った2つのレジスタの値に演算を適用した値を出力してくれます。
たとえば、命令の下位3bitが011 = AND、1番レジスタの値が10100101、2番レジスタの値が01010101であれば、00000101を出力します。
ALU自体の実装はそれほど難しくないのでここでは説明を省略します。

3つ目はレジスタの値です。もう少し具体的に言うと、COPY命令でCOPY元となるレジスタの値を出力します。
つまり、機械語命令の4番、5番、6番のbitを見て、出力すべきレジスタ(or8bit入力)のインデックスを決定し、その値を出力するわけです。
たとえば、命令の下位6bitが101000であれば、コピー元は101 = 5のため、5番レジスタの値を出力します。

最後は常にレジスタ0の値を出力する部分です。COND命令ではレジスタ3の値が条件を満たす場合にレジスタ0の値をプログラムカウンタに書き込むため、この固定出力を用意しています。

プログラムカウンタを制御する部品

この部品は↑の「出力を決める部品」からの出力と3番レジスタと命令の3つの入力からプログラムカウンタの値を決定します。この部品は更に2つの部品に分けられます。

1つ目の部品の回路図

1つ目の部品は、レジスタ3の値と機械語命令の下位3bitを受け取って、レジスタが条件を満たすかどうかを評価します。
たとえば、レジスタ3の値が01000000で機械語命令の下位3bitが111であれば、10000000 > 0のため、この部品は真を返します。

2つ目の部品の回路図

2つ目の部品はプログラムカウンタ用のレジスタを内部に持っています。
そのレジスタの値を書き換えるか、それとも一つ分インクリメントするかどうかを、機械語命令の上位2bitと1つ目の部品からの出力をもとに決定します。
当然ながら、レジスタを書き換えるのは、機械語命令の上位2bitが11で1つ目の部品が真を返した場合です。

試しに命令を動作を追ってみる

さて、回路のおおよその説明が終わったので、具体的に命令を例にとってその挙動を確認してみましょう。

CPUが命令を解釈して実行する図

以下のような設定を考えます。

  • 現在のプログラムカウンタの値: 00001000
  • アドレス00001000にある命令: 01000100
  • レジスタ1の値: 01101111
  • レジスタ2の値: 00000001
  • レジスタ3の値: 00000000

(他のレジスタなどの値は今回関係ないため無視します。)

命令は01000100、つまり、足し算命令のため、3番レジスタの値は1番レジスタの値01101111と2番レジスタの値00000001の和である01110000に上書きされるはずです。
実際にこうなることを確認してみます。

まず、「書き込みを制御する部品」の出力を確認します。今回は3番レジスタに書き込みを行うため、3番bitが立っている00001000が出力されます。

次に、「出力を決める部品」を確認します。そのうち、まずは「門番」に着目してみると、DECは機械語命令01000100を受け取って、それがCompute命令であると判断し、1番bitが立っている0010を出力します。
ですので、4種類の個別出力のうちALUの結果が最終的な出力となります。
ALUは1番レジスタの値01101111、2番レジスタの値00000001と機械語命令の下位3bit100 = ADDを受け取るため、和01110000を出力します。
以上をあわせますと、「出力を決める部品」から出力される値は01110000となります。

したがって、「書き込みを制御する部品」と「出力を決める部品」の結果をまとめると、3番レジスタに01110000が書き込まれることになります。

最後に「プログラムカウンタを制御する部品」を確認しておきます。まず、3番レジスタを評価する部品は、機械語命令の下位3bit100 = Alwaysと3番レジスタの書き込み前の値00000000を受け取り、真を返します。ただ、機械語命令の上位2bitが11ではないため、結局、先ほど求めた出力01110000がプログラムカウンタに書き込まれることはありません。

たしかに、この回路に従うと、(次のクロックの立ち上がりにて)3番レジスタに01110000が書き込まれ、プログラムカウンタは1インクリメントされるということが分かりました。

Share on

qawatake
WRITTEN BY
qawatake