aboutsummaryrefslogtreecommitdiff
path: root/dos
diff options
context:
space:
mode:
Diffstat (limited to 'dos')
-rw-r--r--dos/ch375dos.h103
-rw-r--r--dos/chud.c418
-rw-r--r--dos/chudinit.c226
-rw-r--r--dos/chudisk.c895
-rw-r--r--dos/chudisk.h93
-rw-r--r--dos/chudisk.lnk4
-rw-r--r--dos/cio.c147
-rw-r--r--dos/cio.h31
-rw-r--r--dos/inc/bda.h67
-rw-r--r--dos/inc/bpb.h83
-rw-r--r--dos/inc/dosapi.h182
-rw-r--r--dos/inc/dosdrv.h319
-rw-r--r--dos/inc/dosint.h250
-rw-r--r--dos/inc/mbr.h46
-rw-r--r--dos/makefile66
-rw-r--r--dos/utest.c500
-rw-r--r--dos/utils.h60
17 files changed, 3490 insertions, 0 deletions
diff --git a/dos/ch375dos.h b/dos/ch375dos.h
new file mode 100644
index 0000000..d0ee84c
--- /dev/null
+++ b/dos/ch375dos.h
@@ -0,0 +1,103 @@
+/*
+ * CHU - IO routines to CH375 from DOS
+ * Copyright (C) 2024-2026 Javier S. Pedro
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef CH375DOS_H
+#define CH375DOS_H
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <conio.h>
+#include <ch375.h>
+
+/* This file defines the routines that are used by ch375.h to actually IO
+ * to the underlying device. */
+
+struct ch375 {
+ uint16_t io_base;
+};
+
+static inline uint8_t ch375_read_data(struct ch375 *dev)
+{
+ return inp(dev->io_base + CH375_PORT_DATA);
+}
+
+static inline void ch375_write_data(struct ch375 *dev, uint8_t data)
+{
+ outp(dev->io_base + CH375_PORT_DATA, data);
+}
+
+static inline void ch375_send_command(struct ch375 *dev, enum ch375_command cmd)
+{
+ outp(dev->io_base + CH375_PORT_CMD, cmd);
+}
+
+/// True if interrupt line is currently active (i.e. low)
+static inline bool ch375_poll_interrupt(struct ch375 *dev)
+{
+ return !(inp(dev->io_base + CH375_PORT_CMD) & 0x80);
+}
+
+
+/** Reads one transfer block (64 bytes max). */
+#if _M_IX86 >= 200
+// Watcom does not seem able to synthesize rep ins/outs. Do it in 286+ mode.
+static uint8_t ch375_read_data_block(struct ch375 *dev, uint8_t far *buffer);
+#pragma aux ch375_read_data_block = \
+ "mov dx, [bx]" \
+ "in al, dx" \
+ "movzx cx, al" \
+ "rep ins byte ptr es:[di], dx" \
+ __parm [bx] [es di] \
+ __value [al] \
+ __modify exact [cx dx di]
+#else
+static uint8_t ch375_read_data_block(struct ch375 *dev, uint8_t far *buffer)
+{
+ uint8_t i, n = ch375_read_data(dev);
+
+ for (i = 0; i < n; i++) {
+ buffer[i] = ch375_read_data(dev);
+ }
+
+ return n;
+}
+#endif
+
+#if _M_IX86 >= 200
+static void ch375_write_data_block(struct ch375 *dev, uint8_t far * buffer, uint8_t n);
+#pragma aux ch375_write_data_block = \
+ "mov dx, [bx]" \
+ "out dx, al" \
+ "movzx cx, al" \
+ "rep outs dx, byte ptr es:[si]" \
+ __parm [bx] [es si] [al] \
+ __modify exact [cx dx si]
+#else
+static void ch375_write_data_block(struct ch375 *dev, uint8_t far * buffer, uint8_t n)
+{
+ uint8_t i;
+
+ ch375_write_data(dev, n);
+
+ for (i = 0; i < n; i++) {
+ ch375_write_data(dev, buffer[i]);
+ }
+}
+#endif
+
+#endif // CH375DOS_H
diff --git a/dos/chud.c b/dos/chud.c
new file mode 100644
index 0000000..c6d728c
--- /dev/null
+++ b/dos/chud.c
@@ -0,0 +1,418 @@
+/*
+ * CHU - driver for CH375 in disk mode
+ * Copyright (C) 2024 Javier S. Pedro
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <dos.h>
+#include <dosfunc.h>
+#include <io.h>
+
+#include "inc/mbr.h"
+#include "inc/dosdrv.h"
+
+#include "chudisk.h"
+
+uint16_t ch375_port;
+
+bool read_only;
+bool show_fat32;
+
+uint8_t drive;
+uint32_t partition_start;
+uint32_t partition_len;
+
+#define MAX_PARTITIONS 32
+struct partinfo {
+ uint32_t start_sector;
+ uint32_t num_sectors;
+ uint8_t type;
+} partinfo[MAX_PARTITIONS];
+unsigned int num_parts;
+
+static int ioctl_read_control(uint8_t drive, void *buffer, int len)
+{
+ union REGS regs;
+ regs.h.ah = DOS_IOCTL;
+ regs.h.al = 0x04; // Read control string from block device
+ regs.x.bx = drive;
+ regs.x.cx = len;
+ regs.x.dx = FP_OFF(buffer);
+
+ intdos(&regs, &regs);
+
+ if (regs.x.cflag) {
+ return -1;
+ } else {
+ return regs.x.ax;
+ }
+}
+
+static int ioctl_write_control(uint8_t drive, void *buffer, int len)
+{
+ union REGS regs;
+ regs.h.ah = DOS_IOCTL;
+ regs.h.al = 0x05; // Write control string to block device
+ regs.x.bx = drive;
+ regs.x.cx = len;
+ regs.x.dx = FP_OFF(buffer);
+
+ intdos(&regs, &regs);
+
+ if (regs.x.cflag) {
+ return -1;
+ } else {
+ return regs.x.ax;
+ }
+}
+
+static int ioctl_read_abs_sector(uint8_t drive, void *buffer, uint32_t sector, int sectors)
+{
+ CHUDISK_IOCTL_DISK_DATA ciod;
+ union REGS regs;
+
+ ciod.rwabs.buffer = buffer;
+ ciod.rwabs.sector = sector;
+ ciod.rwabs.sectors = sectors;
+
+ regs.h.ah = DOS_IOCTL;
+ regs.h.al = 0x0D; // IOCTL for block devices
+ regs.x.bx = drive;
+ regs.h.ch = DOS_IOCTL_DISK;
+ regs.h.cl = CHUDISK_IOCTL_DISK_READ_ABS_SECTOR;
+ regs.x.dx = FP_OFF(&ciod);
+
+ intdos(&regs, &regs);
+
+ return regs.x.cflag ? -1 : 0;
+}
+
+static void dos_disk_reset()
+{
+ union REGS regs;
+ regs.h.ah = 0x0D; // DOS DISK RESET
+
+ intdos(&regs, &regs);
+}
+
+static int parse_chudisk_control_string(const char *b, int len)
+{
+ // Not sure why I'm doing it this way instead of just sharing a pointer.
+ int i = 0;
+#if 0
+ printf("control string:\n");
+ for (i = 0; i < len; i+=8) {
+ printf(" %hx %hx %hx %hx %hx %hx %hx %hx\n",
+ b[i+0], b[i+1], b[i+2], b[i+3], b[i+4], b[i+5], b[i+6], b[i+7]);
+ }
+ i = 0;
+#endif
+ while (i < len) {
+ char c = b[i++];
+ switch (c) {
+ case 'B':
+ memcpy(&ch375_port, &b[i], sizeof(ch375_port));
+ i+=sizeof(ch375_port);
+ break;
+ case '3':
+ show_fat32 = b[i++];
+ break;
+ case 'R':
+ read_only = b[i++];
+ break;
+ case 'S':
+ memcpy(&partition_start, &b[i], sizeof(partition_start));
+ i+=sizeof(partition_start);
+ break;
+ case 'L':
+ memcpy(&partition_len, &b[i], sizeof(partition_len));
+ i+=sizeof(partition_len);
+ break;
+ case '\0':
+ return 0;
+ default:
+ fprintf(stderr, "Unidentified control string parameter: '%c'\n", c);
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+static bool is_selectable_partition(const MBR_PARTENTRY *partentry)
+{
+ return partentry->type == MBR_FAT12 || partentry->type == MBR_FAT16_32MB
+ || partentry->type == MBR_FAT16 || partentry->type == MBR_FAT16_LBA
+ || (show_fat32 && (partentry->type == MBR_FAT32 || partentry->type == MBR_FAT32_LBA));
+}
+
+static const char *get_partition_type(const MBR_PARTENTRY *partentry)
+{
+ static char buffer[8];
+ switch (partentry->type) {
+ case MBR_EMPTY:
+ return "invalid";
+ case MBR_FAT12:
+ return "fat12";
+ case MBR_FAT16_32MB:
+ case MBR_FAT16:
+ case MBR_FAT16_LBA:
+ return "fat16";
+ case MBR_IFS:
+ return "ntfs";
+ case MBR_FAT32:
+ case MBR_FAT32_LBA:
+ return "fat32";
+ case MBR_EXT:
+ case MBR_EXT_LBA:
+ return "extended";
+ case MBR_LINUX:
+ case MBR_LINUX_SWAP:
+ return "linux";
+ default:
+ sprintf(buffer, "%#X", partentry->type);
+ return buffer;
+ }
+}
+
+
+static const char * show_partition_size(const MBR_PARTENTRY *partentry)
+{
+ static char buffer[12];
+ uint32_t size = partentry->lbaLen / 2;
+ STATIC_ASSERT(SECTOR_SIZE == 512);
+
+ if (size <= 10000) {
+ sprintf(buffer, "%lu KB", size);
+ return buffer;
+ }
+
+ size /= 1024;
+ if (size <= 10000) {
+ sprintf(buffer, "%lu MB", size);
+ return buffer;
+ }
+
+ size /= 1024;
+
+ sprintf(buffer, "%lu GB", size);
+ return buffer;
+}
+
+static const char *get_partition_label(const MBR_PARTENTRY *partentry, const uint32_t start_sector)
+{
+ static char buffer[SECTOR_SIZE];
+ DOS_BPB *bpb = (DOS_BPB*) &buffer[DOS_BPB_OFFSET_ON_DISK];
+
+ switch (partentry->type) {
+ // Focus on well-known types for now
+ case MBR_FAT12:
+ case MBR_FAT16_32MB:
+ case MBR_FAT16:
+ case MBR_FAT16_LBA:
+ case MBR_FAT32:
+ case MBR_FAT32_LBA:
+ break;
+ default:
+ return " ";
+ }
+
+ if (ioctl_read_abs_sector(drive, buffer, start_sector, 1) != 0) {
+ fprintf(stderr, "while reading partition boot record at %lu: %s\n", start_sector, strerror(errno));
+ return " ???? ";
+ }
+
+ if (bpb->sectorsPerFAT != 0) {
+ // Assume FAT16
+ return bpb->fat16.volumeLabel;
+ } else {
+ // Assume FAT32
+ return bpb->fat32.volumeLabel;
+ }
+}
+
+static void print_partition(const uint32_t mbr_sector, const MBR_PARTENTRY *partentry, bool selectable)
+{
+ const bool ebr = mbr_sector != 0;
+ const uint32_t start_sector = mbr_sector + partentry->lbaFirst;
+ const bool is_current = partition_start == start_sector;
+ const char *typeLabel = get_partition_type(partentry);
+ const char *volumeLabel = get_partition_label(partentry, start_sector);
+ const char *sizeLabel = show_partition_size(partentry);
+
+ if (partentry->type == MBR_EMPTY) {
+ // Skip empty partitions no matter what
+ return;
+ }
+ if (ebr && (partentry->type == MBR_EXT || partentry->type == MBR_EXT_LBA)) {
+ // Skip displaying EXT partitions EBR, noisy
+ return;
+ }
+
+ if (selectable) {
+ const unsigned index = num_parts + 1;
+
+ printf(" %c[%2u] %8s %-11.11s %10s \n",
+ is_current ? '*' : ' ', index, typeLabel, volumeLabel, sizeLabel);
+ } else {
+ printf(" %c[ ] %8s %-11.11s %10s \n",
+ is_current ? '*' : ' ', typeLabel, volumeLabel, sizeLabel);
+ }
+
+}
+
+static int scan_disk_partitions()
+{
+ static char buffer[SECTOR_SIZE];
+ uint32_t next_sector = 0;
+
+ printf(" Index %8s %-11.11s %10s \n",
+ "Type", "Label", "Size");
+
+ // Start with MBR at sector 0
+ next_sector = 0;
+ do {
+ const uint32_t this_sector = next_sector;
+ const MBR_PARTENTRY *partentry;
+ int i;
+
+ // Try to read sector
+ if (ioctl_read_abs_sector(drive, buffer, this_sector, 1) != 0) {
+ fprintf(stderr, "while reading %s at disk sector %lu: %s\n",
+ this_sector == 0 ? "MBR" : "EBR",
+ this_sector, strerror(errno));
+ // Fatal for MBR, non-fatal for EBR
+ if (this_sector == 0) {
+ fprintf(stderr, "is the disk present?\n");
+ return -1;
+ }
+ break;
+ }
+
+ // Check for signature
+ if (*(uint16_t*)&buffer[MBR_SIGNATURE_OFFSET_ON_DISK] != 0xAA55U) {
+ fprintf(stderr, "on disk sector %lu: invalid MBR signature %x\n",
+ this_sector, *(uint16_t*)&buffer[MBR_SIGNATURE_OFFSET_ON_DISK]);
+ if (this_sector == 0) return -1;
+ break;
+ }
+
+ // Parse the MBR
+ partentry = (MBR_PARTENTRY*)&buffer[MBR_PARTENTRY_OFFSET_ON_DISK];
+
+ next_sector = 0; // If there is another EBR we will store it here
+ for (i = 0; i < 4 && num_parts < MAX_PARTITIONS; i++) {
+ bool is_selectable = is_selectable_partition(&partentry[i]);
+
+ print_partition(this_sector, &partentry[i], is_selectable);
+
+ if (is_selectable) {
+ partinfo[num_parts].type = partentry[i].type;
+ partinfo[num_parts].start_sector = this_sector + partentry[i].lbaFirst;
+ partinfo[num_parts].num_sectors = partentry[i].lbaLen;
+ num_parts++;
+ }
+ if (partentry[i].type == MBR_EXT || partentry[i].type == MBR_EXT_LBA) {
+ // Go to the EBR next
+ next_sector = this_sector + partentry[i].lbaFirst;
+ }
+ }
+ } while (next_sector != 0 && num_parts < MAX_PARTITIONS);
+
+ return 0;
+}
+
+static void usage()
+{
+ printf("Usage: chud X:, where X is a drive letter handled by CHUDISK.SYS\n");
+}
+
+int main(int argc, const char *argv[])
+{
+ static char buffer[16];
+ int i;
+ unsigned part;
+
+ for (i = 1; i < argc; i++) {
+ // Drive letter?
+ if (argv[i][0] >= 'A' && argv[i][0] <= 'Z' && argv[i][1] == ':') {
+ drive = 1 + argv[i][0] - 'A';
+ } else if (argv[i][0] >= 'a' && argv[i][0] <= 'z' && argv[i][1] == ':') {
+ drive = 1 + argv[i][0] - 'a';
+ }
+ }
+
+ // Now read the driver settings and current partition
+ i = ioctl_read_control(drive, buffer, sizeof(buffer));
+ if (i < 0) {
+ perror("ioctl_read_control");
+ usage();
+ return EXIT_FAILURE;
+ }
+
+ if (parse_chudisk_control_string(buffer, i)) {
+ fprintf(stderr, "wrong IOCTL string returned by driver\n");
+ usage();
+ return EXIT_FAILURE;
+ }
+
+ if (scan_disk_partitions() != 0) {
+ return EXIT_FAILURE;
+ }
+ if (num_parts == 0) {
+ fprintf(stderr, "No partitions found on the disk\n");
+ return EXIT_FAILURE;
+ }
+
+ printf("Select partition in [1..%u] range: ", num_parts);
+ i = scanf("%u", &part);
+
+ if (i != 1) {
+ // Assume cancel
+ return EXIT_FAILURE;
+ }
+
+ if (part == 0 || part > num_parts) {
+ fprintf(stderr, "Invalid partition %u\n", part);
+ return EXIT_FAILURE;
+ }
+
+ printf("Switching drive to partition %u at offset %lu size %lu\n", part,
+ partinfo[part-1].start_sector, partinfo[part-1].num_sectors);
+
+ dos_disk_reset(); // Flush caches before we swap the drive...
+
+ i = 0;
+ buffer[i++] = 'S';
+ memcpy(&buffer[i], &partinfo[part-1].start_sector, sizeof(uint32_t));
+ i+=sizeof(uint32_t);
+ buffer[i++] = 'L';
+ memcpy(&buffer[i], &partinfo[part-1].num_sectors, sizeof(uint32_t));
+ i+=sizeof(uint32_t);
+ buffer[i] = '\0';
+
+ if (ioctl_write_control(drive, buffer, i) < i) {
+ perror("ioctl_write_control");
+ return EXIT_FAILURE;
+ }
+
+ // The above IOCTL will also cause the next mediacheck from DOS to return changed.
+
+ return EXIT_SUCCESS;
+}
diff --git a/dos/chudinit.c b/dos/chudinit.c
new file mode 100644
index 0000000..3dfb782
--- /dev/null
+++ b/dos/chudinit.c
@@ -0,0 +1,226 @@
+/*
+ * CHU - driver for CH375 in disk mode
+ * Copyright (C) 2024 Javier S. Pedro
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#include <stdbool.h>
+#include <string.h>
+
+#include <chuver.h>
+
+#include "inc/dosapi.h"
+#include "inc/dosint.h"
+#include "inc/bda.h"
+
+#include "chudisk.h"
+#include "dlog.h"
+#include "cio.h"
+#include "utils.h"
+
+static int parse_int(const char far * far *s)
+{
+ int i = 0;
+ while (**s >= '0' && **s <= '9') {
+ i = (i * 10) + (**s - '0');
+ (*s)++;
+ }
+ if (**s == ' ' || **s == '\r' || **s == '\n') {
+ return i;
+ } else {
+ return -1;
+ }
+}
+
+static int parse_hex(const char far * far *s)
+{
+ int i = 0;
+ while ((**s >= '0' && **s <= '9') || (**s >= 'A' && **s <= 'F') || (**s >= 'a' && **s <= 'f')) {
+ if (**s >= '0' && **s <= '9') i = (i * 16) + (**s - '0');
+ else if (**s >= 'A' && **s <= 'F') i = (i * 16) + (**s - 'A');
+ else if (**s >= 'a' && **s <= 'f') i = (i * 16) + (**s - 'a');
+ (*s)++;
+ }
+ if (**s == ' ' || **s == '\r' || **s == '\n') {
+ return i;
+ } else {
+ return -1;
+ }
+}
+
+static bool parse_cmdline(const char far *cmdline)
+{
+ // Source command line string
+ // E.g. DEVICE.SYS /N3\r\n
+ const char far *s = cmdline;
+
+ // Skip filename part (search for first sep)
+ while (*s != ' ' && *s != '\r' && *s != '\n') { s++; }
+ if (*s == '\r' || *s == '\n') return true; // No arguments
+ // Now skip whitespace
+ while (*s == ' ') { s++; }
+
+ // Now handle options like /N3 /X4
+ while (*s != '\r' && *s != '\n') {
+ char c = *s++, opt;
+ int num;
+ switch (c) {
+ case '/':
+ opt = *s++;
+ switch (opt) {
+ case 'N':
+ case 'n': // number of units
+ num = parse_int(&s);
+ if (num > 0 && num <= MAX_UNITS) {
+ num_units = num;
+ } else {
+ c_printf("invalid value '%d' for option '%c'\n", num, opt);
+ return false;
+ }
+ break;
+ case 'P':
+ case 'p': // IO port
+ num = parse_hex(&s);
+ if (num > 0) {
+ ch375_dev.io_base = num;
+ } else {
+ c_printf("invalid value '%d' for option '%c'\n", num, opt);
+ return false;
+ }
+ break;
+ case 'R':
+ case 'r': // Read only
+ read_only = true;
+ break;
+ case '3': // Show FAT32 partitions
+ show_fat32 = true;
+ break;
+ default:
+ c_printf("invalid option '%c'\n", opt);
+ return false;
+ }
+ break;
+ case ' ':
+ break;
+ default:
+ c_printf("invalid syntax '%c'\n", c);
+ return false;
+ }
+ }
+
+ dprintf("Configured num drives: %hu\n", num_units);
+
+ return true;
+}
+
+static void fill_initial_bpb(DOS_BPB *bpb)
+{
+ // Dummy, only used during driver init
+ // Most likely these values can be anything you want,
+ // as long as it remotely looks like a valid FAT16 BPB
+ bpb->bytesPerSector = 512;
+ bpb->sectorsPerCluster = 1;
+ bpb->reservedSectors = 0;
+ bpb->numberOfFATs = 2;
+ bpb->rootDirEntries = 16;
+ bpb->numberOfSectors = 512;
+ bpb->mediaType = DOS_BPB_MEDIA_HDD;
+ bpb->sectorsPerFAT = 1;
+ bpb->sectorsPerTrack = 63;
+ bpb->numberOfHeads = 255;
+ bpb->hiddenSectors = 0;
+ bpb->numberOfSectorsLarge = 0;
+ memcpy(bpb->fat16.volumeLabel, "NO NAME ", 11);
+}
+
+void driver_init(DOS_DRS __far *drs)
+{
+ unsigned i;
+ uint16_t dos_version;
+ uint8_t dos_major, dos_minor;
+ uint8_t hw_version;
+
+ dlog_init();
+
+ // Defaults
+ ch375_dev.io_base = 0x260;
+ num_units = 1;
+ read_only = false;
+ show_fat32 = false;
+
+ // If DOS > 7.1, enable FAT32 by default
+ dos_version = dos_get_version();
+ dos_major = dos_version & 0xFF;
+ dos_minor = (dos_version >> 8) & 0xFF;
+ dprintf("DOS version %d.%d\n", dos_major, dos_minor);
+ if (dos_major > 7 || (dos_major == 7 && dos_minor >= 10)) {
+ show_fat32 = true;
+ }
+
+ // Parse command line arguments
+ if (!parse_cmdline(drs->cmd.init.in.cmdline)) {
+ c_puts("CHUDISK: Syntax error in cmdline arguments");
+ goto fail;
+ }
+
+ // Try to initialize CH375
+ dputs("CH375 reset...");
+ ch375_reset(&ch375_dev);
+ bda_wait_ticks(3); // wait 100-150ms
+ dputs("CH375 reset wait done");
+
+ if (ch375_check_exists(&ch375_dev, 0xA5) != (uint8_t)~0xA5) {
+ c_printf("CHUDISK: CH375 not detected at @%x\n", ch375_dev.io_base);
+ goto fail;
+ }
+
+ hw_version = ch375_get_version(&ch375_dev);
+
+ // Do not really set up the chip until it is used
+ ch375_set_usb_mode(&ch375_dev, CH375_USB_MODE_HOST_DISABLED);
+ ch375_enter_sleep(&ch375_dev);
+
+ data.ready = false;
+ data.sectors = 0;
+ data.bytesPerSector = 0;
+
+ // Build the template BPBs
+ drs->cmd.init.out.number_units = num_units;
+ for (i = 0; i < num_units; i++) {
+ fill_initial_bpb(&data.unit[i].bpb);
+ data.bpb_ptrs[i] = &data.unit[i].bpb;
+ }
+ drs->cmd.init.out.bpbs = data.bpb_ptrs;
+ drs->cmd.init.out.break_addr = MK_FP(get_cs(), FP_OFF(&resident_end));
+ drs->status = DOS_DD_STATUS_DONE;
+
+ c_printf("CHUDISK %x.%x (HW v.%x), drives",
+ CHUDISK_VERSION_MAJOR, CHUDISK_VERSION_MINOR, hw_version);
+ for (i = 0; i < num_units; i++) {
+ c_printf(" %c:", drive_index_to_letter(drs->cmd.init.in.first_drive_number + i));
+ }
+#if ENABLE_DLOG
+ c_printf(" (debug logging)");
+#endif
+ c_putc('\n');
+
+ return;
+fail:
+ drs->status = DOS_DD_STATUS_DONE | DOS_DD_STATUS_ERROR | DOS_DD_ERR_GENERAL_FAILURE;
+ drs->cmd.init.out.number_units = 0;
+ drs->cmd.init.out.break_addr = MK_FP(get_cs(), 0); // Deallocate everything
+ return;
+}
diff --git a/dos/chudisk.c b/dos/chudisk.c
new file mode 100644
index 0000000..c7c0968
--- /dev/null
+++ b/dos/chudisk.c
@@ -0,0 +1,895 @@
+/*
+ * CHU - driver for CH375 in disk mode
+ * Copyright (C) 2024 Javier S. Pedro
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#include <i86.h>
+#include <string.h>
+
+#include "inc/dosdrv.h"
+#include "inc/bda.h"
+#include "inc/mbr.h"
+
+#include "chudisk.h"
+#include "dlog.h"
+#include "utils.h"
+
+// Pre-declarations for the two driver routines, note there is a pragma aux for them
+DOS_DD_STRATEGY strategy_routine;
+DOS_DD_INTERRUPT interrupt_routine;
+
+/** This is the actual .SYS header! */
+const DOS_DD_HEADER __based(__segname("_CODE")) header = {
+ MK_FP(-1, -1),
+ DOS_DD_BLOCK | DOS_BLOCK_SUPPORT_32BIT_ADDRESSES
+ | DOS_BLOCK_FORMAT_NON_IBM | DOS_BLOCK_SUPPORT_REMOVABLE_MEDIA
+ | DOS_DD_SUPPORT_IOCTL_RW
+ | DOS_BLOCK_SUPPORT_IOCTL_GENERIC | DOS_DD_SUPPORT_IOCTL_QUERY,
+ strategy_routine,
+ interrupt_routine,
+ "\1CHUDISK"
+};
+
+/** Strategy routine saves DRS here for interrupt routine to use */
+DOS_DRS far * saved_drs;
+
+/** Device struct (used by ch375.h functions) */
+struct ch375 ch375_dev;
+
+// Some settings
+/** Number of units handled by the driver (i.e. number of drive letters reserved. ) */
+unsigned char num_units;
+/** Read-only mode (as if write-protected floppy) */
+bool read_only;
+/** Process FAT32 partitions too. */
+bool show_fat32;
+
+/* Runtime data for the driver. */
+struct chudisk_data data;
+
+/** Buffer used to load boot sectors from disk and partitions. */
+char buffer[512];
+
+/** Waits for the CH375 to generate an interrupt, returns result value.
+ * @param max_ticks max BIOS ticks to wait for interrupt. */
+uint8_t wait_for_ch375_result(unsigned int max_ticks)
+{
+ uint16_t last = bda_get_tick_count_lo();
+ while (!ch375_poll_interrupt(&ch375_dev) && max_ticks > 0) {
+ uint16_t now = bda_get_tick_count_lo();
+ if (now != last) {
+ last = now;
+ max_ticks--;
+ }
+ }
+ if (ch375_poll_interrupt(&ch375_dev)) {
+ uint8_t status = ch375_get_int_status(&ch375_dev);
+ return status;
+ } else {
+ dprintf("CH375 int timeout\n");
+ return 0;
+ }
+}
+
+/** Gets the disk sector from RW command DRS. */
+static inline uint32_t get_start_sector(const DOS_DRS far * drs)
+{
+ return (drs->cmd.rw.start_sector == 0xFFFF
+ ? drs->cmd.rw.start_sector_huge
+ : drs->cmd.rw.start_sector);
+}
+
+static inline uint32_t chs_to_lba(const DOS_BPB * bpb, uint16_t cylinder, uint16_t head, uint16_t sector)
+{
+ uint16_t track = cylinder * bpb->numberOfHeads + head;
+ return mul(track, bpb->sectorsPerTrack) + (sector - 1);
+}
+
+/** Read N sectors from the disk
+ * @return number of sectors read
+ */
+static unsigned disk_read_sectors(uint8_t far *buffer, uint32_t lba, uint8_t num)
+{
+ uint8_t result;
+ unsigned total = 0; // Total bytes written
+
+ dprintf("disk read buf=%Fp sector=%lu num=%u\n", buffer, lba, num);
+
+ // Initiate read operation
+ ch375_disk_read(&ch375_dev, lba, num);
+
+ // Now keep going as long as ch375 says to keep going
+ while ((result = wait_for_ch375_result(10)) == CH375_INT_USB_DISK_READ) {
+ unsigned len;
+
+ // Sanity check
+ if (total / SECTOR_SIZE >= num) {
+ dputs("read overflow");
+ break;
+ }
+
+ ch375_send_command(&ch375_dev, CH375_RD_USB_DATA);
+ len = ch375_read_data_block(&ch375_dev, &buffer[total]);
+
+ //dprintf(" disk read data block len = %d\n", len);
+
+ if (len == 0) {
+ // Done?
+ break;
+ }
+
+ total += len;
+
+ // Continue
+ ch375_send_command(&ch375_dev, CH375_DISK_RD_GO);
+ }
+
+ dprintf(" disk read total bytes = %u, sectors = %u, final status = 0x%hx\n",
+ total, total / SECTOR_SIZE, result);
+
+ return total / SECTOR_SIZE;
+}
+
+/** Write N sectors to the disk
+ * @return number of sectors written
+ */
+static unsigned disk_write_sectors(uint8_t far *buffer, uint32_t lba, uint8_t num)
+{
+ uint8_t result;
+ unsigned total = 0; // Total bytes written
+
+ dprintf("disk write buf=%Fp sector=%lu num=%u\n", buffer, lba, num);
+
+ // Initiate operation
+ ch375_disk_write(&ch375_dev, lba, num);
+
+ // Now keep going as long as ch375 says to keep going
+ while ((result = wait_for_ch375_result(10)) == CH375_INT_USB_DISK_WRITE) {
+ const unsigned len = CH375_BLOCK_SIZE;
+
+ // Sanity check
+ if (total / SECTOR_SIZE >= num) {
+ dputs("write overflow");
+ break;
+ }
+
+ ch375_send_command(&ch375_dev, CH375_WR_USB_DATA7);
+ ch375_write_data_block(&ch375_dev, &buffer[total], len);
+
+ total += len;
+
+ // Continue
+ ch375_send_command(&ch375_dev, CH375_DISK_WR_GO);
+ }
+
+ dprintf(" disk write total bytes = %u, sectors = %u, final status = 0x%hx\n",
+ total, total / SECTOR_SIZE, result);
+
+ return total / SECTOR_SIZE;
+}
+
+/** Enables CH375 USB host mode, initializes "disk" mode on it
+ * and connects to a disk. */
+static bool disk_connect()
+{
+ int tries;
+
+ dputs("switch to usb host");
+ ch375_set_usb_mode(&ch375_dev, CH375_USB_MODE_HOST_AUTO_SOF);
+ if (wait_for_ch375_result(4) != CH375_INT_USB_CONNECT) {
+ dputs("could not turn on usb host");
+ return false;
+ }
+
+ dputs("try init disk");
+ ch375_disk_init(&ch375_dev);
+ if (wait_for_ch375_result(20) != CH375_INT_USB_SUCCESS) {
+ dputs("could not init disk");
+ return false;
+ }
+
+ // looks like we have to keep polling by asking "is the disk ready?"
+ // even if each time we ask we are already polling
+ tries = 3;
+ while (tries-- > 0) {
+ ch375_test_disk_ready(&ch375_dev);
+ if (wait_for_ch375_result(20) == CH375_INT_USB_SUCCESS) {
+ dputs("disk now init");
+ return true;
+ }
+ bda_wait_tick();
+ }
+
+ dputs("disk failed to init");
+ return false;
+}
+
+/** Scans the disk for partitions. */
+static int disk_scan()
+{
+ uint32_t next_sector;
+ int num_parts, i;
+
+ dprintf("disk scan show_fat32=%d\n", show_fat32);
+
+ // Clear current partitions
+ for (i = 0; i < num_units; ++i) {
+ data.unit[i].exists = false;
+ data.unit[i].ready = false;
+ data.unit[i].start_sector = 0;
+ data.unit[i].sectors = 0;
+ }
+
+ // Refresh disk size
+ ch375_send_command(&ch375_dev, CH375_DISK_SIZE);
+ if (wait_for_ch375_result(4) != CH375_INT_USB_SUCCESS) {
+ return DOS_DD_ERR_MEDIA_UNAVAILABLE;
+ }
+
+ ch375_send_command(&ch375_dev, CH375_RD_USB_DATA);
+ if (ch375_read_data_block(&ch375_dev, buffer) < 8) {
+ dprintf("disk size command return less than 8 bytes\n");
+ return DOS_DD_ERR_UNKNOWN_MEDIA;
+ }
+
+ data.sectors = SWAPNC_32(*(uint32_t*)&buffer[0]);
+ data.bytesPerSector = SWAPNC_32(*(uint32_t*)&buffer[4]);
+
+ dprintf("sectors %lu bytesPerSector %lu\n", data.sectors, data.bytesPerSector);
+ if (data.bytesPerSector != SECTOR_SIZE) {
+ return DOS_DD_ERR_UNKNOWN_MEDIA;
+ }
+
+ // Start creating partitions/units from partition tables
+ num_parts = 0;
+
+ // Start with MBR at sector 0
+ next_sector = 0;
+ do {
+ const MBR_PARTENTRY *partentry;
+ const uint32_t this_sector = next_sector;
+
+ dprintf("reading boot record at sector %lu\n", this_sector);
+
+ // Try to read sector
+ if (disk_read_sectors(buffer, this_sector, 1) != 1) {
+ // Fatal for MBR, non-fatal for EBR
+ if (this_sector == 0) return DOS_DD_ERR_READ_FAULT;
+ break;
+ }
+
+ // Check for signature
+ if (*(uint16_t*)&buffer[510] != 0xAA55U) {
+ dprintf(" invalid MBR signature %x\n", *(uint16_t*)&buffer[510]);
+ if (this_sector == 0) return DOS_DD_ERR_UNKNOWN_MEDIA;
+ break;
+ }
+
+ // Parse the MBR
+ partentry = (MBR_PARTENTRY*)&buffer[446];
+
+ next_sector = 0; // If there is another EBR we will store it here
+ for (i = 0; i < 4 && num_parts < MAX_UNITS; i++) {
+ dprintf(" partition %d type %x start %lu len %lu\n", i, partentry[i].type, partentry[i].lbaFirst, partentry[i].lbaLen);
+
+ if (partentry[i].type == MBR_FAT12 || partentry[i].type == MBR_FAT16_32MB
+ || partentry[i].type == MBR_FAT16 || partentry[i].type == MBR_FAT16_LBA
+ || (show_fat32 && (partentry[i].type == MBR_FAT32 || partentry[i].type == MBR_FAT32_LBA)) ) {
+ data.unit[num_parts].exists = true;
+ data.unit[num_parts].start_sector = this_sector + partentry[i].lbaFirst;
+ data.unit[num_parts].sectors = partentry[i].lbaLen;
+ dprintf(" added as unit %u starts at %lu\n", num_parts, data.unit[num_parts].start_sector);
+ num_parts++;
+ } else if (partentry[i].type == MBR_EXT || partentry[i].type == MBR_EXT_LBA) {
+ // Go to the EBR next
+ next_sector = this_sector + partentry[i].lbaFirst;
+ }
+ }
+ } while (next_sector != 0 && num_parts < MAX_UNITS);
+
+ // OK!
+ dprintf("found %u partitions\n", num_parts);
+ data.ready = true;
+
+ return 0;
+}
+
+static int disk_init()
+{
+ ch375_test_disk_ready(&ch375_dev);
+ if (wait_for_ch375_result(4) != CH375_INT_USB_SUCCESS) {
+ // Disk is not even connected, try to connect
+ if (!disk_connect()) {
+ // Nothing works: fail
+ return DOS_DD_ERR_DRIVE_NOT_READY;
+ }
+ }
+ // Disk is now ready: initiate scan for partitions
+ return disk_scan();
+}
+
+/** Return the volume label from the cached BPB, i.e. from the last time the drive was accessed. */
+static inline const char * unit_get_volume_label(uint8_t unit)
+{
+ const DOS_BPB *bpb = &data.unit[unit].bpb;
+
+ if (bpb->sectorsPerFAT != 0) {
+ // FAT16
+ return bpb->fat16.volumeLabel;
+ } else {
+ // FAT32
+ return bpb->fat32.volumeLabel;
+ }
+}
+
+/** Try to initialize logigcal drive 'unit'. This also initializes the entire disk
+ * if it was not done yet (scanning all partitions).
+ * It will read the BPB of the corresponding partition and cache it. */
+static int unit_init(uint8_t unit)
+{
+ DOS_BPB *bpb = &data.unit[unit].bpb;
+ int err;
+
+ // Check if disk is ready, and initialize it if not
+ if (!data.ready) {
+ if (err = disk_init()) {
+ return err;
+ }
+ }
+
+ // Disk should be ready, partitions enumerated
+ if (!data.unit[saved_drs->unit].exists) {
+ dputs("disk is ready, but partition does not exist");
+ return DOS_DD_ERR_MEDIA_UNAVAILABLE;
+ }
+
+ // Try to read starting sector of partition into buffer
+ if (disk_read_sectors(buffer, data.unit[unit].start_sector, 1) != 1) {
+ // Cannot read start sector...
+ return DOS_DD_ERR_READ_FAULT;
+ }
+
+ // For now, copy largest possible BPB into our internal cache
+ memcpy(bpb, &buffer[DOS_BPB_OFFSET_ON_DISK], sizeof(DOS_BPB));
+
+ dprintf("unit %u BPB sectorsPerCluster=%u sectorsPerTrack=%u numberOfHeads=%u\n",
+ unit, bpb->sectorsPerCluster,
+ bpb->sectorsPerTrack, bpb->numberOfHeads);
+ dprintf(" bytesPerSector=%u numberOfFats=%u sectorsPerFat=%u \n",
+ bpb->bytesPerSector,bpb->numberOfFATs,
+ bpb->sectorsPerFAT);
+ dprintf(" numberOfSectors=%u numberOfSectorsLarge=%lu\n",
+ bpb->numberOfSectors, bpb->numberOfSectorsLarge);
+
+ if (bpb->sectorsPerFAT != 0) {
+ // FAT12/16
+ dprintf(" %s volumeSerialNumber=%lx volumeLabel='%.11s' fsType='%.8s'\n",
+ "FAT16", bpb->fat16.volumeSerialNumber, bpb->fat16.volumeLabel, bpb->fat16.fsType);
+ } else if (bpb->fat32.sectorPerFatLarge != 0) {
+ dprintf(" %s volumeSerialNumber=%lx volumeLabel='%.11s' fsType='%.8s'\n",
+ "FAT32", bpb->fat32.volumeSerialNumber, bpb->fat32.volumeLabel, bpb->fat32.fsType);
+ } else {
+ dprintf(" -> unknown filesystem\n");
+ return DOS_DD_ERR_UNKNOWN_MEDIA;
+ }
+
+ data.unit[saved_drs->unit].ready = true;
+ return 0;
+}
+
+static bool unit_check_media_changed(uint8_t unit)
+{
+ if (data.ready) {
+ // Recheck if disk is still ready
+ ch375_test_disk_ready(&ch375_dev);
+ if (wait_for_ch375_result(4) == CH375_INT_USB_SUCCESS) {
+ // Disk is ready. But is partition ready ?
+ if (data.unit[unit].ready) {
+ // Disk ready, partition ready. Perfect!
+ dputs("mediaquery -> disk ready -> unit ready -> not changed");
+ return false;
+ } else {
+ // Disk ready, but this partition is not.
+ // Likely first time access for this partition.
+ dputs("mediaquery -> disk ready -> unit !ready -> changed");
+ return true;
+ }
+ } else {
+ // CH375 is not ready but we thought it was. USB has been removed!
+ dputs("mediaquery -> disk not ready -> changed");
+ // Invalidate disk ready flag so that we rescan for partitions
+ data.ready = false;
+ return true;
+ }
+ } else {
+ // We do not think CH375 is ready. First time we check, or USB not present.
+ dputs("mediaquery -> !ready -> changed");
+ return true;
+ }
+}
+
+void strategy_routine(DOS_DRS __far *drs)
+{
+ load_ds_from_cs();
+ //dprintf("strategy routine drs=%Fp len=%u func=%u\n", drs, drs->len, drs->function);
+ saved_drs = drs;
+}
+
+void interrupt_routine()
+{
+ int err;
+ load_ds_from_cs();
+ //dprintf("interrupt routine drs=%Fp len=%u func=%u\n", saved_drs, saved_drs->len, saved_drs->function);
+ switch (saved_drs->function) {
+ case DOS_DD_FUNC_INIT:
+ driver_init(saved_drs);
+ break;
+ case DOS_DD_FUNC_MEDIA_CHECK:
+ if (saved_drs->unit >= num_units) {
+ saved_drs->status = DOS_DD_ERR_UNKNOWN_UNIT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ saved_drs->cmd.mediacheck.out.result = unit_check_media_changed(saved_drs->unit)
+ ? DOS_DD_MEDIAQUERY_CHANGED : DOS_DD_MEDIAQUERY_NOT_CHANGED;
+ saved_drs->cmd.mediacheck.out.volume_id = unit_get_volume_label(saved_drs->unit);
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ break;
+ case DOS_DD_FUNC_BUILD_BPB:
+ dprintf("build bpb unit=%hu\n", saved_drs->unit);
+ if (saved_drs->unit >= num_units) {
+ dputs("unit is unknown");
+ saved_drs->status = DOS_DD_ERR_UNKNOWN_UNIT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ // Try to initialize disk and unit
+ if (err = unit_init(saved_drs->unit)) {
+ saved_drs->status = err | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ // Return pointer to our cached BPB
+ saved_drs->cmd.buildbpb.out.bpb = &data.unit[saved_drs->unit].bpb;
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ break;
+ case DOS_DD_FUNC_READ:
+ dprintf("read unit=%hu sector=%u large_sector=%lu len=%u\n", saved_drs->unit,
+ saved_drs->cmd.rw.start_sector, saved_drs->cmd.rw.start_sector_huge, saved_drs->cmd.rw.len);
+ if (saved_drs->unit >= num_units) {
+ saved_drs->status = DOS_DD_ERR_UNKNOWN_UNIT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ if (!data.ready || !data.unit[saved_drs->unit].ready) {
+ dputs("partition not ready");
+ // We don't try to initialize the drive here, DOS must do that by calling buildBPB
+ saved_drs->status = DOS_DD_ERR_MEDIA_UNAVAILABLE | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ if (saved_drs->cmd.rw.len > 128) {
+ // Too many sectors at once!
+ // TODO: Should we allow partial reads? When I tried even "type <file>" crashes.
+ // TODO: Should we also check for segment wraparound?
+ saved_drs->cmd.rw.len = 0;
+ saved_drs->status = DOS_DD_ERR_READ_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ } else if (saved_drs->cmd.rw.len >= 1) {
+ uint32_t sector = get_start_sector(saved_drs);
+ if (sector > data.unit[saved_drs->unit].sectors) {
+ saved_drs->cmd.rw.len = 0;
+ saved_drs->status = DOS_DD_ERR_SECTOR_NOT_FOUND | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ // Make absolute
+ sector += data.unit[saved_drs->unit].start_sector;
+ if (sector > data.sectors) {
+ saved_drs->cmd.rw.len = 0;
+ saved_drs->status = DOS_DD_ERR_SEEK | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+
+ saved_drs->cmd.rw.len = disk_read_sectors(saved_drs->cmd.rw.buffer, sector, saved_drs->cmd.rw.len);
+ if (saved_drs->cmd.rw.len == 0) {
+ // Not sure if required
+ saved_drs->status = DOS_DD_ERR_READ_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ }
+ saved_drs->cmd.rw.volume_id = unit_get_volume_label(saved_drs->unit);
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ break;
+ case DOS_DD_FUNC_WRITE:
+ case DOS_DD_FUNC_WRITE_VERIFY: // TODO Not doing the verify yet part...
+ dprintf("write unit=%hu sector=%u large_sector=%lu len=%u\n", saved_drs->unit,
+ saved_drs->cmd.rw.start_sector, saved_drs->cmd.rw.start_sector_huge, saved_drs->cmd.rw.len);
+ if (saved_drs->unit >= num_units) {
+ saved_drs->status = DOS_DD_ERR_UNKNOWN_UNIT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ if (!data.ready || !data.unit[saved_drs->unit].ready) {
+ dputs("partition not ready");
+ saved_drs->status = DOS_DD_ERR_MEDIA_UNAVAILABLE | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ if (read_only) {
+ saved_drs->status = DOS_DD_ERR_WRITE_PROTECT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ if (saved_drs->cmd.rw.len > 128) {
+ // Too much!
+ saved_drs->cmd.rw.len = 0;
+ saved_drs->status = DOS_DD_ERR_WRITE_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ } else if (saved_drs->cmd.rw.len >= 1) {
+ // Check relative sector value against partition limit
+ uint32_t sector = get_start_sector(saved_drs);
+ if (sector > data.unit[saved_drs->unit].sectors) {
+ saved_drs->cmd.rw.len = 0;
+ saved_drs->status = DOS_DD_ERR_SECTOR_NOT_FOUND | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ // Make absolute and check against disk limit
+ sector += data.unit[saved_drs->unit].start_sector;
+ if (sector > data.sectors) {
+ saved_drs->cmd.rw.len = 0;
+ saved_drs->status = DOS_DD_ERR_SEEK | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+
+ saved_drs->cmd.rw.len = disk_write_sectors(saved_drs->cmd.rw.buffer, sector, saved_drs->cmd.rw.len);
+ if (saved_drs->cmd.rw.len == 0) {
+ // Not sure if required
+ saved_drs->status = DOS_DD_ERR_WRITE_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+
+ if (saved_drs->function == DOS_DD_FUNC_WRITE_VERIFY) {
+ // Verify one sector at a time. Use our internal buffer.
+ uint8_t far * origbuf = saved_drs->cmd.rw.buffer;
+ unsigned i;
+ for (i = 0; i < saved_drs->cmd.rw.len; i++) {
+ dprintf(" verifying sector %u\n", i);
+ if (disk_read_sectors(buffer, sector + i, 1) != 1) {
+ saved_drs->status = DOS_DD_ERR_READ_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+
+ if (_fmemcmp(&origbuf[i * SECTOR_SIZE], buffer, SECTOR_SIZE) != 0) {
+ dprintf(" cmp error for sec %u\n", i);
+ saved_drs->status = DOS_DD_ERR_CRC | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ }
+ }
+ }
+ saved_drs->cmd.rw.volume_id = unit_get_volume_label(saved_drs->unit);
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ break;
+ case DOS_DD_FUNC_DEVICE_OPEN:
+ dputs("device open");
+ // Do nothing here -- we init the device on build bpb, as with older DOSes
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ break;
+ case DOS_DD_FUNC_DEVICE_CLOSE:
+ dputs("device close");
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ break;
+ case DOS_DD_FUNC_REMOVEABLE_MEDIA:
+ dputs("removable device");
+ // If the disk is fixed, we must set "busy" bit here. We are removable.
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ break;
+ case DOS_DD_FUNC_IOCTL_READ:
+ dprintf("ioctl read unit=%hu buf=%Fp len=%u\n", saved_drs->unit,
+ saved_drs->cmd.ioctlrw.buffer, saved_drs->cmd.ioctlrw.len);
+ if (saved_drs->unit >= num_units) {
+ saved_drs->status = DOS_DD_ERR_UNKNOWN_UNIT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ // If there's enough space, return our internal config string.
+ if (saved_drs->cmd.ioctlrw.len >= 3 + 2 + 1 + 5 + 1) {
+ uint8_t far * buf = saved_drs->cmd.ioctlrw.buffer;
+ int len = 0;
+ // B = base port (2 byte)
+ buf[len++] = 'B';
+ _fmemcpy(&buf[len], &ch375_dev.io_base, sizeof(ch375_dev.io_base));
+ len += sizeof(ch375_dev.io_base);
+ // 3 = showing FAT32 (1 byte)
+ buf[len++] = '3';
+ buf[len++] = show_fat32;
+ // R = read only (1 byte)
+ buf[len++] = 'R';
+ buf[len++] = read_only;
+ // S = partition start (4 bytes)
+ buf[len++] = 'S';
+ _fmemcpy(&buf[len], &data.unit[saved_drs->unit].start_sector, sizeof(uint32_t));
+ len += sizeof(uint32_t);
+ buf[len++] = '\0';
+ saved_drs->cmd.ioctlrw.len = len;
+ } else {
+ saved_drs->cmd.ioctlrw.len = 0;
+ }
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ break;
+ case DOS_DD_FUNC_IOCTL_WRITE:
+ dprintf("ioctl write unit=%hu buf=%Fp len=%u\n", saved_drs->unit,
+ saved_drs->cmd.ioctlrw.buffer, saved_drs->cmd.ioctlrw.len);
+ if (saved_drs->unit >= num_units) {
+ saved_drs->status = DOS_DD_ERR_UNKNOWN_UNIT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ if (saved_drs->cmd.ioctlrw.len >= 2) {
+ uint8_t far * buf = saved_drs->cmd.ioctlrw.buffer;
+ int i = 0;
+ do {
+ char c = buf[i++];
+ switch (c) {
+ case 'S': // Partition start (5 bytes)
+ _fmemcpy(&data.unit[saved_drs->unit].start_sector, &buf[i], sizeof(uint32_t));
+ i += sizeof(uint32_t);
+ dprintf(" unit=%hu new start=%lu\n", saved_drs->unit, data.unit[saved_drs->unit].start_sector);
+ // Mark unit as valid but needing refresh
+ data.unit[saved_drs->unit].exists = true;
+ data.unit[saved_drs->unit].ready = false;
+ break;
+ case 'L': // Partition len (5 bytes)
+ _fmemcpy(&data.unit[saved_drs->unit].sectors, &buf[i], sizeof(uint32_t));
+ i += sizeof(uint32_t);
+ dprintf(" unit=%hu new len=%lu\n", saved_drs->unit, data.unit[saved_drs->unit].sectors);
+ // Mark unit as valid but needing refresh
+ data.unit[saved_drs->unit].exists = true;
+ data.unit[saved_drs->unit].ready = false;
+ break;
+ case '\0':
+ break;
+ default:
+ dprintf(" invalid control string char '%c' at %d\n", c, i-1);
+ saved_drs->cmd.ioctlrw.len = i;
+ saved_drs->status = DOS_DD_ERR_WRITE_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ } while (i < saved_drs->cmd.ioctlrw.len);
+ } else {
+ saved_drs->cmd.ioctlrw.len = 0;
+ saved_drs->status = DOS_DD_ERR_WRITE_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ break;
+ case DOS_DD_FUNC_IOCTL_GENERIC:
+ dprintf("ioctl unit=%hu %x:%x buf=%Fp\n", saved_drs->unit,
+ saved_drs->cmd.ioctl.category, saved_drs->cmd.ioctl.minor, saved_drs->cmd.ioctl.data);
+ if (saved_drs->unit >= num_units) {
+ saved_drs->status = DOS_DD_ERR_UNKNOWN_UNIT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ // DOS can call these functions without doing a mediacheck/buildBPB
+ // Do not want to "forget" about a media change, so can't just call buildBPB here.
+ // But some functions (like get device parameters?) seem to act like a buildBPB.
+ if (unit_check_media_changed(saved_drs->unit)) {
+ // Init the disk, but not the partition, until we are sure we want to.
+ if (!data.ready) {
+ if (err = disk_init()) {
+ // Cannot access to disk or scan for partitions? No IOCTLs for you!
+ saved_drs->status = err | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ }
+ }
+ if (saved_drs->cmd.ioctl.category == DOS_IOCTL_DISK) {
+ DOS_IOCTL_DISK_DATA far * iod = saved_drs->cmd.ioctl.data;
+ CHUDISK_IOCTL_DISK_DATA far * ciod = saved_drs->cmd.ioctl.data;
+ DOS_BPB * bpb = &data.unit[saved_drs->unit].bpb;
+
+ switch (saved_drs->cmd.ioctl.minor) {
+ case DOS_IOCTL_DISK_GET_DEVICE_PARAMETERS: // Get CHS values (full disk!) and BPB
+ dprintf(" get_device_parameters specialFunctions=0x%x\n", iod->devparams.specialFunctions);
+ // If partition's BPB is not cached, read it from the disk
+ if (!data.unit[saved_drs->unit].ready) {
+ // This acts as if buildBPB was called
+ if ((err = unit_init(saved_drs->unit))) {
+ // TODO Now would be a good time to talk about a default BPB...
+ saved_drs->status = err | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ }
+
+ iod->devparams.specialFunctions = DOS_DEVICEPARAMS_USE_DEVICE_BPB | DOS_DEVICEPARAMS_REGULARTRACKS;
+ iod->devparams.deviceType = DOS_DEVICEPARAMS_HDD;
+ iod->devparams.deviceAttributes = DOS_DEVICEPARAMS_REMOVABLE;
+ if (bpb->numberOfHeads && bpb->sectorsPerTrack) {
+ // Compute number of cylinders of entire disk based on this part's BPB...
+ iod->devparams.numberOfCylinders =
+ div(data.sectors, bpb->numberOfHeads * bpb->sectorsPerTrack);
+ // TODO Check for overflows...
+ } else {
+ iod->devparams.numberOfCylinders = 0;
+ }
+ iod->devparams.mediaType = 0;
+
+ // Return only DOS3.x BPB -- not sure if caller will have space for anything larger.
+ _fmemcpy(&iod->devparams.bpb, bpb, sizeof(DOS_BPB_DOS3));
+
+ dprintf(" get_device_parameters -> numberOfCylinders=%u\n", iod->devparams.numberOfCylinders);
+
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ return;
+ case DOS_IOCTL_DISK_GET_MEDIA_ID: // Get the volume label
+ if (!data.unit[saved_drs->unit].ready) {
+ // TODO Should we reload BPB here like above?
+ dputs(" but partition is not ready");
+ saved_drs->status = DOS_DD_ERR_MEDIA_UNAVAILABLE | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ iod->mid.infoLevel = 0;
+ if (bpb->sectorsPerFAT != 0) {
+ iod->mid.serialNumber = bpb->fat16.volumeSerialNumber;
+ _fmemcpy(iod->mid.volumeLabel, bpb->fat16.volumeLabel, 11);
+ _fmemcpy(iod->mid.fsType, bpb->fat16.fsType, 8);
+ } else if (bpb->fat32.sectorPerFatLarge != 0) {
+ iod->mid.serialNumber = bpb->fat32.volumeSerialNumber;
+ _fmemcpy(iod->mid.volumeLabel, bpb->fat32.volumeLabel, 11);
+ _fmemcpy(iod->mid.fsType, bpb->fat32.fsType, 8);
+ }
+ dprintf(" get media id -> volumeSerialNumber=%lx volumeLabel='%.11Fs' fsType='%.8Fs'\n",
+ iod->mid.serialNumber, iod->mid.volumeLabel, iod->mid.fsType);
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ return;
+ case DOS_IOCTL_DISK_SET_MEDIA_ID: // Set the volume label, etc.
+ if (!data.unit[saved_drs->unit].exists) {
+ dputs(" but partition does not exist!");
+ saved_drs->status = DOS_DD_ERR_MEDIA_UNAVAILABLE | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ dprintf(" set media id to volumeSerialNumber=%lx volumeLabel='%.11Fs' fsType='%.8Fs'\n",
+ iod->mid.serialNumber, iod->mid.volumeLabel, iod->mid.fsType);
+ if (read_only) {
+ saved_drs->status = DOS_DD_ERR_WRITE_PROTECT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ // Here we always reload BPB -- do not want to write back outdated stuff.
+ if (disk_read_sectors(buffer, data.unit[saved_drs->unit].start_sector, 1) != 1) {
+ // Cannot read start sector...
+ saved_drs->status = DOS_DD_ERR_READ_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ bpb = (DOS_BPB*) &buffer[DOS_BPB_OFFSET_ON_DISK];
+ if (bpb->sectorsPerFAT != 0) {
+ bpb->fat16.volumeSerialNumber = iod->mid.serialNumber;
+ _fmemcpy(bpb->fat16.volumeLabel, iod->mid.volumeLabel, 11);
+ _fmemcpy(bpb->fat16.fsType, iod->mid.fsType, 8);
+ } else if (bpb->fat32.sectorPerFatLarge != 0) {
+ bpb->fat32.volumeSerialNumber = iod->mid.serialNumber;
+ _fmemcpy(bpb->fat32.volumeLabel, iod->mid.volumeLabel, 11);
+ _fmemcpy(bpb->fat32.fsType, iod->mid.fsType, 8);
+ } else {
+ saved_drs->status = DOS_DD_ERR_UNKNOWN_MEDIA | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ if (disk_write_sectors(buffer, data.unit[saved_drs->unit].start_sector, 1) != 1) {
+ // Cannot re-write start sector...
+ saved_drs->status = DOS_DD_ERR_WRITE_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ // Re-sync our internal BPB
+ memcpy(&data.unit[saved_drs->unit].bpb, bpb, sizeof(DOS_BPB));
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ return;
+ case DOS_IOCTL_DISK_READ_TRACK_ON_LOGICAL_DEVICE: // Absolute read
+ dprintf(" read track cylinder %u head %u sector %u sectors %u buf %Fp -> lba %lu\n",
+ iod->rw.cylinder, iod->rw.head, iod->rw.firstSector, iod->rw.sectors, iod->rw.buffer,
+ chs_to_lba(bpb, iod->rw.cylinder, iod->rw.head, iod->rw.firstSector + 1));
+ // Need the partition's BPB to map CHS to LBA...
+ if (!data.unit[saved_drs->unit].ready) {
+ saved_drs->status = DOS_DD_ERR_MEDIA_UNAVAILABLE | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ if (disk_read_sectors(iod->rw.buffer, chs_to_lba(bpb, iod->rw.cylinder, iod->rw.head, iod->rw.firstSector + 1), iod->rw.sectors) != iod->rw.sectors) {
+ saved_drs->status = DOS_DD_ERR_READ_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ return;
+ case DOS_IOCTL_DISK_WRITE_TRACK_ON_LOGICAL_DEVICE:
+ dprintf(" write track cylinder %u head %u sector %u sectors %u buf %Fp -> lba %lu\n",
+ iod->rw.cylinder, iod->rw.head, iod->rw.firstSector, iod->rw.sectors, iod->rw.buffer,
+ chs_to_lba(bpb, iod->rw.cylinder, iod->rw.head, iod->rw.firstSector + 1));
+ // Need the partition's BPB to map CHS to LBA...
+ if (!data.unit[saved_drs->unit].ready) {
+ saved_drs->status = DOS_DD_ERR_MEDIA_UNAVAILABLE | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ if (read_only) {
+ saved_drs->status = DOS_DD_ERR_WRITE_PROTECT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ if (disk_write_sectors(iod->rw.buffer, chs_to_lba(bpb, iod->rw.cylinder, iod->rw.head, iod->rw.firstSector + 1), iod->rw.sectors) != iod->rw.sectors) {
+ saved_drs->status = DOS_DD_ERR_WRITE_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ return;
+ case DOS_IOCTL_DISK_FORMAT_TRACK_ON_LOGICAL_DEVICE:
+ dprintf(" formatting track cylinder %u head %u\n",
+ iod->fv.cylinder, iod->fv.head);
+ if (!data.unit[saved_drs->unit].exists) {
+ dputs(" but partition does not exist!");
+ saved_drs->status = DOS_DD_ERR_MEDIA_UNAVAILABLE | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ if (read_only) {
+ saved_drs->status = DOS_DD_ERR_WRITE_PROTECT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ // Total nop. Should we zerofill the track or something?
+ iod->fv.specialFunctions = 1; // Not supported by BIOS?
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ return;
+ case CHUDISK_IOCTL_DISK_READ_ABS_SECTOR:
+ dprintf(" read abs sector %lu sectors %u buf %Fp\n",
+ ciod->rwabs.sector, ciod->rwabs.sectors, ciod->rwabs.buffer);
+ if (disk_read_sectors(ciod->rwabs.buffer, ciod->rwabs.sector, ciod->rwabs.sectors) != ciod->rwabs.sectors) {
+ saved_drs->status = DOS_DD_ERR_READ_FAULT | DOS_DD_STATUS_ERROR_DONE;
+ return;
+ }
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ return;
+ }
+ }
+
+ dputs(" ioctl not supported\n");
+ saved_drs->status = DOS_DD_ERR_UNKNOWN_COMMAND | DOS_DD_STATUS_ERROR_DONE;
+ break;
+ case DOS_DD_FUNC_GET_LOGICAL_DEVICE:
+ dprintf("get logical device unit=%hu\n", saved_drs->unit);
+ saved_drs->unit = 0;
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ break;
+ case DOS_DD_FUNC_SET_LOGICAL_DEVICE:
+ // We do not support this. Ignore it.
+ dprintf("set logical device unit=%hu\n", saved_drs->unit);
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ break;
+ case DOS_DD_FUNC_IOCTL_QUERY:
+ dprintf("ioctl query %x:%x buf=%Fp\n", saved_drs->cmd.ioctl.category, saved_drs->cmd.ioctl.minor, saved_drs->cmd.ioctl.data);
+ switch (saved_drs->cmd.ioctl.category) {
+ case DOS_IOCTL_DISK:
+ switch (saved_drs->cmd.ioctl.minor) {
+ case DOS_IOCTL_DISK_GET_DEVICE_PARAMETERS:
+ case DOS_IOCTL_DISK_GET_MEDIA_ID:
+ case DOS_IOCTL_DISK_SET_MEDIA_ID:
+ case DOS_IOCTL_DISK_READ_TRACK_ON_LOGICAL_DEVICE:
+ case DOS_IOCTL_DISK_WRITE_TRACK_ON_LOGICAL_DEVICE:
+ case DOS_IOCTL_DISK_FORMAT_TRACK_ON_LOGICAL_DEVICE:
+ case CHUDISK_IOCTL_DISK_READ_ABS_SECTOR:
+ // Supported
+ saved_drs->status = DOS_DD_STATUS_DONE;
+ return;
+ }
+ break;
+ }
+ // Unsupported
+ saved_drs->status = DOS_DD_ERR_UNKNOWN_COMMAND | DOS_DD_STATUS_ERROR_DONE;
+ break;
+ default:
+ dprintf("unknown function received %u\n", saved_drs->function);
+ break;
+ }
+}
+
+// Required by Watcom ABI -- since we do not link to the runtime
+void _small_code() {}
+
+// Symbol to let initialization code know the end of resident data
+char resident_end;
diff --git a/dos/chudisk.h b/dos/chudisk.h
new file mode 100644
index 0000000..1d2fbe4
--- /dev/null
+++ b/dos/chudisk.h
@@ -0,0 +1,93 @@
+/*
+ * CHU - driver for CH375 in disk mode
+ * Copyright (C) 2024 Javier S. Pedro
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef CHUDISK_H
+#define CHUDISK_H
+
+#include <stdbool.h>
+
+#include "inc/dosdrv.h"
+#include "ch375dos.h"
+
+#define MAX_UNITS 4
+
+#define SECTOR_SIZE 512
+
+// Configuration values shared between driver init code and resident code
+extern struct ch375 ch375_dev;
+
+extern unsigned char num_units;
+
+extern bool read_only;
+
+extern bool show_fat32;
+
+/** Runtime data */
+extern struct chudisk_data
+{
+ /** Pointers to all BPBs, to return to DOS during init. */
+ DOS_BPB * bpb_ptrs[MAX_UNITS];
+
+ /** Whether we are ready to handle IO, i.e. disk init, partitions scanned. */
+ bool ready;
+
+ /** Current media: number of sectors. */
+ uint32_t sectors;
+ /** Current media: bytes per sector (usually 512). */
+ uint32_t bytesPerSector;
+
+ struct chudisk_unit {
+ /** Whether the partition exists (start_sector points to it) */
+ bool exists;
+ /** Note in theory may be zero (superfloppy) */
+ uint32_t start_sector;
+ /** Number of sectors for this partition. */
+ uint32_t sectors;
+ /** Whether the partition's BPB was loaded. */
+ bool ready;
+ /** DOS device params (contains BPB), only valid if 'ready'. */
+ DOS_BPB bpb;
+ } unit[MAX_UNITS];
+} data;
+
+// Functions called by both init part and resident part
+extern void driver_init(DOS_DRS __far *drs);
+
+extern char resident_end;
+
+extern uint8_t wait_for_ch375_result(unsigned int max_ticks);
+
+
+// CHUDISK IOCTLs
+// These are used with the DISK major
+// TODO Not sure we can really happily add custom minors here,
+// but then again DOS outright refuses to accept any other major
+enum CHUDISK_IOCTL_DISK_MINOR {
+ CHUDISK_IOCTL_DISK_READ_ABS_SECTOR = 0xC1
+};
+
+typedef _Packed union CHUDISK_IOCTL_DISK_DATA
+{
+ struct CHUDISK_RWABSSECTOR {
+ uint32_t sector;
+ uint8_t sectors;
+ void far * buffer;
+ } rwabs;
+} CHUDISK_IOCTL_DISK_DATA;
+
+#endif // CHUDISK_H
diff --git a/dos/chudisk.lnk b/dos/chudisk.lnk
new file mode 100644
index 0000000..1768998
--- /dev/null
+++ b/dos/chudisk.lnk
@@ -0,0 +1,4 @@
+format dos com
+
+option statics
+option map
diff --git a/dos/cio.c b/dos/cio.c
new file mode 100644
index 0000000..43a2a27
--- /dev/null
+++ b/dos/cio.c
@@ -0,0 +1,147 @@
+/*
+ * CHU - driver for CH375 in disk mode
+ * Copyright (C) 2024 Javier S. Pedro
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/* Simple printf function for console, a bit wasteful for what it is used.
+ * But it is part of the init code, so it will be thrown away.
+ * dlog.h is more powerful but only in debug builds. */
+
+
+#include <stdarg.h>
+#include <stdbool.h>
+#include <limits.h>
+#include <i86.h>
+
+#include "inc/dosapi.h"
+
+#include "cio.h"
+
+static inline void c_llputc(char c)
+{
+ dos_print_char(c);
+}
+
+void c_utoa(unsigned num, unsigned base)
+{
+ char buf[6];
+ int i = 0;
+
+ do {
+ unsigned digit = num % base;
+
+ if (digit < 10) {
+ buf[i] = '0' + digit;
+ } else {
+ buf[i] = 'a' + (digit - 10);
+ }
+
+ i++;
+ num /= base;
+ } while (num > 0 && i < sizeof(buf));
+
+ while (i--) {
+ c_llputc(buf[i]);
+ }
+}
+
+void c_itoa(int num, unsigned base)
+{
+ unsigned unum;
+
+ if (num < 0) {
+ c_llputc('-');
+ unum = -num;
+ } else {
+ unum = num;
+ }
+
+ c_utoa(unum, base);
+}
+
+unsigned c_strtou(const char * *str)
+{
+ unsigned i = 0;
+ const char *s = *str;
+ char c = *s;
+ while (c >= '0' && c <= '9') {
+ i = (i * 10) + (c - '0');
+ c = *(++s);
+ }
+ *str = s;
+ return i;
+}
+
+static void c_printstr(const char *s)
+{
+ while (*s) { c_llputc(*s++); }
+}
+
+void c_printf(const char *fmt, ...)
+{
+ va_list va;
+ char c;
+
+ va_start(va, fmt);
+
+ while ((c = *(fmt++))) {
+ if (c == '%') {
+ switch ((c = *(fmt++))) {
+ case 'd':
+ c_itoa(va_arg(va, int), 10);
+ break;
+ case 'u':
+ c_utoa(va_arg(va, unsigned), 10);
+ break;
+ case 'x':
+ case 'p':
+ c_utoa(va_arg(va, unsigned), 16);
+ break;
+ case 's':
+ c_printstr(va_arg(va, const char *));
+ break;
+ case 'c':
+ c_putc(va_arg(va, char));
+ break;
+ case '%':
+ c_llputc('%');
+ break;
+ }
+ } else {
+ c_putc(c);
+ }
+ }
+
+ va_end(va);
+}
+
+void c_putc(char c)
+{
+ if (c == '\n') {
+ c_llputc('\r');
+ }
+ c_llputc(c);
+}
+
+
+void c_puts(const char *s)
+{
+ char c;
+ while ((c = *(s++))) {
+ c_putc(c);
+ }
+ c_putc('\n');
+}
diff --git a/dos/cio.h b/dos/cio.h
new file mode 100644
index 0000000..24a67ae
--- /dev/null
+++ b/dos/cio.h
@@ -0,0 +1,31 @@
+/*
+ * CHU - driver for CH375 in disk mode
+ * Copyright (C) 2024 Javier S. Pedro
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#ifndef CIO_H
+#define CIO_H
+
+void c_utoa(unsigned num, unsigned base);
+void c_itoa(int num, unsigned base);
+unsigned c_strtou(const char * *str);
+
+void c_printf(const char *fmt, ...);
+void c_putc(char c);
+void c_puts(const char *s);
+
+#endif // CIO_H
diff --git a/dos/inc/bda.h b/dos/inc/bda.h
new file mode 100644
index 0000000..b447221
--- /dev/null
+++ b/dos/inc/bda.h
@@ -0,0 +1,67 @@
+/*
+ * VBMouse - BIOS data area access routines
+ * Copyright (C) 2022 Javier S. Pedro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef BDA_H
+#define BDA_H
+
+#define BIOS_DATA_AREA_SEGMENT 0x40
+
+static inline uint32_t bda_get_dword(unsigned int offset) {
+ uint32_t __far *p = MK_FP(BIOS_DATA_AREA_SEGMENT, offset);
+ return *p;
+}
+
+static inline uint16_t bda_get_word(unsigned int offset) {
+ uint16_t __far *p = MK_FP(BIOS_DATA_AREA_SEGMENT, offset);
+ return *p;
+}
+
+static inline uint8_t bda_get_byte(unsigned int offset) {
+ uint8_t __far *p = MK_FP(BIOS_DATA_AREA_SEGMENT, offset);
+ return *p;
+}
+
+#define bda_get_ebda_segment() bda_get_word(0x0e)
+
+#define bda_get_equipment() bda_get_word(0x10)
+
+#define bda_get_video_mode() bda_get_byte(0x49)
+#define bda_get_num_columns() bda_get_word(0x4a)
+#define bda_get_video_page_size() bda_get_word(0x4c)
+#define bda_get_cur_video_page() bda_get_word(0x62)
+#define bda_get_last_row() bda_get_byte(0x84)
+#define bda_get_char_height() bda_get_word(0x85)
+
+#define bda_get_tick_count() bda_get_dword(0x6c)
+#define bda_get_tick_count_lo() bda_get_word(0x6c)
+
+static inline void bda_wait_tick() {
+ uint16_t cur_ticks = bda_get_tick_count_lo();
+ do {
+ //pause();
+ } while (cur_ticks == bda_get_tick_count_lo());
+}
+
+static inline void bda_wait_ticks(unsigned int ticks) {
+ for (; ticks > 0; ticks--) {
+ bda_wait_tick();
+ }
+}
+
+#endif // BDA_H
diff --git a/dos/inc/bpb.h b/dos/inc/bpb.h
new file mode 100644
index 0000000..8df04a1
--- /dev/null
+++ b/dos/inc/bpb.h
@@ -0,0 +1,83 @@
+#ifndef BPB_H
+#define BPB_H
+
+#include <stdint.h>
+#include "sassert.h"
+
+#define DOS_BPB_OFFSET_ON_DISK 0x0B
+
+typedef _Packed struct DOS_BPB_DOS3
+{
+ /** Number of bytes per sector. */
+ uint16_t bytesPerSector;
+ /** Number of sectors per cluster. Must be power of 2. */
+ uint8_t sectorsPerCluster;
+ /** Number of reserved sectors. */
+ uint16_t reservedSectors;
+ /** Number of FATs. */
+ uint8_t numberOfFATs;
+ /** Maximum number of entries in the root directory. */
+ uint16_t rootDirEntries;
+ /** Total number of sectors on the drive. If drive larger than 32MB, this field is zero, see numberOfSectorsLarge. */
+ uint16_t numberOfSectors;
+ /** Media descriptor byte. */
+ uint8_t mediaType;
+ /** Number of sectors used by a FAT. */
+ uint16_t sectorsPerFAT;
+ /** Number of sectors per track. */
+ uint16_t sectorsPerTrack;
+ /** Number of heads. */
+ uint16_t numberOfHeads;
+ /** Number of hidden sectors on the drive, from the start of the drive. */
+ uint32_t hiddenSectors;
+ /** Number of sectors, only used if drive > 32MB. */
+ uint32_t numberOfSectorsLarge;
+} DOS_BPB_DOS3;
+STATIC_ASSERT(sizeof(DOS_BPB_DOS3) == 25);
+
+typedef _Packed struct DOS_BPB_DOS4
+{
+ struct DOS_BPB_DOS3;
+ uint8_t physicalDriveNumber;
+ uint8_t flags;
+ uint8_t extendedBootSignature;
+ uint32_t volumeSerialNumber;
+ char volumeLabel[11];
+ char fsType[8];
+} DOS_BPB_DOS4;
+STATIC_ASSERT(sizeof(DOS_BPB_DOS4) == 25 + 26);
+
+typedef _Packed struct DOS_BPB_FAT32
+{
+ struct DOS_BPB_DOS3;
+ uint32_t sectorPerFatLarge;
+ uint16_t extendedFlags;
+ uint16_t version;
+ uint32_t rootDirCluster;
+ uint16_t infoSector;
+ uint16_t backupBootSector;
+
+ char bootFileName[12];
+ uint8_t physicalDriveNumber;
+ uint8_t flags;
+ uint8_t extendedBootSignature;
+ uint32_t volumeSerialNumber;
+ char volumeLabel[11];
+ char fsType[8];
+} DOS_BPB_FAT32;
+STATIC_ASSERT(sizeof(DOS_BPB_FAT32) == 25 + 54);
+
+/** Union of all BPB types. Default is DOS4 / FAT16. */
+typedef union DOS_BPB
+{
+ struct DOS_BPB_DOS3;
+ struct DOS_BPB_DOS4 fat16;
+ struct DOS_BPB_FAT32 fat32;
+} DOS_BPB;
+
+typedef enum DOS_BPB_MEDIA_TYPE
+{
+ DOS_BPB_MEDIA_HDD = 0xF8
+} DOS_BPB_MEDIA_TYPE;
+
+#endif // BPB_H
diff --git a/dos/inc/dosapi.h b/dos/inc/dosapi.h
new file mode 100644
index 0000000..18dbde0
--- /dev/null
+++ b/dos/inc/dosapi.h
@@ -0,0 +1,182 @@
+/*
+ * VBMouse - Interface to some DOS int 21h services
+ * Copyright (C) 2022 Javier S. Pedro
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef INT21DOS_H
+#define INT21DOS_H
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <dos.h>
+#include "sassert.h"
+
+#define DOS_PSP_SIZE 256
+
+enum dos_error {
+ DOS_ERROR_SUCCESS = 0,
+ DOS_ERROR_INVALID_FUNCTION = 1,
+ DOS_ERROR_FILE_NOT_FOUND = 2,
+ DOS_ERROR_PATH_NOT_FOUND = 3,
+ DOS_ERROR_TOO_MANY_OPEN_FILES = 4,
+ DOS_ERROR_ACCESS_DENIED = 5,
+ DOS_ERROR_INVALID_HANDLE = 6,
+ DOS_ERROR_ARENA_TRASHED = 7,
+ DOS_ERROR_NOT_ENOUGH_MEMORY = 8,
+ DOS_ERROR_INVALID_BLOCK = 9,
+ DOS_ERROR_BAD_ENVIRONMENT = 10,
+ DOS_ERROR_BAD_FORMAT = 11,
+ DOS_ERROR_INVALID_ACCESS = 12,
+ DOS_ERROR_INVALID_DATA = 13,
+ DOS_ERROR_OUT_OF_MEMORY = 14,
+ DOS_ERROR_INVALID_DRIVE = 15,
+ DOS_ERROR_CURRENT_DIRECTORY = 16,
+ DOS_ERROR_NOT_SAME_DEVICE = 17,
+ DOS_ERROR_NO_MORE_FILES = 18,
+ DOS_ERROR_WRITE_PROTECT = 19,
+ DOS_ERROR_BAD_UNIT = 20,
+ DOS_ERROR_NOT_READY = 21,
+ DOS_ERROR_BAD_COMMAND = 22,
+ DOS_ERROR_CRC = 23,
+ DOS_ERROR_BAD_LENGTH = 24,
+ DOS_ERROR_SEEK = 25,
+ DOS_ERROR_NOT_DOS_DISK = 26,
+ DOS_ERROR_SECTOR_NOT_FOUND = 27,
+
+ DOS_ERROR_GEN_FAILURE = 31,
+
+ DOS_ERROR_HANDLE_EOF = 38,
+ DOS_ERROR_HANDLE_DISK_FULL = 39,
+
+ DOS_ERROR_FILE_EXISTS = 80,
+ DOS_ERROR_CANNOT_MAKE = 82,
+};
+
+// General functions
+
+static inline void dos_print_char(char c);
+#pragma aux dos_print_char = \
+ "mov ah, 0x02" \
+ "int 0x21" \
+ __parm [dl] \
+ __modify [ah]
+
+static inline uint8_t dos_read_char(void);
+#pragma aux dos_read_char = \
+ "mov ah, 0x08" \
+ "int 0x21" \
+ __value [al] \
+ __modify [ax]
+
+static inline void dos_print_string(const char *s);
+#pragma aux dos_print_string = \
+ "mov ah, 0x09" \
+ "int 0x21" \
+ __parm [dx] \
+ __modify [ah]
+
+static inline uint16_t dos_get_version(void);
+#pragma aux dos_get_version = \
+ "mov ah, 0x30" \
+ "int 0x21" \
+ __value [ax] \
+ __modify [bx cx]
+
+// Memory manipulation functions
+
+typedef __segment segment_t;
+
+enum dos_allocation_strategy {
+ DOS_FIT_FIRST = 0,
+ DOS_FIT_BEST = 1,
+ DOS_FIT_LAST = 2,
+
+ DOS_FIT_HIGH = 0x80,
+ DOS_FIT_HIGHONLY = 0x40,
+};
+
+/** Converts bytes to paragraphs (16 bytes), rounding up. */
+static inline unsigned get_paragraphs(unsigned bytes)
+{
+ return (bytes + 15) / 16;
+}
+
+static unsigned dos_query_allocation_strategy(void);
+#pragma aux dos_query_allocation_strategy = \
+ "mov ax, 0x5800" \
+ "int 0x21" \
+ __value [ax]
+
+static int dos_set_allocation_strategy(unsigned strategy);
+#pragma aux dos_set_allocation_strategy = \
+ "clc" \
+ "mov ax, 0x5801" \
+ "int 0x21" \
+ "jc end" \
+ "mov ax, 0" \
+ "end:" \
+ __parm [bx] \
+ __value [ax]
+
+static bool dos_query_umb_link_state(void);
+#pragma aux dos_query_umb_link_state = \
+ "mov ax, 0x5802" \
+ "int 0x21" \
+ __value [al]
+
+static int dos_set_umb_link_state(bool state);
+#pragma aux dos_set_umb_link_state = \
+ "clc" \
+ "mov ax, 0x5803" \
+ "int 0x21" \
+ "jc end" \
+ "mov ax, 0" \
+ "end:" \
+ __parm [bx] \
+ __value [ax]
+
+/** Allocates a new DOS segment.
+ * @returns either the allocated segment or 0 if any error. */
+static __segment dos_alloc(unsigned paragraphs);
+#pragma aux dos_alloc = \
+ "clc" \
+ "mov ah, 0x48" \
+ "int 0x21" \
+ "jnc end" \
+ "mov ax, 0xB" \
+ "end:" \
+ __parm [bx] \
+ __value [ax]
+
+/** Frees DOS segment. */
+static void dos_free(segment_t segment);
+#pragma aux dos_free = \
+ "mov ah, 0x49" \
+ "int 0x21" \
+ __parm [es] \
+ __modify [ax]
+
+/** Sets a given PSP as the current active process. */
+static void dos_set_psp(segment_t psp);
+#pragma aux dos_set_psp = \
+ "mov ah, 0x50" \
+ "int 0x21" \
+ __parm [bx] \
+ __modify [ax]
+
+#endif // INT21DOS_H
diff --git a/dos/inc/dosdrv.h b/dos/inc/dosdrv.h
new file mode 100644
index 0000000..0c33c54
--- /dev/null
+++ b/dos/inc/dosdrv.h
@@ -0,0 +1,319 @@
+#ifndef DOSDRV_H
+#define DOSDRV_H
+
+#include <stdint.h>
+#include "bpb.h"
+#include "sassert.h"
+
+typedef enum DOS_DD_ERROR
+{
+ DOS_DD_ERR_WRITE_PROTECT = 0,
+ DOS_DD_ERR_UNKNOWN_UNIT = 1,
+ DOS_DD_ERR_DRIVE_NOT_READY = 2,
+ DOS_DD_ERR_UNKNOWN_COMMAND = 3,
+ DOS_DD_ERR_CRC = 4,
+ DOS_DD_ERR_BAD_DRS_LEN = 5,
+ DOS_DD_ERR_SEEK = 6,
+ DOS_DD_ERR_UNKNOWN_MEDIA = 7,
+ DOS_DD_ERR_SECTOR_NOT_FOUND = 8,
+ DOS_DD_ERR_PRINTER_OUT_OF_PAPER = 9,
+ DOS_DD_ERR_WRITE_FAULT = 10,
+ DOS_DD_ERR_READ_FAULT = 11,
+ DOS_DD_ERR_GENERAL_FAILURE = 12,
+ DOS_DD_ERR_MEDIA_UNAVAILABLE = 0xE,
+ DOS_DD_ERR_INVALID_DISK_CHANGE = 0xF
+} DOS_DD_ERROR;
+
+typedef enum DOS_DD_STATUS {
+ DOS_DD_STATUS_DONE = 1 << 8,
+ DOS_DD_STATUS_BUSY = 1 << 9,
+ DOS_DD_STATUS_ERROR = 1 << 15,
+
+ DOS_DD_STATUS_ERROR_DONE = DOS_DD_STATUS_ERROR | DOS_DD_STATUS_DONE
+} DOS_DD_STATUS;
+
+typedef enum DOS_DD_MEDIAQUERY_RESULT {
+ DOS_DD_MEDIAQUERY_CHANGED = -1,
+ DOS_DD_MEDIAQUERY_UNKNOWN = 0,
+ DOS_DD_MEDIAQUERY_NOT_CHANGED = 1
+} DOS_DD_MEDIAQUERY_RESULT;
+
+typedef _Packed struct DOS_DRS
+{
+ /** The length of this structure. */
+ uint8_t len;
+ /** The subunit the operation is for. */
+ uint8_t unit;
+
+ uint8_t function;
+
+ uint16_t status;
+
+ uint32_t reserved1;
+ uint32_t reserved2;
+
+ union {
+ union {
+ struct {
+ uint8_t reserved;
+ /** End of memory available for driver. */
+ void __far * avail_addr;
+ /** Command line (i.e. the DEVICE= line used to load this driver. ) */
+ const char __far * cmdline;
+ /** First drive that is going to be allocated for this driver. */
+ uint8_t first_drive_number;
+ } in;
+ struct {
+ /** Number of units/drive letters to allocate for this driver. */
+ uint8_t number_units;
+ /** Break addr: everything after this address is deallocated after init. */
+ void __far * break_addr;
+ /** Pointer to the BPB array. */
+ DOS_BPB * __far * bpbs;
+ uint8_t reserved;
+ /** Display error message on failure. */
+ uint16_t error_msg_flag;
+ } out;
+ } init;
+ union {
+ struct {
+ /** Media descriptor of the current media. */
+ uint8_t cur_media_desc;
+ } in;
+ struct {
+ uint8_t reserved;
+ uint8_t result;
+ /** Volume ID of previous loaded media. */
+ char far * volume_id;
+ } out;
+ } mediacheck;
+ union {
+ struct {
+ /** Media descriptor of the current media. */
+ uint8_t cur_media_desc;
+ /** Buffer with sector 0 of the disk, depending on driver flags */
+ void __far * sector_zero;
+ } in;
+ struct {
+ uint8_t reserved;
+ void __far * sector_zero;
+ /** Address for the new BPB. */
+ DOS_BPB __far * bpb;
+ } out;
+ } buildbpb;
+ struct {
+ uint8_t data;
+ /** Input: pointer to buffer. */
+ void __far * buffer;
+ /** Input/Output: buffer len / actual len */
+ uint16_t len;
+ } ioctlrw;
+ struct {
+ /** Input: current media descriptor byte. */
+ uint8_t cur_media_desc;
+ /** Input: pointer to buffer. */
+ void __far * buffer;
+ /** Input: Number of sectors to read/write. Output: number sectors actually read/written. */
+ uint16_t len;
+ /** Input: Begin sector. Or, if 0xFFFF, see start_sector_huge. */
+ uint16_t start_sector;
+ /** Output: Current Volume ID. */
+ char far * volume_id;
+ uint32_t start_sector_huge;
+ } rw;
+ struct {
+ uint8_t category;
+ uint8_t minor;
+ uint32_t reserved;
+ void __far * data;
+ } ioctl;
+ } cmd;
+} DOS_DRS;
+
+typedef void (DOS_DD_STRATEGY)(DOS_DRS __far *drs);
+typedef void (DOS_DD_INTERRUPT)(DOS_DRS __far *drs);
+#pragma aux DOS_DD_STRATEGY __parm __caller [es bx] __modify [] __loadds __far
+#pragma aux DOS_DD_INTERRUPT __parm __caller [] __modify [] __loadds __far
+
+typedef enum DOS_DD_FUNCTIONS
+{
+ DOS_DD_FUNC_INIT = 0,
+ DOS_DD_FUNC_MEDIA_CHECK = 1,
+ DOS_DD_FUNC_BUILD_BPB = 2,
+ DOS_DD_FUNC_IOCTL_READ = 3,
+ DOS_DD_FUNC_READ = 4,
+ DOS_DD_FUNC_NONDESTRUCTIVE_READ = 5,
+ DOS_DD_FUNC_INPUT_STATUS = 6,
+ DOS_DD_FUNC_INPUT_FLUSH = 7,
+ DOS_DD_FUNC_WRITE = 8,
+ DOS_DD_FUNC_WRITE_VERIFY = 9,
+ DOS_DD_FUNC_OUTPUT_STATUS = 10,
+ DOS_DD_FUNC_OUTPUT_FLUSH = 11,
+ DOS_DD_FUNC_IOCTL_WRITE = 12,
+ DOS_DD_FUNC_DEVICE_OPEN = 13,
+ DOS_DD_FUNC_DEVICE_CLOSE = 14,
+ DOS_DD_FUNC_REMOVEABLE_MEDIA = 15,
+ DOS_DD_FUNC_OUTPUT_UNTIL_BUSY = 16,
+ DOS_DD_FUNC_IOCTL_GENERIC = 19,
+ DOS_DD_FUNC_GET_LOGICAL_DEVICE = 23,
+ DOS_DD_FUNC_SET_LOGICAL_DEVICE = 24,
+ DOS_DD_FUNC_IOCTL_QUERY = 25,
+} DOS_DD_COMMAND;
+
+
+/** Device driver (.sys) header */
+typedef _Packed struct DOS_DD_HEADER
+{
+ void __far *nextDevice;
+ uint16_t attributes;
+ DOS_DD_STRATEGY *strategyCb;
+ DOS_DD_INTERRUPT *interrupCb;
+ char name[8];
+} DOS_DD_HEADER;
+
+typedef enum DOS_DD_ATTRIBUTES
+{
+ /** Character-device driver. */
+ DOS_DD_CHAR = 1 << 15,
+ /** Block-device driver. */
+ DOS_DD_BLOCK = 0,
+
+ /** Supports IOCTL Read/Write funcs */
+ DOS_DD_SUPPORT_IOCTL_RW = 1 << 14,
+
+ /** Support IOCTL query funcs. */
+ DOS_DD_SUPPORT_IOCTL_QUERY = 1 << 7,
+
+ // Character-device specific
+ DOS_CHAR_CURRENT_STDIN_DEVICE = 1 << 0,
+ DOS_CHAR_CURRENT_STDOUT_DEVICE = 1 << 1,
+ DOS_CHAR_CURRENT_NUL_DEVICE = 1 << 2,
+ DOS_CHAR_CURRENT_CLOCK_DEVICE = 1 << 3,
+ DOS_CHAR_SPECIAL_FAST_CONSOLE_IO = 1 << 4,
+ DOS_CHAR_OUTPUT_UNTIL_BUSY = 1 << 13,
+
+ // Block-device specific
+ /** The driver support 32-bit sector address. (Can use the _huge fields. ) */
+ DOS_BLOCK_SUPPORT_32BIT_ADDRESSES = 1 << 1,
+
+ /** Support Get/Set Logical Drive and generic IOCTL funcs. */
+ DOS_BLOCK_SUPPORT_IOCTL_GENERIC = 1 << 6,
+
+ /** Supports Open/Close/Removable media funcs */
+ DOS_BLOCK_SUPPORT_REMOVABLE_MEDIA = 1 << 11,
+
+ /** if set, DOS not read the 1st sector of the 1st FAT during build bpb. */
+ DOS_BLOCK_FORMAT_NON_IBM = 1 << 13,
+} DOS_DD_ATTRIBUTES;
+
+// Generic IOCTLs
+
+typedef enum DOS_IOCTL_CATEGORY
+{
+ DOS_IOCTL_SERIAL = 0x1,
+ DOS_IOCTL_CONSOLE = 0x3,
+ DOS_IOCTL_PARALLEL = 0x5,
+ DOS_IOCTL_DISK = 0x8,
+} DOS_IOCTL_CATEGORY;
+
+typedef enum DOS_IOCTL_MINOR
+{
+ DOS_IOCTL_CONSOLE_SET_ITERATION_COUNT = 0x45,
+ DOS_IOCTL_CONSOLE_SELECT_CODE_PAGE = 0x4A,
+ DOS_IOCTL_CONSOLE_START_CODE_PAGE_PREPARE = 0x4C,
+ DOS_IOCTL_CONSOLE_END_CODE_PAGE_PREPARE = 0x4D,
+ DOS_IOCTL_CONSOLE_GET_IERATION_COUNT = 0x65,
+ DOS_IOCTL_CONSOLE_QUERY_SELECTED_CODE_PAGE = 0x6A,
+ DOS_IOCTL_CONSOLE_QUERY_CODE_PAGE_PREPARE_LIST = 0x6B,
+
+ DOS_IOCTL_DISK_SET_DEVICE_PARAMETERS = 0x40,
+ DOS_IOCTL_DISK_WRITE_TRACK_ON_LOGICAL_DEVICE = 0x41,
+ DOS_IOCTL_DISK_FORMAT_TRACK_ON_LOGICAL_DEVICE = 0x42,
+ DOS_IOCTL_DISK_SET_MEDIA_ID = 0x46,
+ DOS_IOCTL_DISK_GET_DEVICE_PARAMETERS = 0x60,
+ DOS_IOCTL_DISK_READ_TRACK_ON_LOGICAL_DEVICE = 0x61,
+ DOS_IOCTL_DISK_VERIFY_TRACK_ON_LOGICAL_DEVICE = 0x62,
+ DOS_IOCTL_DISK_GET_MEDIA_ID = 0x66,
+ DOS_IOCTL_DISK_SENSE_MEDIA_TYPE = 0x68,
+} DOS_IOCTL_MINOR;
+
+
+// Block-device / disk data structures
+
+typedef _Packed struct DOS_DEVICEPARAMS
+{
+ uint8_t specialFunctions;
+ uint8_t deviceType;
+ uint16_t deviceAttributes;
+ uint16_t numberOfCylinders;
+ uint8_t mediaType;
+ DOS_BPB_DOS3 bpb;
+} DOS_DEVICEPARAMS;
+
+enum DOS_DEVICEPARAMS_SPECIAL_FUNCTIONS
+{
+ /** Use the device BPB for all subsequent Build BPB requests. */
+ DOS_DEVICEPARAMS_USE_DEVICE_BPB = (1 << 0),
+ /** Ignore all fields except the TRACKLAYOUT structure appended after DEVICEPARAMS */
+ DOS_DEVICEPARAMS_TRACKLAYOUT = (1 << 1),
+ /** The sectors in the track are all the same size, and sector numbers are in the range [1..N]
+ * where N is the total number of sectors in the track. */
+ DOS_DEVICEPARAMS_REGULARTRACKS = (1 << 2),
+};
+
+enum DOS_DEVICEPARAMS_DEVICE_TYPE
+{
+ DOS_DEVICEPARAMS_320_360K = 0x0,
+ DOS_DEVICEPARAMS_1_2_MB = 0x1,
+ DOS_DEVICEPARAMS_720K = 0x2,
+ DOS_DEVICEPARAMS_8INCH_SD = 0x3,
+ DOS_DEVICEPARAMS_8INCH_DD = 0x4,
+ DOS_DEVICEPARAMS_HDD = 0x5,
+ DOS_DEVICEPARAMS_TAPE = 0x6,
+ DOS_DEVICEPARAMS_1_44_MB = 0x7,
+ DOS_DEVICEPARAMS_OPTICAL = 0x8,
+ DOS_DEVICEPARAMS_2_88_MB = 0x9
+};
+
+enum DOS_DEVICEPARAMS_DEVICE_ATTR
+{
+ DOS_DEVICEPARAMS_REMOVABLE = 0,
+ DOS_DEVICEPARAMS_FIXED = 1,
+ DOS_DEVICEPARAMS_HAS_DOORLOCK = 1 << 1
+};
+
+typedef _Packed struct DOS_MEDIA_ID
+{
+ /** Must be zero. */
+ uint16_t infoLevel;
+ uint32_t serialNumber;
+ char volumeLabel[11];
+ char fsType[8];
+} DOS_MEDIA_ID;
+
+typedef _Packed struct DOS_RWBLOCK
+{
+ uint8_t specialFunctions;
+ uint16_t head;
+ uint16_t cylinder;
+ uint16_t firstSector;
+ uint16_t sectors;
+ void far * buffer;
+} DOS_RWBLOCK;
+
+typedef _Packed struct DOS_FVBLOCK
+{
+ uint8_t specialFunctions;
+ uint16_t head;
+ uint16_t cylinder;
+} DOS_FVBLOCK;
+
+typedef _Packed union DOS_IOCTL_DISK_DATA
+{
+ DOS_DEVICEPARAMS devparams;
+ DOS_MEDIA_ID mid;
+ DOS_FVBLOCK fv;
+ DOS_RWBLOCK rw;
+} DOS_IOCTL_DISK_DATA;
+
+#endif // DOSSYS_H
diff --git a/dos/inc/dosint.h b/dos/inc/dosint.h
new file mode 100644
index 0000000..cfa855d
--- /dev/null
+++ b/dos/inc/dosint.h
@@ -0,0 +1,250 @@
+#ifndef DOSINT_H
+#define DOSINT_H
+
+// Internal DOS structures
+
+// For documentation of these, better grab "Undocumented DOS" or similar book
+// These are probably valid only on DOS >= 4
+
+typedef _Packed struct dos_current_directory_structure {
+ char curr_path[67];
+ uint16_t flags;
+ uint32_t disk_blk;
+ void __far * info;
+ char pad1[11];
+} DOSCDS;
+STATIC_ASSERT(sizeof(DOSCDS) == 88);
+
+enum dos_cds_flags {
+ DOS_CDS_FLAG_PHYSICAL = 0x4000,
+ DOS_CDS_FLAG_NETWORK = 0x8000
+};
+
+typedef _Packed struct dos_system_file_table_entry {
+ uint16_t num_handles;
+ uint16_t open_mode;
+ uint8_t attr;
+ uint16_t dev_info;
+ void __far *dpb;
+ uint16_t start_cluster;
+ uint16_t f_time;
+ uint16_t f_date;
+ uint32_t f_size;
+ uint32_t f_pos;
+ uint16_t last_rel_cluster;
+ uint16_t last_abs_cluster;
+ uint16_t dir_sector;
+ uint8_t dir_entry;
+ char filename[11];
+} DOSSFT;
+STATIC_ASSERT(sizeof(DOSSFT) == 43);
+
+enum dos_sft_dev_flags {
+ DOS_SFT_FLAG_NETWORK = 0x8000,
+ DOS_SFT_FLAG_TIME_SET = 0x4000,
+ DOS_SFT_FLAG_CLEAN = 0x0040,
+ DOS_SFT_DRIVE_MASK = 0x001F
+};
+
+typedef _Packed struct dos_search_data_block {
+ uint8_t drive;
+ char search_templ[11];
+ uint8_t search_attr;
+ uint16_t dir_entry;
+ uint16_t par_clstr;
+ char pad1[4];
+} DOSSDB;
+STATIC_ASSERT(sizeof(DOSSDB) == 21);
+
+enum dos_sdb_drive_flags {
+ DOS_SDB_DRIVE_FLAG_NETWORK = 0x80,
+ DOS_SDB_DRIVE_MASK = 0x1F
+};
+
+typedef _Packed struct dos_lock_params {
+ uint32_t start_offset;
+ uint32_t region_size;
+} DOSLOCK;
+
+typedef _Packed struct dos_direntry {
+ char filename[11];
+ uint8_t attr;
+ char pad1[10];
+ uint16_t f_time;
+ uint16_t f_date;
+ uint16_t start_cluster;
+ uint32_t f_size;
+} DOSDIR;
+STATIC_ASSERT(sizeof(DOSDIR) == 32);
+
+typedef _Packed struct dos_swappable_area {
+ uint8_t criterr;
+ uint8_t indos;
+ uint8_t drive_num;
+ uint8_t lasterr_dum[9];
+ uint8_t __far *cur_dta;
+ uint16_t cur_psp;
+ uint8_t pad1[4];
+ uint8_t cur_drive;
+ uint8_t pad2[135];
+ char fn1[128];
+ char fn2[128];
+ DOSSDB sdb;
+ DOSDIR found_file;
+ DOSCDS drive_cdscopy;
+ uint8_t fcb_fn1[11];
+ uint8_t pad3[1];
+ uint8_t fcb_fn2[11];
+ uint8_t pad4[11];
+ uint8_t search_attr;
+ uint8_t open_mode;
+ uint8_t pad5[51];
+ DOSCDS __far *drive_cds;
+ uint8_t pad6[12];
+ uint16_t fn1_csoffset;
+ uint16_t fn2_csoffset;
+ uint8_t pad7[71];
+ uint16_t openex_act;
+ uint16_t openex_attr;
+ uint16_t openex_mode;
+ uint8_t pad8[29];
+ DOSSDB rename_srcfile;
+ DOSDIR rename_file;
+} DOSSDA;
+STATIC_ASSERT(offsetof(DOSSDA, cur_dta) == 0xC);
+STATIC_ASSERT(offsetof(DOSSDA, cur_drive) == 0x16);
+STATIC_ASSERT(offsetof(DOSSDA, fn1) == 0x9E);
+STATIC_ASSERT(offsetof(DOSSDA, fn2) == 0x11E);
+STATIC_ASSERT(offsetof(DOSSDA, sdb) == 0x19E);
+STATIC_ASSERT(offsetof(DOSSDA, found_file) == 0x1B3);
+STATIC_ASSERT(offsetof(DOSSDA, search_attr) == 0x24D);
+STATIC_ASSERT(offsetof(DOSSDA, open_mode) == 0x24E);
+STATIC_ASSERT(offsetof(DOSSDA, drive_cds) == 0x282);
+STATIC_ASSERT(offsetof(DOSSDA, openex_act) == 0x2DD);
+STATIC_ASSERT(offsetof(DOSSDA, openex_mode) == 0x2E1);
+
+typedef _Packed struct dos_list_of_lists {
+ char pad1[22];
+ DOSCDS __far *cds;
+ char pad2[7];
+ uint8_t last_drive;
+} DOSLOL;
+
+typedef _Packed struct dos_nls_table {
+ uint8_t table_id;
+ void __far *table_data;
+} NLSTABLE;
+
+typedef _Packed struct file_char_table {
+ uint16_t size; // table size (not counting this word)
+ uint8_t unk1; // ??? (01h for MS-DOS 3.30-6.00)
+ uint8_t lowest; // lowest permissible character value for filename
+ uint8_t highest; // highest permissible character value for filename
+ uint8_t unk2; // ??? (00h for MS-DOS 3.30-6.00)
+ uint8_t first_x; // first excluded character in range \ all characters in this
+ uint8_t last_x; // last excluded character in range / range are illegal
+ uint8_t unk3; // ??? (02h for MS-DOS 3.30-6.00)
+ uint8_t n_illegal; // number of illegal (terminator) characters
+ uint8_t illegal[1]; // characters which terminate a filename: ."/\[]:|<>+=;,
+} FCHAR;
+
+static inline int drive_letter_to_index(char letter)
+{
+ if (letter >= 'A' && letter <= 'Z') return letter - 'A';
+ else if (letter >= 'a' && letter <= 'z') return letter - 'a';
+ else return -1;
+}
+
+static inline char drive_index_to_letter(int index)
+{
+ return 'A' + index;
+}
+
+static inline DOSSDA __far * dos_get_swappable_dos_area(void);
+#pragma aux dos_get_swappable_dos_area = \
+ "push ds" \
+ "mov ax, 0x5D06" \
+ "int 0x21" \
+ "jnc success" \
+ "xor si, si" \
+ "mov es, si" \
+ "jmp end" \
+ "success: mov ax, ds" \
+ "mov es, ax" \
+ "end: pop ds" \
+ __value [es si] \
+ __modify [ax cx dx]
+
+static inline DOSLOL __far * dos_get_list_of_lists(void);
+#pragma aux dos_get_list_of_lists = \
+ "mov ax, 0x5200" \
+ "int 0x21" \
+ __value [es bx] \
+ __modify [ax]
+
+static inline uint16_t dos_sft_decref(DOSSFT __far *sft);
+#pragma aux dos_sft_decref = \
+ "mov ax, 0x1208" \
+ "int 0x2F" \
+ __parm [es di] \
+ __value [ax]
+
+// Network redirector interface
+
+enum DOS_REDIR_SUBFUNCTION {
+ DOS_FN_RMDIR = 0x01,
+ DOS_FN_MKDIR = 0x03,
+ DOS_FN_CHDIR = 0x05,
+ DOS_FN_CLOSE = 0x06,
+ DOS_FN_COMMIT = 0x07,
+ DOS_FN_READ = 0x08,
+ DOS_FN_WRITE = 0x09,
+ DOS_FN_LOCK = 0x0A,
+ DOS_FN_UNLOCK = 0x0B,
+ DOS_FN_GET_DISK_FREE = 0x0C,
+ DOS_FN_SET_FILE_ATTR = 0x0E,
+ DOS_FN_GET_FILE_ATTR = 0x0F,
+ DOS_FN_RENAME = 0x11,
+ DOS_FN_DELETE = 0x13,
+ DOS_FN_OPEN = 0x16,
+ DOS_FN_CREATE = 0x17,
+ DOS_FN_FIND_FIRST = 0x1B,
+ DOS_FN_FIND_NEXT = 0x1C,
+ DOS_FN_CLOSE_ALL = 0x1D,
+ DOS_FN_DO_REDIR = 0x1E,
+ DOS_FN_PRINTSETUP = 0x1F,
+ DOS_FN_FLUSH = 0x20,
+ DOS_FN_SEEK_END = 0x21,
+ DOS_FN_ATEXIT = 0x22,
+ DOS_FN_QUALIFY = 0x23,
+ DOS_FN_OPEN_EX = 0x2E
+};
+
+enum OPENEX_ACTIONS {
+ OPENEX_FAIL_IF_EXISTS = 0x00,
+ OPENEX_OPEN_IF_EXISTS = 0x01,
+ OPENEX_REPLACE_IF_EXISTS = 0x02,
+ OPENEX_FAIL_IF_NEW = 0x00,
+ OPENEX_CREATE_IF_NEW = 0x10,
+};
+
+enum OPENEX_MODE {
+ OPENEX_MODE_READ = 0,
+ OPENEX_MODE_WRITE = 1,
+ OPENEX_MODE_RDWR = 2,
+ OPENEX_MODE_MASK = 3,
+ OPENEX_SHARE_COMPAT = 0x00,
+ OPENEX_SHARE_DENYALL = 0x10,
+ OPENEX_SHARE_DENYWRITE = 0x20,
+ OPENEX_SHARE_DENYREAD = 0x30,
+ OPENEX_SHARE_DENYNONE = 0x40,
+ OPENEX_SHARE_MASK = 0x70,
+};
+
+enum OPENEX_RESULT {
+ OPENEX_FILE_OPENED = 1,
+ OPENEX_FILE_CREATED = 2,
+ OPENEX_FILE_REPLACED = 3,
+};
+
+#endif // DOSINT_H
diff --git a/dos/inc/mbr.h b/dos/inc/mbr.h
new file mode 100644
index 0000000..d45727d
--- /dev/null
+++ b/dos/inc/mbr.h
@@ -0,0 +1,46 @@
+#ifndef MBR_H
+#define MBR_H
+
+#include "sassert.h"
+
+enum MBR_PARTTYPE
+{
+ MBR_EMPTY = 0,
+ MBR_FAT12 = 0x01,
+ MBR_FAT16_32MB = 0x04,
+ MBR_EXT = 0x05,
+ MBR_FAT16 = 0x06,
+ MBR_IFS = 0x07,
+ MBR_FAT32 = 0x0B,
+ MBR_FAT32_LBA = 0x0C,
+ MBR_FAT16_LBA = 0x0E,
+ MBR_EXT_LBA = 0x0F,
+ MBR_LINUX_SWAP = 0x82,
+ MBR_LINUX = 0x83,
+};
+
+typedef _Packed struct MBR_CHS
+{
+ uint8_t head;
+ unsigned sector : 6;
+ unsigned cyl : 10;
+} MBR_CHS;
+
+STATIC_ASSERT(sizeof(MBR_CHS) == 3);
+
+typedef _Packed struct MBR_PARTENTRY
+{
+ uint8_t status;
+ MBR_CHS chsFirst;
+ uint8_t type;
+ MBR_CHS chsLast;
+ uint32_t lbaFirst;
+ uint32_t lbaLen;
+} MBR_PARTENTRY;
+
+STATIC_ASSERT(sizeof(MBR_PARTENTRY) == 16);
+
+#define MBR_PARTENTRY_OFFSET_ON_DISK 446
+#define MBR_SIGNATURE_OFFSET_ON_DISK 510
+
+#endif // MBR_H
diff --git a/dos/makefile b/dos/makefile
new file mode 100644
index 0000000..aa328c2
--- /dev/null
+++ b/dos/makefile
@@ -0,0 +1,66 @@
+#!/usr/bin/env wmake
+
+# Debug Settings
+!ifeq debug 1
+# Dump debug log to serial port, 0x3F8 UART (COM1)
+debug_cflags = -DENABLE_DLOG=1 -DDLOG_TARGET=DLOG_TARGET_SERIAL -DDLOG_TARGET_PORT=0x3F8
+!endif
+
+# All binaries to build
+bins = chudisk.sys chud.exe utest.exe
+
+# Object files for chudisk
+chudisk_objs = chudisk.obj chudinit.obj cio.obj
+
+# Compiler arguments for DOS
+dos_cflags = -bt=dos -i=../inc -3 -ms -osi -ox -w3 -wcd=202 $(debug_cflags)
+# -ms to use small memory model (this assumes ss == ds)
+# -osi to optimize for size, put intrinsics inline (to avoid runtime calls)
+# -ox extra optimization (impact is not very big)
+# -w3 enables warnings
+# -wcd=202 disables the unreferenced function warning (e.g., for inline functions in headers)
+
+# Compiler arguments for the DOS drivers
+dosdrv_cflags = $(dos_cflags) -zu -s -g=DGROUP
+# -zu since ss != ds in the driver
+# -s to disable stack checks, since it inserts calls to the runtime
+# -g=DGROUP put everything into the same segment
+
+# Full compiler command lines
+compile_dos = *wcc -fo=$^@ $(dos_cflags) $[@
+compile_dosdrv = *wcc -fo=$^@ $(dosdrv_cflags) $[@
+
+# Object files
+
+.BEFORE:
+ # We need DOS and Windows headers, not host platform's
+ set include=$(%watcom)/h/win;$(%watcom)/h
+
+all: $(bins) .SYMBOLIC
+
+chudisk.sys: chudisk.lnk $(chudisk_objs) .AUTODEPEND
+ *wlink @$[@ name $@ file { $(chudisk_objs) }
+
+chudisk.obj: chudisk.c .AUTODEPEND
+ $(compile_dosdrv)
+
+chudinit.obj: chudinit.c .AUTODEPEND
+ $(compile_dosdrv)
+
+cio.obj: cio.c .AUTODEPEND
+ $(compile_dosdrv)
+
+chud.exe: chud.obj .AUTODEPEND
+ *wlink name $@ system dos file { chud.obj }
+
+chud.obj: chud.c .AUTODEPEND
+ $(compile_dos)
+
+utest.exe: utest.obj .AUTODEPEND
+ *wlink system dos name $@ file { utest.obj }
+
+utest.obj: utest.c .AUTODEPEND
+ $(compile_dos)
+
+clean: .SYMBOLIC
+ rm -f $(bins) *.obj
diff --git a/dos/utest.c b/dos/utest.c
new file mode 100644
index 0000000..7aa8750
--- /dev/null
+++ b/dos/utest.c
@@ -0,0 +1,500 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <time.h>
+#include <dos.h>
+
+#include "ch375dos.h"
+
+struct ch375 ch375_dev = {
+ .io_base = 0x260
+};
+
+#define SWAPNC_32(w) (\
+ (((w) & 0x000000FFUL) << 24) |\
+ (((w) & 0x0000FF00UL) << 8) |\
+ (((w) & 0x00FF0000UL) >> 8) |\
+ (((w) & 0xFF000000UL) >> 24)\
+ )
+
+static uint8_t buffer[64];
+
+typedef uint16_t Centisecs;
+#define CENTISECS_MAX (60 * 100U)
+
+/// Gets the current wallclock centisecond (1/100 of a second).
+// Note this wraps around every minute, so max value is CENTISECS_MAX.
+static Centisecs get_centisecs()
+{
+ struct dostime_t now;
+ _dos_gettime(&now);
+ return (now.second * 100U) + now.hsecond;
+}
+
+static inline Centisecs centisecs_remaining(Centisecs start, Centisecs deadline)
+{
+ Centisecs now = get_centisecs();
+ if (now < start) {
+ // Rollover happened
+ now += CENTISECS_MAX;
+ }
+ if (now >= deadline) {
+ return 0;
+ } else {
+ return deadline - now;
+ }
+}
+
+static void delay_centisecs(Centisecs timeout)
+{
+ Centisecs start = get_centisecs();
+ Centisecs deadline = start + timeout;
+ while (centisecs_remaining(start, deadline)) {
+ printf("Delay deadline=%u now=%u\n", deadline, get_centisecs());
+ }
+}
+
+
+typedef bool (*PollFunction)(void);
+
+static bool wait_with_timeout(Centisecs timeout, PollFunction pf) {
+ Centisecs start = get_centisecs();
+ Centisecs deadline = start + timeout;
+ Centisecs last_print = 0;
+ while (!pf() && centisecs_remaining(start, deadline)) {
+ if (last_print != get_centisecs()) {
+ printf("Wait with timeout deadline=%u now=%u\n", deadline, get_centisecs());
+ last_print = get_centisecs();
+ }
+ }
+ if (pf()) {
+ printf("Wait OK, %d centisecs elapsed\n", get_centisecs() - start);
+ return true;
+ } else {
+ printf("Wait timeout\n");
+ return false;
+ }
+ //return pf();
+}
+
+static bool poll_interrupt(void) {
+ return ch375_poll_interrupt(&ch375_dev);
+}
+
+static bool poll_test_connect(void) {
+ return ch375_read_data(&ch375_dev) != 0;
+}
+
+static bool wait_interrupt_with_timeout(unsigned int timeout) {
+ return wait_with_timeout(timeout, poll_interrupt);
+}
+
+static void dump_int_status()
+{
+ char i = ch375_poll_interrupt(&ch375_dev) ? 'I' : ' ';
+ printf("Int Status: [%c] 0x%hx\n", i, ch375_get_int_status(&ch375_dev));
+ // Above also clears pending ints...
+}
+
+void chip_reset()
+{
+ printf("Chip reset\n");
+ ch375_reset(&ch375_dev);
+ delay_centisecs(5);
+}
+
+void chip_sleep()
+{
+ printf("Chip sleep\n");
+ ch375_enter_sleep(&ch375_dev);
+}
+
+void usb_host_off()
+{
+ printf("Set usb mode disabled...\n");
+ ch375_set_usb_mode(&ch375_dev, CH375_USB_MODE_HOST_DISABLED);
+ if (!wait_with_timeout(1, poll_test_connect) || ch375_read_data(&ch375_dev) != CH375_RET_SUCESS) {
+ printf("Set usb mode failed\n");
+ }
+}
+
+void usb_host_reset(void)
+{
+ printf("Set usb mode reset...\n");
+ ch375_set_usb_mode(&ch375_dev, CH375_USB_MODE_HOST_RESET);
+ if (!wait_with_timeout(1, poll_test_connect) || ch375_read_data(&ch375_dev) != CH375_RET_SUCESS) {
+ printf("Set usb mode reset failed\n");
+ }
+}
+
+bool usb_host_on(void)
+{
+ printf("Set usb mode...\n");
+ ch375_set_usb_mode(&ch375_dev, CH375_USB_MODE_HOST_AUTO_SOF);
+ if (!wait_with_timeout(1, poll_test_connect) || ch375_read_data(&ch375_dev) != CH375_RET_SUCESS) {
+ printf("Set usb mode failed\n");
+ return false;
+ }
+
+ dump_int_status();
+
+ return true;
+}
+
+bool usb_host_connect(void)
+{
+ uint8_t status;
+
+ printf("Test connect...\n");
+ ch375_test_connect(&ch375_dev);
+ if (!wait_with_timeout(1, poll_test_connect)) {
+ printf("Test connect timedout\n");
+ }
+ status = ch375_read_data(&ch375_dev);
+ printf("Test connect status = 0x%hx\n", status);
+ switch (status) {
+ case CH375_INT_USB_CONNECT:
+ printf("USB device connected\n");
+ return true;
+ case CH375_INT_USB_READY:
+ printf("USB device ready\n");
+ return true;
+ case CH375_INT_USB_DISCONNECT:
+ printf("USB device not connected\n");
+ default:
+ return false;
+ }
+}
+
+static int usb_host_wait_int_read_data(Centisecs timeout, uint8_t buffer[])
+{
+ uint8_t status;
+ if (!wait_with_timeout(timeout, poll_interrupt)) {
+ return -1;
+ }
+
+ // Get and acknowledge interrupt
+ status = ch375_get_int_status(&ch375_dev);
+ printf("Wait int read data status=0x%hx\n", status);
+ if (status != CH375_INT_USB_SUCCESS) {
+ return -status;
+ }
+
+ // USB host mode doesn't need to do unlock, so we can use DATA0 here?
+ ch375_send_command(&ch375_dev, CH375_RD_USB_DATA0);
+ return ch375_read_data_block(&ch375_dev, buffer);
+}
+
+static bool init_disk()
+{
+ uint8_t status;
+
+ printf("Disk init...\n");
+ ch375_disk_init(&ch375_dev);
+ if (!wait_interrupt_with_timeout(100)) {
+ printf("Disk init timeout\n");
+ }
+ status = ch375_get_int_status(&ch375_dev);
+ printf("Disk init status = 0x%hx\n", status);
+
+ return status == CH375_INT_USB_SUCCESS;
+}
+
+static bool disk_ready()
+{
+ uint8_t status;
+ ch375_test_disk_ready(&ch375_dev);
+ if (!wait_interrupt_with_timeout(100)) {
+ printf("Disk ready timeout\n");
+ }
+ status = ch375_get_int_status(&ch375_dev);
+ printf("Disk ready status = 0x%hx\n", status);
+ return status == CH375_INT_USB_SUCCESS;
+}
+
+static void disk_size()
+{
+ uint8_t status;
+
+ printf("Disk size...\n");
+ ch375_send_command(&ch375_dev, CH375_DISK_SIZE);
+ if (!wait_interrupt_with_timeout(100)) {
+ printf("Disk size timeout\n");
+ }
+ status = ch375_get_int_status(&ch375_dev);
+ printf("Disk size status = 0x%hx\n", status);
+
+ if (status == CH375_INT_USB_SUCCESS) {
+ int len;
+
+ ch375_send_command(&ch375_dev, CH375_RD_USB_DATA);
+
+ len = ch375_read_data_block(&ch375_dev, buffer);
+ printf("Read %d bytes\n", len);
+
+ if (len >= 8) {
+ uint32_t sectors = SWAPNC_32(*(uint32_t*)&buffer[0]),
+ bytes_per_sector = SWAPNC_32(*(uint32_t*)&buffer[4]);
+ uint32_t kbytes = sectors / 2;
+ uint32_t mbytes = kbytes / 1024;
+
+ printf("%hx %hx %hx %hx %hx %hx %hx %hx\n",
+ buffer[0], buffer[1], buffer[2], buffer[3],
+ buffer[4], buffer[5], buffer[6], buffer[7]);
+ printf("%lu sectors, %lu bytes per sector, %u MiB\n", sectors, bytes_per_sector, mbytes);
+ }
+ }
+}
+
+int disk()
+{
+ bool ready = disk_ready();
+ int i = 0;
+
+ if (ready) {
+ printf("Disk ready\n");
+ } else {
+ printf("Disk not ready\n");
+
+ if (!usb_host_connect()) {
+ return EXIT_FAILURE;
+ }
+
+ if (!init_disk()) {
+ return EXIT_FAILURE;
+ }
+
+ i = 3;
+ while (i >= 0 && !(ready = disk_ready())) {
+ i--;
+ printf("Give more time..\n");
+ }
+
+ if (ready) {
+ printf("Disk now ready\n");
+ } else {
+ printf("Disk NOT ready\n");
+ return EXIT_FAILURE;
+ }
+ }
+
+ dump_int_status();
+
+ disk_size();
+
+ return EXIT_SUCCESS;
+}
+
+int disk_read(uint32_t lba)
+{
+ uint8_t status;
+ unsigned total = 0;
+ unsigned i;
+ ch375_disk_read(&ch375_dev, lba, 1);
+ if (!wait_interrupt_with_timeout(100)) {
+ printf("Disk read timeout\n");
+ }
+ while ((status = ch375_get_int_status(&ch375_dev) == CH375_INT_USB_DISK_READ)) {
+ int len;
+ ch375_send_command(&ch375_dev, CH375_RD_USB_DATA);
+
+ if (total >= 512) {
+ printf("Read buffer overflow!");
+ break;
+ }
+
+ len = ch375_read_data_block(&ch375_dev, &buffer[total]);
+ printf("Disk read data block len = %d\n", len);
+ if (len > 0) {
+ total += len;
+
+ ch375_send_command(&ch375_dev, CH375_DISK_RD_GO);
+ if (!wait_interrupt_with_timeout(100)) {
+ printf("Disk read more timeout\n");
+ }
+ } else if (len == 0) {
+ break;
+ }
+ }
+ printf("Disk read status = 0x%hx\n", status);
+
+ printf("Disk read total bytes = %u\n", total);
+
+ for (i = 512 - (8*4); i < total; i+=8) {
+ printf("%hx %hx %hx %hx %hx %hx %hx %hx\n",
+ buffer[i+0], buffer[i+1], buffer[i+2], buffer[i+3],
+ buffer[i+4], buffer[i+5], buffer[i+6], buffer[i+7]);
+ }
+
+ return EXIT_SUCCESS;
+}
+
+int autou()
+{
+ uint8_t status;
+
+#if 1
+ usb_host_reset();
+ delay(100);
+#endif
+
+#if 1
+ if (!usb_host_on()) {
+ return EXIT_FAILURE;
+ }
+ delay(100);
+#endif
+
+#if 1
+ // TODO: Try to get descriptor in high speed, then retry in low speed?
+ ch375_set_usb_speed(&ch375_dev, CH375_USB_SPEED_LOW);
+#endif
+
+#if 1
+ printf("Auto setup...\n");
+
+ ch375_auto_setup(&ch375_dev);
+ ch375_send_command(&ch375_dev, CH375_AUTO_SETUP);
+ if (!wait_interrupt_with_timeout(100)) {
+ printf("Auto setup timeout\n");
+ }
+ status = ch375_get_int_status(&ch375_dev);
+ printf("Auto setup status = 0x%hx\n", status);
+
+ delay(100);
+#endif
+
+ if (status == CH375_INT_USB_SUCCESS) {
+ printf("Auto setup success\n");
+ } else {
+ printf("Auto setup fail, status=0x%x\n", status);
+ }
+
+ dump_int_status();
+
+ return EXIT_SUCCESS;
+}
+
+int host()
+{
+ usb_device_descriptor *dev_descr;
+ int n;
+
+ //ch375_set_retry(&ch375_dev, CH375_RETRY_NAK_INFINITE | CH375_RETRY_TIMEOUT_MAX);
+
+#if 0
+ usb_host_reset();
+ //delay(100);
+#endif
+
+#if 1
+ if (!usb_host_on()) {
+ return EXIT_FAILURE;
+ }
+ //delay(100);
+#endif
+
+#if 1
+ // TODO: Try to get descriptor in high speed, then retry in low speed?
+ ch375_set_usb_speed(&ch375_dev, CH375_USB_SPEED_LOW);
+#endif
+
+#if 0
+ if (!usb_host_connect()) {
+ return EXIT_FAILURE;
+ }
+#endif
+
+ dump_int_status();
+
+ printf("Reading descriptor %d...\n", USB_DESCRIPTOR_TYPE_DEVICE);
+ ch375_get_descriptor(&ch375_dev, USB_DESCRIPTOR_TYPE_DEVICE);
+ n = usb_host_wait_int_read_data(100, buffer);
+ if (n >= 0) {
+ printf("Got descriptor 1 size = %d\n", n);
+ } else {
+ printf("Failed to get descriptor 1, err=0x%hx\n", -n);
+ return EXIT_FAILURE;
+ }
+
+ dump_int_status();
+
+ dev_descr = (usb_device_descriptor*)buffer;
+
+ printf("Got USB device VID:PID %04x:%04x\n", dev_descr->idVendor, dev_descr->idProduct);
+
+ printf("Reading descriptor %d...\n", USB_DESCRIPTOR_TYPE_CONFIG);
+ ch375_get_descriptor(&ch375_dev, USB_DESCRIPTOR_TYPE_CONFIG);
+ n = usb_host_wait_int_read_data(100, buffer);
+ if (n >= 0) {
+ printf("Got descriptor 2 size = %d\n", n);
+ } else {
+ printf("Failed to get descriptor 2, err=0x%hx\n", -n);
+ return EXIT_FAILURE;
+ }
+
+ dump_int_status();
+
+ return EXIT_SUCCESS;
+}
+
+int main(int argc, const char *argv[])
+{
+ char mode = '?';
+ char *port = getenv("CH375_PORT");
+
+ if (port) {
+ unsigned long lport = strtoul(port, NULL, 16);
+ ch375_dev.io_base = lport;
+ }
+
+ if (argc > 1) {
+ mode = argv[1][0];
+ }
+
+ printf("Test mode %c using IO port 0x%x\n", mode, ch375_dev.io_base);
+
+ dump_int_status();
+
+ switch (mode) {
+ case 'd':
+ return disk();
+ case 'e':
+ return disk_read(0);
+ case 'a':
+ return autou();
+ case 'm':
+ return host();
+ case 'h':
+ usb_host_on();
+ break;
+ case 'c':
+ usb_host_connect();
+ break;
+ case 'o':
+ usb_host_off();
+ break;
+ case 'b':
+ usb_host_reset();
+ break;
+ case 's':
+ chip_sleep();
+ break;
+ case 'r':
+ chip_reset();
+ break;
+ case 'i':
+ dump_int_status();
+ break;
+ case 'v':
+ printf("IC Version: 0x%hx\n", ch375_get_version(&ch375_dev));
+ break;
+ case '?':
+ printf("test modes: d e a m h c o b s r i v\n");
+ break;
+ default:
+ printf("Unknown mode %c\n", mode);
+ return EXIT_FAILURE;
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/dos/utils.h b/dos/utils.h
new file mode 100644
index 0000000..138a774
--- /dev/null
+++ b/dos/utils.h
@@ -0,0 +1,60 @@
+/*
+ * CHU - driver for CH375 in disk mode
+ * Copyright (C) 2024 Javier S. Pedro
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#ifndef UTILS_H
+#define UTILS_H
+
+#include <stdint.h>
+
+static inline __segment get_cs(void);
+#pragma aux get_cs = "mov ax, cs" value [ax] modify exact [] nomemory
+
+static inline __segment get_ds(void);
+#pragma aux get_ds = "mov ax, ds" value [ax] modify exact [] nomemory
+
+static void load_ds_from_cs(void);
+#pragma aux load_ds_from_cs = \
+ "push cs" \
+ "pop ds" \
+ __modify __exact []
+
+#define SWAPNC_32(w) (\
+ (((w) & 0x000000FFUL) << 24) |\
+ (((w) & 0x0000FF00UL) << 8) |\
+ (((w) & 0x00FF0000UL) >> 8) |\
+ (((w) & 0xFF000000UL) >> 24)\
+ )
+
+static inline uint16_t div(uint32_t dividend, uint16_t divisor);
+#pragma aux div = \
+ "div bx" \
+ __parm [dx ax] [bx] \
+ __value [ax] \
+ __modify __exact [dx ax]
+
+static inline uint32_t mul(uint16_t a, uint16_t b);
+#pragma aux mul = \
+ "mul dx" \
+ __parm [ax] [dx] \
+ __value [dx ax] \
+ __modify __exact [dx ax]
+
+
+
+#endif // UTILS_H