8080エミュレータを作ってCP/M-80を動かす(その1)

CP/M

久しぶりにCP/M-80を触りたくなりました。「RunCPM」などのソフトを使えばLinuxでCP/M-80アプリを動かせますが、せっかくならということでIntel 8080のエミュレータから作成してみることにします。作業の流れは、①8080エミュレータの作成、②エミュレータの動作確認、③CP/M-80のBDOSもしくはBIOSに相当するコードを作成、④CP/M-80のビルド、⑤CP/M-80と各種アプリの動作確認、という感じになるでしょう。作業環境は、Ubuntu(22.04 LTS)とします。

8080エミュレータの作成

最初に8080エミュレータを作ります。Z80にしなかったのは、もちろん8080の方が簡単だからです。オペコードが244種類しかありませんし、レジスタもフラグもZ80より少なくて楽です。CP/M-80やCP/M-80アプリは、基本的には8080で動作するように作られていますから、今回の目的には十分です。

エミュレータはCで書くことにしました。慣れているからというのはありますが、今後、ラズパイPicoなどのマイコンで動かすことも考慮したからです。

極力シンプルにするため、レジスタはグローバル変数とし、RAMはunsigned char ram[65536]という(これまたグローバルな)配列にします。プログラム本体では、現在のPCレジスタが指す位置からオペコードをfetch()関数で取得し、それをexec()関数で実行する処理をループさせます。exec()内では、switch~case文で各オペコードに応じた関数を呼び出します。普通の(愚直な)実装方法です。

8080のオペコード一覧は「Intel 8080 instruction set」を、各オペコードの動作については中日電工さんが公開している「8080命令説明書」を参考にしながらコードを実装していきました。割り込みに関するDI/EI命令と、入出力用のIN/OUT命令については、今回の用途では使わない見込みなので、ダミーの変数を読み書きするだけにしました。また、タイミングが重要なアプリを実行する予定もないため、実行サイクルについては無視します。

注意すべきなのは、Cフラグ(キャリーフラグ)とAフラグ(ハーフキャリーフラグ)の扱いです。加算命令では演算結果が8ビットの範囲を超えるときにCフラグが「1」になるのに対し、減算命令では(補数加算の)演算結果が8ビットの範囲を超えないときにCフラグが「1」になります。一方Aフラグは、加算命令でも減算命令でも、下位4ビット同士の演算結果が4ビットの範囲を超えるときに「1」になります。

