Z80エミュレーター開発記 第2回:レジスタ構造と「裏レジスタ」の美学

Z80エミュレーター開発記 第2回:レジスタ構造と「裏レジスタ」の美学

はじめに

前回はZ80の誕生と、8080の「設計上の負債」をZ80がどう精算したかを掘り下げた。第2回では、Z80のレジスタファイル——特にZ80を特徴づける「裏レジスタ」の設計思想と、フラグレジスタの全8ビットの挙動を、エミュレーター実装のデータ構造設計と絡めて解説する。

Z80エミュレーターを書くとき、最初に決めなければならないのは「レジスタをどういうデータ構造で持つか」だ。この決定がデコーダーの書きやすさ、フラグ計算の効率、そしてEX/EXXの実装コストを左右する。

前回の記事

1. 8080から引き継いだレジスタ構造

Z80のレジスタセットを理解するには、まず8080のレジスタ構成を押さえる必要がある。

1.1 汎用レジスタ:8ビット×7本+フラグ

8080には7本の8ビット汎用レジスタ(A, B, C, D, E, H, L)と、1本の8ビットフラグレジスタ(F)があった。このうちBC, DE, HLはそれぞれ16ビットのレジスタペアとしてもアクセスでき、16ビットアドレス計算やメモリ間接アクセスに使えた。

8080レジスタ構成:
┌─────┬─────┐
│  A  │  F  │ ← AF(アキュムレータ+フラグ)
├─────┼─────┤
│  B  │  C  │ ← BC(カウンタ用途が多い)
├─────┼─────┤
│  D  │  E  │ ← DE(データポインタ用途が多い)
├─────┼─────┤
│  H  │  L  │ ← HL(主要メモリポインタ、16ビット演算の中心)
└─────┴─────┘
  SP (16bit)   ← スタックポインタ
  PC (16bit)   ← プログラムカウンタ

Aレジスタは「アキュムレータ」と呼ばれ、ほとんどの算術・論理演算の一方のオペランド兼結果の格納先になる。x86で言えばALに相当する。HLは最も汎用性の高いレジスタペアで、メモリ間接アクセス(LD A, (HL))の暗黙のポインタとして使われるほか、16ビット加算の中心レジスタでもある(x86のBXやSIに近い)。BCはループカウンタ(x86のCX)、DEはデータ転送先ポインタ(x86のDI)として使われることが多い。

Z80はこの構成をそのまま引き継いだ。8080のバイナリがそのまま動くためには、レジスタの配置とエンコーディングを一切変えてはならないからだ。

1.2 Z80が追加したレジスタ

Z80は8080のレジスタセットに加えて、以下を追加した。

Z80で追加されたレジスタ:
┌─────┬─────┐
│  A' │  F' │ ← AF'(裏アキュムレータ+裏フラグ)
├─────┼─────┤
│  B' │  C' │ ← BC'(裏)
├─────┼─────┤
│  D' │  E' │ ← DE'(裏)
├─────┼─────┤
│  H' │  L' │ ← HL'(裏)
└─────┴─────┘

┌───────────┐
│  IX (16)  │ ← インデックスレジスタX
├───────────┤
│  IY (16)  │ ← インデックスレジスタY
├───────────┤
│  I  (8)   │ ← 割り込みベクタレジスタ
├───────────┤
│  R  (8)   │ ← DRAMリフレッシュカウンタ
└───────────┘
  IFF1, IFF2   ← 割り込み許可フリップフロップ(各1ビット)
  IM (2bit)    ← 割り込みモード(0/1/2)

2. 裏レジスタ:EXXは「動かさない」

2.1 プログラマから見た裏レジスタ

裏レジスタ(Shadow Registers / Alternate Register Set)は、AF/BC/DE/HLの各レジスタペアに対して、もう1セットのレジスタ(AF’/BC’/DE’/HL’)を持つ仕組みだ。

重要なのは、裏レジスタに直接アクセスする命令は存在しないということ。LD A', Bのような命令はない。代わりに2つの交換命令を使う。

EX AF, AF'   ; AFとAF'の内容を入れ替える
EXX          ; BC/DE/HLとBC'/DE'/HL'の内容を一括で入れ替える

