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.