複雑だったのはDAA命令です。各所で解説されているのですが、いまいち理解できない部分があったので、これについては既存の8080エミュレータ(https://github.com/superzazu/8080 )を参考にして、次のように実装しました。add8()は、8ビット加算命令を共通化する関数です。三つ目の引数は、キャリーフラグを加算に含めるかどうかを示します(「0」は含めない、「1」は含める)。

case 0x27:     //DAA
  f_bak = regF;
  add = 0;
  if ((regF & flagA) || ((regA & 0x0F) > 9)) {
    add += 0x06;
  }
  if ((regF & flagC) ||
      ((regA & 0xF0) >= 0x90) && ((regA & 0x0F) > 9) ||
      ((regA & 0xF0) > 0x90)) {
    add += 0x60;
    f_bak |= flagC;
  }
  regA = add8(regA, add, 0);
  regF &= ~flagC;
  regF |= (f_bak & flagC);
  break;

BDOSエミュレーションとCPUテストアプリの実行

ぼちぼちと実装していき、2日程度ですべての命令の処理を記述できたので、CP/M-80上で動くCPUテストアプリを実行してみます。今回使うCPUテストアプリ(後述)では、CP/M-80のBDOSコールのうち、ファンクション2(コンソール出力)とファンクション9(文字列出力)を使いますので、それをエミュレーションします。具体的には次のようなbdos()関数を用意しておき、PCレジスタの内容が「0x0005」になったときに呼び出すことにします。

void bdos(unsigned char number) {
  unsigned char c, low, high;
  switch(number) {
    case 2:
      putchar(regE);
      break;
    case 9:
      int i = 0;
      while ((c = ram[(regD << 8) + regE + i++]) != '$')
        putchar(c);
      break;
    default:
      break;
  }
  low = ram[regSP++];
  high = ram[regSP++];
  regPC = (high << 8) + low;
}

コードを見ると分かるように、CP/M-80の文字列の終端文字は「$」です。

8080用のCPUテストアプリには、「TST8080.COM」「8080PRE.COM」「8080EXM.COM」「CPUTEST.COM」などがあります。これらは、https://github.com/superzazu/8080/tree/master/cpu_tests などから入手可能です。

CP/M-80のアプリは、アドレス0100Hを先頭にして主メモリーに配置し、0100Hにジャンプすれば実行できます。そのため、CPUテストアプリも次のようなコードで読み込んで実行します。

#define LOAD_FILE "8080EXM.COM"
FILE *fd = fopen(LOAD_FILE, "r");
if (!fd) {
  fprintf(stderr, "can't open %s", LOAD_FILE);
  exit(EXIT_FAILURE);
}
unsigned short i = 0x0100;
while ((int c = fgetc(fd)) != EOF) {
  ram[i++] = (unsigned char)(c & 0xFF);
}
fclose(fd);

for(;;) {
  opcode = fetch();
  exec(opcode);
  if (regPC == 0x0005) {
    bdos(regC);
  } else if (regPC == 0x0000) {
    putchar('\n');
    exit(0);
  }
}

TST8080.COMの実行結果は次の通りでした。問題なさそうです。

$ ./i8080-test
MICROCOSM ASSOCIATES 8080/8085 CPU DIAGNOSTIC
 VERSION 1.0  (C) 1980

 CPU IS OPERATIONAL

8080PRE.COMの実行結果は次の通りでした。こちらも問題なさそうです。

$ ./i8080-test
8080 Preliminary tests complete

しかし、8080EXM.COMを実行すると、次のようにエラーが表示されました。

$ ./i8080-test
8080 instruction exerciser
dad <b,d,h,sp>................  PASS! crc is:14474ba6
aluop nn......................  ERROR **** crc expected:9e922f9e found:7799ea9d
aluop <b,c,d,e,h,l,m,a>.......  ERROR **** crc expected:cf762c86 found:b3491c2a
<daa,cma,stc,cmc>.............  PASS! crc is:bb3f030c
<inr,dcr> a...................  PASS! crc is:adb6460e
<inr,dcr> b...................  PASS! crc is:83ed1345
<inx,dcx> b...................  PASS! crc is:f79287cd
<inr,dcr> c...................  PASS! crc is:e5f6721b
<inr,dcr> d...................  PASS! crc is:15b5579a
<inx,dcx> d...................  PASS! crc is:7f4e2501
<inr,dcr> e...................  PASS! crc is:cf2ab396
<inr,dcr> h...................  PASS! crc is:12b2952c
<inx,dcx> h...................  PASS! crc is:9f2b23c0
<inr,dcr> l...................  PASS! crc is:ff57d356
<inr,dcr> m...................  PASS! crc is:92e963bd
<inx,dcx> sp..................  PASS! crc is:d5702fab
lhld nnnn.....................  PASS! crc is:a9c3d5cb
shld nnnn.....................  PASS! crc is:e8864f26
lxi <b,d,h,sp>,nnnn...........  PASS! crc is:fcf46e12
ldax <b,d>....................  PASS! crc is:2b821d5f
mvi <b,c,d,e,h,l,m,a>,nn......  PASS! crc is:eaa72044
mov <bcdehla>,<bcdehla>.......  PASS! crc is:10b58cee
sta nnnn / lda nnnn...........  PASS! crc is:ed57af72
<rlc,rrc,ral,rar>.............  PASS! crc is:e0d89235
stax <b,d>....................  PASS! crc is:2b0471e9
Tests complete

原因を調べるためにCPUTEST.COMを実行すると、次のように表示されました。

$ ./i8080-test

DIAGNOSTICS II V1.2 - CPU TEST
COPYRIGHT (C) 1981 - SUPERSOFT ASSOCIATES

ABCDEFGHIJKLMNOPQRSTUVWXYZ
CPU IS 8080/8085
BEGIN TIMING TEST
END TIMING TEST

CPU FAILED:
ERROR COUNT 0001H

INSTRUCTION SEQUENCE WAS A00000H
REGISTER f CONTAINS 46H
BUT SHOULD CONTAIN 59H
REGISTER VALUE BEFORE INSTRUCTION SEQUENCE WAS 56H
TEST NUMBER  01A7H

重要なのは「INSTRUCTION SEQUENCE WAS A00000H」の部分です。これは「A0」というオペコード、つまり「ANA B」命令の実行時にエラーが生じたことを示しています。エラーの内容は続く部分に示されていて、命令実行後のFレジスタ(フラグレジスタ)の値が正しくないとのことです。

調査したところ、AND演算をする8080の命令(ANA/ANI)は、Aフラグの扱いがインテルの一部のマニュアルとは異なる動作をするようです(詳しくはこちらこちらこちらなどを参照してください)。初期のマニュアルでは「Aフラグには影響しない」や「Aフラグはクリアされる」などと書かれていました。それが後に出たマニュアルでは、AND演算をする命令で「X and Y」という処理をするとき、XとYのどちらかの下位4ビットの最上位ビットが「1」ならばAフラグがセットされると説明されています。ただし、ANI命令ではAフラグはクリアされると書かれているマニュアルもあって、実際にどうなのかは議論が続いていました。しかし、互換品を含めて8080を多数コレクションしている人が動作を調べた結果、(AMDの互換品以外は)ANA命令とANI命令の両方で、XとYのどちらかの下位4ビットの最上位ビットが「1」ならばAフラグがセットされることが分かったそうです。

そこで、そのようにエミュレータのコードを書き換えたところ、次のように8080EXM.COMがエラーなく完走するようになりました。

$ ./i8080-test
8080 instruction exerciser
dad <b,d,h,sp>................  PASS! crc is:14474ba6
aluop nn......................  PASS! crc is:9e922f9e
aluop <b,c,d,e,h,l,m,a>.......  PASS! crc is:cf762c86
<daa,cma,stc,cmc>.............  PASS! crc is:bb3f030c
<inr,dcr> a...................  PASS! crc is:adb6460e
<inr,dcr> b...................  PASS! crc is:83ed1345
<inx,dcx> b...................  PASS! crc is:f79287cd
<inr,dcr> c...................  PASS! crc is:e5f6721b
<inr,dcr> d...................  PASS! crc is:15b5579a
<inx,dcx> d...................  PASS! crc is:7f4e2501
<inr,dcr> e...................  PASS! crc is:cf2ab396
<inr,dcr> h...................  PASS! crc is:12b2952c
<inx,dcx> h...................  PASS! crc is:9f2b23c0
<inr,dcr> l...................  PASS! crc is:ff57d356
<inr,dcr> m...................  PASS! crc is:92e963bd
<inx,dcx> sp..................  PASS! crc is:d5702fab
lhld nnnn.....................  PASS! crc is:a9c3d5cb
shld nnnn.....................  PASS! crc is:e8864f26
lxi <b,d,h,sp>,nnnn...........  PASS! crc is:fcf46e12
ldax <b,d>....................  PASS! crc is:2b821d5f
mvi <b,c,d,e,h,l,m,a>,nn......  PASS! crc is:eaa72044
mov <bcdehla>,<bcdehla>.......  PASS! crc is:10b58cee
sta nnnn / lda nnnn...........  PASS! crc is:ed57af72
<rlc,rrc,ral,rar>.............  PASS! crc is:e0d89235
stax <b,d>....................  PASS! crc is:2b0471e9
Tests complete

これで、ひとまず8080エミュレータが完成しました。完成したコードは https://tsueyasu.com/wp-content/uploads/2024/08/i8080-emulator.zip から入手できます。

コメント

タイトルとURLをコピーしました