Z80エミュレーター開発記 第3回:命令セットとプレフィックスデコードのカラクリ

Z80エミュレーター開発記 第3回:命令セットとプレフィックスデコードのカラクリ

はじめに

前回はZ80のレジスタ構造——裏レジスタのフリップフロップ実装と、フラグレジスタの全8ビットの挙動を掘り下げた。第3回では、それらのレジスタを操作する「命令セット」のエンコーディングに踏み込む。

Z80の命令セットは一見すると巨大で混沌としている。公式158命令、プレフィックス付きを展開すると700超のオペコード。しかしその内部には、8080のオペコード設計を土台にした明確なパターンが存在する。このパターンを理解すると、エミュレーターのデコーダーを「700個のswitch-case」ではなく「パターン分解+テーブル参照」で書けるようになる。

前回の記事

1. オペコードの構造:8ビットを3フィールドに分解する

Z80(および8080)のオペコードの1バイトは、以下の3つのフィールドに分解できる。

オペコード 1バイト: [x x y y y z z z]
                     ├─┤ ├───┤ ├───┤
                      x    y     z
                    (2bit)(3bit)(3bit)

さらに y は p(2bit) と q(1bit) に分解できる:
  y = [p p q]

例えば LD B, C(オペコード: 0x41 = 01000001)は以下のように分解される。

0x41 = 0b01_000_001
       x=1  y=0  z=1
       → x=1 のとき: LD r[y], r[z]
       → r[0]=B, r[1]=C
       → LD B, C

レジスタの番号付けは8080から引き継がれたもので、以下の対応がある。

番号8ビットレジスタ16ビットペア (p)
0BBC
1C—
2DDE
3E—
4HHL
5L—
6(HL) ※メモリ間接SP
7AAF

この分解を使うと、x=0/1/2/3の4つのブロックに命令が整理できる。

x=0: 各種制御命令(NOP, LD 16bit, INC/DEC, JR, DAA, CPL, SCF, CCF 等)

x=1: 8ビットLD命令群。LD r[y], r[z] — 全64通りのレジスタ間転送。ただし LD (HL), (HL)(y=6, z=6)だけは HALT 命令に割り当てられている。

x=2: ALU命令群。ALU[y] A, r[z] — ADD/ADC/SUB/SBC/AND/XOR/OR/CP の8演算 × 8レジスタ。

x=3: 各種制御命令(RET, POP, JP, CALL, RST, OUT, IN, EX, DI, EI 等)

この構造により、エミュレーターのデコーダーは以下のように書ける。

function decodeUnprefixed(opcode) {
  const x = (opcode >> 6) & 3;
  const y = (opcode >> 3) & 7;
  const z = opcode & 7;
  const p = y >> 1;
  const q = y & 1;

  switch (x) {
    case 0: /* 制御・LD imm・INC/DEC・JR */ break;
    case 1: /* LD r,r  or HALT */ break;
    case 2: /* ALU A,r */ break;
    case 3: /* RET・JP・CALL・RST・misc */ break;
  }
}

これで256個のオペコードが4×8×8の構造に収まる。700個のswitch-caseとは別次元の見通しの良さだ。

2. CBプレフィックス:ビット操作とシフト命令

CBプレフィックスは、8080にはなかったビット操作・シフト命令を格納する「第2のオペコードテーブル」を開く。

[CB] [オペコード]
      └→ CB後の1バイトを、同じ x/y/z 分解で解釈

CB空間では256個のオペコードが以下のように割り当てられている。

x=0 (CB 00〜CB 3F): シフト・ローテート命令

y命令動作
0RLC r[z]左回転(ビット7がキャリーとビット0に)
1RRC r[z]右回転(ビット0がキャリーとビット7に)
2RL r[z]キャリーを含めた左回転(9ビットローテート)
3RR r[z]キャリーを含めた右回転
4SLA r[z]算術左シフト(ビット0は0に)
5SRA r[z]算術右シフト(ビット7は保持)
6SLL r[z]非公式:論理左シフト(ビット0は1に)
7SRL r[z]論理右シフト(ビット7は0に)

