W.I.S. Laboratory
menu-bar

CPU


Intel 8080 のアーキテクチャとISAについて

ZilogのZ80は8bitCPUの中であまりにも有名だが、この元になったのがIntelの8080というCPUだ。
Z80は8080のISAにCB、EDのプリフィックスでエスケープして2バイト命令を、DD、FDプリフィックスで3バイト命令、そしてDD-CBやFD-CBでエスケープして4バイト命令を追加し、さらに難解だった(らしい)Intelのニモニックを一掃して初心者に覚えやすい(らしい)ニモニックに変更している。
追加された命令は、そのほとんどが8080で「こんなときに、こんな命令があったらなぁ」というものであり、確かに便利な8bitCPUになっている。
その反面、8080が本来持っていた哲学に反した命令拡張、ニモニックが変更されたことでアドレッシングモードがはっきりしない、プリフィックスによる命令拡張といった点で、明らかな“継ぎ足し感”を醸し出している。
そのZ80のアーキテクチャやISAについてはこちら

ここではZ80へと進化拡張をされる前身である8080のアーキテクチャとISAについて書いてみたいと思う。
5V単一電源で動作しないとか、DRAMのリフレッシュ機能がないとか、いろいろと問題児ではあるもののそういったことはまた今度。
8080はZ80と同じくアキュムレータマシンなので、演算はすべてアキュムレータ(A)と何かの間で行われる。
Z80にあるようなインデックスレジスタや裏レジスタは存在しない。

一応、8080のアドレッシングモードを一覧しておく。
8080のアドレッシングモードは非常に少ない。

1. レジスタ (レジスタ直接指定。8bitと16bitがある。Mもこれに含む)
2. イミディエイト (即値。8bitと16bitがある)
3. レジスタインダイレクト (レジスタペアBCとDEによる間接指定。HLはこれに含まない。演算命令では使えない)
4. エクステンド (2バイトでのメモリアドレス直接指定。指定先のサイズは8bitと16bitがある。演算命令では使えない)
8080には、このCPUの前身である8008の設計思想がやや含まれている。
この8008はメモリへのアクセスはMレジスタ(HLレジスタペアによる間接指定)でしか行えなかった。
他のレジスタ間接やアドレス直接指定によるアクセスは、演算はもとよりロードもストアも一切できない仕様だったのだ。
「HLレジスタをポインタとして指し示す先のメモリは8bit汎用レジスタ“M”である」というのが8008の哲学であり、それはそのまま8080の哲学として受け継がれた。
すなわち8080は、アキュムレータを含めて汎用レジスタを8本(A,B,C,D,E,H,L,M)持つアーキテクチャだということが重要であり、「メモリと汎用レジスタに扱いの差異がない(同じアドレッシングモードが使える)」というのが、当時の他の8bitCPUには見られない8080最大の特徴といえるのではないかと思う。
この「HLレジスタが指し示す先のメモリ内容は8bitレジスタである」というIntelの哲学を理解しておかないとアドレッシングモードがよく分からなくなり、「メモリのロードやストアは、BCやDEでの間接指定は“STAX”や“LDAX”なのに、HLでの間接指定だけどうして“MOV”なの?」と頭がこんがらかってくる。(HLレジスタインダイレクトアドレッシングではなく、Mレジスタアドレッシングになるため。Z80のニモニック表記だと(HL)なので、このアドレッシングの違いがニモニック上から分からなくなってしまった)

ただ、私によく分からないのが8bitの即値を8bitレジスタへ格納するニモニックは“MOVE(MVI)”だが、16bitの即値を16bitレジスタへ格納するのは“LOAD(LXI)”であるという点だ。
設計時、「8bitの即値はレジスタへの『移動』だが、16bitの即値はメモリ上に置かれた8bitの値2つをそれぞれ2つのレジスタに『積む』動作である」という考えがあったのかもしれない。

そしてすべての命令は、即値とアドレス値以外のオペランド込みで1バイトに収められている。
8080の命令セットにプリフィックスは一切存在せず、すべてのオペコードは1バイトであり、メモリが高価だった時代、徹底的に命令密度を上げようとしたIntelの根性が垣間見れる。
全82命令、ニモニックは2~4文字をとり、オペランドは0~2つを記述する。
第1オペランドが1種類に限られるものは、ニモニック内にオペランドも記述する。(“ANA”や“LHLD”など)
第2オペランドも限られるものは、オペランドのみのニモニックとなる。(“PCHL”や“SPHL”など)

アドレッシングモードもニモニック内に記述する。
イミディエイトアドレッシングは最後に“I”が付く。(“MVI”や“ANI”など)
16bitのレジスタアドレッシングとレジスタインダイレクトアドレッシングは最後に“X”が付く。(“LDAX”や“INX”など。ただし、DAD/PUSH/POPは例外)
16bitのエクステンドアドレッシングは最後に“D”が付く。(“LHLD”や“SHLD”など。ただし、DADは例外)
16bitのイミディエイトアドレッシングは最後に“XI”が付く。(“LXI”のみ)

