A Program for the IMSAI 8080 Clone

I was browsing the web the other day and hit upon some pages about the IMSAI 8080. One of the very early personal computers. It reminded me that I’ve got an IMSAI8080esp clone by the High Nibble that I built a while back. After building it and toying around with it for a few weeks, I had put it back in its box, stored it away and then forgot about it.

I pulled it out of the box and hooked it up. That’s a photo of it above. It occurred to me that I should write a little program to animate the 8 output LEDs on the front panel. I had seen some DEC PDP-11/70 front panel kits on YouTube. Several of them had some of the front panel lights that ran a continuous pattern. I think they call it Blinkenlights. There’s no reason I couldn’t do something similar for my IMSAI clone.

I remember back in 1976 when I finished building my original IMSAI 8080. It cost $500 for a kit of parts. Today that’s equivalent to over $2800. That’s a photo of it below. I found a listing for a little game that was about 20 or so bytes of Intel 8080 machine code. It was called “Kill the Bit”. I keyed it into the computer using the front panel switches. Then set the address of the first byte of the program and hit the run button. On the output port a single LED would move from bit 8 to bit 1 and then recycle. The object was to turn off the LED by clicking the switch below the LED that was lit before it moved. If you matched it, the LED would turn off. If you missed, an additional bit would appear that you would now have to turn off. You won by turning off all the LEDs.

Now, a few decades later, I have a clone of that computer. Rather than write a game, I decided to write a program to display a series of patterns on the 8 LEDs of the front panel. I could write it in BASIC, but why not get closer to the hardware and write it in 8080 assembly language.

One reason I decided to do an assembler program is I’ve always enjoyed writing in assembler. Higher level languages such as BASIC or C are easier to write, but each line of a high level language generates many lines of equivalent assembler code. In general, you trade ease of use for larger programs. When you’ve got less than 64 kilobytes of memory, smaller is better. Consider that 64 kilobytes is about 0.000003814697265625% of the memory that one of today’s PC’s have. Many of which come with 16 gigabytes of memory.

The IMSAI8080esp comes with a set of virtual floppy disks that contain vintage software. I’m running the CP/M 2.2 operating system, an early ancestor of MS-DOS. Among other things it comes with a compiler, ASM, to compile assembler programs and turn them into files that will run on the machine. You need an editor to create the assembler source file. A editor called ED comes with CP/M. Below is a screen shot of the IMSAI8080esp desktop. It shows the TTY: virtual device which emulates a VT100 terminal. The TTY: screen is showing the output of the CP/M SURVEY command. It gives an overview of the running CP/M system. The icons on the left side are other virtual devices that can be used.

A word processor called WordStar can also be used for the same purpose as ED and is on another virtual disk. WordStar is a bit more complicated but is also more versatile. I decided to go with WordStar. Fortunately, documentation for the ancient program is still available. Below is a screen shot of using WordStar to enter the program.

It’s been a long time since I’ve used the 8080 instruction set. My career has been working in the IBM mainframe world. Needless to say the instruction set of an early microprocessor is much more primitive than a mainframe. The registers (high speed storage locations) are only 8 bits (one byte) wide. The IBM mainframes I’ve worked on had registers at least 32 bits (four bytes) wide. While the mainframe has instructions that can directly modify or move data in RAM memory with only a single base address loaded into a register. The 8080 needs to involve its registers to not only address the memory but to also move data from one location to another. Below are sections of reference cards showing some of the instructions of each machine.

Another difference is the use of a stack. An IBM mainframe doesn’t directly involve the concept of a stack, while an 8080 microprocessor is very much a stack based machine. You can think of a stack as a group of contiguous memory locations that is reserved for storing and retrieving register values. A stack has a starting point and when you push (store) a register value on the stack the stack pointer is modified to point to the next available stack slot. In the case of the 8080 the stack builds from the bottom up. When you pop (retrieve) a value from the stack to load a register the stack pointer moves back a slot. It is a serial resource so values that pushed onto the stack are popped out in reverse order. In the case of the 8080 each stack slot is two bytes long. You need to be sure that your stack is large enough for your purposes or you will start overwriting memory outside of the stack. You can’t tell the microprocessor how large the stack is, it just assumes that the next slot is free to write to.

