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
; 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.