CPU


1bit CPU を設計してみる



前回、ALU長とデータバス長のみが1bitのCPUを考えました。
今回はレジスタ長も1bitにして、少し本気で設計してみます。
前回は「実用的かどうか」に重点を置きましたが、今回はその点は無視します。(とはいえ、一応プログラムが書けて、それが走るくらいのものを目指します)

基本構成

ALUとレジスタとデータバスが1bitのCPUです。
レジスタ長1bitですから、一度に計算できる数値は1が最大です。
レジスタ値が1を越えると桁があふれて0になり、キャリーフラグが立ちます。
レジスタ値が0を下回ると1になり、やはりキャリーフラグが立ちます。
それより大きな数は、複数のレジスタを束ねてキャリーフラグで繋ぐか、メモリ上にワークを用意して自力でソフトウェア的に解決する必要があります。
マイナス値(2の補数表現)も、当然BCD演算も浮動小数点も、すべて自力でコーディングです。

命令は14bit固定長、メモリ間演算は無しのロード・ストアアーキテクチャとします。
イミディエイトは命令中に埋め込みます。(なにせ1bitの空きがあれば良いわけですので)
また1bitですから、エンディアンという概念もありません。(正確にはプログラマーが決めればよいことになります)

プログラムカウンタは0000000000000000番地からスタートし、1111111111111111番地まで1bitずつインクリメントしていきます。
命令フェッチも1bitずつですが、命令長は14bit固定なので、その長さまでループしてフェッチを繰り返し、14bit分のインストラクション用バッファに溜まったところでデコードします。
命令が実行される時点で、プログラムカウンタは次の1bit目を差しています。
つまり、プログラマーからはプログラムカウンタは14bitずつ進んでいるように見えます。
ロード・ストアアーキテクチャですが、パイプラインはありません。

CPUの名前は「W10(ダブリューイチマル)」としました。

レジスタセット

W10は1bitのデータレジスタを32本(d0 ~ d31)、16bitのアドレスレジスタを8本(a0 ~ a7)持ちます。
データレジスタのうちd0はハードで0固定(ゼロレジスタ)、d30はゼロフラグ、d31はキャリーフラグです。
アドレスレジスタのうちa7はプログラムカウンタ、a6はプロシージャレジスタです。
スタックは適当なアドレスレジスタを使って自前で用意し、ロード・ストア命令とインクリメント・デクリメント命令を組み合わせて実現します。
つまり自由に使えるのは、データレジスタ29本とアドレスレジスタ6本です。
データレジスタが29本あるとはいえ、1本1bitですからあっという間に使い切ってしまいそうです。

アドレスバスまで1bitではプログラムすら走りませんので、アドレスレジスタをデータレジスタとは別に用意します。
考え方は6800や6502などの8bit CPUと同じです。
アドレスバスとアドレスレジスタは16bitとします。
16bitだと8KiBの範囲をアクセス出来ます。
「ん?64KiBじゃないの?」と思うかもしれませんが、それは今の世の中に「メモリアドレスは8bit単位で割り振るものだ」という固定観念があるためです。
データバスが1bitなのですから、当然メモリアドレスも1bit単位で割り振られます。
つまり1bit×2^16=65536bit、すなわち8KiBです。
この狭い世界に、コード・データやスタックなどすべてを置くことになります。
アドレス値は命令中に埋め込めませんので、面倒ですがプログラムカウンタ+ディスプレースメント相対でメモリから拾ってきます。(SuperHのような感じです)
アドレスレジスタ+ディスプレースメントでのアクセスできません。

フラグレジスタはゼロとキャリーの2本です。
サインフラグもハーフキャリーも、1bitの世界ではまったく意味をなさないので存在しません。
フラグの動作は、Z80に倣って演算命令直後に変化させることにします。
両フラグが立っていない状態は、0+1と1+0、1-0で1になったときです。
キャリーが立つのは、1+1で0になったときと、0-1で1になったときです。

全レジスタを一覧しておきます。

d0 ゼロレジスタ(ハードで0固定) 1bit
d1~d29 汎用レジスタ 1bit
d30 ゼロフラグ 1bit
d31 キャリーフラグ 1bit
a0~a5 汎用アドレスレジスタ 16bit
a6 プロシージャレジスタ 16bit
a7 プログラムカウンタ 16bit

インストラクション

オペランドにレジスタ3つを取ると冗長になりそうなので、2つとします。
レジスタ指定は最大32本×2なので、10bit必要です。
レジスタサイズの概念はありません(1bitですから・・)ので、サイズ指定用のbitは不要です。
ちょっと狭苦しいですが、オペコードは4bitで指定します。
というわけで、14bit固定長命令です。
アドレスレジスタや即値を扱う命令、オペランドが1つしかない命令には、何桁か空きビットが出来るので、ここに命令を拡張しています。