When you push a register onto the stack it actually stores a register pair. The 8080 has a set of registers labeled A, B, C, D, E, H, and L, each of them 8 bits wide. There is also a stack pointer register called SP it is the only register that is 16 bits wide since it assumed that it will always contain a memory address. Anything loaded into the SP is assumed to be the address of a stack. The A register is called the accumulator and is a special register that performs math and logical operations. The other registers can be used individually or in pairs. The pairs are B/C, D/E and H/L. So when you push the B register you actually push both the B and C registers. B is pushed first then C. To load data from the stack you do a pop. If you pop into the B/C pair, C is loaded first then B. You can also push a special pair called the PSW. It consists of the accumulator and the status byte.

In the earlier screenshot showing WordStar, you can also see the beginning of the program. I’ve named it Liteshow. The beginning is where I save CP/M’s stack pointer and then set the stack pointer to my own stack. The code with the label “EPILOG” is were the program will exit. It reloads CP/M’s stack pointer and then returns to CP/M.

The status byte is a control register where each bit is a flag that represents the current state of the machine. For example, there is a carry flag that indicates when a math function causes the value of the accumulator to overflow or underflow its 8 bits. There is a zero flag that indicates whether the previous instruction resulted in a zero value. It also has a parity flag to indicate the parity of the accumulator. The parity flag is set if the sum of the bits is even. If the sum is odd the parity flag is cleared. For example, if the accumulator contains the number 5 it would have the bit pattern “00000101”. The count of the set bits is 2, therefore the parity is even. There are two other flags, but I’ve gone on long enough about the status byte.

Sometimes when you are programming and have a bug you’re trying to squash, you need a print out. The CP/M assembler produces a .HEX file that contains the machine code. It also produces a .PRN file that has a printout of the assembly. Any lines with an error are marked. The IMSAI8080esp has a virtual line printer. To print your assembly to the virtual printer you use the PIP command as shown below.

If you click the LPT: icon on the IMSAI8080esp desktop you can view what is on the printer as shown below. You can get an actual paper print by clicking the printer icon on the right hand side.

This program will be writing a series of bit patterns to the output section of the front panel. (The eight LEDs in the upper left on the front panel.) The first pattern will move a single bit from left to right then wrap around and do it again. The next pattern will be the opposite, right to left. The third pattern will run a single bit from left to right then, instead of wrapping around, it will then go from right to left. Kind of a bouncing pattern. Lastly, a pattern of two bits will start at the outer edges and move to the center. When they meet in the middle it will reverse and go back to the edges.

The first two patterns are basically the same except for the direction. All I need to do is start with a pattern of binary bits ‘1000000’ (80 in hexadecimal) then rotate the bits to the right one bit at a time until the bit pattern is ‘00000001’ and then start over with ‘10000000’. The second one will do the same except it would start with ‘00000001’ and rotate the bits to the left. The third one will simply combine the two and do one pass rotating right and then one pass rotating left. The final pattern will start with ‘10000001’ (81 hex), then ‘01000010’ (42 hex), ‘00100100’ (24 hex), ‘00011000’ (18 hex). Once at the center, it will run the pattern backwards.

Even though this is a slow early generation microprocessor (4Mhz clock), it still runs quite fast compared to a human. If I let the pattern run at machine speed, you would barely notice that the LEDs were flashing. To make it that a person can actually see the changes I need to put in a delay loop each time I output a new pattern. To accomplish this, I load the B/C pair with 7 at the top of the program. Then I load the H/L pair with 0 for each pattern change. At the end of each output I add B/C to the 16 bit value in the H/L pair. This means that I’m counting from 0 to 65,536 by 7’s. 65,536 (64k) is the maximum value of that can be contained in 16 bits. When the count overflows the carry bit is set and I know I’ve waited long enough and then continue with the rest of the program.

