NAND型 CPU
2015年3月16日
演算がNANDと左シフトのみが可能な、6ビットCPUを考えてみた。
ここのところ、PICやLPCなどの組込みマイコンと、MZ-80Kの話題ばかり続いたが、このブログは本来、副題にもあるとおり「IC・トランジスタで出来たコンピューターを設計・製作する」ためのもので、久々にその初心の話。
NANDの組み合わせだけですべての論理演算が可能であるのはよく知られていることであり、初期のコンピューターの中には、このロジックで作成されていたものもあったらしい。
ということは、CPUで使える演算子としてNANDだけを用意しておけば良いはずで、あと、上位ビットと下位ビットとの間での情報のやりとりとして、シフト演算(左シフト)があれば、すべての演算が行える。加えて、条件分岐できれば、CPUとしての体裁が整うはずである。
この、必要最小限の機能を持ったCPUを如何にシンプルな構造で構築するかというのを、この1-2週間考えていた。
CPUの規格
今のところ、必要最小限の構造としてバランス良く仕上がっていると思われるのは、6ビットのCPUである(仮名:NAND6)。命令長は固定で、12ビット(2ワード)。RISCに分類されると思われる。
レジスターは、CPU内部に保持しているものは、一時記憶用のものを除けばプログラムカウンター(8ビット)のみである。汎用レジスターは6ビットのものが4つ扱えるが、これらは外部RAMを利用している。
命令セットは以下の通り4種類。
00xxyy zzzzzz
レジスターyyとリテラル値zzzzzzとの間のNAND値を、レジスターxxに代入
01xxyy yyyyyy
レジスターxxを左シフトし(MLBは1)、キャリーがあればyyyyyyyy0にジャンプ
10xxyy zzzzzz
レジスターyyとアドレスzzzzzzのメモリー値との間のNAND値を、レジスターxxに代入
11xxyy zzzzzz
レジスターxxとレジスターyyとの間のNAND値を、アドレスzzzzzzのメモリーに代入
アドレス空間は9ビット(512 ワード)で、うち、前の方の448 ワードにプログラムがおかれ、最後の64 ワードがRAMである。RAMのうち、最初の4 ワードは4つのレジスター(R0, R1, R2, R3)として用いられる。
どの様なプログラムが組めるか
RISCにおきまりのパターンだが、R0は特殊なレジスターであり、NAND6では常に0x3Fにしておかなければならない。NOP (000000 000000)を実行することでR0に0x3Fが代入されるので、通常はR0の値を変更してはいけないという規約にしておく。これにより、無条件ジャンプは0100yy yyyyyyで表現できるし、00xx00 zzzzzzを実行すればzzzzzzのNOT値を、レジスターxxに代入することが出来る。
スタックも何もないので、サブルーチンを使うにはRISCでよく使われている間接アドレスジャンプを行なわなければならない。が、実際には間接アドレスジャンプは実装されていない。ただし、プログラムカウンターがRAM領域やレジスター領域を参照することが出来るので、無条件ジャンプ命令をRAM領域に書き込んでおき、そこにジャンプすることで実現できる。規約では、CALLを実行の際、レジスターR2, R3に戻ってくるアドレスにジャンプする命令を代入しておくことにする。RET命令は、JUMP 0x1C2 (レジスターR2のアドレス)と同義である。
以上のことを考えて、アセンブリ構文を以下のようにしてみた。
エミュレーター付きの簡易アセンブラーを作成し、この構成でどれぐらい出来るのか、調べてみた。試しに、6ビットどうしの加減算が出来るかどうかプログラムを書いてみたところ、無事に書けた。ということは、448 ワードに収まる範囲内では、何でも出来ると言うことだろう。以下、加減算ルーチンアセンブリ。
回路図
回路設計は、アルテラのQuartus II ver 9.0 sp2を用いて行なっている。wave formが使える最後のバージョンである。下は、全体の構造。
入力はリセットとクロック。出力に9ビットのアドレスと、読み込み・書き込みシグナル。加えて、6ビットの入出力データーライン。今回のものは、全体的にかなりコンパクトに収まったので、以下、全回路図を記しておきたい。
ファイル一式はこちら(とりあえず、転載不可のライセンス)。
クロック作成回路
CLK0-CLK5の作成部分、クロックの立ち下がりと共にフリップフロップの出力が変化するので微妙なところだが、フリップフロップのシグナル変化よりもANDゲートの入力(各ゲートの上の方)が遅れることはまず無いだろうから、ハザード無しに問題なく動くはず。
動作は、以下の通り
6ビットのフリップフロップを用いて、6つのステップを作成している。ただし、最後のステップだけ2倍の時間を取っているが、これはメモリーの書き込みに対応したもので、書き込みシグナル(CLK5)の終了後もアドレスラインとデーターラインを有効に保持しておくためのもの。それぞれのステップでの動作は、次の通り。
ステップ0:コマンド読み取り。
ステップ1:レジスター2読み取り。
ステップ2:レジスター1読み取り。
ステップ3:オペランド読み取り。
ステップ4:オペランドの示すアドレスからの値読み取り。
ステップ5:計算結果書き込み及びプログラムカウンターの更新。
プログラムカウンター
プログラムカウンターは、ロード機能付きの8ビットのバイナリーカウンターである。アドレスラインは9ビットだが、最下位ビットにはカウンターは用いず、クロック回路からのシグナル(STP3)を用いている。カウントアップもしくは値のロード(ジャンプ命令用)はステップ5のクロックシグナル(CLK5)で。ステップ0もしくはステップ3で、アドレスラインに出力され、このタイミングで命令コードをRAMから読み込む。
コマンド取得回路
ステップ0及びステップ3で、データーラインから値を読み込んで保存するだけの、純粋な回路。
演算及び一時レジスター
演算を行なって、RAMに保存するまで一時的に値を保持しておく回路。中央にある6つのNANDゲートとその右のセレクター(74157)が、いわゆるALU(Arithmetic Logic Unit)である。セレクター入力のBの方は1ビットずつずれた状態になっていて、左ビットシフト(MLBは1)に相当。最初の値はステップ1(CLK1)で取り込み、2つ目の値との演算結果を、命令の種類に応じて、ステップ2,3,または4(CLK2, CLK3, CLK4)で保存する。結果は、ステップ5でデーターラインに出力。
アドレスライン作成回路
命令の種類に応じて、各ステップでアドレスラインに値を出力する回路。演算用の値を取り込んだり、演算値を書き込んだりするときに用いられる。ステップ0及びステップ3では、プログラムカウンターからアドレスが出力されるので、それ以外のステップ(1,2,4,5)が担当である。A6-A8は、プログラムカウンターから値が取得される。回路図中でオープンドレインをH出力(常にハイインピーダンス)になっているのは、Quartus IIでアセンブラーを通すに必要なため入れてある。
リード・ライト信号作成回路
RAMからの読み込みと書き込み用の信号を作成する部分。ステップ0から4まで(STP0-STP4)は常に読み込みで、ステップ5(CLK5)で書き込みである。
動作確認
以下のようにQuartus II上にコンピューターを作成し、上記の加減算ルーチンを入力して、動作させてみたところ、思った通りに動作することが確認できた。
今後について
このCPUは、2年ほど前に考えたCPUの規格に比べて、ずいぶんコンパクトに仕上がっている。アルテラの小規模CPLD、EPM3064に入れることが出来るので、とりあえずそれで組んでみようか。ただし、演算がNANDだけというのは、コンピューターの仕組みを考える上での思考実験的なものとしては面白いが、実機を組むとなると、CPUそのものが小さく押さえられる反面、プログラムが大きくなりがちである。最終的に構築する予定のトランジスターコンピューターでは、ROMも手作りの予定なので、結局は回路が大きくなってしまう可能性もある。この辺りのバランスを考えながら、ICやトランジスターで組む前に、もう少し規格を吟味してみたい。ちなみに、このNAND6を74シリーズのICだけで組むと、24-25個が必要になる。
ここのところ、PICやLPCなどの組込みマイコンと、MZ-80Kの話題ばかり続いたが、このブログは本来、副題にもあるとおり「IC・トランジスタで出来たコンピューターを設計・製作する」ためのもので、久々にその初心の話。
NANDの組み合わせだけですべての論理演算が可能であるのはよく知られていることであり、初期のコンピューターの中には、このロジックで作成されていたものもあったらしい。
ということは、CPUで使える演算子としてNANDだけを用意しておけば良いはずで、あと、上位ビットと下位ビットとの間での情報のやりとりとして、シフト演算(左シフト)があれば、すべての演算が行える。加えて、条件分岐できれば、CPUとしての体裁が整うはずである。
この、必要最小限の機能を持ったCPUを如何にシンプルな構造で構築するかというのを、この1-2週間考えていた。
CPUの規格
今のところ、必要最小限の構造としてバランス良く仕上がっていると思われるのは、6ビットのCPUである(仮名:NAND6)。命令長は固定で、12ビット(2ワード)。RISCに分類されると思われる。
レジスターは、CPU内部に保持しているものは、一時記憶用のものを除けばプログラムカウンター(8ビット)のみである。汎用レジスターは6ビットのものが4つ扱えるが、これらは外部RAMを利用している。
命令セットは以下の通り4種類。
00xxyy zzzzzz
レジスターyyとリテラル値zzzzzzとの間のNAND値を、レジスターxxに代入
01xxyy yyyyyy
レジスターxxを左シフトし(MLBは1)、キャリーがあればyyyyyyyy0にジャンプ
10xxyy zzzzzz
レジスターyyとアドレスzzzzzzのメモリー値との間のNAND値を、レジスターxxに代入
11xxyy zzzzzz
レジスターxxとレジスターyyとの間のNAND値を、アドレスzzzzzzのメモリーに代入
アドレス空間は9ビット(512 ワード)で、うち、前の方の448 ワードにプログラムがおかれ、最後の64 ワードがRAMである。RAMのうち、最初の4 ワードは4つのレジスター(R0, R1, R2, R3)として用いられる。
どの様なプログラムが組めるか
RISCにおきまりのパターンだが、R0は特殊なレジスターであり、NAND6では常に0x3Fにしておかなければならない。NOP (000000 000000)を実行することでR0に0x3Fが代入されるので、通常はR0の値を変更してはいけないという規約にしておく。これにより、無条件ジャンプは0100yy yyyyyyで表現できるし、00xx00 zzzzzzを実行すればzzzzzzのNOT値を、レジスターxxに代入することが出来る。
スタックも何もないので、サブルーチンを使うにはRISCでよく使われている間接アドレスジャンプを行なわなければならない。が、実際には間接アドレスジャンプは実装されていない。ただし、プログラムカウンターがRAM領域やレジスター領域を参照することが出来るので、無条件ジャンプ命令をRAM領域に書き込んでおき、そこにジャンプすることで実現できる。規約では、CALLを実行の際、レジスターR2, R3に戻ってくるアドレスにジャンプする命令を代入しておくことにする。RET命令は、JUMP 0x1C2 (レジスターR2のアドレス)と同義である。
以上のことを考えて、アセンブリ構文を以下のようにしてみた。
: | No operation. Store 0x3F in R0. | |
: | Store NAND value of Rx and Ry in Rz. | |
: | Store NAND value of Rx and Ry in memory (zz). | |
: | Store NAND value of Rx and memory (yy) in Rz. | |
: | Store NAND value of Rx and 6 bit value yy in Rz. | |
: | Jump to address shown by label:. | |
: | Shift left Rx. | |
: | Shift left Rx. Jump to address shown by label: if carry. | |
: | Store 6 bit value yy in Rx. | |
: | Store return address in R2 and R3, then jump. | |
: | Jump to 1C2. |
エミュレーター付きの簡易アセンブラーを作成し、この構成でどれぐらい出来るのか、調べてみた。試しに、6ビットどうしの加減算が出来るかどうかプログラムを書いてみたところ、無事に書けた。ということは、448 ワードに収まる範囲内では、何でも出来ると言うことだろう。以下、加減算ルーチンアセンブリ。
/* Subtraction (sub:) and subtraction with carry (sbc:) functions. Parameters: (04), (05): NOT of input values r1: carry (either 3f or 00; if adc: is called) Return: (04): NOT of result value r1: carry (either 3f or 00) Addition (add:) and addition with carry (adc:) functions. Parameters: (04), (05): NOT of input values r1: carry (either 3f or 00; if adc: is called) Return: (04): NOT of result value r1: carry (either 3f or 00) Registers will be used as follows: R1: loop counter R2: keeps result after 3rd and 4th NAND calculation R3: keeps result after 2nd NAND calculation and shift value 04: keeps result after 1st NAND calculation Following is a half adder. Calculation is done from left to right. Note that result R3 is shifted to left. +---------------------|\ | | O--R2--|\ | +-------|/ | O--R2 R2--+-|\ | +--|/ |\ | O--04--+--+----------- | -----| O--R3 R3--+-|/ | | |/ | +-|\ | | | O--R3--------+ +------------|/ */ NOP LD r2,15 LD r3,32 LD r1,3f NAND r2,r0,(04) NAND r3,r0,(05) CALL adc: NAND r0,(04),r2 end: JUMP end: sub: LD r1,00 sbc: NAND r0,r2,(0e) NAND r0,r3,(0f) NAND r0,(05),r3 NAND r3,r3,r3 NAND r3,r3,(05) NAND r1,r1,r1 call adc: NAND r1,r1,r1 NAND r0,(0e),r2 NAND r0,(0f),r3 ret add: LD r1,00 adc: NAND r0,r2,(06) NAND r0,r3,(07) NAND r0,(04),r2 NAND r0,(05),r3 SJIC r1,adc0: JUMP addstart: adc0: NAND r2,r3,(04) NAND r3,(04),r3 NAND r2,(04),r2 NAND r2,r3,r2 NAND r0,(04),r3 SJIC r3,carrystart: ; MLB=1 addstart: LD r1,3e ; 6 times loop add0: NAND r2,r3,(04) NAND r3,(04),r3 NAND r2,(04),r2 NAND r2,r3,r2 NAND r0,(04),r3 SJIC r3,carry1: ; MLB=1 NAND r3,3e,r3 NAND r3,r3,r3 ; MLB=0 SJIC r1,add0: NAND r0,r2,(04) NAND r0,(06),r2 NAND r0,(07),r3 ld r1,00 ret carrystart: LD r1,3e ; 6 times loop carry0: NAND r2,r3,(04) NAND r3,(04),r3 NAND r2,(04),r2 NAND r2,r3,r2 NAND r0,(04),r3 SL r3 ; MLB=1 carry1: NAND r3,3e,r3 NAND r3,r3,r3 ; MLB=0 SJIC r1,carry0: NAND r0,r2,(04) NAND r0,(06),r2 NAND r0,(07),r3 ld r1,3f ret eof:このプログラムは、もう少し吟味すれば、半分ぐらいの大きさに出来そうである。
回路図
回路設計は、アルテラのQuartus II ver 9.0 sp2を用いて行なっている。wave formが使える最後のバージョンである。下は、全体の構造。
入力はリセットとクロック。出力に9ビットのアドレスと、読み込み・書き込みシグナル。加えて、6ビットの入出力データーライン。今回のものは、全体的にかなりコンパクトに収まったので、以下、全回路図を記しておきたい。
ファイル一式はこちら(とりあえず、転載不可のライセンス)。
クロック作成回路
CLK0-CLK5の作成部分、クロックの立ち下がりと共にフリップフロップの出力が変化するので微妙なところだが、フリップフロップのシグナル変化よりもANDゲートの入力(各ゲートの上の方)が遅れることはまず無いだろうから、ハザード無しに問題なく動くはず。
動作は、以下の通り
6ビットのフリップフロップを用いて、6つのステップを作成している。ただし、最後のステップだけ2倍の時間を取っているが、これはメモリーの書き込みに対応したもので、書き込みシグナル(CLK5)の終了後もアドレスラインとデーターラインを有効に保持しておくためのもの。それぞれのステップでの動作は、次の通り。
ステップ0:コマンド読み取り。
ステップ1:レジスター2読み取り。
ステップ2:レジスター1読み取り。
ステップ3:オペランド読み取り。
ステップ4:オペランドの示すアドレスからの値読み取り。
ステップ5:計算結果書き込み及びプログラムカウンターの更新。
プログラムカウンター
プログラムカウンターは、ロード機能付きの8ビットのバイナリーカウンターである。アドレスラインは9ビットだが、最下位ビットにはカウンターは用いず、クロック回路からのシグナル(STP3)を用いている。カウントアップもしくは値のロード(ジャンプ命令用)はステップ5のクロックシグナル(CLK5)で。ステップ0もしくはステップ3で、アドレスラインに出力され、このタイミングで命令コードをRAMから読み込む。
コマンド取得回路
ステップ0及びステップ3で、データーラインから値を読み込んで保存するだけの、純粋な回路。
演算及び一時レジスター
演算を行なって、RAMに保存するまで一時的に値を保持しておく回路。中央にある6つのNANDゲートとその右のセレクター(74157)が、いわゆるALU(Arithmetic Logic Unit)である。セレクター入力のBの方は1ビットずつずれた状態になっていて、左ビットシフト(MLBは1)に相当。最初の値はステップ1(CLK1)で取り込み、2つ目の値との演算結果を、命令の種類に応じて、ステップ2,3,または4(CLK2, CLK3, CLK4)で保存する。結果は、ステップ5でデーターラインに出力。
アドレスライン作成回路
命令の種類に応じて、各ステップでアドレスラインに値を出力する回路。演算用の値を取り込んだり、演算値を書き込んだりするときに用いられる。ステップ0及びステップ3では、プログラムカウンターからアドレスが出力されるので、それ以外のステップ(1,2,4,5)が担当である。A6-A8は、プログラムカウンターから値が取得される。回路図中でオープンドレインをH出力(常にハイインピーダンス)になっているのは、Quartus IIでアセンブラーを通すに必要なため入れてある。
リード・ライト信号作成回路
RAMからの読み込みと書き込み用の信号を作成する部分。ステップ0から4まで(STP0-STP4)は常に読み込みで、ステップ5(CLK5)で書き込みである。
動作確認
以下のようにQuartus II上にコンピューターを作成し、上記の加減算ルーチンを入力して、動作させてみたところ、思った通りに動作することが確認できた。
今後について
このCPUは、2年ほど前に考えたCPUの規格に比べて、ずいぶんコンパクトに仕上がっている。アルテラの小規模CPLD、EPM3064に入れることが出来るので、とりあえずそれで組んでみようか。ただし、演算がNANDだけというのは、コンピューターの仕組みを考える上での思考実験的なものとしては面白いが、実機を組むとなると、CPUそのものが小さく押さえられる反面、プログラムが大きくなりがちである。最終的に構築する予定のトランジスターコンピューターでは、ROMも手作りの予定なので、結局は回路が大きくなってしまう可能性もある。この辺りのバランスを考えながら、ICやトランジスターで組む前に、もう少し規格を吟味してみたい。ちなみに、このNAND6を74シリーズのICだけで組むと、24-25個が必要になる。