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.