Title: 8080 IO string echo
Date: December 21, 2018
Tags: altair programming
========================================

The echo we're used to is a program that takes a string then writes the whole
string back out.  We don't need to invoke another program so we can skip that
part and just modify our echo program to work on strings instead of characters.

The echo on UNIX systems takes an input string as a whole (because we're usually
passing it as a parameter) and then prints the whole string back out.  Since
modern UNIX systems execute programs through a shell, the string sent to echo is
restricted somewhat by what the shell can allow.  For our use case, I'll
restrict our string to only printable characters and space.  And, since we don't
have a shell, we'll add some of our own user-friendly features.


000     LXI SP          061 ; Set stack pointer
001             000Q    000 ; to 000400Q
002             001Q    001
003     LXI H           041 ; Set heap pointer
004             200Q    200
005             000Q    000
006     MVI A           076 ; Initialize heap pointer
007             000Q    000 ; with a null
010     MVI A           076 ; Reset ACIA
011             003Q    003
012     OUT             323 ; 2SIO control
013             020Q    020 ; is at 020Q
014     MVI A           076 ; Set to 9600 baud 8n1
015             225Q    225
016     OUT             323
017             020Q    020
020     EI              373 ; Enable interrupts
021     NOP             000 ; Wait loop
022     JMP             303
023             021Q    021
024             000Q    000

; Delete
031     LDA             072 ; Check if empty string
032             300Q    300
033             000Q    000
034     CPI             376 ; By checking for initialized heap
035             000Q    000
036     JZ              312 ; Goto "end"
037             131Q    131
040             000Q    000
041     DCX H           053 ; Decrement heap pointer
042     MVI M           066 ; Zero character in memory
043             000Q    000
044     MVI B           006 ; Write backspace that
045             010Q    010 ; moves cursor back one space
046     CALL            315
047             133Q    133
050             000Q    000
051     MVI B           006 ; Write a space to overwrite
052             040Q    040 ; character on terminal
053     CALL            315
054             133Q    133
055             000Q    000
056     MVI B           006 ; Write another backspace to
057             010Q    010 ; put the cursor back
060     CALL            315
061             133Q    133
062             000Q    000
063     JMP             303 ; Goto "end"
064             131Q    131
065             000Q    000

; Interrupt handler
070     IN              333 ; Read character
071             021Q    021
072     CPI             376 ; If carriage return
073             015Q    015
074     JZ              312 ; Goto "print"
075             166Q    166
076             000Q    000
077     CPI             376 ; If backspace
100             177Q    177
101     JZ              312 ; Goto "delete"
102             031Q    031
103             000Q    000
104     CPI             376 ; If escape
105             033Q    033
106     JZ              312 ; Goto "reset"
107             146Q    146
110             000Q    000
111     CPI             376 ; If lowest printable character
112             040Q    040 ; is greater
113     JC              332 ; Goto "end"
114             131Q    131
115             000Q    000
116     CPI             376 ; If largest printable character
117             176Q    176 ; is not greater
120     JNC             332 ; Goto "end"
121             131Q    131
122             000Q    000
123     MOV M,A         167 ; Write character to memory
124     INX H           043 ; Increment heap pointer
125     MOV B,A         107 ; Copy character to B register
126     CALL            315 ; Call "write char"
127             133Q    133
130             000Q    000
; end
131     EI              373 ; Re-enable interrupts
132     RET             311 ; Go back to wait loop

; Write char - assumes character is in B register
133     IN              333 ; Read serial port status
134             020Q    020
135     RRC             017 ; Shift off bit 0
136     RRC             017 ; Shift off bit 1 into carry
137     JNC             322 ; If bit 1 was 0
140             133Q    133 ; not ready to send
141             000Q    000
142     MOV A,B         170 ; Get character from B
143     OUT             323 ; Write to terminal
144             021Q    021
145     RET             311 ; Go back to where we came

