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