これらの仕様によりZ80のような「書けそうで書けないコード」がほとんど存在しない。
全ニモニックを一覧しておく。

MOV r,r / MVI r,n / LXI rr,nn / LDA nn / STA nn / LHLD nn / SHLD nn / LDAX B / LDAX D / STAX B / STAX D / SPHL / XCHG / ADD r / ADI n / ADC r / ACI n / SUB r / SUI n / SBB r / SBI n / CMP r / CPI n / INR r / DCR r / INX rr / DCX rr / DAD rr / DAA / ANA r / ANI n / ORA r / ORI n / XRA r / XRI n / CMA / RLC / RRC / RAL / RAR / JMP nn / JNZ nn / JZ nn / JNC nn / JC nn / JPO nn / JPE nn / JP nn / JM nn / PCHL / CALL nn / CNZ nn / CZ nn / CNC nn / CC nn / CPO nn / CPE nn / CP nn / CM nn / RST p / RET / RNZ / RZ / RNC / RC / RPO / RPE / RP / RM / PUSH rr / PUSH PSW / POP rr / POP PSW / XTHL / STC / CMC / IN n / OUT n / NOP / HLT / DI / EI

Z80では当たり前とも言えることが意外に出来ない面があり、逆を言えばZ80はそれだけ「痒いところに手が届くようにしたCPU」であるとも言える。
例えば16bitの演算は、INC/DECとキャリーを伴わない足し算しかできない。(HLレジスタと他の16bitレジスタとのDAD命令のみであり、ADCやSBC命令は存在しない)
本来8bitCPUなのだから16bitの足し算が出来るだけでもありがたいと言うべき、というか「このCPUの16bitレジスタはアドレスを示すものであってデータではないのだ」という思想が少し見える。
16bitの引き算をしたいなら、2の補数を使ってがんばるしかない。(例えば HL - DE がしたいなら DとEそれぞれをAにMOV、XRA Aしてから戻し、INX Dした後にDAD Dすれば良い)
そしてBCやDEをポインタとしての対メモリ演算はできない。(後述)
他にも、ローテート出来るのはアキュムレータのみ、シフト命令は存在しない、ビット操作命令は何一つない、Bレジスタをカウンタとしたループ機能がない、相対ジャンプができない、などの特徴がある。
Cレジスタを介したIN/OUT、ブロック転送・ブロック入出力、インデックスレジスタ、裏レジスタ関連はさすがに無くても違和感はないが。

そしてかなり重要な点が、アキュムレータ-メモリ間演算は一切出来ない仕様になっているということだ。
実は、8080は即値を除いてアキュムレータ-汎用レジスタ間演算しかできない。
8080の汎用レジスタとは、B,C,D,E,H,L,Mの7種類であり、演算はアキュムレータとこれらのうちどれかでしか行えない。
つまり、アキュムレータ-“Mレジスタ”間の演算(ADD M や SBB M など)が事実上の対メモリ演算ということになるが、設計上の哲学ではこれは「レジスタ間演算」である、ということを理解しておかないと、頭がさらにこんがらかる原因になる。
何度も書くが、「HLレジスタが示す先のメモリ内容は8bit汎用レジスタである」というのが8080の哲学なのだ。
つまり8080の演算は「レジスタアドレッシング」と「イミディエイトアドレッシング」でしか使えない。

8080は、6800や6502とはまったく設計思想の異なるCPUであり、当時にしてはレジスタ本数がやたら多いのは、この「対メモリ演算ができない」という設計から来たものだと思える。

例えば6502はレジスタがアキュムレータ1本しかない代わりに、メモリのゼロページ(先頭256バイト)を汎用レジスタのようにアクセスできるので、256本の汎用レジスタ(または128本の16bitアドレスレジスタ)の“代わり”として扱える。
さらに命令長が1バイト長くなってもいいなら、メモリ全域をアドレス直接指定で演算対象にできる。

ひきかえ、8080はアキュムレータと6本の汎用レジスタを持ち、汎用レジスタは3本のアドレスレジスタとしても使用でき、さらにHLの値を増減することでメモリ全域を65536本分の汎用レジスタ“そのもの”として扱える。
しかし演算はアキュムレータ-レジスタ間のみであり、アドレス直接指定で“メモリ”を演算対象にはできない。

ちなみにスタックポインタSPはやや特殊な16bitのアドレスレジスタだが、8bitに分割できずPUSH/POP/CALL/RETなどで自動的に値が増減し、SPが関わる転送命令で16bitの値を一命令で転送できる(SPHL/PUSH/POP)以外は、BCやDEとほぼ同等の扱いとなっている。

一見覚えにくくて分かりにくいとも思える8080のニモニックは、こうした設計の背景にある哲学が見えてくると、美しくそしてスッキリと分かりやすいものになる。
同時にZ80のニモニックは確かに覚えやすいが、CPUのアーキテクチャや設計者の哲学は返って分かりにくいものに思えてくる。