; Reset
146     LXI H           041 ; Reset heap pointer
147             300Q    300
150             000Q    000
151     MVI M           066 ; Re-initialize heap pointer
152             000Q    000
153     CALL            315 ; Write EOL
154             224Q    224
155             000Q    000
156     JMP             303 ; Got "end"
157             131Q    131
160             000Q    000

; Print
166     CALL            315 ; Write EOL
167             224Q    224
170             000Q    000
171     LDA             072 ; Check if empty string
172             300Q    300
173             000Q    000
174     CPI             376
175             000Q    000
176     JZ              312 ; Goto "end"
177             131Q    131
200             000Q    000
201     MVI M           066 ; Terminate string
202             000Q    000 ; with null
203     LXI H           041 ; Set heap pointer
204             300Q    300 ; to start of string
205             000Q    000
206     MOV A,M         176 ; Get character from memory
207     CPI             376 ; If null, we're done
210             000Q    000
211     JZ              312 ; Goto "reset"
212             146Q    146
213             000Q    000
214     MOV B,A         107 ; Copy character to B register
215     CALL            315 ; Write to terminal
216             133Q    133
217             000Q    000
220     INX H           043 ; Increment heap pointer
221     JMP             303 ; Goto next character
222             206Q    206
223             000Q    000

; Write EOL
224     MVI B           006 ; Write carriage return
225             012Q    012
226     CALL            315
227             133Q    133
230             000Q    000
231     MVI B           006 ; Write new line
232             015Q    015
233     CALL            315
234             133Q    133
235             000Q    000
236     RET             311 ; Go back to where we came


This one took me a few iterations to get right and work all the bugs out.  That
explains some of the gaps in memory.  I added a couple of sub-routines during
re-architecting and also quickly realized I had to use polling on output.  The
Altair can overwhelm a 9600 baud connection after 5 characters.  I'm not sure
what the physical limitations are that gets it to exactly 5 character but there
you go.

The biggest feature I wanted, and the hardest to get right was to have the
backspace key working as one would expect.  In the early days of the Altair, a
teletype was the typical IO device and since characters were printed on paper,
there as no point in deleting.  Usually backspace either printed an '_' or
re-printed the character being deleted.  Once terminals were available,
backspace could function as we know it today.

I also wanted to ignore non-printable characters.  And I wanted to use escape to
abort string entry.  These features will be useful for command entry in an
upcoming project.

## Breaking it down ##

# Main program #
The main program is pretty much the same as the previous echo implementations
except we also store a heap pointer in registers HL here instead of in the
interrupt handler.  This way we can keep track of where we are in the string as
we jump in and out of the interrupt handler for each character.  We also
initialize the pointer to 0 so we can detect if the string is empty.  I had
missed this initially and if enter was pressed first, it would print two
carriage return + new lines which wasn't what I wanted to happen.  It also
became necessary in order to prevent backspacing past the start of the heap.

# Delete #
Delete just does some terminal gymnastics to remove a character from the screen.
We need to make sure backspace wasn't the first thing entered since there will
be no character to delete in that case.  Otherwise, decrement the heap pointer
and replace the last character with null.  This is necessary in case this was
the first character.  It will re-initialize the heap pointer so we don't
accidentally back up past the beginning of the heap if backspace is entered
again.  The backspace key on my keyboard sends a 177Q (which is delete) but to
move the cursor back a space takes a 010Q (actual backspace).  We send that to
move the cursor, then a space to overwrite the character that was there, and
another 010Q to move the cursor back to accept another character for that spot.

# Interrupt handler #
The first thing the handler does is read the character from the serial port.
There is no need to poll here since the interrupt means we have a character
ready.  Reading the data port clears the interrupt in the serial board and CPU
interrupts are disabled automatically.  We then simply compare values to see
what we got.  If it was an enter, we need to print any characters we've saved.
If it's a backspace, delete a character.  If it's an escape, reset everything.