I want the program to run continuously. Therefore the main part of the program is a loop that resets a repeat counter (currently 10) and then runs each pattern as a subroutine, sequentially. When if finishes the last pattern, it loops back to the top and does it all over again. Of course, if that was all it did, the only way to stop it would be to press the stop key on the front panel, enter a zero address on the front panel, then press the run key. Restarting at address 0 on a CP/M 2.2 machine performs a warm restart getting you back to the familiar A> prompt.

I’d rather be able to just press a key on the keyboard and have the program exit by itself. The easiest way to do that is to call a BDOS routine. CP/M puts BDOS routines in high memory to allow other transient programs to use CP/M services for various functions rather than have the transient program contain them. This can make transient programs smaller and leverages the work that CP/M has already done.

You perform a BDOS routine by executing a CALL instruction to address 0005h. To tell BDOS which function to use you place a function code in the C register. If the particular BDOS function you are using needs additional information you load those values into various other registers depending on which BDOS function you are using. In my case I’m going to use function 06h which is direct console I/O. Function 6 needs to know if this is an input or an output operation. I’m wanting to look for input so I put FFh into register E. When function 6 returns, the A register will contain either the character that was input or 0 if no character is ready. So all I need to do is call function 6 and then compare A to zero. If A is not zero then I need to exit. I do that by turning on an exit flag which will be checked later. Since I will be checking for input at many different places in the program, I created a subroutine to do the function call.

Once the pattern has completed its first pass I need to do it 9 more times. At this point I do a keyboard check and if the exit flag is off I get the repeat count and decrement it by one. If the count is not zero then I go back and do another pass, otherwise I return to the main loop.

With all that defined I wrote the program as shown below. There were a few times when I did some silly things that caused the program to not come anywhere close to doing what I wanted. But, with careful reading of the code and referring to the language manual I finally got past them all. I could go in and try to make it more efficient in a few areas, but it does what I want it to do, so I’m happy.

If you made it this far, thanks! Here is a short video showing it in action. If you want to see the actual program, below the video is my code and explanation. If you’re leaving at this point, thanks for at least making it to here. Sometimes I’m a little passionate about programming and I get carried away. So for the nitty gritty scroll past the video.

The opening code:

        ORG    0100h                ; tell assembler where program will start
        LXI    H,0                  ; clear H/L pair
        DAD    SP                   ; add SP to H/L
        LXI    SP,MYSTACK           ; load SP with our stack pointer
        PUSH   H                    ; save CP/M stack pointer
        CALL   MAIN                 ; start our program
;
EPILOG  POP    H                    ; get CP/M’s stack pointer
        SPHL                        ; load SP with CP/M’s stack pointer
        RET                         ; return to CP/M
; stack definition
        DS     32                   ; define 16 word stack
MYSTACK EQU    $                    ; set constant for our stack address
; define data areas here
PAT     DS     1                    ; storage for bit pattern
REPCNT  DS     1                    ; counter for repeating patterns
QF      DB     0                    ; quit flag – 0 = OK, FFh = quit
PBITS   DB     081h,042h,024h,018h  ; part 1 of table for PING
        DB     024h,042h,018h,0FFh  ; 0FFh marks end of table
; define constants here
DELAY   EQU    07h                  ; delay value: higher = faster
PORT    EQU    0FFh                 ; output port number for LEDs
BDOS    EQU    05h                  ; BDOS entry point
FUNCT   EQU    06h                  ; BDOS function 6 – console i/o
INPUT   EQU    0FFh                 ; BDOS function – 6 get input
ABORT   EQU    0FFh                 ; program quit flag