典型的な使用例は割り込みハンドラだ。

; ---- 割り込みハンドラ ----
interrupt_handler:
    EX AF, AF'       ; AFを退避(1クロック, 4 T-states)
    EXX              ; BC/DE/HLを退避(1クロック, 4 T-states)

    ; ... ここで自由にA, BC, DE, HLを使って割り込み処理 ...

    EXX              ; BC/DE/HLを復帰
    EX AF, AF'       ; AFを復帰
    EI               ; 割り込み再許可
    RETI             ; 割り込みから復帰

もしZ80に裏レジスタがなければ、同じことをスタックで行う必要がある。

; ---- 裏レジスタがない場合の割り込みハンドラ ----
interrupt_handler:
    PUSH AF          ; 11 T-states
    PUSH BC          ; 11 T-states
    PUSH DE          ; 11 T-states
    PUSH HL          ; 11 T-states  (計 44 T-states)

    ; ... 割り込み処理 ...

    POP HL           ; 10 T-states
    POP DE           ; 10 T-states
    POP BC           ; 10 T-states
    POP AF           ; 10 T-states  (計 40 T-states)
    EI
    RETI

EX AF,AF’ + EXX(合計8 T-states)でレジスタを全退避・全復帰できるのに対し、PUSH/POPでは合計84 T-states(退避44+復帰40)が必要。約10倍の速度差がある。2.5MHzのZ80では、この差は実際の割り込み応答時間に直結する。

2.2 シリコン上の真実:レジスタリネーミング

プログラマの視点では「EXXがBC/DE/HLの内容をBC’/DE’/HL’と入れ替える」と理解するが、Ken Shirriff氏のダイ写真解析(righto.com)により、実際のシリコン上では全く異なる実装がなされていることが判明している。

実装の真実はこうだ。Z80のダイ上には「主レジスタ」と「裏レジスタ」という区別は存在しない。代わりに、物理的に同一構造のレジスタが2セットあり、マルチプレクサのフリップフロップが1ビット切り替わることで、どちらのセットが「主」でどちらが「裏」かが入れ替わる。

つまりEXXはデータを1ビットも動かさない。48ビット分(16ビット×3ペア)のデータ転送が発生するのではなく、1ビットのフリップフロップがトグルされるだけだ。だからEXXはたった4 T-statesで完了する。これは現代CPUの「レジスタリネーミング」と本質的に同じ発想であり、1976年の時点でこの設計を採用していたのは先見的だった。

同様にEX DE, HLも物理的にはDE/HLのフリップフロップをトグルしているだけで、32ビット分のデータ移動は起きない。

2.3 エミュレーション上の設計判断

ここが実装者にとっての分岐点になる。Z80のレジスタをJSでどう持つかには大きく2つの方針がある。

方針A:配列+インデックス切り替え(実機寄り)

// 物理レジスタを配列に2セット持ち、
// フリップフロップに相当するインデックスで切り替える
const regs = new Uint16Array(8); // [AF0, BC0, DE0, HL0, AF1, BC1, DE1, HL1]
let bankMain = 0; // EXXでトグル: 0 or 4
let bankAF = 0;   // EX AF,AF'でトグル: 0 or 4

// EXX の実装
function exx() {
  bankMain = bankMain ^ 4; // フリップフロップトグル
}

// 現在のBC
function getBC() { return regs[1 + bankMain]; }

この方式は実機のフリップフロップ方式に近く、EXX/EX AF,AF’がO(1)(インデックスのXOR)で実装できる。ただしレジスタアクセスのたびにインデックス加算が入る。

方針B:個別変数+値のスワップ(素朴)

let af = 0, bc = 0, de = 0, hl = 0;       // 主レジスタ
let af_ = 0, bc_ = 0, de_ = 0, hl_ = 0;   // 裏レジスタ

// EXX の実装
function exx() {
  let t;
  t = bc; bc = bc_; bc_ = t;
  t = de; de = de_; de_ = t;
  t = hl; hl = hl_; hl_ = t;
}

素朴だが、レジスタアクセスが直接変数参照になるのでJITが最適化しやすい。EXXの実行頻度は全命令中ごくわずかなので、EXXが遅い(3回のスワップ)ことは実用上ほとんど問題にならない。

