CPU


1bit CPU はできるのか?



CPUの性能は一般的に、「bit数」「クロック数」で決められます。
RISCでは「パイプライン段数」「パイプラインハザード回避率」、VLIWやEPICでは「ユニット数」「コンパイラ性能」なども性能を決める要素になります。(RISCやCISCという呼び方もすでに前世紀のもので、最近ではそれぞれの特徴を併せ持つCPUが増えてきたこともあり、これらを区別することは無意味とされるようです。VLIWやEPICは区別されているようですが)
最近では「コア数」「キャッシュ容量」「キャッシュメモリの速度」「空いたコアをどこまで有効活用できるか」などもCPUの性能を大きく左右する要因になっています。

さて、もっとも基本的なbit数ですが、現在のWindowsマシンやMacでよく使われているCPUは64bitです。
世界初のマイクロプロセッサと言われる、Intel 4004は4bit。
すべてのマイクロプロセッサはここから始まりました。(マイクロプロセッサよりも昔に発明されたミニコンやそれよりも大きな演算装置は36bitや22bit、18bitなどから始まっています)
そしてマイコンブームを支えたZilog Z80やMotorola 6809、MOS Technology 6502などが8bit。
マイコンからパソコンへ呼び名が変わり、それが趣味のものから実用的な道具として普及し始めた頃のIntel 8086やMotorola 68000が16bit。
少し前までのWindowsマシンやMacなどの主流CPUだったIntel Pentium(初代)やIBM PowerPC(初代)が32bit。
これから先、128bitや256bitといったCPUが主流になってくることでしょう。

つまり、実用的なプロセッサは4bit以上であるということが分かります。
さて、ではそれよりも少ないbit数のプロセッサがもしあったとしたら、それは使い物になるのでしょうか。
例えば1bitのCPUを作ったとして、はたしてそれは実用として通用するのでしょうか。

その答えは、「CPUの『何が1bitなのか』によって使い物になることもあるし、使い物にならないこともある」です。

実はCPUのbit数というのは、大まかに以下の5項目あります。

1. ALU長(何bitまでの数値を一度に計算できるか)
2. レジスタ長(何bitまでの数値を一時的に保管しておくことができるか)
3. データバス長(メモリに対して何bitまでの数値を一度に読み書きできるか)
4. アドレスバス長(8bitを一単位として、何単位までのメモリをリニアに扱えるか)
5. 命令長(一つの命令が何bitで構成されているか(演算性能とは関係ないのでここでは扱いません))

このうちどれが1bitかによって、そのCPUが使えるか使えないかが分かれてきます。
例えば、8bit CPUとして有名なZ80ですが、それぞれの長さがどうなっているかというと、

1. 4bit
2. 8bit(命令によって16bit)
3. 8bit
4. 16bit

です。
同じ8bit CPUである6502は

1. 8bit
2. 8bit
3. 8bit
4. 16bit

さらに同じ8bit CPUである6809は

1. 8bit
2. 8bit(命令によって16bit)
3. 8bit
4. 16bit

同じ8bit CPUでも随分と違いがあることが分かります。
16bit CPUとして有名な8086は、

1. 16bit
2. 16bit(命令によって8bit/32bit)
3. 16bit
4. 20bit

同じ16bit CPUの68000はというと、

1. 32bit
2. 32bit(8bit/16bit/32bit 可変長)
3. 16bit
4. 24bit(配線は23本で常に偶数番地を16bit単位でアクセスし、上位バイトと下位バイトはCPU内部で判別している)

32bit CPUであるPentium(初代)は、

1. 32bit
2. 32bit(命令によって8bit/16bit)
3. 64bit
4. 32bit

すなわち、「そのCPUが何bitか」というのは、この4項目のうちどこを見ているかで変わるということです。
ALU長を見ればZ80は4bit CPUであり、レジスタ長を見れば68000は32bit CPU、データバス長を見ればPentium(初代)は64bit CPUです。
Z80は8bitの演算をするのに内部で2回ループをしていて、68000は32bitの値をレジスタに取り込むのに2回メモリを叩いています。

しかしどうやら長い歴史の中で、CPUのbit長はALU長でもなくアドレスバス長でもなく「レジスタ長とデータバス長のうち短い方」である、という常識が生まれたようです。
というわけで、今回は「データバス長が1bit」のCPUを「1bit CPU」と呼ぶことにします。
つまり「メモリに対して1度に読み書きできるデータの単位が1bit」のCPUということです。
1bitで表現できる数値は0か1のどちらかですので、それより大きい数値は複数回メモリにアクセスすることになります。

メモリにはアドレス番号があり、4bit CPUでは4bit(ニブル)単位、8bitのCPUでは8bit(バイト)単位でアドレス番号が割り振られています。
この考え方を延長すれば、16bit CPUが発明された時点でアドレス番号は16bit(ワード)単位に、32bit CPUが発明された時点でアドレス番号は32bit(ロングワード/ダブルワード)単位になるのが自然といえます。(グラフィックボードなどのGPUは実際に32bit以上を単位とするものが多いです)
しかしCPUのアドレス番号の単位は8bitに据え置きにされ、64bitの時代になった今でもそのままです。
16bit CPUが開発された当時、メモリが非常に高価だったので可能な限り有効活用しようとされたのかもしれません。

さて。
データバスが1bitとなると、今までのこの常識を見直さないといけません。
1bit CPUでは、データは1bit単位でアクセスされるので、アドレス番号も1bit単位で割り振られることになります。
ということは、当然アドレスバス長はものすごく長くないと実用的とは言えません。
例えばアドレスバス長が16bitだった場合、8bit CPUなら64KiBまで扱えるのに、1bit CPUではその1/8、つまり8KiBまでしかアクセスできないことになります。
32bitでやっと512MiBです。
現在の32bit CPUがアクセス出来る範囲である、4GiBをカバーしようと思ったら35bitが必要です。
そもそも1bit単位でアドレス管理ができるメモリコントローラが必要です。