8080と並んで有名な6502と比較した場合のコードサイズについて。
8080はレジスタ間演算命令のコードをオペランド込みで1バイトに収めている。
6502もオペコードは1バイトだが、演算対象のオペランドが常にメモリなので、例えゼロページを使ったとしても演算命令は必ず2バイトになる。
6502のゼロページと8080の汎用レジスタを同等のものと捉えた上で、あるプログラミングのケースを考えてみる。

例えば、

$10 → B
$11 → C
$12 → D
$13 → E
$14 → H
$15 → L

と対応させ、各々すでに初期値は与えられていて、現在のアキュムレータの値にこれら6本の値をすべて加算してくことを考える。

6502だとADC命令しかないので、演算のたびにキャリーリセットが必要となり、さらにADC命令のオペランドにゼロページのアドレスを指定する1バイトが必要となる。

clc ; 18
adc $10 ; 65 00
clc ; 18
adc $11 ; 65 01
clc ; 18
adc $12 ; 65 02
clc ; 18
adc $13 ; 65 03
clc ; 18
adc $14 ; 65 04
clc ; 18
adc $15 ; 65 05

と、全18バイトになる。

8080だと単にADD命令が続くだけで済む。

add b ; 80
add c ; 81
add d ; 82
add e ; 83
add h ; 84
add l ; 85

と、全6バイトに収まり、6502の3分の1というサイズに収まってしまう。

同じように6502のゼロページと8080の汎用レジスタを同等のものとして考え、1番目と2番目のレジスタを加算して3番目のレジスタに収めることを考える。

6502だと、

lda $10 ; a5 00
clc ; 18
adc $11 ; 65 01
sta $12 ; 85 02

と、7バイトになる。

8080だと、

mov a,b ; 78
add c ; 81
mov d,a ; 57

と3バイトになり、これも半分未満のサイズに収まる。

次はメモリ内容を操作する場合。
例えば、あるアドレスにあるメモリの内容と、その次の番地にあるメモリの内容を足して、その結果をさらに次の番地へ格納する場合を考えてみる。

6502の場合はゼロページアドレッシングが使えるので、メモリの対象がゼロページ内かどうかでコードサイズが変わる。

ゼロページ内の場合、

lda $10 ; a5 00
clc ; 18
adc $11 ; 65 01
sta $12 ; 85 02

と、7バイトに収まるが、

ゼロページ外の場合、例えば0xa000番地となると、どんなアドレッシングモードを使うかによって分かれてくる。
アブソリュートアドレッシングを使うと

lda $a000 ; ad 00 a0
clc ; 18
adc $a001 ; 6d 01 a0
sta $a002 ; 8d 02 a0

と、10バイトになる。

インダイレクト・インデックストYアドレッシングを使うと、

lda #00h ; a9 00
sta $10 ; 85 00
lda #a0h ; a9 a0
sta $11 ; 85 01
ldy #0 ; a0 00
lda ($10),y ; b1 00
iny ; c8
clc ; 18
adc ($10),y ; 71 00
iny ; c8
sta ($10),y ; 91 00

と、19バイトに膨れ上がる。

8080の場合はどのアドレスであっても、

lxi h,0a000h ; 21 00 a0
mov a,m ; 7e
inx h ; 23
add m ; 86
inx h ; 23
mov m,a ; 77

と、8バイトになる。

ちょっとおまけ。Z80だと、

ld hl,0a000h
ld a,(hl)
inc hl
add a,(hl)
inc hl
ld (hl),a

コードサイズはもちろん8080と同じく8バイト。

優劣を論ずる気はないが、平均すると8080の方がややコードサイズが小さく、メモリの節約になると思える。

6502や6800、6809などと比較すると、8080のアドレッシングモードは非常に少ない(そのためか8080やZ80で何年コードを書いても“アドレッシングモード”という概念があまりよく分からないままコーディング技術だけが向上していくような気がする)が、HLペアレジスタ間接アドレッシング=Mレジスタ直接アドレッシングという独特の設計と、当時としては多い汎用レジスタとの組み合わせによって、メモリのブロック転送や配列に対する一括計算処理などはずっとコーディングがしやすかったと思う。
8080は「豊富なアドレッシングモードを用意することだけがCPUにとって必ずしも良いわけではない」という見本のようなCPUだったと言える気がする。
ただその代わり、この独特の設計哲学を理解しないままコーディングしていると、命令とオペランドの組み合わせに存在するものとしないものは丸暗記するしかないという羽目に陥ってしまう。
Z80ではニモニックが変わってしまってそれがさらに顕著になってしまったため、実際当時は丸暗記していた人も多かったように思う。
またアセンブラを使わず、16進の命令コードまでも暗記してコーディングする人もいたが、これはアドレッシングモードが非常に少ないCPUだからこそ出来たということでもある。

[ 戻る ]
saluteweb