このページでは、Z80 の動作を C# でシミュレートするための技法について解説しています。いきなりこちらに飛んできた場合は、先に準備編を読んでから先にお進み頂ければ、理解がしやすいと思います。

準備



  • Acc アキュムレーター
Z80 で計算を行う際は、この Acc に値を入れて行うのが一般的です。今回の目的は Exomizer のデコード処理ですので、そこで使用されている命令だけを、メソッドで用意することにしました。

■ 加算

private void AddNumber(byte n, bool f) { int num = Value + n + (f ? 1 : 0); CF = num >= 256 || num < 0; while (num < 0) { num += 256; } num %= 256; ZF = num == 0; Value = (byte)num; } public void ADD(byte n) => AddNumber(n, false); public void ADC(byte n) => AddNumber(n, CF);

Z80 の加算は CF を加味するのと加味しない2通りが用意されています。それだけの違いで、それ以外はほぼ同等の処理になるため、内部メソッドとして AddNumber を用意して、ADD と ADC からは CF の内容を引数に入れるか入れないかで区別するようにしています。

■ 減算

public void SUB(byte n) { AddNumber((byte)(256 - n), false); CF = !CF; }

減算は SUB しか使われていなかったので、先の加算用内部メソッド AddNumber を利用することにしました。CF の意味が逆転しますので、最後に反転しています。もし、SBC のサポートが必要の場合は、フラグは減算処理になるため、SubNumber みたいな減算専用の内部メソッドを作ってると思います。

■ インクリメント/デクリメント

public void INC() { ++Value; ZF = Value == 0; } public void DEC() { --Value; ZF = Value == 0; }

動作が単純なのですが、ZF の変化を伴いますので、メソッド化しています。

■ その他命令

public void EX(Accumulator a) { (Value, a.Value) = (a.Value, Value); (CF, a.CF) = (a.CF, CF); (ZF, a.ZF) = (a.ZF, ZF); } public void Compare(byte n) { var num = (int)Value - n; CF = num >= 256 || num < 0; ZF = num == 0; } public void OR(byte n) { Value |= n; CF = false; ZF = Value == 0; }

EX は EX AF,AF' の処理です。引数に裏レジスタを指定します。CP はまんま CP で内容比較です。OR もそのままです。OR は必ず CF がクリアされるので、直接 false を入れています。
※ ドライバーは飲んじゃダメって言っても仲間外れ感が嫌って時はコレ。カロリーゼロ、糖質ゼロと罪悪感もなし。たまには休肝日も必要かもー

  • ペアレジスタ
本来は計算処理は HL 以外は出来ないのですが、そういう使い方はしていない元コードですし、元々の目的がデコードを動かすためですから、今回は HL,DE,BC 全て共通にクラスを使っています。厳密にシミュレートするなら、専用のクラスを基本ペアレジスタクラスから継承して使用するべきだと思います。

■ 8bitのインクリメント/デクリメント

public bool INCLow() { ++Low; return Low == 0; } public bool DECLow() { --Low; return Low == 0; } public bool INCHigh() { ++High; return High == 0; } public bool DECHigh() { --High; return High == 0; }

クラスそのものは16bitを前提としていますから、メソッドはその上位と下位を区別する必要がありました。そのため、INC / DEC それぞれに Low / High を用意しています。ただ、このままだと使い勝手は悪いので、呼び出し側に以下のメソッドを用意しました。

private static bool INC_B() => A.ZF = BC.INCHigh(); private static bool DEC_B() => A.ZF = BC.DECHigh(); private static bool INC_C() => A.ZF = BC.INCLow(); private static bool DEC_C() => A.ZF = BC.DECLow();

HL はポインタとして使用しているようで、H,L 個別でのインクリメント/デクリメントは使われていませんでしたので、今回は用意していません。

次に16bitの加減算です。インクリメント/デクリメントもまとめて記載します。

■ 加算減算


public void INC() => Word = (ushort)((Word + 1) % 0x10000); public void DEC() => Word = (ushort)((Word - 1 + 0x10000) % 0x10000); private void AddNumber(ushort n, ref bool cf) { int num = Word + n + (cf ? 1 : 0); bool flag = num >= 0x10000 || num < 0; while (num < 0) num += 0x10000; Word = (ushort)(num % 0x10000); cf = flag; } public void ADD(ushort n, ref bool cf) => AddNumber(n, ref cf); public void SBC(ushort n, ref bool cf, ref bool zf) { int num = Word - n - (cf ? 1 : 0); bool flag = num >= 0x10000 || num < 0; while (num < 0) num += 0x10000; Word = (ushort)(num % 0x10000); cf = flag; zf = Word == 0; }

