Recently I picked up an original IBM PC off of Facebook marketplace. This was an IBM Model 5150: the PC that started it all. It came with a Model F keyboard, a CGA video card, and two 5.25" 360k floppy drives. It did not have a hard drive; this came later with the IBM PC XT 5160. The fact it came with double sided drives and had later ROM versions suggest a ship date in 1983 or so.
Only problem: it did not come with any software, such as an operating system. And while I had some blank floppies (the same kind is used on my Tandy Color Computer 3), and one can find copies of IBM PC DOS online, I did not have another computer available to write such an image to a disk; motherboards with the appropriate floppy controller chips to write to 5.25" disks were largely phased out after the Pentium 3.
I considered a few options. One was to finangle a way to write PC-formatted sectors and data onto a disk from my Tandy Color Computer 3, which I probably could have done with some creative programming writing directly to the Western Digital 1793 floppy controller chip. Another was to simply have someone mail a disk to me; there are some services to do such for a nominal fee or I’m sure I could have found someone in the retro community.
But then I noticed: much like other home computers in the era, the 5150 PC would boot to a BASIC ROM if no boot floppy was present, in this case IBM Cassette BASIC, a licensed version of Microsoft BASIC. Like other BASIC implementations, IBM Cassette BASIC could write data into specific memory locations (using the POKE command), and could instruct the machine to jump to specific memory locations (using the CALL or USRn commands). Normally this mechanism is for BASIC programmers to implement machine language to gain performance beyond what the BASIC interpreter could perform. But really it could be anything the processor can execute…and it became my way in.
Readers of my past posts may recall I soldered an Altair 8800 together from scratch, and a normal part of operating that machine was to key in a bootloader on the front panel switches. This bootloader would then read a larger program (or operating system) into memory over a serial port. So I thought: why not use BASIC to poke a bootloader into memory that does the same thing on the IBM PC? Even better, could I also write that bootloader to the boot sector of a floppy disk?
It turns out that yes, this is totally doable. And it would be a fun project. So I did it.
The first step was re-learning Intel x86 assembly. I’d learned it during my Computer Science undergrad work, and it wasn’t a huge stretch to pick it up again, especially as I’d recently learned its ancestor chip for my Altair, the Intel 8080. Then I needed an assembly program to read data from the serial port (an 8250 UART chip) and then jump to that code:
mov ah,0x0E ; use BIOS call for simple character output
mov al,0x42 ; to output 'B' on the screen
int 0x10 ; to show it booted
xor ax,ax ; initialize data segment to 0 so code
mov ds,ax ; writes to correct location
; init UART here
; The 8250 chip used on the IBM PC used 5 different ports
; for various IO, including the data buffers and
; command/control/status registers. The x86 uses the dx register
; to address IO ports above 255, hence all the manipulation
; of the dx register in the following code segment...the code is
; writing to different registers on the 8250 chip. mov dx,0x2f9 ; COM2
;mov dx,0x3f9 ; COM1, switch comments if needed
out dx,al ; turn off all UART interrupts
out dx,al ; set UART chip to receive new baud rate
out dx,al ; set low byte divisor to 0xc (9600 baud)
out dx,al ; set high byte divisor to 0
out dx,al ; disable baud rate config and set 8N1
out dx,al ; turn on DTR and RTS
mov bx,0x7eff ; assign BX pointer to memory address 0x7eff
in al,dx ; read serial status
and al,0x1 ; check if byte is available to read
jz rdloop ; loop if byte not available
in al,dx ; read byte from serial
mov [bx],al ; store byte to BX pointer
dec bl ; decrement pointer
jnz rdloop ; loop until reached 0x7e00
jmp 0x7E00 ; once done, jump to 0x7e00
Assembled, this code is 65 bytes. I’d have to type those bytes into BASIC, so I wanted to keep it as short as possible. Note over half the code is just initializing the 8250 serial port chip.
I didn’t want to execute those 65 bytes directly from BASIC; I wanted to write them out to the boot sector of a floppy disk so I could easily execute this code without keying it in every time. So I needed code to write a memory segment to the floppy. And this code did need to execute from BASIC, so I included the appropriate register save and return code for such:
push es ; save ES and BP registers so return to BASIC
push bp ; works
xor dx,dx ; set dx to 0: write to drive 0 head 0
mov es,dx ; also set extra segment to 0 as the bios
; call for writing a disk sector looks for
; data at es:bx
mov bx,01200h ; write starting memory address 0x1200
mov ax,0301h ; write command 0x03, write 1 sector
mov cx,0001h ; write to track 0, sector 1
mov di,[bp+8] ; save the BIOS call return status in the
mov [di],ax ; passed BASIC variable
pop bp ; restore the ES and BP registers
retf 2 ; return to BASIC cleanly
This works out to 29 bytes, which I’d also have to type into BASIC. So next was to write the BASIC program that would load both code blocks into memory, then execute the second block that would write the first block to a floppy disk. Dusting off my BASIC skills, I wrote the following program (note the data segment defined on line 100 matches up with the ‘org’ statement in the second assembly code segment):
10 CLEAR ,&H1000
20 DEF SEG=&H120
30 FOR I=0 TO 64
40 READ J
50 POKE I,J
70 POKE 510,&H55
80 POKE 511,&HAA
100 DEF SEG=&H110
110 FOR I=0 TO 28
120 READ J
130 POKE I,J
140 PRINT J
180 CALL SUBRT(RET)
200 PRINT RET
1000 DATA 180,14,176,66,205,16,49,192,142,216,186,249,2,238,176
1010 DATA 128,131,194,2,238,176,12,131,234,3,238,48,192,66,238
1020 DATA 176,3,131,194,2,238,180,3,66,238,187,255,126,66,236,36
1030 DATA 1,116,251,131,234,5,236,131,194,5,136,7,254,203,117
1040 DATA 238,233,191,1
1080 DATA 6,85,137,229,49,210,142,194,187,0,18,184,1,3,185,1,0
1090 DATA 205,19,139,126,8,137,5,93,7,202,2,0
Its a bit to type in, but only takes a few minutes. First, the code tells the BASIC interpreter to not to use any memory above 0x1000 (so BASIC won’t overwrite the later pokes). The code then pokes 65 bytes of code block one into 0x1200. Note the code then pokes 0x55AA into the end of the 512 byte block, as we need those bytes in that place on the floppy for it to be recognized as a boot record by the BIOS. Then the code pokes the 29 bytes of code block two into 0x1100. Then the code executes that code at 0x1100 via a CALL function, and prints the return code, which should be ‘1,’ BASIC’s decimal interpretation for 0x0001, or “00 No Error” followed by “01 Sectors Written.”
Fingers crossed, I typed ‘run.’ I got the red light on the floppy drive and a good return code. It seemed to have worked. Upon rebooting, I expected ‘B’ showing that my code was running and was waiting for more code to arrive over the serial ports. Here goes nothing:
Note all the above code is what I came up with after a few iterations to get it correct. I had a few mistakes at first. For example, I was naively writing the data from the serial point to the start of memory at location 0 (a habit I’d picked up from programming the Altair 8800), but then found I’d get very unexpected behavior after reading only 140 or so bytes. It took longer than it should have for me to realize I was overwriting the x86’s interrupt vector table, so an interrupt (like the new incoming serial data I was receiving) would put my machine into a broken state. Once I moved the destination target to 0x7e00 instead, my approach clicked into place. My first test program displayed the letter ‘K’ over and over again, which worked great:
From here, having proven I could load a program over the serial port, I could conceptually load any software I could write over the serial port, and bypass the need for floppies altogether. But I wanted to use the plethora of software out there coded for use on disk and with an operating system. So back to fixing my chicken-and-egg problem: I needed to write a program that would read a disk image in over serial, and write it out to the physical disk.
So I wrote out this program, and an accompanying python script that would send this program and the subsequent required disk data over to the IBM PC. Since I didn’t have to poke the data for these programs into memory by hand, I could do little extras like error checking and status reporting. These programs, and the above assembly and basic code, are available for others to use at https://github.com/barberd/pc-serial-bootstrap. Look for lwdisk.asm and senddisk.py specifically. This code would first output ‘K’ to show it had started, read the head, track, and sector, repeat them to the screen (the three digits on each line in the screenshot below), read the 512 data bytes to write, write the sector to disk, then loop to do it again.
It took some time for the transfer (running at only 9600 baud), but it worked great. I was able to transfer an IBM PC DOS disk image to a real disk. Voila: I now had a working operating system floppy to boot the 5150 PC.
This only warranted a minor celebration: having just the OS doesn’t give me much else to do other than look at the A> prompt. I needed software (apps, games, whatever) to do anything useful or fun. I could keep writing disk images the same way, but its a bit cumbersome and inefficient if I just want a single file. An easier method would be to get some communications software installed so I could just download software directly over the serial port. Most communications packages I found were too big to run on my PC, until I found an old version of PC Kermit (v1.20) that was only 16k. Bingo! I wrote this to disk using the same method, got it configured, installed g-kermit on my attached Linux workstation, and now can download software just like an old school dial-up BBS.
I loved this project. I got to relearn x86 assembly, apply lessons learned from the Altair project on bootstrapping via the serial port, learn the 8250 UART chip, learn how tracks are formatted into sectors on a floppy, and learn the IBM PC BIOS calls. It was so much more rewarding than just buying a pre-loaded floppy disk. And at the end, my daughter and I also got to play some old school PC games too. A great project!