DOS Serial File Transfer
I was doing research on how to "bootstrap" an old DOS based computer with more functional software, like a proper terminal emulator. Because the floppy drive was broken, I had to get this transferred over the existing RS-232 serial port.
The initial results were a couple of programs: A "decode" program meant to be run on DOS, written in x86 assembly, to receive the data. An "encode" program meant to be run on Linux, written in C, to send the data. Very simple encoding is used; each byte is split into two nibbles and added with 0x30 to produce a valid ASCII character.
Unfortunately I was not able to run the programs with larger amounts of data without loosing bits and pieces. Even at very low baud rates like 1200 data still got lost and enabling/disabling flow control made no difference. However, I found that other people have been suffering from these kinds of issues in the past as well. So I found some other alternatives, and the best one so far is Kermit. The MS-DOS Kermit package also provides a BASIC bootstrap program that can be run in QBASIC on the DOS computer.
Anyway, for future reference, here is the "decode" program:
(Assemble it under Linux with NASM like so: nasm decode.asm -fbin -o decode.com)
org 0x100 ; DOS COM file start offset. section .text start: ; Call BIOS to initialize serial port. mov ah, 0x00 mov al, 0xe3 ; 9600,8,n,1 mov dx, 0x00 ; COM1 int 0x14 ; Call DOS to create new file and handle. mov ah, 0x3c mov cx, 0 ; Standard attributes. mov dx, filename int 0x21 jc end_file_create_error mov [filehandle], ax read_loop: ; Call BIOS to read byte from serial port. mov ah, 0x02 mov dx, 0x00 ; COM1 int 0x14 and ah, 0x80 jnz end_read_serial_error ; Check if DOS-style EOF. cmp al, 0x1A je end_success ; Determine high or low nibble next. mov bl, [have_seen_high_nibble] cmp bl, 0 jne convert_low_nibble convert_high_nibble: sub al, 0x30 mov cl, 4 shl al, cl mov [high_nibble], al mov byte [have_seen_high_nibble], 1 jmp read_loop convert_low_nibble: sub al, 0x30 or al, [high_nibble] mov byte [have_seen_high_nibble], 0 ; Call DOS to write to file. mov [write_buffer], al mov ah, 0x40 mov bx, [filehandle] mov cx, 1 ; One byte at a time. mov dx, write_buffer int 0x21 jc end_file_write_error ; Call DOS to display '.' for progress. mov ah, 0x2 mov dl, '.' int 0x21 jmp read_loop end_success: ; Call DOS to close file handle. mov ah, 0x3e mov bx, [filehandle] int 0x21 ; Call DOS to terminate program. mov ah, 0x4c mov al, 0 ; 0 = OK int 0x21 end_file_create_error: mov [error_code], ax ; Call DOS to display 'C' signifying "Create Error". mov ah, 0x2 mov dl, 'C' int 0x21 ; Call DOS to display error code. ; mov ah, 0x2 mov dl, [error_code] add dl, 0x30 int 0x21 jmp end_error end_read_serial_error: ; Call DOS to display 'R' signifying "Read Error". mov ah, 0x2 mov dl, 'R' int 0x21 ; Call BIOS to get serial port status mov ah, 03 mov dx, 0x00 ; COM1 int 0x14 mov [error_code], ax ; Call DOS to display error code(s). mov ah, 0x2 mov dl, [error_code] ; Modem Status mov cl, 4 shr dl, cl ; High Nibble add dl, 0x30 int 0x21 ; mov ah, 0x2 mov dl, [error_code] ; Modem Status and dl, 0x0f ; Low Nibble add dl, 0x30 int 0x21 ; mov ah, 0x2 mov dl, [error_code+1] ; Port Status mov cl, 4 shr dl, cl ; High Nibble add dl, 0x30 int 0x21 ; mov ah, 0x2 mov dl, [error_code+1] ; Port Status and dl, 0x0f ; Low Nibble add dl, 0x30 int 0x21 jmp end_error end_file_write_error: mov [error_code], ax ; Call DOS to display 'W' signifying "Write Error". mov ah, 0x2 mov dl, 'W' int 0x21 ; Call DOS to display error code. ; mov ah, 0x2 mov dl, [error_code] add dl, 0x30 int 0x21 end_error: ; Call DOS to terminate program. mov ah, 0x4c mov al, 1 ; 1 = Error int 0x21 section .data: high_nibble: db 0 have_seen_high_nibble: db 0 error_code: filehandle: dw 0 write_buffer: db 0 filename: db "DECODE.BIN",0
And the "encode" program:
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <termios.h> #include <sys/ioctl.h> int main(int argc, char *argv[]) { int c, fd, result; unsigned char byte; struct termios attr; if (argc != 2) { fprintf(stderr, "Usage: %s <TTY>\n", argv[0]); return 1; } fd = open(argv[1], O_RDWR | O_NOCTTY); if (fd == -1) { fprintf(stderr, "open() failed with errno: %d\n", errno); return 1; } result = tcgetattr(fd, &attr); if (result == -1) { fprintf(stderr, "tcgetattr() failed with errno: %d\n", errno); close(fd); return 1; } attr.c_cflag = B9600 | CS8 | CRTSCTS | CLOCAL; attr.c_iflag = 0; attr.c_oflag = 0; attr.c_lflag = 0; result = tcsetattr(fd, TCSANOW, &attr); if (result == -1) { fprintf(stderr, "tcgetattr() failed with errno: %d\n", errno); close(fd); return 1; } while ((c = fgetc(stdin)) != EOF) { byte = ((c & 0xf0) >> 4) + 0x30; write(fd, &byte, 1); byte = (c & 0x0f) + 0x30; write(fd, &byte, 1); } byte = 0x1a; /* DOS EOF */ write(fd, &byte, 1); close(fd); return 0; }