CP/Mのシステムイメージファイルができたタイミングで、ディスクイメージも作成しておきましょう。CBIOS.ASMは、四つのディスクを想定したコードになっているので、ディスクイメージも四つ作成することにします。ディスクイメージの作成後は、一部のBIOSコールのエミュレーション用関数を実装し、CP/Mを起動するところまでやります。
Cpmtoolsのインストールとディスクイメージの作成
「Cpmtools」は、LinuxなどのPOSIX準拠システム上で、CP/Mのディスクイメージを操作するのに便利なツール集です。まずはこれをインストールします。Ubuntu 22.04 LTSでは、次のコマンドでインストールできます。
$ sudo apt install cpmtools
diskdefsファイルを作成する
Cpmtoolsに含まれるコマンドは、カレントディレクトリなどにある「diskdefs」ファイルを参照して、操作するディスクのフォーマットを判断します。そのため、diskdefsファイルをカレントディレクトリに作成し、次の設定を記述します。
diskdef ibm-3740
seclen 128
tracks 128
sectrk 64
blocksize 2048
maxdir 127
skew 0
boottrk 1
os 2.2
end
「ibm-3740」は、ディスクフォーマット名を指定しないときに使われるデフォルトのディスクフォーマット名です。以降の記述では、CBIOS.ASMのカスタマイズ時に設定したパラメータを指定します。
ディスクイメージを作成してCP/M用のファイルをコピー
diskdefsファイルを作成したら、Cpmtoolsに含まれるコマンドでさまざまな操作が可能になります。例えば、「disk1.dat」という1Miバイトのディスクイメージファイルを作成し、それをCP/M用にフォーマットするには、次のコマンドを実行します。mkfs.cpmコマンドの「-b」オプションには、システムトラックに書き込むシステムイメージファイルを指定します。同オプションを指定しない場合には、システムトラックは「0xE5」で埋められます。0xE5は、CP/Mにおいて空き領域を示すコードです。
$ dd if=/dev/zero of=disk1.dat bs=4096 count=256
$ mkfs.cpm -b CPM.img disk1.dat
作成したCP/Mのディスクイメージファイルにファイルをコピーするには、cpmcpコマンドを使います。例えば、カレントディレクトリにある「PIP.COM」ファイルをdisk1.datファイルにコピーするには、次のようにcpmcpコマンドを実行します。
$ cpmcp disk1.dat PIP.COM 0:
最後に指定している「0:」はユーザー番号を示します。CP/M-80のファイルシステムでは(現在普通に使われている)名前付きのディレクトリは利用できませんが、その代わり、「0」~「15」までのユーザー番号でファイルを分別できます。デフォルトのユーザー番号は「0」です。
ディスクイメージファイルのファイル一覧を表示するには、cpmlsコマンドを使います。「MBASIC.COM」「PIP.COM」ファイルをコピーした状態でcpmlsコマンドを実行すると、次のように表示されます。
$ cpmls disk1.dat
0:
mbasic.com
pip.com
なお、MBASIC.COMは、Microsoft BASICの実行ファイルです。http://www.retroarchive.org/cpm/lang/lang.htm などから入手できます。
同様の手順で「disk2.dat」「disk3.dat」「disk4.dat」も作成しておきます。disk1.datを単純にコピーしても構いません。
BIOSコールのエミュレーション用関数を作成
カスタマイズしたCBIOS.ASMファイルでは、次のように四つのBIOSコールの処理を外部に丸投げしています。
conin: ;console character into register a
jmp 0ffe0h
ret
conout: ;console character output from register c
mov a,c ;get to accumulator
jmp 0ffe1h
ret
read: ;perform read operation (usually this is similar to write
jmp 0ffe2h
write: ;perform a write operation
jmp 0ffe3h
そのため、これに対応する処理をCで書く必要があります。
conin/conoutの実装
コンソールに対する文字の入出力をする「conin」「conout」は簡単で、次のように(ひとまず)実装できます。
void bios() {
unsigned char c;
switch (regPC) {
case 0xFFE0:
c = getchar();
regA = (c & 0x7F);
break;
case 0xFFE1:
c = regA;
putchar(c);
break;
}
}
read/writeの実装
続いて、1セクター(128バイト)ずつディスクのデータを読み書きする「read」「write」を実装します。これらのBIOSコールでは、アクセス対象となるディスクに関する情報(トラック番号、セクター番号、DMAアドレス、ディスク番号)を取得する必要があります。ディスクに関する情報は、次のアセンブリコードで確保したBIOS領域に格納されます。そこで、まずはこの領域のアドレスを調べます。
track: ds 2 ;two bytes for expansion
sector: ds 2 ;two bytes for expansion
dmaad: ds 2 ;direct memory address
diskno: ds 1 ;disk number 0-15
ここで役立つのがCBIOS.ASMのアセンブル時に作成した「CBIOS.lst」ファイルです。これを見ると、この領域がアドレスF365Hから始まる部分にあることが分かります。
295 F365 track: ds 2 ;two bytes for expansion
296 F367 sector: ds 2 ;two bytes for expansion
297 F369 dmaad: ds 2 ;direct memory address
298 F36B diskno: ds 1 ;disk number 0-15
これが分かれば、readは次のように実装できます。
#define DISKINFO 0xF365
void bios() {
int i, fd, diskinfo, track, sector, dmaad, diskno;
unsigned char buffer[128];
switch (regPC) {
case 0xFFE2:
diskinfo = DISKINFO;
track = ram[diskinfo++] + (ram[diskinfo++] << 8);
sector = ram[diskinfo++] + (ram[diskinfo++] << 8);
dmaad = ram[diskinfo++] + (ram[diskinfo++] << 8);
diskno = ram[diskinfo];
if (diskno == 0) fd = open("disk1.dat", O_RDONLY);
else if (diskno == 1) fd = open("disk2.dat", O_RDONLY);
else if (diskno == 2) fd = open("disk3.dat", O_RDONLY);
else if (diskno == 3) fd = open("disk4.dat", O_RDONLY);
if (fd == -1) {
regA = 1;
break;
}
if (lseek(fd, (track*64 + sector) * 128, SEEK_SET) == -1) {
regA = 1;
close(fd);
break;
}
if (read(fd, buffer, 128) == -1) {
regA = 1;
close(fd);
break;
}
for (i=0; i<128; i++) {
ram[dmaad + i] = buffer[i];
}
regA = 0;
close(fd);
break;
}
}
シンプルにするために、セクターを一つ読み出すたびにファイルをopen()/close()するコードにしています。こんなコードでも、実際のフロッピディスクに比べるとはるかに高速にデータを読み出せますから、特に問題はありません。
同様にwriteも次のように実装できます。
#define DISKINFO 0xF365
void bios() {
int i, fd, diskinfo, track, sector, dmaad, diskno;
unsigned char buffer[128];
switch (regPC) {
case 0xFFE3:
diskinfo = DISKINFO;
track = ram[diskinfo++] + (ram[diskinfo++] << 8);
sector = ram[diskinfo++] + (ram[diskinfo++] << 8);
dmaad = ram[diskinfo++] + (ram[diskinfo++] << 8);
diskno = ram[diskinfo];
if (diskno == 0) fd = open("disk1.dat", O_WRONLY | O_SYNC);
else if (diskno == 1) fd = open("disk2.dat", O_WRONLY | O_SYNC);
else if (diskno == 2) fd = open("disk3.dat", O_WRONLY | O_SYNC);
else if (diskno == 3) fd = open("disk4.dat", O_WRONLY | O_SYNC);
if (fd == -1) {
regA = 1;
break;
}
if (lseek(fd, (track*64 + sector) * 128, SEEK_SET) == -1) {
regA = 1;
close(fd);
break;
}
for (i=0; i<128; i++) {
buffer[i] = ram[dmaad + i];
}
if (write(fd, buffer, 128) == -1) {
regA = 1;
close(fd);
break;
}
regA = 0;
close(fd);
break;
}
}
readと違うのは、ファイルのopen()時に、同期書き込みを指示する「O_SYNC」フラグを指定していることです。これによって変更がすぐにディスクイメージファイルに反映されます。
CP/M-80起動用のプログラムを作成
BIOSコールをエミュレーションできたので、次に示すコードのCP/M-80起動用のプログラム「startcpm.c」を作成します。このプログラムでは、CP/MのシステムイメージをアドレスDC00Hに読み込み、ウォームブート用のルーチンがあるF203Hからエミュレーションを開始します。コールドブート用のルーチンがあるF200Hにジャンプした場合には、BOOTラベルに移動してシステムイメージの読み込みから処理をやり直します。FFE0Hより上位のアドレスにジャンプした場合には、bios()関数を呼び出します。なお、BIOSコールの呼び出し用アドレス(FFE0H~FFE3H)には、RET命令のオペコード(0xC9)を書き込んでいます。
#include "i8080.h"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#define DISKINFO 0xF365
void bios() {
int straddr, i, fd, diskinfo, track, sector, dmaad, diskno;
unsigned char c, buffer[128];
switch (regPC) {
case 0xFFE0:
c = getchar();
regA = (c & 0x7F);
break;
case 0xFFE1:
c = regA;
putchar(c);
break;
case 0xFFE2:
diskinfo = DISKINFO;
track = ram[diskinfo++] + (ram[diskinfo++] << 8);
sector = ram[diskinfo++] + (ram[diskinfo++] << 8);
dmaad = ram[diskinfo++] + (ram[diskinfo++] << 8);
diskno = ram[diskinfo];
if (diskno == 0) fd = open("disk1.dat", O_RDONLY);
else if (diskno == 1) fd = open("disk2.dat", O_RDONLY);
else if (diskno == 2) fd = open("disk3.dat", O_RDONLY);
else if (diskno == 3) fd = open("disk4.dat", O_RDONLY);
if (fd == -1) {
regA = 1;
break;
}
if (lseek(fd, (track*64 + sector) * 128, SEEK_SET) == -1) {
regA = 1;
close(fd);
break;
}
if (read(fd, buffer, 128) == -1) {
regA = 1;
close(fd);
break;
case 0xFFE3:
diskinfo = DISKINFO;
track = ram[diskinfo++] + (ram[diskinfo++] << 8);
sector = ram[diskinfo++] + (ram[diskinfo++] << 8);
dmaad = ram[diskinfo++] + (ram[diskinfo++] << 8);
diskno = ram[diskinfo];
if (diskno == 0) fd = open("disk1.dat", O_WRONLY | O_SYNC);
else if (diskno == 1) fd = open("disk2.dat", O_WRONLY | O_SYNC);
else if (diskno == 2) fd = open("disk3.dat", O_WRONLY | O_SYNC);
else if (diskno == 3) fd = open("disk4.dat", O_WRONLY | O_SYNC);
if (fd == -1) {
regA = 1;
break;
}
if (lseek(fd, (track*64 + sector) * 128, SEEK_SET) == -1) {
regA = 1;
close(fd);
break;
}
for (i=0; i<128; i++) {
buffer[i] = ram[dmaad + i];
}
if (write(fd, buffer, 128) == -1) {
regA = 1;
close(fd);
break;
}
regA = 0;
close(fd);
break;
default:
break;
}
}
int main(int argc, char *argv[]) {
unsigned char opcode;
BOOT:
regPC = 0xF203; // warm boot
regSP = 0xFFFF;
regA = regB = regC = regD = regE = regH = regL = 0;
regF = 0b00000010;
intr_flag = 0;
FILE *fd = fopen("CPM.img", "r");
if (!fd) {
perror("can't open CPM.img");
exit(EXIT_FAILURE);
}
int c;
unsigned short i = 0xDC00;
while ((c = fgetc(fd)) != EOF) {
ram[i++] = (unsigned char)(c & 0xFF);
}
fclose(fd);
for(int i=0; i<4; i++) ram[0xFFE0 + i] = 0xC9;
printf("\nCP/M VERSION 2.2 EXTENSION 62k SYSTEM\n");
printf(" Copyright (C) 1979 by Digital Research\n");
for(;;) {
opcode = fetch();
exec(opcode);
// CP/M cold boot
if (regPC == 0xF200) {
goto BOOT;
}
// CP/M BIOS call
if (regPC >= 0xFFE0) {
bios();
}
}
}
CP/M-80の起動
startcpm.cをビルドしてできたstartcpmファイルを実行すると、次のようにCP/M-80が起動してCP/M-80アプリを利用できます。
$ ./startcpm
CP/M VERSION 2.2 EXTENSION 62k SYSTEM
Copyright (C) 1979 by Digital Research
A>dir
dir
A: PIP COM : MBASIC COM : CPUTEST COM : 8080EXM COM
A: 8080PRE COM : TST8080 COM
A>tst8080
tst8080
MICROCOSM ASSOCIATES 8080/8085 CPU DIAGNOSTIC
VERSION 1.0 (C) 1980
CPU IS OPERATIONAL
A>
CP/M-80終了用のコードは未実装なので、終了させる場合にはCtrl+cを入力してください。
bios()関数のconinコードを改良する
このままでもある程度のCP/Mアプリは動くのですが、MBASICのようにうまく動かないアプリもあります。MBASICがうまく動かないのは、LinuxとCP/M-80では、改行キーを押したときに入力されるコードが違うためです。そこで、bios()関数のconinコードを改良して、「0x0A」が入力された場合には、それを「0x0D」に変換することにします。また、ついでなので、Ctrl+oの入力でプログラムを終了できるようにします。
具体的には、bios()関数のconinコードを次のように書き換えます。
case 0xFFE0:
c = getchar();
if (c == 0x0A) {
c = 0x0D;
}
if (c == 0x0F) {
exit(0);
}
regA = (c & 0x7F);
break;
この書き換えにより、次のようにMBASICが動作するようになります。
$ ./startcpm
CP/M VERSION 2.2 EXTENSION 62k SYSTEM
Copyright (C) 1979 by Digital Research
A>mbasic
mbasic
BASIC-80 Rev. 5.21
[CP/M Version]
Copyright 1977-1981 (C) by Microsoft
Created: 28-Jul-81
32824 Bytes free
Ok
10 print "Hello!"
10 print "Hello!"
run
run
Hello!
Ok
system
system
A>
しかし、これでもまだ問題があります。入力した文字列が不必要にローカルエコー表示されてしまっています。これを回避するために、bios()関数をさらに次のように書き換えます。この改良コードでは、termiosという端末設定機能を使って、入力をRAWモード([Enter]キーを押さなくても入力を可能にするモード)にし、さらにローカルエコーを無効にしています。
#include <termios.h>
void bios() {
int straddr, i, fd, diskinfo, track, sector, dmaad, diskno;
unsigned char c, buffer[128];
struct termios settings, save_settings;
switch (regPC) {
case 0xFFE0:
tcgetattr(0,&save_settings);
settings = save_settings;
settings.c_lflag &= ~(ECHO|ICANON);
settings.c_cc[VTIME] = 0;
settings.c_cc[VMIN] = 1;
tcsetattr(0, TCSANOW, &settings);
c = getchar();
tcsetattr(0, TCSANOW, &save_settings);
if (c == 0x0A) {
c = 0x0D;
}
if (c == 0x0F) {
exit(0);
}
regA = (c & 0x7F);
break;
}
}
改良後は、MBASICも問題なく利用可能になります。
$ ./startcpm
CP/M VERSION 2.2 EXTENSION 62k SYSTEM
Copyright (C) 1979 by Digital Research
A>mbasic
BASIC-80 Rev. 5.21
[CP/M Version]
Copyright 1977-1981 (C) by Microsoft
Created: 28-Jul-81
32824 Bytes free
Ok
10 print "Hello!"
run
Hello!
Ok
system
A>
ここまでの作業で、当初の目的は達成できました。作成したプログラムのコードは、https://tsueyasu.com/wp-content/uploads/2024/08/startcpm.zip から入手できます。次回は、スクリーンエディタ「WordMaster」を利用できるようにします。
コメント