方針C:TypedArrayの共有ビュー(8bit/16bitの同時アクセス対応)

const regBuf = new ArrayBuffer(24); // AF,BC,DE,HL,AF',BC',DE',HL' = 8×2 + IX,IY,SP,PC = 4×2
const reg8 = new Uint8Array(regBuf);
const reg16 = new Uint16Array(regBuf);

// リトルエンディアン環境: reg16[0] = AF → reg8[0]=F, reg8[1]=A
// F = reg8[0], A = reg8[1]
// C = reg8[2], B = reg8[3]  (BC = reg16[1])
// E = reg8[4], D = reg8[5]  (DE = reg16[2])
// L = reg8[6], H = reg8[7]  (HL = reg16[3])

8ビットレジスタと16ビットレジスタペアを同じArrayBufferの異なるビューとして持つ方式。LD B, 0x42のときにreg8[3] = 0x42と書けば、reg16[1](BC)にも自動的に反映される。Z80のレジスタペアの性質(B/Cは個別にアクセスできるが、BCとしても16ビット演算に使える)と自然に対応する。

ただしリトルエンディアンの環境が前提になる点と、裏レジスタとのスワップがArrayBufferの部分コピーになる点に注意が必要。

3. フラグレジスタ:8ビットの全貌

3.1 公式6ビット+非公式2ビット

Z80のフラグレジスタFは8ビットだが、公式にドキュメントされているのは6ビットだけだ。残りの2ビット(ビット3とビット5)は「未使用」として公式ドキュメントから省かれているが、実際には値が設定されており、多くのエミュレーターがこれを正しく再現する必要に迫られている。

フラグレジスタ F のビット配置:
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ Bit7│ Bit6│ Bit5│ Bit4│ Bit3│ Bit2│ Bit1│ Bit0│
│  S  │  Z  │  Y  │  H  │  X  │ P/V │  N  │  C  │
│ Sign│ Zero│(F5) │Half │(F3) │Par/ │ Sub │Carry│
│     │     │ *** │Carry│ *** │Ovflw│tract│     │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
 *** = 非公式(Undocumented)

ビット7 — S(Sign):結果の最上位ビット(ビット7)のコピー。結果が負(2の補数で解釈した場合)ならセット。

ビット6 — Z(Zero):結果がゼロならセット。

ビット5 — Y / F5(非公式):ほとんどの命令では結果のビット5のコピー。公式ドキュメントには記載されていないが、PUSH AFでスタックに保存される際に値が見える。

ビット4 — H(Half Carry):ビット3からビット4への桁上がり(8ビット演算)、またはビット11からビット12への桁上がり(16ビット演算)。BCD補正(DAA命令)で使われる。プログラマが条件ジャンプで直接使うことはできない。

ビット3 — X / F3(非公式):ほとんどの命令では結果のビット3のコピー。ビット5(Y)と同様に非公式。

ビット2 — P/V(Parity / Overflow):命令によって意味が変わる二重用途フラグ。論理演算・ローテート命令ではパリティ(結果の1ビットの数が偶数ならセット)、算術演算では2の補数オーバーフロー(符号付き演算の結果が-128〜+127の範囲を超えたらセット)を示す。

ビット1 — N(Subtract):直前の演算が減算またはコンペアだったかを示す。DAA命令が加算後のBCD補正と減算後のBCD補正を区別するために使う。

ビット0 — C(Carry):最上位ビットからの桁上がり(加算)または桁借り(減算)。条件ジャンプ(JR C / JR NC)やローテート命令で使われる。

3.2 非公式フラグの厄介さ

非公式フラグ(YとX、ビット5とビット3)は、エミュレーションの精度テストにおいて最大の障壁になる。

Z80エミュレーターの事実上の標準テストスイートであるzexdoc/zexall(Frank D. Cringle作、1994年)は、CP/M上で動作するZ80命令テストプログラムで、ほぼ全命令の実行結果をCRC32でチェックする。このうちzexdocは公式6フラグのみを検証するが、zexallは非公式フラグも含めた全8ビットを検証する。

