|
@@ -0,0 +1,522 @@
|
|
|
+/*
|
|
|
+ * abcdisk.c
|
|
|
+ *
|
|
|
+ * Emulate an ABC80/800 disk controller
|
|
|
+ */
|
|
|
+
|
|
|
+#include "fw.h"
|
|
|
+#include "ff.h"
|
|
|
+
|
|
|
+/* Option flags, mostly for debugging */
|
|
|
+#define NOTTHERE 0
|
|
|
+#define READONLY 0
|
|
|
+#define INTERLEAVE 0
|
|
|
+
|
|
|
+/* Disk controller I/O state machine */
|
|
|
+enum controller_state {
|
|
|
+ ctl_uninit, /* Disks not initialized */
|
|
|
+ ctl_command, /* Expecting command */
|
|
|
+ ctl_upload, /* Expecting data block upload */
|
|
|
+ ctl_download /* Expecting data block download */
|
|
|
+};
|
|
|
+
|
|
|
+enum drive_flags {
|
|
|
+ DF_MOUNTED, /* Host file open, drive exported */
|
|
|
+ DF_READONLY, /* Drive is readonly */
|
|
|
+ DF_DISABLED /* Drive is disabled */
|
|
|
+};
|
|
|
+
|
|
|
+/* Per-drive state */
|
|
|
+struct drive_state {
|
|
|
+ FIL file; /* Host file */
|
|
|
+ char name[4]; /* Drive name */
|
|
|
+ uint16_t sectors; /* Total size in 256-byte sectors */
|
|
|
+ uint8_t ilmsk, ilfac; /* Software interleaving parameters */
|
|
|
+ enum drive_flags flags;
|
|
|
+ enum drive_flags force; /* Option to force flags */
|
|
|
+};
|
|
|
+
|
|
|
+/* Per-controller state */
|
|
|
+struct ctl_state {
|
|
|
+ enum controller_state state;
|
|
|
+ uint8_t k[4]; /* Command bytes */
|
|
|
+ uint8_t clustshift; /* log2(clustersize/256) */
|
|
|
+ uint8_t devsel; /* I/O device select */
|
|
|
+ uint16_t maxsectors; /* Maximum sectors for this controller */
|
|
|
+ uint8_t c, h, s; /* Disk geometry */
|
|
|
+ uint8_t drives; /* Total drives present */
|
|
|
+ bool newaddr; /* "New addressing" */
|
|
|
+ bool fmtdata_in_buf; /* Use user-provided formatting data */
|
|
|
+ const char name[4]; /* Name of controller (disk type) */
|
|
|
+ uint8_t ilmsk, ilfac; /* Software interleaving parea */
|
|
|
+ uint8_t error; /* Error code status */
|
|
|
+ struct drive_state drv[8]; /* Per-drive state */
|
|
|
+ uint8_t buf[4][256]; /* 4 host buffers @ 256 bytes */
|
|
|
+};
|
|
|
+
|
|
|
+/*
|
|
|
+ * DOSGEN depends on this value... and different DOSGEN
|
|
|
+ * expect different values. If this value is wrong, DOSGEN
|
|
|
+ * will spin forever on "testing sector..."
|
|
|
+ */
|
|
|
+#define OUT_OF_RANGE 0x21 /* Status code for an invalid sector */
|
|
|
+
|
|
|
+enum controller_types {
|
|
|
+ MOx,
|
|
|
+ MFx,
|
|
|
+ SFx,
|
|
|
+ HDx,
|
|
|
+ CONTROLLER_TYPES
|
|
|
+};
|
|
|
+
|
|
|
+static struct ctl_state controllers[CONTROLLER_TYPES] = {
|
|
|
+ /*
|
|
|
+ * MOx: covers all of these formats:
|
|
|
+ * SSSD = 40×8×1 (80K, FD2/DD80),
|
|
|
+ * SSDD = 40×16×1 (160K, FD2D/DD82/ABC830) and
|
|
|
+ * DSDD = 40×16×2 (320K, FD4D/DD84/DD52)
|
|
|
+ */
|
|
|
+ [MOx] = {
|
|
|
+ .devsel = 45,
|
|
|
+ .clustshift = 0,
|
|
|
+ .maxsectors = 40 * 2 * 16,
|
|
|
+ .c = 40, .h = 2, .s = 16,
|
|
|
+ .fmtdata_in_buf = true,
|
|
|
+ .ilmsk = 15,
|
|
|
+ .ilfac = 7,
|
|
|
+ .name = "mo"
|
|
|
+ },
|
|
|
+
|
|
|
+ /* MFx: DSQD = 80×16x2 (640K, ABC832/834) */
|
|
|
+ [MFx] = {
|
|
|
+ .devsel = 44,
|
|
|
+ .clustshift = 2,
|
|
|
+ .maxsectors = 80 * 2 * 16,
|
|
|
+ .c = 80, .h = 2, .s = 16,
|
|
|
+ .name = "mf"
|
|
|
+ },
|
|
|
+
|
|
|
+ /* SFx: 8" floppy (DD88, ABC838) */
|
|
|
+ [SFx] = {
|
|
|
+ .devsel = 46,
|
|
|
+ .clustshift = 2,
|
|
|
+ .maxsectors = (77 * 2 - 1) * 26, /* Track 0, side 0 not used */
|
|
|
+ .c = 77, .h = 2, .s = 26,
|
|
|
+ .name = "sf"
|
|
|
+ },
|
|
|
+
|
|
|
+ [HDx] = {
|
|
|
+ .devsel = 36,
|
|
|
+ .clustshift = 5,
|
|
|
+ .newaddr = true, /* Actually irrelevant for clustshift = 5 */
|
|
|
+ .maxsectors = (239 * 32 - 1) * 32, /* Maximum supported by UFD-DOS */
|
|
|
+ .c = 238, .h = 16, .s = 64,
|
|
|
+ .name = "hd"
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+static inline bool mounted(const struct drive_state *drv)
|
|
|
+{
|
|
|
+ return !!(drv->flags & DF_MOUNTED);
|
|
|
+}
|
|
|
+
|
|
|
+static inline struct drive_state *cur_drv_mutable(struct ctl_state *state)
|
|
|
+{
|
|
|
+ return &state->drv[state->k[1] & 7];
|
|
|
+}
|
|
|
+
|
|
|
+static inline const struct drive_state *cur_drv(const struct ctl_state *state)
|
|
|
+{
|
|
|
+ return &state->drv[state->k[1] & 7];
|
|
|
+}
|
|
|
+
|
|
|
+static inline unsigned int cur_sector(const struct ctl_state *state)
|
|
|
+{
|
|
|
+ uint8_t k2 = state->k[2], k3 = state->k[3];
|
|
|
+
|
|
|
+ if (state->newaddr)
|
|
|
+ return (k2 << 8) + k3;
|
|
|
+ else
|
|
|
+ return (((k2 << 3) + (k3 >> 5)) << state->clustshift) + (k3 & 31);
|
|
|
+}
|
|
|
+
|
|
|
+/* Get physical sector number, after interleaving */
|
|
|
+static inline unsigned int
|
|
|
+virt2phys(const struct drive_state *drv, unsigned int sector)
|
|
|
+{
|
|
|
+ unsigned int ilmsk = drv->ilmsk;
|
|
|
+ unsigned int ilfac = drv->ilfac;
|
|
|
+
|
|
|
+ sector = (sector & ~ilmsk) | ((sector * ilfac) & ilmsk);
|
|
|
+ return sector;
|
|
|
+}
|
|
|
+
|
|
|
+static inline unsigned int phys_sector(const struct ctl_state *state)
|
|
|
+{
|
|
|
+ return virt2phys(cur_drv(state), cur_sector(state));
|
|
|
+}
|
|
|
+
|
|
|
+static inline off_t file_pos(const struct ctl_state *state)
|
|
|
+{
|
|
|
+ return phys_sector(state) << 8;
|
|
|
+}
|
|
|
+
|
|
|
+static inline bool cur_sector_valid(const struct ctl_state *state)
|
|
|
+{
|
|
|
+ uint8_t k3 = state->k[3];
|
|
|
+
|
|
|
+ if (!state->newaddr && ((k3 & 31) >> state->clustshift))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ return cur_sector(state) < cur_drv(state)->sectors;
|
|
|
+}
|
|
|
+
|
|
|
+static inline bool file_pos_valid(const struct ctl_state *state)
|
|
|
+{
|
|
|
+ return file_pos(state) < cur_drv(state)->hf->filesize - 255;
|
|
|
+}
|
|
|
+
|
|
|
+static inline uint8_t *cur_buf(struct ctl_state *state)
|
|
|
+{
|
|
|
+ return state->buf[state->k[1] >> 6];
|
|
|
+}
|
|
|
+
|
|
|
+static void disk_reset_state(struct ctl_state *state)
|
|
|
+{
|
|
|
+ int i;
|
|
|
+
|
|
|
+ state->state = disk_k0;
|
|
|
+ state->error = 0;
|
|
|
+ state->in_ptr = -1;
|
|
|
+ state->out_ptr = 0;
|
|
|
+ state->notready_ctr = NOT_READY;
|
|
|
+
|
|
|
+ for (i = 0; i < 8; i++)
|
|
|
+ f_sync(&state->drv[i].file);
|
|
|
+}
|
|
|
+
|
|
|
+static struct drive_state *
|
|
|
+name_to_drive(const char *drive)
|
|
|
+{
|
|
|
+ int sel;
|
|
|
+ struct ctl_state *state;
|
|
|
+ unsigned int ndrive;
|
|
|
+
|
|
|
+ /* All drive names are three letters long */
|
|
|
+ if (strlen(drive) != 3)
|
|
|
+ return NULL;
|
|
|
+
|
|
|
+ ndrive = drive[2] - '0';
|
|
|
+ if (ndrive > 7)
|
|
|
+ return NULL;
|
|
|
+
|
|
|
+ if (!memcmp("dr", drive, 2)) {
|
|
|
+ /* DRx alias for MOx (matches "old DOS") */
|
|
|
+ return &mo_state.drv[ndrive];
|
|
|
+ }
|
|
|
+
|
|
|
+ for (sel = 0; sel < 64; sel++) {
|
|
|
+ state = sel_to_state[sel];
|
|
|
+ if (!state)
|
|
|
+ continue;
|
|
|
+ if (!memcmp(state->name, drive, 2))
|
|
|
+ return &state->drv[ndrive];
|
|
|
+ }
|
|
|
+
|
|
|
+ return NULL; /* No such disk */
|
|
|
+}
|
|
|
+
|
|
|
+bool valid_drive_name(const char *drive)
|
|
|
+{
|
|
|
+ return name_to_drive(drive) != NULL;
|
|
|
+}
|
|
|
+
|
|
|
+static int mount_drive(struct drive_state *drv, const char *filename)
|
|
|
+{
|
|
|
+ if (mounted(drv)) {
|
|
|
+ f_close(&drv->file);
|
|
|
+ drv->flags &= ~DF_MOUNTED;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (drv->force & DF_DISABLED)
|
|
|
+ return -1;
|
|
|
+
|
|
|
+ drv->flags = drv->force & ~DF_MOUNTED;
|
|
|
+
|
|
|
+ if (!filename) {
|
|
|
+ return 0; /* Explicit unmount */
|
|
|
+ } else {
|
|
|
+ while (1) {
|
|
|
+ BYTE mode = FA_OPEN_EXISTING | FA_READ;
|
|
|
+
|
|
|
+ if (!(drv->flags & DF_READONLY))
|
|
|
+ mode |= FA_WRITE;
|
|
|
+
|
|
|
+ FRESULT rv = f_open(&drv->file, filename, mode);
|
|
|
+
|
|
|
+ if (rv == FR_WRITE_PROTECTED && (mode & FA_WRITE)) {
|
|
|
+ drv->flags |= DF_READONLY;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (rv != FR_OK)
|
|
|
+ drv->flags |= DF_DISABLED;
|
|
|
+ else
|
|
|
+ drv->flags |= DF_MOUNTED;
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!(drv->flags & DF_MOUNTED))
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Smaller than the standard disk size? Treat the sectors
|
|
|
+ * beyond the end as bad.
|
|
|
+ */
|
|
|
+ unsigned int filesec = f_size(drv->file) >> 8;
|
|
|
+ drv->sectors = (filesec && filesec < state->maxsectors)
|
|
|
+ ? filesec : state->maxsectors;
|
|
|
+
|
|
|
+ /* Interleaving parameters */
|
|
|
+#if INTERLEAVE
|
|
|
+ drv->ilfac = state->ilfac;
|
|
|
+ drv->ilmsk = state->ilmsk;
|
|
|
+#else
|
|
|
+ drv->ilfac = drv->ilmsk = 0;
|
|
|
+#endif
|
|
|
+
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+static const char * const disk_pfx_abc80[] = { "/abcdisk.80/", "/abcdisk/", "/", NULL };
|
|
|
+static const char * const disk_pfx_abc800[] = { "/abcdisk.800/", "/abcdisk/", "/", NULL };
|
|
|
+
|
|
|
+static void disk_init(struct ctl_state *state)
|
|
|
+{
|
|
|
+ uint32_t abc_status = ABC_STATUS;
|
|
|
+ const char * const *pfx_list;
|
|
|
+ int i;
|
|
|
+
|
|
|
+ /* If no ABC, don't initialize disks; should in fact unmount them all */
|
|
|
+ if (!(abc_status & ABC_STATUS_LIVE))
|
|
|
+ return;
|
|
|
+
|
|
|
+ pfx_list = (abc_status & ABC_STATUS_800) ? disk_pfx_abc800 : disk_pfx_abc80;
|
|
|
+
|
|
|
+ /* If any of these don't exist we simply report device not ready */
|
|
|
+ state->drives = 0;
|
|
|
+
|
|
|
+ for (i = 0; i < 8; i++) {
|
|
|
+ struct drive_state *drv = &state->drv[i];
|
|
|
+ unsigned int filesec;
|
|
|
+ const char **pfx;
|
|
|
+
|
|
|
+ snprintf(drv->name, sizeof drv->name, "%-.2s%c", state->name, i + '0');
|
|
|
+
|
|
|
+ for (pfx = pfx_list; *pfx; pfx++) {
|
|
|
+ char filename_buf[64];
|
|
|
+ snprintf(filename_buf, sizeof filename_buf, "%s%s", *pfx, drv->name);
|
|
|
+
|
|
|
+ if (!mount_drive(drv, filename_buf)) {
|
|
|
+ state->drives++;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ disk_reset_state(state);
|
|
|
+}
|
|
|
+
|
|
|
+static void setup_dma(struct drive_state *drv, unsigned int len, uint8_t devsel, bool out_dir)
|
|
|
+{
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+static void do_next_command(struct ctl_state *state)
|
|
|
+{
|
|
|
+ struct drive_state *drv = cur_drv_mutable(state);
|
|
|
+ uint8_t *buf = cur_buf(state);
|
|
|
+
|
|
|
+ if (state->k[0] & 0x01) {
|
|
|
+ /* READ SECTOR */
|
|
|
+ if (!) {
|
|
|
+ state->error = 0x80; /* Device not ready */
|
|
|
+ } else if (!cur_sector_valid(state)) {
|
|
|
+ state->error = OUT_OF_RANGE;
|
|
|
+ } else if (!file_pos_valid(state)) {
|
|
|
+ state->error = 0x08; /* CRC error(?) */
|
|
|
+ } else {
|
|
|
+ UINT rlen = 0;
|
|
|
+ FRESULT rv;
|
|
|
+
|
|
|
+ rv = f_lseek(&drv->file, file_pos(state));
|
|
|
+ if (rv == FR_OK)
|
|
|
+ rv = f_read(&drv->file, buf, 256, &rlen);
|
|
|
+ if (rv != FR_OK || rlen != 256) {
|
|
|
+ state->error = 0x08; /* CRC error */
|
|
|
+ }
|
|
|
+ }
|
|
|
+ state->k[0] &= ~0x01; /* Command done */
|
|
|
+ }
|
|
|
+ if (state->k[0] & 0x02) {
|
|
|
+ /* SECTOR TO HOST */
|
|
|
+
|
|
|
+ state->in_ptr = 0;
|
|
|
+ state->state = disk_download;
|
|
|
+ state->k[0] &= ~0x02; /* Command done */
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (state->k[0] & 0x04) {
|
|
|
+ /* SECTOR FROM HOST */
|
|
|
+ state->state = disk_upload;
|
|
|
+ state->out_ptr = 0;
|
|
|
+ state->k[0] &= ~0x04; /* Command done */
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (state->k[0] & 0x08) {
|
|
|
+ /* WRITE SECTOR */
|
|
|
+ if (!hf) {
|
|
|
+ state->error = 0x80; /* Not ready */
|
|
|
+ } else if (!file_wrok(hf)) {
|
|
|
+ state->error = 0x40; /* Write protect */
|
|
|
+ } else if (!cur_sector_valid(state)) {
|
|
|
+ state->error = OUT_OF_RANGE;
|
|
|
+ if (tracing(TRACE_DISK)) {
|
|
|
+ fprintf(tracef, "%s: write: disk sector out of range: %u/%u (cluster %u/%u)\n",
|
|
|
+ drv->name, cur_sector(state), drv->sectors,
|
|
|
+ cur_sector(state) >> state->clustshift,
|
|
|
+ drv->sectors >> state->clustshift);
|
|
|
+ }
|
|
|
+ } else if (!file_pos_valid(state)) {
|
|
|
+ state->error = 0x08; /* CRC error(?) */
|
|
|
+ } else {
|
|
|
+ if (hf->map) {
|
|
|
+ memcpy(hf->map + file_pos(state), buf, 256);
|
|
|
+ } else {
|
|
|
+ clearerr(hf->f);
|
|
|
+ fseek(hf->f, file_pos(state), SEEK_SET);
|
|
|
+ fwrite(buf, 1, 256, hf->f);
|
|
|
+ if (ferror(hf->f))
|
|
|
+ state->error = 0x20; /* Write fault */
|
|
|
+ }
|
|
|
+ }
|
|
|
+ state->k[0] &= ~0x08; /* Command done */
|
|
|
+ }
|
|
|
+ if (state->k[0] & 0x10 && state->k[1] & 0x08) {
|
|
|
+ state->out_ptr = 0;
|
|
|
+ /* FORMAT */
|
|
|
+ if (!drv->hf) {
|
|
|
+ state->error = 0x80; /* Not ready */
|
|
|
+ } else if (!file_wrok(hf)) {
|
|
|
+ state->error = 0x40; /* Write protect */
|
|
|
+ } else {
|
|
|
+ unsigned int s, c0, c1, s0, s1;
|
|
|
+ unsigned int cylsec = state->s * state->h;
|
|
|
+ uint8_t data[256];
|
|
|
+ unsigned int fmtsec, filesec;
|
|
|
+
|
|
|
+ /* Sector count produced by format */
|
|
|
+ fmtsec = state->maxsectors;
|
|
|
+
|
|
|
+ /* For non-MO-drives, this seems to be internally generated */
|
|
|
+ memset(data, 0x40, 256);
|
|
|
+
|
|
|
+ if (state->fmtdata_in_buf) {
|
|
|
+ /*
|
|
|
+ * MO drives put the sector image in the buffers, for
|
|
|
+ * backwards compatibility and to support single density.
|
|
|
+ *
|
|
|
+ * Right before the F7 header CRC opcode is a density byte;
|
|
|
+ * 00 for single, and 01 for double. The data begins after
|
|
|
+ * a byte of FB.
|
|
|
+ */
|
|
|
+ bool single = false;
|
|
|
+ const uint8_t *p, *ep;
|
|
|
+
|
|
|
+ ep = state->buf[1];
|
|
|
+ for (p = state->buf[0]+1; p < ep; p++) {
|
|
|
+ if (*p == 0xf7)
|
|
|
+ single = (p[-1] == 0);
|
|
|
+ if (*p == 0xfb)
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ fmtsec >>= single;
|
|
|
+
|
|
|
+ if (*p++ == 0xfb) { /* Data block found */
|
|
|
+ if (single) {
|
|
|
+ /* Really two 128-byte sectors! */
|
|
|
+ memcpy(data, p, 128);
|
|
|
+ memcpy(data+128, p, 128);
|
|
|
+ } else {
|
|
|
+ memcpy(data, p, 256);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Adjust the size of the accessible device to the smallest
|
|
|
+ * of the physical file and the formatted size
|
|
|
+ */
|
|
|
+ filesec = drv->hf->filesize >> 8;
|
|
|
+ drv->sectors = (filesec && filesec < fmtsec) ? filesec : fmtsec;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * k2 and k3 contain the first and last cylinder numbers to
|
|
|
+ * format, inclusively. The last cylinder may be partial due
|
|
|
+ * to virtual remapping, e.g. for sf floppies.
|
|
|
+ */
|
|
|
+
|
|
|
+ c0 = state->k[2];
|
|
|
+ s0 = c0 * cylsec;
|
|
|
+ c1 = state->k[3] + 1;
|
|
|
+ s1 = c1 * cylsec;
|
|
|
+
|
|
|
+ if (tracing(TRACE_DISK)) {
|
|
|
+ fprintf(tracef, "%s: formatting cyl %u..%u, sectors %u..%u\n",
|
|
|
+ drv->name, c0, c1-1, s0, s1-1);
|
|
|
+ }
|
|
|
+
|
|
|
+ clearerr(hf->f);
|
|
|
+ state->error = 0;
|
|
|
+
|
|
|
+ for (s = s0; s < s1; s++) {
|
|
|
+ unsigned int ps = virt2phys(drv, s);
|
|
|
+ if (ps >= drv->sectors) {
|
|
|
+ state->error |= 0x02; /* Track 0/Lost data? */
|
|
|
+ break;
|
|
|
+ } else if (hf->map) {
|
|
|
+ memcpy(hf->map + (ps << 8), data, 256);
|
|
|
+ } else {
|
|
|
+ fseek(hf->f, ps << 8, SEEK_SET);
|
|
|
+ fwrite(data, 1, 256, hf->f);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (ferror(hf->f))
|
|
|
+ state->error |= 0x20; /* Write fault */
|
|
|
+ }
|
|
|
+ state->k[1] &= ~0x08;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!(state->k[1] & 0x38))
|
|
|
+ state->k[0] &= ~0x10;
|
|
|
+
|
|
|
+ if (tracing(TRACE_DISK)) {
|
|
|
+ if (state->trace_dump) {
|
|
|
+ trace_dump(TRACE_DISK, drv->name, buf, 256);
|
|
|
+ state->trace_dump = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ state->state = disk_k0;
|
|
|
+}
|
|
|
+
|
|
|
+static void disk_reset(uint8_t sel)
|
|
|
+{
|
|
|
+ struct ctl_state *state = sel_to_state[sel];
|
|
|
+
|
|
|
+ if (state && state->state != disk_need_init)
|
|
|
+ disk_reset_state(state);
|
|
|
+}
|
|
|
+
|