Title: Altair Boot Loader
Date: November 24, 2019
Tags: altair programming
========================================
Entering everything via the front panel was going to get overly tedious very
quickly. Early on, boot loaders were produced that would bootstrap more complex
programs read from paper tape, cassette, and eventually floppy disk.
Famously, Microsoft got it's start by writing a BASIC interpreter for the new
Altair 8800. Of course, no one was expected to load BASIC via the front panel
switches every time they turned the machine on. BASIC was available on paper
tape and cassette. Some teletype models had paper tape readers built in and
cassette players were, of course, quite common in the household.
To load BASIC or other programs from these devices into the Altair's memory
quickly, very simple programs were included to bootstrap the process. The
bootstrap program had to be entered manually so they were very short with little
to no feedback. They simply copied bytes from the serial port or cassette
interface and wrote them to memory.
As my programs are planned to get more complex, I needed similar functionality
to more quickly load larger programs. I don't have a paper tape punch and
reader so I'll cheat and use my laptop to stream the bytes to the serial port.
To the Altair, it's all the same and I've already decided to cheat and write
code in a text editor and use my laptop as a terminal. I might as well use it
as a paper tape reader, too.
Since paper tapes or cassettes store binary data, I had to do a little extra
work to convert my streamed ASCII hand assembled code into binary data before
writing to memory. Also, since I'm connecting to the Altair with a terminal
emulator, I included some simple feedback.
Let's take a look at an actual boot loader from the day.
4K BASIC boot loader:
0000 org 0
;
; 2SIO Loader for 4K BASIC Version 3.2
;
; ** Set sense switches A11, A10 ON **
;
0000 3E03 mvi a,3 ;reset then init ACIA
0002 D310 out 010h
0004 3E15 mvi a,015h ;015h for 8N1, 011h for 8N2
0006 D310 out 010h
; H = msb of load address
; L = lsb of load address = length of loader = leader byte
0008 21AE0F lxi h,00faeh ;4K BASIC V3.2
; lxi h,00fc2h ;4K BASIC V4.0
; lxi h,01fc2h ;8K BASIC v4.0
; lxi h,03fc2h ;Extended BASIC V4.0
; lxi h,07ec2h ;Extended Disk BASIC v5.0
000B 311A00 loop lxi sp,stack ;init SP so a RET jumps to loop
000E DB10 in 010h ;get 2sio status
0010 0F rrc ;new byte available?
0011 D0 rnc ;no (jumps back to loop)
0012 DB11 in 011h ;get the byte
0014 BD cmp l ;new byte = leader byte?
0015 C8 rz ;yes (jumps back to loop)
0016 2D dcr l ;not leader, decrement address
0017 77 mov m,a ;store the byte (reverse order)
0018 C0 rnz ;loop until L = 0
0019 E9 pchl ;jump to code just downloaded
001A 0B00 stack dw loop
001C end
The format is a little different than I use because it's in assembler printable
output. Addresses are in the first column, and assembled bytes are in the
second column, both in hexadecimal. Parameters to opcodes are on the same line,
but you'll see the address will increment by the number of bytes the parameters
used up.
You can see how short this is. It sets up the serial port, the length of the
expected data is hard coded but they use part of the length as leader bytes on
the tape, and bytes are read in a loop. The paper tape leader is skipped and
then data is read and written to memory. BASIC is then executed automatically.
Nothing remarkable except they use a little trick to save bytes by setting the
stack pointer to the address of the start of the loop so they can use returns
instead of jumps. Returns don't need to be provided with an address like jumps
do. Saves 2 bytes each time. However, the LXI at address 000BH to reset the
stack pointer and the stack value defined at 001AH costs 5 bytes. There are 3
returns which, if written as jumps, would have added 6 bytes. So ultimately
they save one byte. Maybe not wort much here, but with a larger set of
comparisons that could add up to some savings. Every byte counts, especially
when entering data manually through the front panel.
My boot loader:
; Requires first 2 bytes, high then low, as start address in xx xxx xxx form
; Reads bytes in xx xxx xxx format until invalid character
; start up
177400 LXI SP 061 ; Set stack pointer
177401 000Q 000
177402 000Q 000
177403 MVI A 076 ; Reset ACIA
177404 003Q 003
177405 OUT 323 ; for terminal port
177406 020Q 020
177407 MVI A 076 ; 9600 baud 8N1 no interrupts
177410 025Q 025
177411 OUT 323 ; for terminal port
177412 020Q 020
; print prompt
177413 MVI B 006
177414 '\r' 012
177415 CALL 315 ; print char
177416 142Q 142
177417 377Q 377
177420 MVI B 006
177421 '\n' 015
177422 CALL 315 ; print char
177423 142Q 142
177424 377Q 377
177425 MVI B 006
177426 '<' 074
177427 CALL 315 ; print char
177430 142Q 142
177431 377Q 377
; read address
; as xx xxx xxx xx xxx xxx
; not x xxx xxx xxx xxx xxx
177432 CALL 315 ; get byte
177433 070Q 070
177434 377Q 377
177435 MOV H,D 142 ; store high byte
177436 CALL 315 ; get byte
177437 070Q 070
177440 377Q 377
177441 MOV L,D 152 ; store low byte
; read opcode
177442 CALL 315 ; read byte
177443 070Q 070
177444 377Q 377
; write
177445 MOV A,D 172 ; put opcode in A
177446 MOV M,A 167 ; write opcode to address
177447 INX H 043 ; Increment address
177450 MVI B 006 ; print dot
177451 '.' 056
177452 CALL 315 ; print char
177453 142Q 142
177454 377Q 377
177455 JMP 303 ; goto read opcode
177456 042Q 042
177457 377Q 377
; error
177460 MVI B 006 ; Error
177461 '!' 041
177462 CALL 315 ; print char
177463 142Q 142
177464 377Q 377
177465 JMP 303 ; stop here
177466 065Q 065
177467 377Q 377
; read byte
; first char (only 2 bits allowed)
177470 MVI E 036 ; set number of bits
177471 374Q 374
177472 MVI D 026 ; clear opcode
177473 000Q 000
177474 MVI B 006 ; set char counter
177475 003Q 003
; char
177476 CALL 315 ; get char
177477 131Q 131
177500 377Q 377
177501 MOV C,A 117 ; save in C
177502 ANA E 243 ; check for valid octal
177503 XRI 356
177504 060Q 060
177505 JNZ 302 ; error
177506 060Q 060
177507 377Q 377
177510 MOV A,D 172 ; get opcode
177511 RLC 007 ; shift 3
177512 RLC 007
177513 RLC 007
177514 MOV D,A 127 ; store in D
177515 MOV A,C 171 ; restore char
177516 ANI 346 ; convert from ASCII to number
177517 007Q 007
177520 ADD D 202 ; add existing octal
177521 MOV D,A 127 ; store back to D
177022 MVI E 036 ; set number of bits
177023 370Q 370
177524 DCR B 005
177525 JNZ 302 ; next char
177526 076Q 076
177527 377Q 377
177530 RET 311
; get char
177531 IN 333 ; wait for character
177532 020Q 020
177533 RRC 017
177534 JNC 322 ; goto wait
177535 131Q 131
177536 377Q 377
177537 IN 333 ; read character
177540 021Q 021
177541 RET 311
; write char to terminal
; expects character in B register
177542 IN 333 ; Read status
177543 020Q 020
177544 RRC 017
177545 RRC 017
177546 JNC 322 ; Not ready to send
177547 142Q 142
177550 377Q 377
177551 MOV A,B 170 ; Get char from B register
177552 OUT 323 ; Write to terminal
177553 021Q 021
177554 RET 311
Right off the bat, this is quite a bit longer. I need to accommodate arbitrary
programs so I don't hardcode a length and don't bother with leader bytes. I
read all data as ASCII characters instead of binary bytes so I can manually type
programs in if I want to. That requires a large conversion routine. I also
allow the first two bytes to define a start address. I print a prompt as I
originally intended this to be a more interactive program but then remembered
it's just a bootstrap. The interactive program can be loaded next. I left it
in so I'd know my terminal was connected and the boot loader was running. I
also added feedback for each byte written to memory so I know it was progressing
and a minimalist error output when invalid data was encountered. Still pretty
simple but versatile enough to load anything I need. I have another copy of
this loader assembled to a 000000Q start address. This way I can enter the boot
loader at either end of the memory and then load a program at the other end. I
decided not to automatically jump to the start address once the load was
finished so I could reset and load more data to another location.
The only thing worth looking at in my code is 'read byte'. Since I'm using
octal for data and assembling to octal, it takes a little translating to get a
byte from ASCII characters. The first character of three always only represents
2 bits, digits 0 through 3. I loop 3 times, for the three characters. First, I
store a mask to remove bits other than the expected number of octal bits in the
E register. I set it to 374Q for the first loop and AND it with the character
to test that only the first 2 bits had data, once the 060Q for ASCII is removed.
I store the bits to D by shifting the bits then adding them to D. I then set
the mask to 370Q to allow for 3 bits, digits 0 - 7. Loop twice more and D will
contain the single byte represented by 3 ASCII characters. I see now that I
don't need to clear the D register at 177472Q since any junk in there will be
shifted out by the time the loop is done. I tend to be overly cautious about
that sort of thing.
Working with hexadecimal would be easier since it's always 4 bits, and you only
need 2 ASCII characters to represent a byte.
A little long, and a little flawed, but it'll help me enter longer programs
more quickly. I'll need the help soon.
--------------------------------------------------------------------------------
There was another way to enter programs into the Altair. A little easier than
the front panel, but not as easy as loading from media with a boot loader. And
if you wrote the program yourself on paper, you'd need a way to load it into the
Altair before you could save it to media, if you had that option.
Monitors, small programs that allowed you to examine, write to, and read memory
filled that gap. I'll talk about those next.
[0]
https://en.wikipedia.org/wiki/Altair_BASIC
gopher://gopherpedia.com:70/0/Altair BASIC