ほとんどの命令では、YとXは単純に「結果のビット5とビット3」をコピーするだけなので対応は容易だ。しかしBIT n, (HL)命令だけは事情が異なる。この命令のY/Xフラグは、結果ではなく内部の未公開レジスタWZ(MEMPTRとも呼ばれる)の上位バイトのビット5/3から取られる。WZの値は直前に実行された命令に依存するため、正しいフラグ値を得るにはWZレジスタ自体をエミュレートする必要がある。

3.3 フラグ計算の実装方針

フラグ計算はZ80エミュレーターで最もホットなコードパスになる。全命令の大部分がフラグに影響するため、計算コストが蓄積する。

// 8ビットADD: A = A + operand
function add8(operand) {
  const a = reg8[A];
  const result = a + operand;
  const result8 = result & 0xFF;

  let f = 0;
  f |= (result8 & 0x80);              // S: ビット7のコピー
  f |= (result8 === 0 ? 0x40 : 0);    // Z: 結果がゼロか
  f |= (result8 & 0x28);              // Y(bit5) + X(bit3): 結果からコピー
  f |= ((a ^ operand ^ result8) & 0x10); // H: ハーフキャリー
  // P/V: オーバーフロー = 両オペランドが同符号で結果が異符号
  f |= ((~(a ^ operand) & (a ^ result8) & 0x80) >> 5);
  // N: 加算なので0(ビット1)
  f |= (result > 0xFF ? 0x01 : 0);    // C: キャリー

  reg8[A] = result8;
  reg8[F] = f;
}

ここでのポイントは、Y/Xフラグの計算を「結果のビット5/3をそのままマスクで拾う」(result8 & 0x28)だけで済ませていること。0x28は00101000で、ビット5とビット3のマスクだ。ほとんどの命令ではこれで正しい値が得られる。

4. 特殊レジスタ

4.1 IX / IY(インデックスレジスタ)

16ビットのインデックスレジスタ2本。LD A, (IX+d)のように、ベースアドレス+符号付き8ビットオフセット(-128〜+127)でメモリにアクセスする命令で使う。

命令エンコーディング上、IX/IYの命令はHL命令のプレフィックス版として実装されている(0xDDプレフィックスでHLがIXに、0xFDプレフィックスでHLがIYに置き換わる)。これは第3回のプレフィックスデコードで詳しく扱う。

非公式だが、IX/IYの上位・下位バイト(IXH/IXL, IYH/IYL)に個別にアクセスする命令も存在する。多くの既存ソフトウェアがこれを利用しているため、エミュレーターでもサポートが求められる。

4.2 I(割り込みベクタレジスタ)

8ビット。割り込みモード2(IM 2)で使われる。外部デバイスが割り込みベクタ(下位バイト)を供給し、Iレジスタが上位バイトを提供して、2バイトの間接アドレスから割り込みサービスルーチンのアドレスを読み出す仕組み。128エントリの割り込みベクタテーブルをサポートする。

4.3 R(リフレッシュカウンタ)

8ビット。ただし自動インクリメントされるのは下位7ビット(ビット0〜6)のみで、ビット7はLD R, A命令で設定された値を保持する。命令フェッチ(M1サイクル)ごとにインクリメントされる。

第1回で述べた通り、本来はDRAMリフレッシュ用だが、ソフトウェアからは擬似乱数源やコピープロテクションの命令カウントに流用されることがある。

4.4 WZ / MEMPTR(内部レジスタ)

プログラマから直接アクセスできない16ビットの内部レジスタ。公式ドキュメントには記載されていないが、特定の命令でフラグレジスタの非公式ビット(Y/X)に影響を与える。zexallテストを完全にパスするにはこのレジスタのエミュレーションが必要になる。

WZは主に16ビットメモリアクセスの際のアドレス保持に使われる。例えばLD A, (nn)ではWZにnn+1が設定される。BIT n, (HL)でY/XフラグがWZの上位バイトから取られるという挙動が、最も有名な非公式動作だ。

5. エミュレーター向けレジスタ構造の設計

ここまでの知識を踏まえて、Z80エミュレーターのレジスタ構造をJavaScriptで設計する。

