In /emu_8080.c, implement enough of the BDOS for MBASIC load and save commands
authorNick Downing <nick@ndcode.org>
Thu, 4 Dec 2025 11:14:02 +0000 (22:14 +1100)
committerNick Downing <nick@ndcode.org>
Thu, 11 Dec 2025 13:03:55 +0000 (00:03 +1100)
emu_8080.c

index c220af6..3bf7d4f 100644 (file)
@@ -1,8 +1,12 @@
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <unistd.h>
 #if ALT_BACKEND
 #include "8080/i8080.h"
 #else
@@ -57,6 +61,21 @@ uint8_t mem[MEM_SIZE];
 uint8_t io_read[IO_SIZE];
 uint8_t io_write[IO_SIZE];
 
+// BDOS emulation
+uint16_t bdos_dma;
+
+// drive to directory mapping
+// string from bdos_drives[] is prepended to filename from FCB
+// string must be less than ~0xf0 bytes long so eventual path doesn't overflow
+#define N_BDOS_DRIVES 1
+char *bdos_drives[N_BDOS_DRIVES] = {""};
+
+#define N_BDOS_FILES 10
+struct bdos_file {
+  int fd; // -1 if not in use
+  uint16_t fcb;
+} bdos_files[N_BDOS_FILES];
+
 int load_ihx(char *name) {
   FILE *fp = fopen(name, "r");
   if (fp == NULL) {
@@ -172,10 +191,97 @@ int load_ihx(char *name) {
   return entry_point;
 }
 
+// extract path from FCB
+bool fcb_path(uint16_t fcb, char *path) {
+  int drive = mem[fcb];
+  if (drive >= N_BDOS_DRIVES) {
+    errno = ENOTDIR;
+    return false;
+  }
+  char *q = path;
+  for (char *p = bdos_drives[drive]; *p; ++p)
+    *q++ = *p;
+  char *p = q;
+  for (int i = 0; i < 8; ++i)
+    *q++ = tolower(mem[(fcb + 1 + i) & 0xffff] & 0x7f);
+  while (q > p && q[-1] == ' ')
+    --q;
+  *q++ = '.';
+  p = q;
+  for (int i = 0; i < 3; ++i)
+    *q++ = tolower(mem[(fcb + 9 + i) & 0xffff] & 0x7f);
+  while (q > p && q[-1] == ' ')
+    --q;
+  if (q == p)
+    --q;
+  *q = 0;
+  return true;
+}
+
+bool fcb_open(uint16_t fcb, char *path, int flags, int mode) {
+  for (int i = 0; i < N_BDOS_FILES; ++i)
+    if (bdos_files[i].fd < 0) {
+      int fd = open(path, flags, mode);
+      if (fd == -1)
+        return false;
+      bdos_files[i] = (struct bdos_file){.fd = fd, .fcb = fcb};
+      return true;
+    }
+  errno = EMFILE;
+  return false;
+}
+
+int fcb_read(uint16_t fcb, uint16_t dma) {
+  for (int i = 0; i < N_BDOS_FILES; ++i)
+    if (bdos_files[i].fd >= 0 && bdos_files[i].fcb == fcb) {
+      uint8_t buf[0x80];
+      int count = (int)read(bdos_files[i].fd, buf, 0x80);
+      if (count == -1)
+        return -1;
+      if (count == 0)
+        return 0;
+      int j;
+      for (j = 0; j < count; ++j)
+        mem[(dma + j) & 0xffff] = buf[j];
+      for (; j < 0x80; ++j)
+        mem[(dma + j) & 0xffff] = 0x1a;
+      return 1;
+    }
+  errno = EBADFD;
+  return -1;
+}
+
+int fcb_write(uint16_t fcb, uint16_t dma) {
+  for (int i = 0; i < N_BDOS_FILES; ++i)
+    if (bdos_files[i].fd >= 0 && bdos_files[i].fcb == fcb) {
+      uint8_t buf[0x80];
+      for (int j = 0; j < 0x80; ++j)
+        buf[j] = mem[(dma + j) & 0xffff];
+      int count = (int)write(bdos_files[i].fd, buf, 0x80);
+      if (count == -1)
+        return -1;
+      if (count < 0x80)
+        return 0;
+      return 1;
+    }
+  errno = EBADFD;
+  return false;
+}
+
+bool fcb_close(uint16_t fcb) {
+  for (int i = 0; i < N_BDOS_FILES; ++i)
+    if (bdos_files[i].fd >= 0 && bdos_files[i].fcb == fcb) {
+      close(bdos_files[i].fd);
+      bdos_files[i].fd = -1;
+      return true;
+    }
+  errno = EBADFD;
+  return false;
+}
+
 int bdos(int a, int c, int de, uint16_t *hl) {
   switch (c) {
-  case 2:
-    // Character output
+  case 2: // C_WRITE - Console output
     de &= 0xff;
     switch (de) {
     case '\n':
@@ -188,8 +294,7 @@ int bdos(int a, int c, int de, uint16_t *hl) {
     }
     break;
 #if 0 // unlike MS-DOS version, the CP/M BASIC-80 uses direct BIOS calls
-  case 6:
-    // Direct console I/O
+  case 6: // C_RAWIO - Direct console I/O
     de &= 0xff;
     if (de == 0xff) {
       int data = getchar();
@@ -221,8 +326,7 @@ int bdos(int a, int c, int de, uint16_t *hl) {
     }
     break;
 #endif
-  case 9:
-    // Display string
+  case 9: // C_WRITESTR - Output string
     for (int data; (data = mem[de]) != '$'; de = (de + 1) & 0xffff)
       switch (data) {
       case '\n':
@@ -234,10 +338,103 @@ int bdos(int a, int c, int de, uint16_t *hl) {
         break;
       }
     break;
-  case 0xc:
-    // Get CP/M version
+  case 0xc: // S_BDOSVER - Return version number
     *hl = 0x22;
     break;
+  case 0xf: // F_OPEN - Open file
+    {
+      char path[0x100];
+      if (!fcb_path(de, path)) {
+        perror(path);
+        a = 0xff;
+        break;
+      }
+      fcb_close(de);
+      if (!fcb_open(de, path, O_RDWR, 0)) {
+        perror(path);
+        a = 0xff;
+        break;
+      }
+      a = 0;
+    }
+    break;
+  case 0x10: // F_CLOSE - Close file
+    if (!fcb_close(de)) {
+      a = 0xff;
+      break;
+    }
+    a = 0;
+    break;
+  case 0x13: // F_DELETE - delete file
+    // note: for now, we don't treat the ? character as a wildcard
+    {
+      char path[0x100];
+      if (!fcb_path(de, path)) {
+        perror(path);
+        a = 0xff;
+        break;
+      }
+      if (unlink(path) == -1) {
+        perror(path);
+        a = 0xff;
+        break;
+      }
+      fcb_close(de);
+      a = 0;
+    }
+    break;
+  case 0x14: // F_READ - read next record
+    switch (fcb_read(de, bdos_dma)) {
+    case -1:
+      perror("fcb_read()");
+      a = 0xff;
+      break;
+    case 0:
+      a = 1; // end of file
+      break;
+    case 1:
+      a = 0;
+      break;
+    default:
+      abort();
+    }
+    break;
+  case 0x15: // F_WRITE - write next record
+    switch (fcb_write(de, bdos_dma)) {
+    case -1:
+      perror("fcb_write()");
+      a = 0xff;
+      break;
+    case 0:
+      a = 2; // disc full
+      break;
+    case 1:
+      a = 0;
+      break;
+    default:
+      abort();
+    }
+    break;
+  case 0x16: // F_MAKE - create file
+    {
+      char path[0x100];
+      if (!fcb_path(de, path)) {
+        perror(path);
+        a = 0xff;
+        break;
+      }
+      fcb_close(de);
+      if (!fcb_open(de, path, O_RDWR | O_CREAT, 0666)) {
+        perror(path);
+        a = 0xff;
+        break;
+      }
+      a = 0;
+    }
+    break;
+  case 0x1a: // F_DMAOFF - Set DMA address
+    bdos_dma = de;
+    break;
   case 0x29: // DRV_ROVEC - Return bitmap of read-only drives
     *hl = 0;
     break;
@@ -450,6 +647,9 @@ int main(int argc, char **argv) {
   mem[BIOS_LISTST] = 0xc9; // ret
   mem[BIOS_SECTRAN] = 0xc9; // ret
 
+  for (int i = 0; i < N_BDOS_FILES; ++i)
+    bdos_files[i].fd = -1;
+
 #if ALT_BACKEND
   i8080 cpu;
   i8080_init(&cpu);