This code sets the starting address for the program and sets up a local stack. It then calls the main program. The lines following ‘MYSTACK’ define the data areas the program will use. Finally the constants are defined. They are symbolic only and do not take up any storage.

The below code defines the main program loop.

; program begins here
MAIN    EQU    $                    ; start of main program
        LXI    B,DELAY              ; load delay value into B/C
RESET   EQU    $                    ; top of main loop
        MVI    A,0Ah                ; set to repeat pattern 10 times
        STA    REPCNT               ; save it to the repeat counter
        CALL   PATRT                ; call the rotate right pattern
        LDA    QF                   ; get quit flag
        CPI    ABORT                ; is quit flag set?
        JZ     DONE                 ;         yes, jump to end code
        MVI    A,0Ah                ; set to repeat pattern 10 times
        STA    REPCNT               ; save it to the repeat counter
        CALL   PATLF                ; call the rotate left pattern
        LDA    QF                   ; get quit flag
        CPI    ABORT                ; is quit flag set?
        JZ     DONE                 ;         yes, jump to end code
        MVI    A,05h                ; set to repeat pattern 5 times
        STA    REPCNT               ; save to repeat counter
        CALL   PPPAT                ; call the bouncing pattern
        LDA    QF                   ; get quit flag
        CPI    ABORT                ; is quit flag set?
        JZ     DONE                 ;         yes, jump to end code
        MVI    A,0Ah                ; set to repeat pattern 10 times
        STA    REPCNT               ; save it to the repeat count
        LDA    QF                   ; get quit flag
        CPI    ABORT                ; is quit flag set?
        JZ     DONE                 ;         yes, jump to end code
        JMP    RESET                ; jump back to top of loop
DONE    EQU    $
        MVI    A,0                  ; prepare to clear output
        OUT    PORT                 ; clear the outpu LEDs
        RET                         ; return to EPILOG code

The main loop calls each pattern in sequence. After the pattern routine returns the quit flag is tested. If it has been set then the loop jumps out to the DONE label which resets the LEDs and returns to the opening code. If the quit flag is not set then the loop continues.

Next the keyboard check routine is defined.

; check keyboard input subroutine
CKKB    EQU    $
        PUSH   B                    ; save registers
        PUSH   D
        PUSH   H
        MVI    C,FUNCT              ; bdos call 6 – console i/o
        MVI    E,INPUT              ; set for console input
        CALL   BDOS                 ; check for input character
        POP    H                    ; restore registers
        POP    D
        POP    B
        RET

The first thing it does is save the registers that will be used. Then it sets the BDOS call number and indicates that the function is for input. It then calls BDOS. Then it restores the registers and returns to the routine that called it.

Next comes the pattern routines, starting with the left to right pattern.

; pattern routines
PATRT   EQU    $
        MVI    A,080h               ; set initial bit pattern
        STA    PAT                  ; store it
        LXI    H,0                  ; initialize H/L delay counter
BEG1    EQU    $
        LDA    PAT                  ; load bit pattern
        CMA                         ; invert A
        OUT    PORT                 ; output the pattern
        DAD    B                    ; increment the display counter
        JNC    BEG1                 ; if delay not expired output again
; delay expired, change pattern and go again
        LDA    PAT                  ; get pattern
        RRC                         ; shift pattern right
        STA    PAT                  ; save the new pattern
        JNC    BEG1                 ; if not pattern end, continue
; if end of pattern check abort and repeats
        CALL   CKKB                 ; check for keyboard input
        CPI    0h                   ; keypress ready?
        JNZ    QUIT1                ;         yes, time to stop
        LXI    H,REPCNT             ; get address of repeat counter
        DCR    M                    ; decrement repeat counter
        JNZ    PATRT                ; repeat pattern
        RET                         ; exit to MAIN
QUIT1   EQU    $
        MVI    A,ABORT              ; get quit flag
        STA    QF                   ; save the quit flag
        RET                         ; exit to MAIN