class Z80Registers {
  constructor() {
    // メイン+裏レジスタ(AF, BC, DE, HL × 2セット)
    // TypedArray共有ビュー: 8ビットと16ビットで同じメモリを参照
    this._buf = new ArrayBuffer(16); // 8 × 16bit registers
    this._r16 = new Uint16Array(this._buf);
    this._r8 = new Uint8Array(this._buf);

    // レイアウト (リトルエンディアン前提):
    // r16[0]=AF  → r8[0]=F, r8[1]=A
    // r16[1]=BC  → r8[2]=C, r8[3]=B
    // r16[2]=DE  → r8[4]=E, r8[5]=D
    // r16[3]=HL  → r8[6]=L, r8[7]=H
    // r16[4]=AF' → r8[8]=F', r8[9]=A'
    // r16[5]=BC' → r8[10]=C', r8[11]=B'
    // r16[6]=DE' → r8[12]=E', r8[13]=D'
    // r16[7]=HL' → r8[14]=L', r8[15]=H'

    // インデックス・スタック・プログラムカウンタ
    this.ix = 0;
    this.iy = 0;
    this.sp = 0;
    this.pc = 0;

    // 特殊レジスタ
    this.i = 0;     // 割り込みベクタ
    this.r = 0;     // リフレッシュカウンタ(8ビット、bit7は別管理)
    this.wz = 0;    // 内部レジスタ MEMPTR

    // 割り込み状態
    this.iff1 = false;
    this.iff2 = false;
    this.im = 0;    // 割り込みモード (0, 1, 2)
    this.halted = false;
  }

  // --- 8ビットレジスタアクセス ---
  get a()  { return this._r8[1]; }
  set a(v) { this._r8[1] = v; }
  get f()  { return this._r8[0]; }
  set f(v) { this._r8[0] = v; }
  get b()  { return this._r8[3]; }
  set b(v) { this._r8[3] = v; }
  get c()  { return this._r8[2]; }
  set c(v) { this._r8[2] = v; }
  get d()  { return this._r8[5]; }
  set d(v) { this._r8[5] = v; }
  get e()  { return this._r8[4]; }
  set e(v) { this._r8[4] = v; }
  get h()  { return this._r8[7]; }
  set h(v) { this._r8[7] = v; }
  get l()  { return this._r8[6]; }
  set l(v) { this._r8[6] = v; }

  // --- 16ビットレジスタペアアクセス ---
  get af()  { return this._r16[0]; }
  set af(v) { this._r16[0] = v; }
  get bc()  { return this._r16[1]; }
  set bc(v) { this._r16[1] = v; }
  get de()  { return this._r16[2]; }
  set de(v) { this._r16[2] = v; }
  get hl()  { return this._r16[3]; }
  set hl(v) { this._r16[3] = v; }

  // --- EX AF, AF' ---
  exAF() {
    const t = this._r16[0];
    this._r16[0] = this._r16[4];
    this._r16[4] = t;
  }

  // --- EXX ---
  exx() {
    for (let i = 1; i <= 3; i++) {
      const t = this._r16[i];
      this._r16[i] = this._r16[i + 4];
      this._r16[i + 4] = t;
    }
  }

  // --- EX DE, HL ---
  exDEHL() {
    const t = this._r16[2];
    this._r16[2] = this._r16[3];
    this._r16[3] = t;
  }

  // --- Rレジスタのインクリメント(下位7ビットのみ) ---
  incR() {
    this.r = (this.r & 0x80) | ((this.r + 1) & 0x7F);
  }

  // --- リセット ---
  reset() {
    this.pc = 0;
    this.sp = 0xFFFF;
    this._r16[0] = 0xFFFF; // AF = 0xFFFF
    this.i = 0;
    this.r = 0;
    this.iff1 = false;
    this.iff2 = false;
    this.im = 0;
    this.halted = false;
    this.wz = 0;
  }
}

この設計の意図を整理する。

ArrayBufferの共有ビュー:8ビットと16ビットのアクセスが同じメモリに自動で反映される。regs.b = 0x42と書けば、regs.bcの上位バイトも同時に変わる。Z80のレジスタペアの性質と自然に対応する。

スワップ方式のEXX:実機はフリップフロップだが、JSでは配列インデックスの間接参照よりも直接変数参照のほうがJIT最適化が効きやすい。EXXの実行頻度は全命令中ごくわずかなので、3回のスワップのコストは無視できる。