ALU長は1bitでもまったく問題ありません。
大きな数値を計算するときは、延々ループさせればよいだけです。(もちろん遅いですが)

あとはレジスタ長です。
レジスタ長が1bitではさすがに使い物にならないでしょう。
C言語やJavaに例えるなら、1つの変数に格納できる数値が0か1のどちらかしかない、という状況です。
メモリ上にワークを用意して、キャリーフラグやボローフラグを挟みながらプログラムでループさせれば不可能でもないですが、私はそんなコーディングしたくありません。
さすがにレジスタ長は今の時代32bit程度は欲しいところです。

畢竟、実用的な1bit CPUとはこんな感じでしょうか。

1. ALU長 1bit
2. レジスタ長 32bit
3. データバス長 1bit
4. アドレスバス長 32bit

アドレスバス長35bitで4GiBの空間にアクセスできますが、そうするとデータレジスタ32bit、アドレスレジスタ35bitとなりますので、データレジスタとアドレスレジスタを分けて実装する必要があります。
なんか昔のCPUみたいで使いづらそうなので、512MiBの壁がありますが、同じ32bit長にしてみました。

つまり、レジスタに値をロードするのに32回メモリを読みに行き、演算するのに32回ループし、演算結果をストアするのに32回メモリを叩き・・というCPUになります。
なんだか、Z80と68000の悪いところを寄せ集めてそれを極めたようなCPUです。

ものすごく遅そうですが、今の技術で作ればZ80などよりはずっと速いCPUになりそうです。

ちなみにALUだけが1bitのCPU(レジスタとデータバスが32~64bitあるので、正確には1bit CPUではない)や、1bit CPUを構成するための主要ロジックを提供するチップと言えるMotorolaのMC14500 ICU(これ単体ではCPUとは呼べそうにない)などは実在しています。


しかし、これだけでは面白くないのでレジスタ長までも1bitにして、少し本気でISAを設計してみます。
使いやすさや実用性などは考えず、プログラムが書けてそれが動く1bit CPUを目指します。

基本構成

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

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

プログラムカウンタは0000000000000000番地からスタートし、1111111111111111番地まで1bitずつインクリメントしていきます。
命令フェッチも1bitずつですが、命令長は14bit固定なので、その長さまでループしてフェッチを繰り返し、14bit分のインストラクション用バッファに溜まったところでデコードします。
命令が実行される時点で、プログラムカウンタは次の1bit目を差しています。
つまり、プログラマーからはプログラムカウンタは14bitずつ進んでいるように見えます。
なのでジャンプ・分岐先アドレスを間違えると即暴走ですが、これは一般的なCPUでも同じことが言えるので問題視しないことにします。
I/Oはありませんが、メモリマップドI/OにすればLD/ST命令で1bitの入出力が可能です。
ロード・ストアアーキテクチャですが、パイプラインはありません。

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=64Kibit、すなわち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


インストラクション

オペランドは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(キャリーフラグ)に入り、データレジスタはゼロクリアされる
※1bitの世界では左右の区別が無意味なのでシフト命令は1つしかない

ROT [データレジスタ]
0110 0001 0 rrrrr
データレジスタをローテイト
実際にはデータレジスタとd31(キャリーフラグ)の値を交換する
※1bitの世界では左右の区別が無意味なのでローテイト命令は1つしかない

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

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

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

AND [データレジスタ(dst)], [データレジスタ(src)]
論理積をとる(結果が0になったときのみゼロフラグが立つ)
1000 ddddd sssss

OR [データレジスタ(dst)], [データレジスタ(src)]
論理和をとる(結果が0になったときのみゼロフラグが立つ)
1001 ddddd sssss

XOR [データレジスタ(dst)], [データレジスタ(src)]
排他的論理和をとる(結果が0になったときのみゼロフラグが立つ)
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のとき分岐(-512~+511)
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と同じです)
桁上がり・桁下がりを含む計算は、このキャリーフラグを介して数珠つなぎに計算します。
無条件ジャンプと無条件リターンはアドレスレジスタ間転送のシンタックスシュガーです。

では早速、このW10に「1 + 1」を計算させるプログラムを書いてみます。

  ldi d1, 1 ; 1を用意する
  ldi d2, 1 ; もう1つ、1を用意する

  mv d31, d0 ; キャリーフラグをリセットしておく
  adc d2, d1 ; 1 + 1 を計算する(当然桁あふれを起こして結果は0となり、あふれた桁がキャリーフラグへ入る)
  mv d3, d31 ; 1桁追加して、そこにキャリーフラグを代入する

; 答えは d2, d3 のレジスタ2本に入っている

1 + 1の結果である2ですら、レジスタ1本に格納できません。
2は2進数で「1 0」ですから、レジスタ2本に分けて答えを格納することになります。

続いてループ処理です。
ループ用カウンタはレジスタ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本ですみます。

以下は2桁の足し算を行うプログラムです。
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桁目をストア

という具合です。

以下は0~255の数値を2倍にしたり半分にしたりするプログラムです。
シフト・ローテイトを使うと、複数レジスタの列を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になる

以上、W10のプログラムの書き方でした。
1bit CPUの使い方と考え方は、8bit CPUや16bit CPUとなんら変わりないということが分かります。
足りない桁はキャリーフラグを介して複数のレジスタを繋げて考えればいいわけです。

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


2019/12/14 更新


[ 戻る ]