y=6の「SLL」は公式ドキュメントに記載されていない非公式命令だ。SLAと同じ動作だがビット0がリセットではなくセットされるという点だけが異なる。回路的には、SLAのビット0入力線が0に固定されているべきところ、y=6のデコードでたまたま1が入力される——という実装上の偶然から生まれた。しかし「SLAの逆数的操作」として有用であり、実際に多くのソフトウェアで使われている。

  • x=1 (CB 40〜CB 7F):
    BIT y, r[z] — ビットテスト。指定ビットが0ならZフラグをセット。

  • x=2 (CB 80〜CB BF):
    RES y, r[z] — ビットリセット(0にする)。

  • x=3 (CB C0〜CB FF):
    SET y, r[z] — ビットセット(1にする)。

合計256命令で、すべてが同じx/y/zパターンで美しく並んでいる。エミュレーターの実装は驚くほどコンパクトになる。

function decodeCB(opcode) {
  const x = (opcode >> 6) & 3;
  const y = (opcode >> 3) & 7;
  const z = opcode & 7;
  const operand = readReg8(z); // r[z]を読む

  switch (x) {
    case 0: rot_shift[y](operand); break; // RLC〜SRL
    case 1: bit(y, operand); break;       // BIT
    case 2: res(y, operand); break;       // RES
    case 3: set(y, operand); break;       // SET
  }
}

3. EDプレフィックス:ブロック命令とI/O

EDプレフィックスは、8080のオペコード空間を壊さずに新命令を追加するための「第3のテーブル」を開く。

CB空間がビット操作に特化しているのに対し、ED空間はZ80の拡張機能をまとめた「雑多な」テーブルだ。

主な命令群:

  • ブロック転送: LDI, LDIR, LDD, LDDR
  • ブロック検索: CPI, CPIR, CPD, CPDR
  • ブロックI/O: INI, INIR, IND, INDR, OUTI, OTIR, OUTD, OTDR
  • 16ビットADC/SBC: ADC HL,rp / SBC HL,rp
  • 割り込みモード: IM 0, IM 1, IM 2
  • 特殊レジスタ: LD A,I / LD A,R / LD I,A / LD R,A
  • RETの変種: RETI(割り込みからの復帰), RETN(NMIからの復帰)

ED空間の特徴は「穴が多い」ことだ。ED 00〜ED 3Fは全て未使用(NOPとして動作)、ED 80〜ED BFも大部分が未使用で、ブロック命令は特定のオペコードにだけ存在する。ED C0〜ED FFも全て未使用。

これらの「穴」にあるオペコードを実行すると、Z80は何もせずに次の命令に進む(NOP相当だが、プレフィックス分のT-statesは消費する)。一部の穴は公式命令のミラー(複製)になっている。例えばED 4C, ED 54, ED 5CはすべてNEG命令と同じ動作をする。

エミュレーターでは、ED空間は穴が多いためテーブル方式が適している。

// ED命令テーブル(256エントリ、大部分はnull→NOP扱い)
const edTable = new Array(256).fill(null);
edTable[0x40] = () => inR(B);     // IN B,(C)
edTable[0x41] = () => outR(B);    // OUT (C),B
edTable[0x42] = () => sbcHL(BC);  // SBC HL,BC
edTable[0x43] = () => ldMemNN(BC);// LD (nn),BC
edTable[0x44] = () => neg();      // NEG
// ...
edTable[0xB0] = () => ldir();     // LDIR
edTable[0xB1] = () => cpir();     // CPIR
// ...

4. DD/FDプレフィックス:驚くほど単純な「読み替え」ルール

DD/FDプレフィックスの仕組みは、理解してしまえば拍子抜けするほど単純だ。

  • DDプレフィックスのルール:
    後続のオペコードに含まれるHL/H/Lの参照を、IX/IXH/IXLに読み替える。ただし(HL)は(IX+d)に読み替わり、その場合のH/Lは読み替えない。

  • FDプレフィックスのルール:
    DDと全く同じだが、IXの代わりにIYを使う。

つまり、DDやFDは「新しい命令テーブル」を開くのではなく、既存のテーブル(無印またはCB)を「IX/IYモードで再解釈させる」フィルターとして機能する。

通常:     ADD A, H     → Aにhの値を加算
DD付き:   ADD A, IXH   → AにIXの上位バイトの値を加算 (非公式)

通常:     LD A, (HL)   → AにHL番地のメモリを読み込む
DD付き:   LD A, (IX+d) → AにIX+d番地のメモリを読み込む

