Serial Port Floppy Drive Emulation
While working on another project I needed to figure out a way to emulate a floppy drive. After doing some research I learned more about how the Interrupt Vector Table on PCs work and how TSR programs operate under DOS. So the result is a DOS TSR program that intercepts BIOS INT 13H calls and forwards these over the serial port to a remote Linux box that operates on a floppy disk image.
I have borrowed some code from my previous Kermit project that also uses x86 assembly with serial ports. This program also shares some of the same limitations; hard coded with baudrate 9600 on the COM1 port. The TSR has been tested on the Bochs emulator and on a real 25MHz 80486SX PC.
Here is the TSR part of the program, assembled with NASM as follows: nasm serialfd.asm -fbin -o serialfd.com
org 0x100 bits 16 cpu 8086 COM1_BASE equ 0x3f8 COM1_THR equ COM1_BASE + 0 ; Transmitter Holding Buffer COM1_RBR equ COM1_BASE + 0 ; Receiver Buffer COM1_IER equ COM1_BASE + 1 ; Interrupt Enable Register COM1_FCR equ COM1_BASE + 2 ; FIFO Control Register COM1_IIR equ COM1_BASE + 2 ; Interrupt Identification Register COM1_LCR equ COM1_BASE + 3 ; Line Control Register COM1_LSR equ COM1_BASE + 5 ; Line Status Register COM1_DLL equ COM1_BASE + 0 ; Divisor Latch Low Byte COM1_DLH equ COM1_BASE + 1 ; Divisor Latch High Byte section .text start: jmp main int13_interrupt: ; Allow other interrupts: sti ; Check if accessing drive 0 (A:) or drive 1 (B:) ; If not, then jump to original interrupt instead. cmp dl, 0 je _int13_interrupt_dl_ok cmp dl, 1 jne original_int13 _int13_interrupt_dl_ok: ; Only operation 0x02 (Read) and 0x03 (Write) are forwarded. ; The rest are bypassed directly and returns OK. cmp ah, 2 je _int13_interrupt_ah_ok cmp ah, 3 jne _int13_interrupt_end _int13_interrupt_ah_ok: ; Save registers: push bx push cx push dx ; Save sectors and operation information on stack for use later: push ax push ax push ax ; Register AL already set. call com_port_send mov al, ah call com_port_send mov al, cl call com_port_send mov al, ch call com_port_send mov al, dl call com_port_send mov al, dh call com_port_send ; Retrieve sector information (stack AL) into DL register: pop dx xor dh, dh mov ax, 512 mul dx ; DX:AX = AX * DX mov cx, ax ; Determine receive (Read) or send (Write) from operation (stack AH): pop ax cmp ah, 3 je _int13_interrupt_send_loop _int13_interrupt_recv_loop: call com_port_recv mov [es:bx], al inc bx loop _int13_interrupt_recv_loop jmp _int13_loop_done _int13_interrupt_send_loop: mov al, [es:bx] call com_port_send inc bx loop _int13_interrupt_send_loop _int13_loop_done: ; Retrieve sector information (stack AL) as sectors handled: pop ax ; Restore registers: pop dx pop cx pop bx _int13_interrupt_end: ; AL register will have same value as upon entering routine. xor ah, ah ; Code 0 (No Error) clc ; Clear error bit. retf 2 original_int13: jmp original_int13:original_int13 ; Will be overwritten runtime! ; Send contents from AL on COM1 port: com_port_send: push dx mov dx, COM1_THR out dx, al mov dx, COM1_LSR _com_port_send_wait: in al, dx and al, 0b00100000 ; Empty Transmit Holding Register test al, al jz _com_port_send_wait pop dx ret ; Return contents in AL on COM1 port: com_port_recv: push dx _com_port_recv_wait: mov dx, COM1_IIR in al, dx and al, 0b00001110 ; Identification cmp al, 0b00000100 ; Enable Received Data Available Interrupt jne _com_port_recv_wait mov dx, COM1_RBR in al, dx pop dx ret ; TSR end marker: tsr_end: main: ; NOTE: No protection to prevent TSR from being loaded twice or more! ; Set Baudrate on COM1 to 9600, divisor = 12: mov dx, COM1_LCR in al, dx or al, 0b10000000 ; Set Divisor Latch Access Bit (DLAB). out dx, al mov dx, COM1_DLL mov al, 0xc out dx, al mov dx, COM1_DLH mov al, 0 out dx, al mov dx, COM1_LCR in al, dx and al, 0b01111111 ; Reset Divisor Latch Access Bit (DLAB). out dx, al ; Disable and clear FIFO on COM1, to put it in 8250 compatibility mode: mov dx, COM1_FCR mov al, 0b00000110 ; Clear both FIFOs. out dx, al ; NOTE: Not tested what happens if this is run on an actual 8250 chip... ; Set mode on COM1 to 8 data bits, no parity and 1 stop bit: mov dx, COM1_LCR mov al, 0b00000011 ; 8-N-1 out dx, al ; Enable interrupt bit on COM1: mov dx, COM1_IER in al, dx or al, 0b00000001 ; Enable Received Data Available Interrupt out dx, al ; Call DOS to get original interrupt handler: mov al, 0x13 mov ah, 0x35 int 0x21 mov word [original_int13 + 3], es mov word [original_int13 + 1], bx ; Call DOS to set interrupt handler: mov al, 0x13 mov ah, 0x25 ; DS is already same as CS, no need to change. mov dx, int13_interrupt int 0x21 ; Terminate and Stay Resident: mov dx, tsr_end shr dx, 1 shr dx, 1 shr dx, 1 shr dx, 1 add dx, 0x11 ; Add 0x1 for remainder and 0x10 for PSP. mov ax, 0x3100 int 0x21
And here the Linux counterpart in C, just compile with GCC:
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <termios.h> #include <sys/ioctl.h> #include <string.h> #define REGISTER_AL 0 #define REGISTER_AH 1 #define REGISTER_CL 2 #define REGISTER_CH 3 #define REGISTER_DL 4 #define REGISTER_DH 5 #define SECTOR_SIZE 512 #define HEADS_PER_CYLINDER_DEFAULT 2 #define OPERATION_READ_DISK_SECTORS 0x02 #define OPERATION_WRITE_DISK_SECTORS 0x03 static uint16_t get_sectors_per_track(FILE *fh) { uint16_t spt; fseek(fh, 24, SEEK_SET); /* Offset in Volume Boot Record. */ if (fread(&spt, sizeof(uint16_t), 1, fh) != 1) { return 0; /* Error */ } /* Currently handling 720K and 1.44M floppies. */ if (spt == 9 || spt == 18) { return spt; /* Valid */ } return 0; /* Invalid */ } static void display_help(char *progname) { fprintf(stderr, "Usage: %s <options>\n", progname); fprintf(stderr, "Options:\n" " -h Display this help and exit.\n" " -d DEVICE Use TTY DEVICE.\n" " -a IMAGE Floppy IMAGE for A:\n" " -b IMAGE Floppy IMAGE for B:\n" " -H HPC Force HPC heads per cylinder.\n" " -S SPT Force SPT sectors per track.\n" " -v Verbose debugging output.\n" "\n"); } int main(int argc, char *argv[]) { int result = EXIT_SUCCESS; int i, c, arg; int cylinder, sector, lba; struct termios attr; unsigned char registers[6]; int debug_output = 0; char *tty_device = NULL; int tty_fd = -1; FILE *fh; char *floppy_a_image = NULL; char *floppy_b_image = NULL; FILE *floppy_a_fh = NULL; FILE *floppy_b_fh = NULL; uint16_t floppy_a_spt = 0; uint16_t floppy_b_spt = 0; int spt = 0; int hpc = HEADS_PER_CYLINDER_DEFAULT; char *operation; while ((c = getopt(argc, argv, "hd:a:b:H:S:v")) != -1) { switch (c) { case 'h': display_help(argv[0]); return EXIT_SUCCESS; case 'd': tty_device = optarg; break; case 'a': floppy_a_image = optarg; break; case 'b': floppy_b_image = optarg; break; case 'H': hpc = atoi(optarg); break; case 'S': spt = atoi(optarg); break; case 'v': debug_output = 1; break; case '?': default: display_help(argv[0]); return EXIT_FAILURE; } } if (tty_device == NULL) { fprintf(stderr, "Please specify a TTY!\n"); display_help(argv[0]); return EXIT_FAILURE; } if (floppy_a_image == NULL && floppy_b_image == NULL) { fprintf(stderr, "Please specify at least one floppy image!\n"); display_help(argv[0]); return EXIT_FAILURE; } if (hpc == 0) { fprintf(stderr, "Invalid heads per cylinder!\n"); return EXIT_FAILURE; } /* Open serial TTY device. */ tty_fd = open(tty_device, O_RDWR | O_NOCTTY); if (tty_fd == -1) { fprintf(stderr, "open() on TTY device failed with errno: %d\n", errno); return EXIT_FAILURE; } /* Set TTY into a very raw mode. */ memset(&attr, 0, sizeof(struct termios)); attr.c_cflag = B9600 | CS8 | CLOCAL | CREAD; attr.c_cc[VMIN] = 1; if (tcsetattr(tty_fd, TCSANOW, &attr) == -1) { fprintf(stderr, "tcgetattr() on TTY device failed with errno: %d\n", errno); close(tty_fd); return EXIT_FAILURE; } /* Make sure TTY "Clear To Send" signal is set. */ arg = TIOCM_CTS; if (ioctl(tty_fd, TIOCMBIS, &arg) == -1) { fprintf(stderr, "ioctl() on TTY device failed with errno: %d\n", errno); close(tty_fd); return EXIT_FAILURE; } /* Get information about floppy A: */ if (floppy_a_image != NULL) { floppy_a_fh = fopen(floppy_a_image, "r+b"); if (floppy_a_fh == NULL) { fprintf(stderr, "fopen() for floppy A: failed with errno: %d\n", errno); result = EXIT_FAILURE; goto main_end; } if (spt == 0) { floppy_a_spt = get_sectors_per_track(floppy_a_fh); } else { floppy_a_spt = spt; } if (floppy_a_spt == 0) { fprintf(stderr, "Invalid sectors per track for floppy A:\n"); result = EXIT_FAILURE; goto main_end; } } /* Get information about floppy B: */ if (floppy_b_image != NULL) { floppy_b_fh = fopen(floppy_b_image, "r+b"); if (floppy_b_fh == NULL) { fprintf(stderr, "fopen() for floppy B: failed with errno: %d\n", errno); result = EXIT_FAILURE; goto main_end; } if (spt == 0) { floppy_b_spt = get_sectors_per_track(floppy_b_fh); } else { floppy_b_spt = spt; } if (floppy_b_spt == 0) { fprintf(stderr, "Invalid sectors per track for floppy B:\n"); result = EXIT_FAILURE; goto main_end; } } /* Process input and output. */ while (1) { for (i = 0; i < 6; i++) { if (read(tty_fd, ®isters[i], sizeof(unsigned char)) != 1) { fprintf(stderr, "read() failed with errno: %d\n", errno); result = EXIT_FAILURE; goto main_end; } } if (debug_output) { fprintf(stderr, "AL: 0x%02x\n", registers[REGISTER_AL]); fprintf(stderr, "AH: 0x%02x\n", registers[REGISTER_AH]); fprintf(stderr, "CL: 0x%02x\n", registers[REGISTER_CL]); fprintf(stderr, "CH: 0x%02x\n", registers[REGISTER_CH]); fprintf(stderr, "DL: 0x%02x\n", registers[REGISTER_DL]); fprintf(stderr, "DH: 0x%02x\n", registers[REGISTER_DH]); } if (registers[REGISTER_DL] == 0x00) { spt = floppy_a_spt; fh = floppy_a_fh; } else if (registers[REGISTER_DL] == 0x01) { spt = floppy_b_spt; fh = floppy_b_fh; } else { fprintf(stderr, "Error: Invalid drive number: %02x\n", registers[REGISTER_DL]); result = EXIT_FAILURE; goto main_end; } /* CX = ---CH--- ---CL--- * cylinder : 76543210 98 * sector : 543210 * LBA = ( ( cylinder * HPC + head ) * SPT ) + sector - 1 */ cylinder = ((registers[REGISTER_CL] & 0xc0) << 2) + registers[REGISTER_CH]; sector = registers[REGISTER_CL] & 0x3f; lba = ((cylinder * hpc + registers[REGISTER_DH]) * spt) + sector - 1; if (debug_output) { fprintf(stderr, "Cylinder: %d\n", cylinder); fprintf(stderr, "Sector : %d\n", sector); fprintf(stderr, "SPT : %d\n", spt); fprintf(stderr, "HPC : %d\n", hpc); fprintf(stderr, "LBA : %d\n", lba); fprintf(stderr, "Offset : 0x%x\n", lba * SECTOR_SIZE); } else { switch (registers[REGISTER_AH]) { case OPERATION_READ_DISK_SECTORS: operation = "Read"; break; case OPERATION_WRITE_DISK_SECTORS: operation = "Write"; break; default: operation = "Unknown"; break; } fprintf(stderr, "%s %c: sector=%d, cylinder=%d count=%d\n", operation, (registers[REGISTER_DL] == 0x00) ? 'A' : 'B', sector, cylinder, registers[REGISTER_AL]); } if (fh != NULL) { if (fseek(fh, lba * SECTOR_SIZE, SEEK_SET) == -1) { fprintf(stderr, "fseek() failed with errno: %d\n", errno); result = EXIT_FAILURE; goto main_end; } } switch (registers[REGISTER_AH]) { case OPERATION_READ_DISK_SECTORS: if (debug_output) { fprintf(stderr, "READ SECTOR DATA:\n"); } for (i = 0; i < (SECTOR_SIZE * registers[REGISTER_AL]); i++) { if (fh != NULL) { c = fgetc(fh); } else { c = 0xFF; /* Dummy data if image is not loaded. */ } if (debug_output) { fprintf(stderr, "%02x ", c); if (i % 16 == 15) { fprintf(stderr, "\n"); } } write(tty_fd, &c, sizeof(unsigned char)); } break; case OPERATION_WRITE_DISK_SECTORS: if (debug_output) { fprintf(stderr, "WRITE SECTOR DATA:\n"); } for (i = 0; i < (SECTOR_SIZE * registers[REGISTER_AL]); i++) { read(tty_fd, &c, sizeof(unsigned char)); if (fh != NULL) { fputc(c, fh); } if (debug_output) { fprintf(stderr, "%02x ", c); if (i % 16 == 15) { fprintf(stderr, "\n"); } } } if (fh != NULL) { fflush(fh); } break; default: fprintf(stderr, "Error: Unhandled operation: %02x\n", registers[REGISTER_AH]); result = EXIT_FAILURE; goto main_end; } } main_end: if (tty_fd != -1) close(tty_fd); if (floppy_a_fh != NULL) fclose(floppy_a_fh); if (floppy_b_fh != NULL) fclose(floppy_b_fh); return result; }
I have also uploaded the code to GitHub in case case of further improvements in the future.