Title: 8080 IO - echo Date: December 08, 2018 Tags: altair programming ======================================== Time to take a leap forward in technology and human-computer interaction. I'm allowing myself to use a terminal to talk to the Altair. A couple of implementations of echo demonstrate serial IO. The simplest program to start with for IO is an echo program. It covers the basics of interrupts and how to read and write to the terminal via the serial port. It's not much of thing on it's own. On today's computers echoing is a thing that just happens before you've actually even executed a program you're trying to run. But at the same time, seeing what you're about to command the computer to do is a necessary part of interacting with it. From this we can build programming environments like BASIC or a shell environment or any other kind of text interaction. The code for echo using the 88-2SIO board comes almost entirely from the Altair clone materials[0] as demonstrated in a video about interrupts[1]. I merely had to add the echo part that writes the character back out to the serial port. Code for echoing characters using the 2SIO board in the Altair clone: 000 LXI SP 061 ; Set stack pointer 001 000Q 000 ; to 000400Q 002 001Q 001 003 MVI A 076 ; Reset ACIA 004 003Q 003 005 OUT 323 ; 2SIO control 006 020Q 020 ; is at 020Q 007 MVI A 076 ; Set to 8n1 010 225Q 225 011 OUT 323 012 020Q 020 013 EI 373 ; Enable interrupts 014 NOP 000 ; Wait loop 015 JMP 303 016 014Q 014 017 000Q 000 ; Interrupt handler 070 IN 333 ; Read character 071 021Q 021 ; from address 021Q 072 OUT 323 ; Write character back out 073 021Q 021 ; also to address 021Q 074 EI 373 ; Re-enable interrupts 075 RET 311 ; Return to wait loop ## Breaking it down ## We start by setting the stack pointer to 000400Q which is the highest 8 bit address. No special reason to put it there versus any where else. We then send a 003Q to the 2SIO board which is value for the "master reset" to reset the board's configuration and state. The control address for the first serial port is address 020Q. When OUT and IN are used, the 1 byte address is not a memory address in RAM but a hardware address. We then configure the 2SIO board's first serial port to 8 data bits, no parity, 1 stop bit, and 9600 baud. After setting up the serial port, we simply enable interrupts and enter a NOP loop to wait for data. The interrupt handler simply reads the data address of the 2SIO's first port which we know is populated because of the interrupt. Interrupts are automatically disabled in the CPU once we jump to the handler, and the interrupt generated by the board is acknowledged by reading the data address. If you remember the 88-VI-RTC board required the software to explicitly acknowledge the interrupt since there was no data to read from the clock. We simply write the byte straight back to the data address and the 2SIO board will send it out the port. The IN and OUT addresses can be the same because the board knows when an IN instruction is being executed and when an OUT instruction is being executed and will handle the data appropriatly. # Using the 2SIO # Configuring The serial ports on the 2SIO have to be configured for the number of bits, parity, etc. The configuration bits (from 7 to 0): ================================================================================ || Bit || Value || Function || ================================================================================ | 7 | 0 | Receive interrupt disabled. | | |--------+-------------------------------------------------------------| | | 1 | Receive interrupt enabled. | |-------+--------+-------------------------------------------------------------| | 6-5 | 0 0 | RTS = low, transmission interrupts disabled. | | |--------+-------------------------------------------------------------| | | 0 1 | RTS = low, transmission interrupts enabled. | | |--------+-------------------------------------------------------------| | | 1 0 | RTS = high, transmission interrupts disabled. | | |--------+-------------------------------------------------------------| | | 1 1 | RTS = high, transmits a break level on the transmit data | | | | output, transmission interrupts disabled. | |-------+--------+-------------------------------------------------------------| | 4-2 | 0 0 0 | 7 data bits, 2 stop bits, even parity. | | |--------+-------------------------------------------------------------| | | 0 0 1 | 7 data bits, 2 stop bits, odd parity. | | |--------+-------------------------------------------------------------| | | 0 1 0 | 7 data bits, 1 stop bit, even parity. | | |--------+-------------------------------------------------------------| | | 0 1 1 | 7 data bits, 1 stop bit, odd parity. | | |--------+-------------------------------------------------------------| | | 1 0 0 | 8 data bits, 2 stop bits, no parity. | | |--------+-------------------------------------------------------------| | | 1 0 1 | 8 data bits, 1 stop bit, no parity. | | |--------+-------------------------------------------------------------| | | 1 1 0 | 8 data bits, 1 stop bit, even parity. | | |--------+-------------------------------------------------------------| | | 1 1 1 | 8 data bits, 1 stop bit, odd parity. | |-------+--------+-------------------------------------------------------------| | 1-0 | 0 0 | Set the clock divide to 1. | | |--------+-------------------------------------------------------------| | | 0 1 | Set the clock divide to 16. | | |--------+-------------------------------------------------------------| | | 1 0 | Set the clock divide to 64. | | |--------+-------------------------------------------------------------| | | 1 1 | Master reset. | -------------------------------------------------------------------------------- We used 10010101 to: enable receive interrupts, set RTS = low with transmission interrupts disabled, set 8 data bits, 1 stop bit with no parity, and set the clock divide to 16. RTS is Request to Send (later repurposed as Ready to Receive) and is part of a handshake protocol some devices used. This simple serial IO doesn't need it. You can read more info on Wikipedia's RS-232[2] page. The clock divide selects the baud rate. The 2SIO board is hardwired to a selected baud rate. The Altair clone allows this to be configured in firmware and is set to 9600 baud by default. In software, one could choose a divide of 64 and get a slower baud rate (as slow as 27.5 baud) or a divide of 1 to get a faster baud rate without rewiring the board. The chip multiplies the baud rate by 16 so a divide of 16 gives you the straight baud rate as wired. With a wired baud of 9600, we could also run at 2400 baud or 153,600 baud which seems really fast to me for this era so I'm not sure it's actually valid. The documentation doesn't mention speeding up the baud rate, just slowing it down and 9600 is the fastest hardwired rate. # Status # In addition to using interrupts to interact with the serial port, the 2SIO can be polled for data. The status address (the same as used for configuration) can be read and the bits (from 0 to 7) tell the following: ================================================================================ || Bit || Name || Description || ================================================================================ | 0 | Receive Data Register | 1 indicates data is ready in the data | | | full | register. | |-------+------------------------+---------------------------------------------| | 1 | Transmit Data Register | 1 indicates data has been transmitted and | | | empty | is ready for more data to send. | |-------+------------------------+---------------------------------------------| | 2 | Data Carrier Detect | 1 when carrier is NOT detected. When it | | | | goes high, generates an interrupt if | | | | Receive Interrupts are enabled. | |-------+------------------------+---------------------------------------------| | 3 | Clear to Send | 0 means a clear to send signal from a | | | | modem. | |-------+------------------------+---------------------------------------------| | 4 | Framing Error | 1 indicates a synchronization error. | |-------+------------------------+---------------------------------------------| | 5 | Receiver Overrun | 1 means a character was not read from the | | | | data register before the next character was | | | | recieved. | |-------+------------------------+---------------------------------------------| | 6 | Parity Error | 1 indicates that the parity bit does not | | | | match the number of 1's in the received | | | | character as specified by the configured | | | | odd or even parity. | |-------+------------------------+---------------------------------------------| | 7 | Interrupt Request | 1 when the interrupt request line is LOW. | -------------------------------------------------------------------------------- You can see that for more complex communication, like through a modem, polling the status register gives a lot of information and control that simple interrupts do not. # ASCII codes # With this fancy new technology, we can interact with the Altair. We can enter characters and have things happen based on what is entered. Let's start by swapping out the interrupt handler and instead of getting the entered character back out, print out the ASCII code in octal. ; Interrupt handler 070 IN 333 ; Read character 071 021Q 021 ; from address 021Q 072 LXI H 041 ; Set heap address for storing data 073 200Q 200 ; to 000200Q 074 000Q 000 075 MOV B A 107 ; Save character in B 076 ANI 346 ; Mask all but low octal digit 077 007Q 007 ; 00 000 111 100 XRI 356 ; Add 60Q to convert to ASCII 101 060Q 060 102 MOV M A 167 ; Save digit to heap 103 INX H 168 ; Increment heap pointer 104 MOV A B 170 ; Restore original character value 105 RRC 017 ; Shift off low octal digit 106 RRC 017 107 RRC 017 110 MOV B A 107 ; Save shifted character 111 ANI 346 ; Mask all but low octal digit 112 007Q 007 113 XRI 356 ; Add 60Q to convert to ASCII 114 060Q 060 115 MOV M A 167 ; Save digit to heap 116 MOV A B 170 ; Restore shifted character 117 RRC 017 ; Shift off another octal digit 120 RRC 017 121 RRC 017 122 ANI 346 ; Mask all but remaining octal bits 123 003Q 003 ; which this time is only 2 bits 124 XRI 356 ; Add 60Q to convert to ASCII 125 060Q 060 126 OUT 323 ; Write high digit out immediately 127 021Q 021 130 MOV A M 176 ; Read middle digit from heap 131 OUT 323 ; Write middle digit out 132 021Q 021 133 DCX H 053 ; Decrement heap pointer 134 MOV A M 176 ; Read last digit from heap 135 OUT 323 ; Write last digit out 136 021Q 137 MVI A 076 ; Write a new line 140 012Q 012 141 OUT 323 142 021Q 021 143 MVI A 076 ; Write a carriage return 144 015Q 015 145 OUT 323 146 021Q 021 147 EI 373 ; Enable interrupts 150 RET 311 ; Return to our NOP loop until the next character # Breaking this one down # This code does a few interesting things. We create a heap pointer to store data to memory. We have to do our own memory management. There is no OS to call malloc and no compiler to dynamically store to memory and give us the address back. I think a C compiler would store this data on the stack. I chose not to do that because stack operations on the 8080 work on 16 bit register pairs and I needed to store 24 bits. I had an extra byte. Or so I initially though. I realized later I didn't need to store the last byte, I could leave it in the accumulator and spit it right out the serial port. If I go back and rewrite this, I wouldn't use memory at all since I could fit those 2 bytes into the C and D registers. It may not be as efficient as it could have been but it's a chance to learn the ways in which memory can be managed. I have to be the compiler, assembler, and operating system in addition to the programmer. Next, read in the character from the 2SIO port's data address. This acknowledges the interrupt. Pick a heap pointer. Heap grows up, the stack grows down. When they meet, bad things happen, even in computers today. It works this way because when you call PUSH in the CPU, it automatically decrements the address pointed to before writing the data to memory and POP increments. It's just been hardwired since the beginning. So start the heap low but leave room in case I have to modify the program and it gets longer. Then we save the character as entered into the B register so we can use it later. Since we are converting the value to octal digits, in 8 bits, there will be 3 digits. Consisting of the low order 3 bits, the middle 3 bits, and the high 2 bits that remain. A bitwise AND with 007Q will zero out all but the low 3 bits which will remain unchanged. A numeric value is 60Q less than it's ASCII character so we just need to add 60Q to whatever the bits are to be able to print it. Instead of adding 60Q, I use XOR because I know the bits that 060Q will land in are zero. I thought bit operations might be quicker but the documented timing indicates there is no difference. Then we write that digit to the heap and increment the heap pointer. Do all the same steps for the next digit. When we get to the last digit, which is only 2 bits. We mask it with 003Q before XORing with 060Q and instead of saving to memory, we can just write it out to the serial port. We then read the next digit off the heap and write it, decrement the heap pointer, read and then write the last byte. And for readability, we follow the 3 octal digits with a new line and carriage return. Re-enable interrupts and return to wait for the next character. --------------------------------------------------------------------------- I'll stop here and continue next time with a string-aware version of echo and a couple of string implementations. [0] http://altairclone.com/downloads/interrupt_acknowledge.pdf [1] https://www.youtube.com/watch?v=l5CHGm1eOio [2] https://en.wikipedia.org/wiki/RS-232#RTS,_CTS,_and_RTR