重要な例外がいくつかある。

  • EX DE, HLは読み替えない:
    DD EX DE, HLはEX DE, IXにはならない。そのままEX DE, HLとして実行される。

  • EXXは読み替えない:
    同上。

  • ED命令は読み替えない:
    DD ED xxのDDは無視され、ED xxとして実行される。

  • 連続プレフィックス:
    DD DD xxのように同じプレフィックスが連続した場合、最後のプレフィックスだけが有効。途中のDD/FDはNOP相当(ただしT-statesは消費する)。

  • (HL)と H/L の読み替えの非対称性:
    これが最大の罠で、LD H, (HL)にDDプレフィックスを付けるとLD H, (IX+d)になる。LD IXH, (IX+d)にはならない。(HL)がIX+dに読み替わった場合、同じ命令内の他のH/Lへの参照はそのまま(読み替えない)。つまりLD IXH, (IX+d)という命令は存在しない。

エミュレーターでは、DDプレフィックスを検出したら「現在のindexReg = IX」というフラグを立て、後続のデコードで(HL)をreadMem(IX+d)に、H/LをIXH/IXLに読み替える——ただし(HL)が出現した場合はH/Lの読み替えを抑制する——というロジックで実装できる。

5. DDCB/FDCB:4バイト命令と「おまけ」の副作用

Z80の命令の中で最も奇妙なのが、DDCB(またはFDCB)の二重プレフィックスだ。

通常のCB命令は2バイト(CB + オペコード)だが、DD + CB の場合は4バイトになる。しかもバイトの順序が普通と違う。

通常CB:   [CB] [opcode]
DDCB:     [DD] [CB] [displacement d] [opcode]
                     ↑ オペコードの前にdが来る!

なぜdがopcodeの前に来るのか。これはZ80のマイクロコード(内部制御シーケンス)の設計上の制約から来ている。(IX+d)のアドレス計算には加算が必要で、その加算をオペコードのデコードと並列に行うために、dを先に読み出す必要があったのだ。

DDCB命令では、z=6(通常は(HL)を意味する位置)が公式命令で、z≠6は非公式命令になる。

  • 公式命令の例:
    DDCB 05 06 → RLC (IX+5)

  • 非公式命令の例:
    DDCB 05 00 → RLC (IX+5) の結果を(IX+5)に書き戻すと同時にBレジスタにもコピー

この「結果をレジスタにもコピーする」副作用は、回路設計から自然に発生したものだ。CB命令のx=0のブロック(シフト・ローテート)やx=2/3のブロック(RES/SET)では、結果をr[z]に書き戻す回路が存在する。z=6のときだけ(IX+d)に書き戻すように制御されるが、z≠6のときは通常通りr[z]にも書き戻される。Zilogはこの動作を「意図しない副産物」として公式ドキュメントから省いたが、回路的には完全に一貫した動作をしている。

実用上、これは「メモリ上の値をシフトして、その結果をすぐにレジスタでも使いたい」という場面で有用であり、一部のゲームソフトやデモシーンで活用された。

function decodeDDCB(d, opcode) {
  const x = (opcode >> 6) & 3;
  const y = (opcode >> 3) & 7;
  const z = opcode & 7;
  const addr = (regs.ix + signExtend(d)) & 0xFFFF;
  let val = readMem(addr);

  switch (x) {
    case 0: val = rotShift[y](val); break; // RLC〜SRL
    case 1: bit(y, val); return;           // BIT(書き戻しなし)
    case 2: val = val & ~(1 << y); break;  // RES
    case 3: val = val | (1 << y); break;   // SET
  }
  writeMem(addr, val);
  if (z !== 6) writeReg8(z, val); // 非公式:結果をr[z]にもコピー
}

最後の1行 if (z !== 6) writeReg8(z, val) が非公式動作の全てだ。この1行があるかないかで、zexallテストの合否が分かれる。

6. 非公式命令のまとめ

Z80の非公式命令をカテゴリ別に整理する。

6.1 IXH / IXL / IYH / IYL

DDプレフィックスがH/Lの参照をIXH/IXLに読み替える副産物。公式ドキュメントには記載されていないが、eZ80やR800では公式命令として採用されている。多くの実用ソフトウェアが使っているため、エミュレーターでのサポートは必須。