Then we have to check if the character is in the printable range.  This is
slightly tricky because the CPI instruction makes it easy to know if values are
equal, the zero bit is set to 1.  And makes it easy if the Accumulator is less
than the compared value, the carry bit will be set to 1.  Which means that to
know if the Accumulator is greater, you have to check that the zero bit is 0,
which tells you the values are unequal, and that the carry bit is 0, which tells
you that the Accumulator is not greater than.  Just the carry bit being 0 means
the Accumulator is less than or equal so both bits would have to be checked.
We need to write the comparisons such that the Accumulator can be tested with
one bit so we can use an easy jump instruction.  We compare the lowest printable
character with the character in the Accumulator and check that it is greater by
testing the carry bit.  Ignore the character and jump back to the input loop if
the carry bit is 1.  Then check if the largest printable character is *not*
greater than the character in the Accumulator.  Jump out if the carry bit is 0.
We don't know if the characters are equal or not, but we don't have to care
since if the Accumulator is equal to the highest printable character, then it's
a printable character we need to deal with.

If we've made it this far, we have a printable character.  Save it to memory at
the current heap pointer location then increment the heap pointer.  This makes
sure we overwrite our null initialization so we know we don't have an empty
string anymore.  Copy the character to the B register and call write char to
echo it back right away.  Then go back to the wait loop for the next character.

# Write char #
This is one sub-routine that seemed necessary once I switched to polling.  It's
a bit of tedium that is nice to have in one place.  I thought about how to pass
the character to be printed and instead of using memory, I just put the
character into the B register which is otherwise unused.  The routine reads the
status register of the serial port, and shifts the lower 2 bits off through the
carry flag.  So if bit 1 was a 1, the result of the two shifts will leave the
carry bit set to 1.  A 1 would indicate that the port is ready to transmit.  I
could have used ANI 002Q here and checked for zero.  I've seen both used.  Once
it's ready, copy the character out of the B register into the Accumulator and
write it out to the serial port.

# Reset #
Reset is another sub-routine that seemed like a good idea.  It's called twice in
this program but could be used more in other programs.  It resets the heap
pointer to the starting address.  Re-initializes it with a null.  Writes a
carriage return and newline to get the terminal cursor on a fresh line and goes
back to the character input process.

# Print #
Print is called when we hit enter so the first thing to do is print a carriage
return and newline to start echoing on an empty line.  Check if the string is
empty by checking if the first character is still our initialized pointer.  If
so, there is nothing to print so jump to setting up for character entry.  If we
have a string of some length, terminate it with null so we know where the end is
when we start iterating over it.  Set the heap pointer back to the start and
grab the character from memory.  Check if it's null and go to reset if it is,
else copy the character to the B register and call write char to print it.
Increment the heap pointer and go to the next character.  We loop until we get
to the null character we put at the end of the string.  Turns out null
terminated strings are really easy to deal with in this scenario.

# Write EOL #
This was another sub-routine that seemed obvious.  It's only called from two
locations in this program but for formatted output, it'll be used all the time
so it will be nice to have.  It simply uses write char to write a carriage
return and new line to the terminal.  If you work with files across UNIX and
Windows you'll know how annoying these guys can be.  On a terminal, new line by
itself isn't enough.  You have to translate that into a carriage return and a
newline.

## Concluding Thoughts ##
An obvious problem with the program is that there is no check for max string
length.  You could over flow the heap and scribble over your stack.

I tried to ignore non-printable characters but didn't consider the arrow keys or
function keys which aren't useful to print but don't send just unprintable
characters, either.

The code could be neater and optimized a bit.  Instead of all the 'goto end'
jumps, I could have done EI and RET right there, for example.
Originally I had planned on experimenting with different string implementations
but I am going to hold off on that for now.  I have fun things to try instead.

--------------------------------------------------------------------------------

A command entry interface like this opens the door for improvements to my
workflow.  Instead of coding on paper and painstakingly re-writing and
re-addressing everything when I need to make changes, and instead of using the
switches on the Altair's front panel, I could enter code through a program and
even have it do some of the assembling for me.