命令セット

以下のような命令セットにしました。
空いてるところにいろいろと詰め込んだのでマシンコードは汚くなってしまいました。
アドレッシングモード?なんですかそれ。

s = ソース
d = ディスティネーション
n = 数値
r = データレジスタ
a = アドレスレジスタ

■データ移動

移動命令ではフラグは変化しません。

LD [データレジスタ(dst)], [アドレスレジスタ(src)]
アドレスレジスタが示す先のメモリから1bitロード
0001 00 ddddd sss
※LD D0, An は何も起こらない

ST [アドレスレジスタ(dst)], [データレジスタ(src)]
アドレスレジスタが示す先のメモリへ1bitストア
0001 01 sssss ddd

LDI [データレジスタ(dst)], [即値(0 or 1)]
1bitの即値をロード
0001 10 ddddd n 00
※LDI D0, n は何も起こらない

MVA [アドレスレジスタ(dst)], [アドレスレジスタ(src)]
アドレスレジスタ間移動
0001 11 00 ddd sss

LDAR [アドレスレジスタ(dst)], [ディスプレースメント相対(7bit)]
メモリからアドレス値をロード(プログラムカウンタ相対 -64bit ~ +63bit)
0010 ddd nnnnnnn

MV [データレジスタ(dst)], [データレジスタ(src)]
データレジスタ間移動
0011 ddddd sssss
※MV D0, Dnn は何も起こらない

■演算

すべての演算命令は、実行直後にフラグが変化します。(アドレスレジスタに対する命令を除く)

ADC [データレジスタ(dst)], [データレジスタ(src)]
キャリー付き加算(dst = dst + src + d31)
0100 ddddd sssss

SBC [データレジスタ(dst)], [データレジスタ(src)]
ボロー付き減算(dst = dst - src - d31)
0101 ddddd sssss

INC [データレジスタ]
データレジスタをインクリメント
0001 11 01 0 rrrrr

DEC [データレジスタ]
データレジスタをデクリメント
0001 11 10 0 rrrrr

SFT [データレジスタ]
0110 0000 0 rrrrr
データレジスタをシフト
実際にはデータレジスタの値がd31(キャリーフラグ)に入り、データレジスタはゼロクリアされる

ROT [データレジスタ]
0110 0001 0 rrrrr
データレジスタをローテイト
実際にはデータレジスタとd31(キャリーフラグ)の値を交換する

INCA [アドレスレジスタ]
アドレスレジスタをインクリメント(フラグ変化無し)
0110 0010 000 aaa

DECA [アドレスレジスタ]
アドレスレジスタをデクリメント(フラグ変化無し)
0110 0011 000 aaa

CMP [データレジスタ(dst)], [データレジスタ(src)]
dst - src の結果をフラグにのみ反映
0111 ddddd sssss

AND [データレジスタ(dst)], [データレジスタ(src)]
論理積をとる
1000 ddddd sssss

OR [データレジスタ(dst)], [データレジスタ(src)]
論理和をとる
1001 ddddd sssss

XOR [データレジスタ(dst)], [データレジスタ(src)]
排他的論理和をとる
1010 ddddd sssss

■分岐

B [ディスプレースメント相対(10bit)]
無条件分岐(-512~+511)
1011 nnnnnnnnnn

BZ [ディスプレースメント相対(10bit)]
d30(ゼロフラグ)が1のとき分岐(-512~+511)
1100 nnnnnnnnnn

BNZ [ディスプレースメント相対(10bit)]
d30(ゼロフラグ)が0のとき分岐(-512~+511)
1101 nnnnnnnnnn

BC [ディスプレースメント相対(10bit)]
d31(キャリーフラグ)が1のとき分岐(-512~+511)
1110 nnnnnnnnnn

BNC [ディスプレースメント相対(10bit)]
d31(キャリーフラグ)が0のとき分岐(-511~+512)
1111 nnnnnnnnnn

■ジャンプ/コール

J [アドレスレジスタ]
無条件ジャンプ(アドレスレジスタの内容をa7にコピー)
アセンブラが MVA A7, Ann に展開

JZ [アドレスレジスタ]
d30(ゼロフラグ)が1のときジャンプ
0110 0101 000 aaa

JNZ [アドレスレジスタ]
d30(ゼロフラグ)が0のときジャンプ
0110 0101 001 aaa

JC [アドレスレジスタ]
d31(キャリーフラグ)が1のときジャンプ
0110 0101 010 aaa