The routine starts by loading its beginning pattern. Then saves it so it can be retrieved later. Then the H/L pair is set to 0. The output loop is next, starting by reloading the bit pattern. A quirk of the output LED panel is that it displays the inverse of the value that is output. If the value is ‘1000000’ then the display shows ‘01111111’. So to display what I actually want I need to invert the value. That is what the CMA instruction does. The DAD instruction adds the value of the specified register pair to the H/L pair. IF the H/L pair overflows the carry bit is set. If the carry bit is not set then the output loop is performed again. If it carry bit is set, then the pattern is reloaded and rotated to the right and stored for the next pass. If the rotate pushes a 1 bit out then the carry flag is set otherwise it is cleared. If the carry flag is set it jumps to the start of the output loop to do the next pattern.

At the end of the pattern pass a call is made to check for keyboard input. If the return value is not 0 then it jumps to the quit section which sets the quit flag and exits back to the MAIN routine. If the value is 0 then the address of the repeat counter is loaded into the H/L pair and the value at that address is decremented by 1. If the repeat counter is not zero then the pattern pass is repeated, otherwise the routine exits to MAIN.

I think I need to clarify the DCR instruction. The M that follows DCR is not label for a memory location nor is it a register name. Instead it is a special mnemonic that indicates the byte to be decremented is addressed by the H/L pair.

The next routine called by MAIN is the right to left pattern. It is exactly the same as the the above routine, but instead of using an RRC it uses an RLC to rotate the pattern to the left.

;
PATLF   EQU    $
        MVI    A,001h               ; set initial bit pattern
        STA    PAT                  ; store it
        LXI    H,0                  ; initialize H/L delay counter
BEG2    EQU    $
        LDA    PAT                  ; load bit pattern
        CMA                         ; invert A
        OUT    PORT                 ; output the pattern
        DAD    B                    ; increment the display counter
        JNC    BEG2                 ; if delay not expired output again
; delay expired, change pattern and go again
        LDA    PAT                  ; get pattern
        RLC                         ; shift pattern left
        STA    PAT                  ; save the new pattern
        JNC    BEG2                 ; if not pattern end, continue
; if end of pattern check for abort and repeats
        CALL   CKKB                 ; check for keyboard input
        CPI    0h                   ; keypress ready?
        JNZ    QUIT2                ;         yes, time to stop
        LXI    H,REPCNT             ; get address of repeat counter
        DCR    M                    ; decrement repeat counter
        JNZ    PATLF                ;         if not 0, repeat pattern
        RET                         ; exit to MAIN
QUIT1   EQU    $
        MVI    A,ABORT              ; get quit flag
        STA    QF                   ; save the quit flag
        RET                         ; exit to MAIN

The next pattern routine is the ‘bounce’ pattern. It is a combo routine using parts of both PATRT and PATLF.

;
PPPAT   EQU    $
        MVI    A,080h               ; set initial bit pattern
        STA    PAT                  ; store it
        LXI    H,0                  ; initialize H/L delay counter
PPR     EQU    $
        LDA    PAT                  ; load bit pattern
        CMA                         ; invert A
        OUT    PORT                 ; output the pattern
        DAD    B                    ; increment the display counter
        JNC    PPR                  ; if delay not expired output again
; delay expired, change pattern and go again
        LDA    PAT                  ; get pattern
        RRC                         ; shift pattern right
        STA    PAT                  ; save the new pattern
        JNC    PPR                  ; if not pattern end, continue
; if end of pattern check for abort and repeats
        CALL   CKKB                 ; check for keyboard input
        CPI    0h                   ; keypress ready?
        JNZ    PPQUIT               ;         yes, time to stop
;
        MVI    A,001h               ; set initial bit pattern
        STA    PAT                  ; store it
        LXI    H,0                  ; initialize H/L delay counter