6.2 SLL (CB 30〜CB 37)

SLA(算術左シフト、ビット0を0に)のy=6版。シフト・ローテート命令テーブルのy=0〜5, y=7は全て公式命令で、y=6だけが「穴」として非公式になっている。動作はSLAと同じだがビット0が1にセットされる。

6.3 DDCB/FDCBの結果レジスタコピー

前節で説明した通り。z≠6の位置にあるDDCB/FDCB命令が、(IX+d)への書き戻しに加えてr[z]にも結果をコピーする。

6.4 ED空間のミラー命令

NEG (ED 44) のミラーが ED 4C, ED 54, ED 5C に、RETN (ED 45) のミラーが ED 55, ED 5D, ED 65, ED 6D, ED 75, ED 7D に存在する。公式には1つだけ割り当てられている命令の、デコーダーの「don’t care」ビットが別の値になったバージョン。

6.5 IN (C) / IN F, (C) — ED 70

IN B,(C)〜IN A,(C)のr[y]テーブルでy=6(通常は(HL))に相当する位置。レジスタへの書き込み先がないため、ポートからの読み込み結果はどこにも保存されないが、フラグは通常通り影響を受ける。I/Oポートの状態をフラグで確認したいだけの用途で使われることがある。

6.6 OUT (C), 0 — ED 71

同様にOUT命令のy=6版。ポートに0を出力する(CMOS版Z80では0xFF)。

7. デコーダーの全体設計

ここまでの知識を統合して、エミュレーターのデコーダーの全体構造を示す。

function step() {
  const opcode = fetchByte(); // PC番地から1バイト読み、PCをインクリメント
  regs.incR();                // Rレジスタをインクリメント(M1サイクル)

  switch (opcode) {
    case 0xCB: decodeCB(fetchByte()); break;
    case 0xDD: decodeDDFD(regs.ix, 'ix'); break;
    case 0xED: decodeED(fetchByte()); break;
    case 0xFD: decodeDDFD(regs.iy, 'iy'); break;
    default:   decodeUnprefixed(opcode); break;
  }
}

function decodeDDFD(indexReg, regName) {
  const opcode = fetchByte();
  regs.incR(); // DD/FD自体もM1サイクルを消費

  if (opcode === 0xCB) {
    // DDCB/FDCB: dが先、opcodeが後
    const d = fetchByte();
    const cbOp = fetchByte();
    decodeDDCB(indexReg, d, cbOp);
  } else if (opcode === 0xDD || opcode === 0xED || opcode === 0xFD) {
    // 連続プレフィックス: 現在のDD/FDはNOP扱い、次のプレフィックスを処理
    // → PCを戻さず、メインのstep()に戻して再デコードさせるか、
    //   ここで再帰的に処理するかは実装次第
    decodeDDFD(/* 新しいindexReg */);
  } else {
    // 通常オペコードのHL/H/LをIX/IY/IXH等に読み替えて実行
    decodeWithIndex(opcode, indexReg, regName);
  }
}

このステートマシンが、Z80のデコーダーの骨格だ。700超のオペコードが、4つのテーブル(無印・CB・ED・DDCB)×パターン分解(x/y/z/p/q)で体系的に処理される。

次回の最終回(第4回)では、これらの命令が実際にバス上でどう動くのか——M1サイクル、メモリリード/ライト、I/Oサイクルのタイミングと、エミュレーターでのサイクル精度設計を掘り下げる。

連載構成

回テーマ内容
第1回インテルからの独立とZ80誕生の歴史ファジンのIntel離脱、Exxonの出資、8080の設計上の負債、Z80の技術革新、CP/Mとの共生
第2回レジスタ構造と「裏レジスタ」の美学AF/BC/DE/HL、IX/IY、裏レジスタ、フラグレジスタの全8ビット、WZレジスタ、JSでの設計判断
第3回(本記事)命令セットとプレフィックスデコードのカラクリx/y/z/p/qパターン分解、CB/ED/DD/FDプレフィックス、DDCB二重プレフィックス、非公式命令
第4回ハードウェアバスとエミュレーションの勘所M1サイクル、DRAMリフレッシュ機構、ポートI/O、JS/WASMでのZ80コア設計