JNC [アドレスレジスタ]
d31(キャリーフラグ)が0のときジャンプ
0110 0101 011 aaa

CALL [アドレスレジスタ]
サブルーチンコール(a7レジスタの内容をa6レジスタにコピー後、アドレスレジスタの内容をa7にコピー)
0110 0110 000 aaa

RET [アドレスレジスタ]
サブルーチンからの復帰(a6レジスタの内容をa7レジスタにコピー)
アセンブラが MVA A7, A6 に展開

CZ [アドレスレジスタ]
d30(ゼロフラグ)が1のときコール
0110 0111 000 aaa

CNZ [アドレスレジスタ]
d30(ゼロフラグ)が0のときコール
0110 0111 001 aaa

CC [アドレスレジスタ]
d31(キャリーフラグ)が1のときコール
0110 0111 010 aaa

CNC [アドレスレジスタ]
d31(キャリーフラグ)が0のときコール
0110 0111 011 aaa

RZ [アドレスレジスタ]
d30(ゼロフラグ)が1のときリターン
0110 1000 000 aaa

RNZ [アドレスレジスタ]
d30(ゼロフラグ)が0のときリターン
0110 1000 001 aaa

RC [アドレスレジスタ]
d31(キャリーフラグ)が1のときリターン
0110 1000 010 aaa

RNC [アドレスレジスタ]
d31(キャリーフラグ)が0のときリターン
0110 1000 011 aaa

■その他

ATOD [データレジスタ(dst)], [アドレスレジスタ(src)]
アドレスレジスタの最下位ビットをデータレジスタに格納し、アドレスレジスタを1bit右ローテイトする
サブルーチン用のスタックを作るときに使用
0000 00 ddddd sss

DTOA [アドレスレジスタ(dst)], [データレジスタ(src)]
データレジスタの値をアドレスレジスタの最下位に格納し、アドレスレジスタを1bit左ローテイトする
サブルーチン用のスタックから復帰するときに使用
0000 01 sssss ddd

以上のような命令セットになりました。


加減算命令はキャリーを伴う計算だけです。
キャリーなし加算・減算は、レジスタが1bitだとほとんど使う機会がないので存在しません。
計算前に mv d31, d0 などとして、キャリーフラグをリセット後に行えば、キャリーなしで計算できます。(6502と同じです)
桁上がり・桁下がりを含む計算は、このキャリーフラグを介して数珠つなぎに計算します。

無条件ジャンプと無条件リターンはアドレスレジスタ間転送のシンタックスシュガーです。

ループ用カウンタはレジスタ1本ではほぼ使い物になりません。(1bitレジスタでは最大2回ループして終わりです)
レジスタ1本をカウンタとして使用したループは以下のような感じです。

  ldi d1, 0
loop:
(処理)
  dec d1
  bnz loop

0からスタートし、デクリメントで1になり、もう一回デクリメントすると0になってゼロフラグが立っておしまいです。
これ以上のループを実現するには、複数のレジスタを束ね、キャリーフラグを介して桁上がり・下がりの計算をします。
16回のループをさせたい場合、レジスタ4本を束ねて

  ldi d1, 0
  mv d2, d1
  mv d3, d1
  mv d4, d1 ; レジスタ1~4を使って、4bitの整数(16)を用意

loop:
  (処理)
  dec d1 ; 1桁目を-1(桁の繰り下がりがキャリーフラグに入る)
  sbc d2, d0 ; 2桁目からキャリーフラグを引く(ゼロレジスタを引くところがミソ。d2 - 0 - d31 となる)
  sbc d3, d0 ; 3桁目からキャリーフラグを引く
  sbc d4, d0 ; 4桁目からキャリーフラグを引く

  cmp d1, d0 ; d1が0かどうかチェック
  bnz loop
  cmp d2, d0
  bnz loop
  cmp d3, d0
  bnz loop
  cmp d4, d0
  bnz loop ; 1~4桁目のすべてが0になるまでループ続行