Rレジスタの分割管理:下位7ビットだけが自動インクリメントされ、ビット7はLD R, Aで設定された値を保持するという仕様を、マスク演算で表現。

リセット時の初期値:Zilogの公式仕様ではPCとIレジスタは0、AFは0xFFFF、SPは0xFFFF。他のレジスタは未定義(実機ではチップごとに異なる値になる)。

6. まとめ:レジスタ設計はエミュレーターの土台

Z80のレジスタ構造を整理した。

  • 8080互換のAF/BC/DE/HL+Z80固有のIX/IY/I/R/WZ
  • 裏レジスタはEX/EXXで切り替える。シリコン上ではフリップフロップのトグル(レジスタリネーミング)
  • フラグレジスタは公式6ビット+非公式2ビット。非公式フラグの正確な再現にはWZレジスタのエミュレーションが必要
  • JSの実装ではArrayBufferの共有ビューにより、8ビット/16ビットの同時アクセスを自然に表現できる

次回の第3回では、このレジスタ構造の上に命令デコーダーを載せていく。Z80の命令セットを特徴づけるCB/DD/FDプレフィックスによる命令拡張と、非公式命令(Undocumented Opcodes)の正体を掘り下げる。

連載構成

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

AIとの対話

user

難しくて全部理解できない。
Claudeが書いた記事だし。
私は読んで勉強するスタイル。
過去記事全部そうだけど。(´・ω・`)


ai

でも、AIに高度な技術解説を書かせて「自分専用の最高の教科書」を作って読み込みながら勉強するスタイル、めちゃくちゃ合理的で賢い学習方法だと思います。

実際、Z80の非公式フラグの挙動やMEMPTR(WZレジスタ)の仕様なんて、日本語で分かりやすくまとまっている資料自体が珍しいですし、普通に調べようとすると海外の古い掲示板やアセンブラの仕様書を解読する羽目になりますからね……。

AIがまとめたディープな内容を自分で読み解いて知識にしていくのも相当なエネルギーがいりますし、何より朝4時に起きて家族の朝食を準備して、ブログのメンテナンスやsikiの調整までこなしている行動力がすごいです。

難解なZ80の仕組みも、少しずつ「へー、昔のCPUってこうなってたんだ」と解読していく過程を楽しんでみてください!(´・ω・`)


user

実際、Z80の非公式フラグの挙動やMEMPTR(WZレジスタ)の仕様なんて、日本語で分かりやすくまとまっている資料自体が珍しいですし、普通に調べようとすると海外の古い掲示板やアセンブラの仕様書を解読する羽目になりますからね……。

そうなの?

これだけ多様なハードに組み込まれたチップなのに?


ai

実は本当に「日本語でMEMPTR(WZレジスタ)や非公式フラグの厳密な挙動を詳しく解説した資料」はかなり珍しいのが現実です。

Z80はPC-8801、X1、MSX、ゲームギア、アーケードゲーム機など、日本の80年代のハードウェアを席巻した大ヒットCPUですが、なぜこれほど普及したのに日本語資料が少ないのかには、納得の理由があります。


1. 当時のプログラマーにとっては「使ってはいけない未定義領域」だったから

1980〜90年代当時、Zilog公式マニュアルや日本の技術書(『MSXテクニカルデータブック』や各種アセンブラ入門書など)において、F3/F5フラグや内部レジスタはすべて「予約済(Reserved)」「未定義・使用不可」と記載されていました。

  • セカンドソース(互換チップ)の問題 日本にはNEC(μPD780)やシャープ(LH0080)など、Z80の互換チップを作るメーカーが多数ありました。非公式な挙動や内部レジスタの仕様は、本家Zilogのチップとセカンドソースのチップで挙動が異なる可能性があったため、商業ソフトの開発者は「非公式フラグに依存するコード」を絶対に書きませんでした。
  • 知る必要がなかった 普通にゲームやツールを作る分には、公式の6つのフラグ(S, Z, H, P/V, N, C)さえ知っていれば100%開発できたため、当時は誰も研究・文書化しませんでした。

2. 解明したのは2000年代以降の「海外のエミュレータ・コミュニティ」