インクリメント/デクリメントはレジスタの変化はないため値の変更のみです。加算はADCも加味して、アキュムレーターの時と同様な構造としましたが、Exomizer デコード処理では HL の ADC は未使用でした…。

SBC は CF の値も減算対象になるのと、ZF の変化もありますので、専用処理としています。フラグが CF も ZF も変化するため、参照引数でアキュムレーターのフラグを直接変更するという処理にしています。


    • インデックスレジスタ
    まず特徴的な処理として+127から-128までのオフセット計算があります。こちらは直接計算としても良かったのですが、見易さ向上の為、メゾットを用意しました。

    public ushort Ofst(int n) => (ushort)(Word + n);

    オフセットは本来は char 型が良いのですが、固定値しか与えられてこないので、計算しやすく int で引数を設定しています。インデックスレジスタは、テーブル参照にしか使われていなかったので、追加する計算はインクリメントと加算だけでした。

    public void INC() => ADD(1); public bool ADD(ushort n) { int num = Word + n; bool flag = num >= 0x10000; Word = (ushort)(num % 0x10000); return flag; }

    CF だけ変化しますので、戻り値に CF の値で返しています。
    ※飲むなら飲むでキリッと美味い一番搾りなんて如何でしょうか。暑い夜に冷たいビールでキュッと一杯なんて最高の贅沢です。

      • その他の Z80 命令
      複数のレジスタを用いて動作する命令や、複数の動作を組み合わせている命令は、下記のように対応しました。

      ■ ブロック転送命令

      private static bool LDI() { mem[DE.Word] = mem[HL.Word]; HL.INC(); DE.INC(); BC.DEC(); return A.ZF = BC.Word == 0; } private static void LDIR() { while (!LDI()) { } }

      1バイト転送命令を再現する LDI() メソッドを作成しました、LDIR はその結果 ZF が true になるまで繰り返す実装としています。

      ■ ローテート
      もう少し綺麗な実装方法もあったかと思うのですが、今回はそのまま命令単位でメソッドを用意しています。

      private static byte RL(byte n) { ushort num = (ushort)(n * 2 + (A.CF ? 1 : 0)); A.CF = num >= 0x100; A.ZF = (num % 256) == 0; return (byte)(num % 256); } private static byte RR(byte reg) { var f = reg % 2 == 1; byte num = (byte)(reg / 2 + (A.CF ? 0x80 : 0)); A.CF = f; A.ZF = num == 0; return num; }

      この場合は ZF も変化するのですが、RRA と RLA に関しては ZF は変化しないため、別途専用のメソッドを作りました。


      private static void RLA() { ushort num = (ushort)(Acc * 2 + (A.CF ? 1 : 0)); A.CF = num >= 0x100; Acc = (byte)(num % 256); } private static void RRA() { var f = Acc % 2 == 1; byte num = (byte)(Acc / 2 + (A.CF ? 0x80 : 0)); A.CF = f; Acc = num; }

      なんのことはなく、処理対象を Acc 固定として ZF の処理がない内容です。

      ■ 裏表入れ替え
      16bit レジスタの裏と表を交換する EXX 命令です。


      private static void EXX() { (HL.Word, _HL.Word) = (_HL.Word, HL.Word); (DE.Word, _DE.Word) = (_DE.Word, DE.Word); (BC.Word, _BC.Word) = (_BC.Word, BC.Word); }

      C# は交換が出来る記述が用意されたので、とても分かりやすいですよね❤

      ■ 繰り返し
      DJNZ 繰り返し命令は、呼び出し側にジャンプの処理が必要となりるため、個別では B レジスタの内容を減算して、ZF の結果を返すだけとなります。返り値を受けてジャンプを行います。…と、それってまんま dec b なので、実装は名称変更だけです。

      private static bool DJNZ() => DEC_B();

      ■ スタック内容交換
      EX (SP),HL です…が、直接交換する方法がないので、一旦 Pop してから続いて HL の内容を Push する実装としました。

      private static void EX_SPHL() { ushort v = stack.Pop(); stack.Push(HL.Word); HL.Word = v; }

      最後に実行した結果についてとサンプルのダウンロードです。
      実行

      ※海外ビールだと私の一押しがハイネケン。不思議に瓶ビールのほうが美味しく感じます。グラスも良いなあって思ったので、特製グラス付きを紹介してみました。オランダ発祥らしいですが、世界中で飲めるのでもはや世界標準です!