という具合です。
また、プロシージャレジスタ1つでは、サブルーチンの中でさらにサブルーチンを呼び出した際に戻れなくなるので、戻り先アドレスを保持するスタックも自前で用意しなくてはなりません。(RISCのアセンブリをやったことがある人は分かると思います)
このスタックは16bit単位で必要なので、ATOD命令とST命令を組み合わせ16回メモリを叩いてスタックに積む必要があります。
当然CALL先に戻るときもDTOA命令とLD命令で16回メモリを叩いてスタックから読み込みます。
a6(プロシージャレジスタ)の値を、a5をスタックポインタとしてメモリにPUSHするには下のようにします。

  atod d1, a6
  atod d2, a6
  atod d3, a6
  atod d4, a6
  atod d5, a6
  atod d6, a6
  atod d7, a6
  atod d8, a6
  atod d9, a6
  atod d10, a6
  atod d11, a6
  atod d12, a6
  atod d13, a6
  atod d14, a6
  atod d15, a6
  atod d16, a6

  st a5, d1
  inca a5
  st a5, d2
  inca a5
  st a5, d3
  inca a5
  st a5, d4
  inca a5
  st a5, d5
  inca a5
  st a5, d6
  inca a5
  st a5, d7
  inca a5
  st a5, d8
  inca a5
  st a5, d9
  inca a5
  st a5, d10
  inca a5
  st a5, d11
  inca a5
  st a5, d12
  inca a5
  st a5, d13
  inca a5
  st a5, d14
  inca a5
  st a5, d15
  inca a5
  st a5, d16

わずか2ByteをスタックにPUSHするだけで、この行数です。
これだとコーディングは単純ですが、データレジスタが16本破壊されてしまうので、

  ldi d1, 0
  mv d2, d1
  mv d3, d1
  mv d4, d1

loop:
  atod d5, a6
  st a5, d5
  inca a5
  
  dec d1
  sbc d2, d0
  sbc d3, d0
  sbc d4, d0

  cmp d1, d0
  bnz loop
  cmp d2, d0
  bnz loop
  cmp d3, d0
  bnz loop
  cmp d4, d0
  bnz loop

とすればレジスタ5本ですみます。
以下は簡単な足し算の例です。
36+57の結果を1bitのリトルエンディアンでメモリのF000番地に格納します。

  ldar a0, work ; アドレスレジスタにアドレス値を読み込む
  b thrw ; ここから下にアドレス値があるのでスキップする
work:
  dw 0f000h ; ワークエリアのアドレス値

thrw:
  ldi d1, 0
  ldi d2, 0
  ldi d3, 1
  ldi d4, 0
  ldi d5, 0
  ldi d6, 1
  ldi d7, 0 ; レジスタd1~d7に36を代入

  ldi d11, 1
  ldi d12, 0
  ldi d13, 0
  ldi d14, 1
  ldi d15, 1
  ldi d16, 1 ; レジスタd11~d16に57を代入

  mv d31, d0 ; キャリーフラグをリセット
  adc d1, d11 ; 1桁目同士を加算(桁あふれはキャリーフラグへ)
  adc d2, d12 ; 2桁目同士とキャリーを加算(桁あふれはキャリーフラグへ)
  adc d3, d13 ; 3桁目同士とキャリーを加算(桁あふれはキャリーフラグへ)
  adc d4, d14 ; 4桁目同士とキャリーを加算(桁あふれはキャリーフラグへ)
  adc d5, d15 ; 5桁目同士とキャリーを加算(桁あふれはキャリーフラグへ)
  adc d6, d16 ; 6桁目同士とキャリーを加算(桁あふれはキャリーフラグへ)
  adc d7, d0 ; 最後はキャリーフラグを足す(ゼロレジスタを足すところがミソ。d7 + 0 + d31 となる)

; この時点で答えは d1 ~ d7 のレジスタ列に入っている

  st a0, d1 ; 1桁目をストア
  inca a0 ; アドレスレジスタを+1
  st a0, d2 ; 2桁目をストア
  inca a0
  st a0, d3 ; 3桁目をストア
  inca a0
  st a0, d4 ; 4桁目をストア
  inca a0
  st a0, d5 ; 5桁目をストア
  inca a0
  st a0, d6 ; 6桁目をストア
  inca a0
  st a0, d7 ; 7桁目をストア

という具合です。
シフトとローテイトを組み合わせると、複数レジスタの列を2倍にしたり1/2にしたりできます。
データレジスタのシフト命令とローテイト命令は、1bitなので左右の区別がありません。
キャリーフラグに影響しない単純シフトや単純ローテイト命令も無意味なので存在しません。
以下はd1~d8 の8bit列を2倍・1/2にする例です。

  sft d1
  rot d2
  rot d3
  rot d4
  rot d5
  rot d6
  rot d7
  rot d8 ; 以上で2倍になる

  sft d8
  rot d7
  rot d6
  rot d5
  rot d4
  rot d3
  rot d2
  rot d1 ; 以上で1/2になる

つまり、CPUの使い方と考え方は8bit CPUや16bit CPUとなんら変わりないということです。
足りない桁はキャリーフラグを介して複数のレジスタを繋げて考えればいいわけです。

それにしてもコーディングはものすごい手間です。
趣味ならまだしも、これを仕事でやろうと思うと頭痛と吐き気がしてきますね・・。


2011/1/10 更新




[ 戻る ]