PPL     EQU    $
        LDA    PAT                  ; load bit pattern
        CMA                         ; invert A
        OUT    PORT                 ; output the pattern
        DAD    B                    ; increment the display counter
        JNC    BEG2                 ; if delay not expired output again
; delay expired, change pattern and go again
        LDA    PAT                  ; get pattern
        RLC                         ; shift pattern left
        STA    PAT                  ; save the new pattern
        JNC    PPL                  ; if not pattern end, continue
; if end of pattern check for abort and repeats
        CALL   CKKB                 ; check for keyboard input
        CPI    0h                   ; keypress ready?
        JNZ    PPQUIT               ;         yes, time to stop
        LXI    H,REPCNT             ; get address of repeat counter
        DCR    M                    ; decrement repeat counter
        JNZ    PPPAT                ; repeat pattern
        RET                         ; exit to MAIN
PPQUIT  EQU    $
        MVI    A,ABORT              ; get quit flag
        STA    QF                   ; save the quit flag
        RET                         ; exit to MAIN

The final pattern called PING and is a double bit pattern stored in the table PBITS near the top of the program. In the case of this pattern, rather than rotating the pattern the table pointer is incremented to obtain the next bit pattern. The other checks are very similar to what I’ve done previously.

;
PING    EQU    $
        LXI    D,PBITS              ; get address of PBITS table in D/E
        LDAX   D                    ; get pattern from the table
        STA    PAT                  ; store pattern
PING1   EQU    $
        LXI    H,0                  ; initialize H/L delay counter
BEG4    EQU    $
        LDA    PAT                  ; load the bit pattern
        CMA                         ; invert A
        OUT    PORT                 ; output the pattern
        DAD    B                    ; increment the display counter
        JNC    BEG4                 ; if delay not expired output again
; delay expired, change pattern and go again
        INX    D                    ; increment the table pointer
        LDAX   D                    ; get pattern from the table
        STA    PAT                  ; save the new pattern
        CALL   CKKB                 ; check for keyboard input
        CPI    0h                   ; keypress ready?
        JNZ    QUIT4                ;         yes, time to stop
        LDA    PAT                  ; get new pattern
        CPI    0FFh                 ; end of table?
        JNZ    PING1                ;         no, display next pattern
        LXI    H,REPCNT             ; get address of repeat counter
        DCR    M                    ; decrement repeat counter
        JNZ    PING                 ; if not zero reload table and go again
        RET                         ; else, exit to MAIN
QUIT4   EQU    $
        MVI    A,ABORT              ; get quit flag
        STA    QF                   ; save the quit flag
        RET                         ; exit to MAIN
;
        END    0100h

This final routine gets the address of the pattern table into the D/E pair, gets the first pattern and saves it. It then clears the delay counter and drops into the display loop. When the delay counter expires it increments the D/E pair to point to the next table entry and loads that byte into A. After saving the new pattern, it checks for keyboard input and if there was some, it jumps to the quit section. Otherwise, it gets the new pattern and if it is not the end of table marker it jumps to the display section. If it is the end of the table, it decrements the repeat counter. If the repeat counter in not 0 then it jumps to the start of the routine to reload the table pointer. Otherwise it returns to the MAIN routine.

Well that’s it. I hope you got something out of this VERY long winded post. I had a lot of fun writing the program. In the next post I will be back to work on the 1/48 scale Atlas D model. Thanks for visiting.

2 thoughts on “A Program for the IMSAI 8080 Clone

  1. This is such a great blog post and an awesome blog I found because I was googling the imsai-8080 replicas. I see you’ve been writing this since 2008. keep it going Luhman! I like the miniature kits you paint as well, you should paint a warhammer 40k mini for fun.
    thanks

    Tom

    1. Thanks for the kind words. I’m glad you liked the post. I plan on keeping it up. I try to get them out once a week, but somehow I find retirement busier than when I was working.

      Randy

Leave a reply to rluhman Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.