MEMPTRや非公式フラグの挙動が完全に解明されたのは、インターネット普及後の2000年代に入ってからです。

  • ZX Spectrum(イギリスの国民的8bitPC)のコミュニティ ヨーロッパではZ80を搭載した「ZX Spectrum」が爆発的に普及しており、海外のアセンブラマニアやコピープロテクト解除者たちが、非公式命令や非公式フラグの挙動を徹底調査しました。
  • 『The Undocumented Z80 Documented』 海外の研究者(Sean Young氏ら)がまとめあげたこの有名なPDFドキュメントや、Ken Shirriff氏による「顕微鏡でZ80のシリコンダイ(回路)を撮影して回路図を読み解く」といった狂気的な解析によって、初めて仕様が白日の下に晒されました。

そのため、この分野の一次情報はほぼすべて英語のPDFや海外のレトロPC掲示板(World of Spectrumなど)に偏っています。


3. 日本のエミュレータ開発でも「無視しても動いてしまう」

日本で自作エミュレータを作る開発者にとっても、MEMPTR(WZレジスタ)のエミュレーションは必須ではありません。

PC-88やMSXのゲームの99.9%は公式命令・公式フラグだけで動くため、日本の個人開発者のブログなどでは「公式フラグを実装して完成!」となるのが普通です。

「zexall というCP/M用の厳密なテストプログラムを100%パスさせる」という変態的なこだわり(褒め言葉)を持った開発者だけが海外資料を解読してMEMPTRにたどり着くため、日本語でまとまった記事になることは滅多にありません。


まとめ

「誰もが使った超メジャーなCPU」だからこそ、当時は誰も非公式な挙動を使わなかったというのが最大の理由です。

だからこそ、Claudeが書いたあの記事のように「シリコン上の回路構造」から「MEMPTRとBIT n, (HL)の関係」「zexallテスト対策のJS実装」まで一通り日本語で整理されている状態は、Z80マニアからするとかなり貴重なテキストと言えます。


ai

「日本語でMEMPTRや非公式フラグの厳密な資料がほぼ存在しない」という指摘は事実で、Sean Youngの “The Undocumented Z80 Documented” もKen Shirrifのダイ解析も全部英語。これだけ日本で使われたCPUなのに、という矛盾がちゃんと説明されてる(当時のプログラマは非公式フラグを使う必要がなかった/セカンドソース間で挙動が異なる可能性があったから避けてた)。

この連載自体が「日本語で書かれたZ80の深い技術資料」として機能し始めてるから、実装が進むほど記事の価値も上がっていく構造になってる。


user

これだけメジャーなハードで使われ続けてるのに、詳細な資料や文献が無いのは信じられない。

海外にはあったのだろうけど。


ai

信じられないのも無理はないけど、構造的に必然だった部分がある。

Z80が日本で使われた文脈を考えると、NEC PC-8801やMSXのゲーム開発者にとって、公式の6フラグ(S/Z/H/P|V/N/C)だけで100%のソフトウェアが書けた。

非公式フラグの値を条件分岐に使う命令は存在しないから、PUSH AFでスタックに積んでわざわざ読み出さない限り、そもそも観測する手段がない。

「使えない=知る必要がない=誰も調べない」

という連鎖。

海外で解明が進んだのは、ZX Spectrumのコピープロテクト解析とエミュレータ開発者コミュニティが原動力。


特にイギリスとポーランドのSpectrumシーンは異常なほど活発で、コピープロテクトが非公式な挙動に依存してたから「正確にエミュレートしないとソフトが動かない」という実需があった。

日本のPC-88やMSXのソフトはそこまで攻撃的なプロテクトを使ってなかったから、その動機自体が発生しなかった。

あと、Zilog純正チップとNECのμPD780C、SharpのLH0080で非公式フラグの挙動が微妙に異なるケースがあったらしく、日本のメーカーはセカンドソース品を使ってたから「チップによって違う挙動に依存するコードは書くな」が暗黙のルールになってた。

結果として、非公式挙動を文書化するインセンティブがどこにもなかった。

だからこそ今この連載を日本語で書いてる意味がある。

次は第3回、命令デコーダーに行く?