Im Magazin „BYTE" wurde im Mai 1979 der folgende Artikel abgedruckt.
Dies ist ein gelungenes Beispiel zum Verarbeiten asynchroner Ereignisse in einem „gepollten" System wie CP/M. Die Einsatzmöglichkeit liegt z.B. im Bereich von MODEM-Programmen.
W D Maurer
University Library Bldg
Boom 634
George Washington University
Washington DC 20052

Simultaneous Input and Output for Your 8080

The process of I/O (input/output) in assembly language on a typical microcomputer system is rather crude. You input the status register and perform a logical AND with a mask consisting of one bit. If the result is not zero, you know the bit was on and the I/O device was therefore ready. In that case, you either input or output the data register, as appropriate. Otherwise, you loop back to input the status register again. On the 8080, it goes like this:
           Input

ILOOP:  IN      ISTAT
        ANI     IREADY
        JZ      ILOOP
        IN      IDATA

           Output

OLOOP:  IN      OSTAT
        ANI     OREADY
        JZ      OLOOP
        OUT     ODATA
where the quantities ISTAT, IDATA, OSTAT, ODATA, IREADY, and OREADY are what is called, in the world of big computers, "installation-dependent" (that is, they differ from one person's 8080 to another). The first four of these might be given by:
        ISTAT   EQU     3
        IDATA   EQU     2
        OSTAT   EQU     3
        ODATA   EQU     2
describing a single channel for both input and output involving two ports, with port numbers 3 and 2. The other two might be given as:
        IREADY  EQU     1
        OREADY  EQU     2
to denote that the rightmost bit of the status register is the input-ready flag and the second bit from the right in this register is the output-ready flag. (Your dealer must supply you with these values, or show you how to find what they are, when you buy your system.) You can also make these into subroutines by adding a return as follows:
INPUT:  IN      ISTAT
        ANI     IREADY
        JZ      INPUT
        IN      IDATA
        RET

OUTPUT: IN      OSTAT
        ANI	OREADY
        JZ      OUTPUT
        OUT     ODATA
        RET
This allows you to CALL INPUT to bring a newly input character into register A, or to CALL OUTPUT whenever you have a new character in register A that you want to put out.
The trouble with this kind of I/O is that it is not simultaneous. When you are doing input, that is all you are doing; when you are doing output, that is all you are doing. Meanwhile, your system is sitting uselessly in a loop, which it is performing several thousands of times, or sometimes (particularly in the case of input) several millions of times. What you need in order to increase the efficiency of your system, if you have 190 bytes of read only memory and 65 bytes of programmable memory to spare, is a simultaneous I/O package which allows you to do input, processing, and output, all at the same time.
The basic idea of simultaneous I/O is that of the queue. Any queue can be considered by analogy to a waiting line for a bus. (The story, told to this author second or third hand, is that in England people line up for buses in lines that look like spirals or, more informally, like the tail of a pig - a shape that is in turn called queue in French, presumably because it looks vaguely like the letter Q.) Consider the characters waiting for the bus as ASCII characters, rather than as local town characters, and consider the bus not as a bus in the technical sense, but (for output) as the actual output device - the teletypewriter video display terminal, Selectric terminal, or whatever. When your routine wants to output a character, this character goes on the end of the queue. It then has to wait for a while until the characters in front of it, which were entered earlier, get on the bus - that is, until they are actually output - before it can be output.
The analogy with the bus is not a perfect one, because a real bus, when it comes along, takes everybody waiting for it all at once. A waiting line in a supermarket at the checkout counter would be a better analogy, because characters, like shoppers, leave the queue one at a time, as well as entering it one at a time.
For input, there is another queue, but this time the input device feeds new characters onto the end of the waiting line, and they come off the front - that is, board the bus - when they are actually used by the program which is asking for input. Several characters might be typed before they were actually used by the program, presumably because it is doing something else, such as a long computation. For output, the use of the queue is more common, because programs typically produce output characters much faster than they can actually be put out; these characters enter the queue and are then output from it, one at a time, while the computer goes on to whatever it has to do next.
Before we discuss how a queue like this is actually implemented, let us digress a bit and answer one fundamental question: how are we to handle three programs going simultaneously - an input program, an output program, and something else which is reading input and writing output? There are two ways, one being the use of interrupts, the other making use of a technique called polling. We shall use polling, mainly because it does not require any special hardware (not all 8080 systems have a priority interrupt control unit) and also allows the user who might not have written his own monitor to use simultaneous I/O without interfering with any interrupt conventions which his monitor might have established.
Polling, in this case, assumes that the functions of watching the input device and the output device to see if they are ready, and taking appropriate action when they are ready, are subroutines of the user's program. We shall call them IPOLL and OPOLL. They are not to be confused with the ordinary I/O subroutines which supply input to the user's program and accept output from it; we shall call these IP and OP. To summarize the functions of our four routines:
  1. IP is called when the user's program wants an input character, and IP returns with that character in register A.
    IP:	PUSH	H	; SAVE HL REGISTER
    	LHLD	FIQ	; FRONT OF INPUT Q TO HL
    	LDA	EIQ	; END OF INPUT. Q (LO) TO A
    	CMP	L	; COMPARE FIQ(LO):EIQ(LO)
    	JNZ	IP3	; IF UNEQUAL, Q NONEMPTY
    IP2:	CALL	OPOLL	; Q EMPTY. TIGHT LOCP
    	CALL	IPOLL	; (KEEP POLLING I AND O)
    	JNC	IP2	; (UNTIL IN CHAR. RECEIVED)
    IP3:	MOV	A,M	; FIRST IN Q CHAR. TO A
    	PUSH	PSW	; SAVE THIS CHARACTER
    	INX	H	; UPDATE FRONT OF INPUT Q
    	MVI	A,TIQ	; WRAPAROUND TEST (COMPARE
    	CMP	L	;  FIQ(LO) AND TOP OF IN Q
    	JNZ	IP4	;  (LO) --  IF =, RESET TO
    	MVI	L,BIQ	;  BOTTOM OF IN Q (LO)
    IP4:	SHLD	FIQ	;  PUT FIQ BACK IN MEMORY
    	POP	PSW	; RESTORE INPUT CHARACTER
    	POP	H	; RESTORE HL REGISTER
    	RET		; OUT OF THIS ROUTINE
    
    Listing 1: Subroutine IP, written in 8080 assembler language and called when the user's program wants an input character. IP returns that character in the A register.

  2. OP is called when the user's program has a character to be output, and this character must be in register A when OP is called.
    OP:	PUSH	PSW	; SAVE A-REGISTER
    	PUSH	H	; SAVE  HL-REGISTER
    	LHLD	EOQ	; END OF OUTPUT Q
    	MOV	M,A	; PUT CHAR. ON END OF Q
    	INX	H	; UPDATE END OF OUTPUT Q
    	MVI	A,TOQ	; WRAPAROUND TEST (COMPARE
    	CMP	L	;  EOQ(LO) AND TOP OF OUT Q
    	JNZ	OP2	;  (LO) -- IF =, RESET TO
    	MVI	L,BOQ	;  BOTTOM OF OUT Q (LO))
    OP2:	LDA	FOQ	; FRONT OF OUTPUT Q (LO)
    	CMP	L	;  TO A -- IF = EOQ (LO)
    	JNZ	OP4	;  AFTER INCR., Q FULL
    OP3:	CALL	IPOLL	; Q FULL. TIGHT LOOP
    	CALL	OPOLL	; (KEEP POLLING I AND O)
    	JNC	OP3	; (UNTIL SMALLER OUT Q)
    OP4:	SHLD	EOQ	; PUT EOQ BACK IN MEMORT
    	CALL	OPOLL	; MAKE SURE OPOLL AND IPOLL
    	CALL	IPOLL	;  ARE CALLED AT LEAST ONCE
    	POP	H	; RESTORE HL-REGISTER
    	POP	PSW	; RESTORE A-REGISTER
    	RET		; OUT OF THIS ROUTINE
    
    Listing 2: Subroutine OP, called when the user's program has a character to be output. This character must be in the A register when OP is called.

  3. IPOLL is called every so often (in a sense to be described more precisely below) to check whether the user has keyed in a new character that has to be placed on the end of the input queue.
    IPOLL:	IN	ISTAT	; GET STATUS BITS (IN)
    	ANI	IREADY	; READY BIT ZERO MEANS
    	RZ		;  NOTHING TYPED - OUT
    	PUSH	H	; SOMETHING TYPED - SAVE
    	IN	IDATA	;  HL REG. AND INPUT IT
    	LHLD	EIQ	; END OF INPUT Q TO HL
    	MOV	M,A	; PUT CHAR. ON END OF Q
    	INX	H	; UPDATE END OF INPUT Q
    	MVI	A,TIQ	; WRAPAROUND TEST (COMPARE
    	CMP	L	;  EIQ(LO) AND TOP OF IN Q
    	JNZ	IPOLL2	;  (LO) -- IF  =, RESET TO
    	MVI	L,BIQ	;  BOTTOM OF IN Q (LO))
    IPOLL2:	LDA	FIQ	; FRONT OF INPUT Q (LO)
    	SUB	L	;  TO A -- IF = EIQ (LO)
    	JZ	IPOLL3	;  AFTER INCR., Q FULL
    	SHLD	EIQ	; NOT FULL. RESTORE EIQ
    IPOLL3:	JNC	IPOLL4	; IF FIQ-EIQ IS NEGATIVE,
    	ADI	LIQ	;  ADD SIZE OF INPUT Q
    IPOLL4:	CPI	IFUDGE	; TEST IN Q WITHIN FUDGE
    	JNC	IPOLL7	;  FACTOR (7) OF BEING
    	LXI	H,IAC	;  FULL. IF SO, BUMP INPUT
    	INR	M	;  ALARM COUNTER BY 1
    IPOLL7:	POP	H	; RESTORE HL REGISTER
    	STC		; SET CARRY (CHAR. THERE)
    	RET		; OUT OF THIS ROUTINE
    
    Listing 3: Subroutine IPOLL, called periodically to check whether the user has keyed in a new character that has to be placed at the end of the input queue.

  4. OPOLL is called every so often to check whether the output device has completed its processing of the previous character to be output; if it has, the next one is sent out.
    OPOLL:	IN	OSTAT	; GET STATUS BITS (OUT)
    	ANI	OREADY	; READY BIT ZERO MEANS
    	RZ		;  PORT STILL BUSY - OUT
    	LDA	IAC	; GET INPUT ALARM COUNTER
    	DCR	A	; AND DECREASE IT BY 1
    	JM	OPOLL1	; IF WAS ZERO, NO ALARM
    	STA	IAC	; STORE DECREASED VALUE
    	MVI	A,CTRLG	; CONTROL-G (BELL) TO A
    	OUT	ODATA	; OUTPUT (TYPING TOO FAST,
    	RET		;  ALARM AND EXIT
    OPOLL1:	PUSH	H	; SAVE HL REGISTER
    	LHLD	FOQ	; FRONT OF OUTPUT Q TO HL
    	LDA	EOQ	; END OF OUT Q (LO) TO A
    	CMP	L	; COMPARE FOQ (LO):EOQ (LO)
    	JZ	OPOLL7	; IF EQUAL, NOTHING IN Q
    	MOV	A,M	; GET FIRST THING IN Q
    	OUT	ODATA	;  AND PUT IT OUT
    	INX	H	; UPDATE FRONT OF OUTPUT Q
    	MVI	A,TOQ	; WRAPAROUND TEST (COMPARE
    	CMP	L	;  FOQ (LO) AND TOP OF OUT
    	JNZ	OPOLL5	;  Q(LO) -- IF  =, RESET TO
    	MVI	L,BOQ	;  BOTTOM OF OUT Q (LO))
    OPOLL5:	SHLD	FOQ	; PUT FOQ BACK IN MEMORY
    OPOLL7:	POP	H	; RESTORE HL REGISTER
    	STC		; SET CARRY (WORK DONE)
    	RET		; OUT OF THIS ROUTINE
    
    Listing 4: Subroutine OPOLL, called periodlcally to check whether the output device has completed its processing of the previous character to be output. If it has, the next character is sent out.
IPOLL and OPOLL are called both from IP and OP and from the user's program. When they are called from IP and OP, they employ an additional feature, not discussed above. IPOLL returns with the carry set if a new character is placed on the input queue, and clear otherwise. OPOLL returns with the carry set if a new character was removed from the output queue and put out, and clear otherwise. This information is used by IP and OP, but it is not needed by the user program. In fact, for the user program, there is no need to distinguish between the functions of calling IPOLL and calling OPOLL. It is enough to have a single subroutine, POLL, whose only function is to call IPOLL and OPOLL and then return; the subroutine POLL can then be called by the user program.
How often must the user program call the subroutine POLL? The answer is that the user program must be so organized that there is never a significant amount of real time during which POLL is not called. (How to ensure this will be described below.) The reason, of course, is that if this is not so, we could have the bad luck to push an input key during such a period of real time, and then, since POLL was not called, that input character will never be placed on the input queue and will therefore never be seen by the user's program. (Remember Murphy's law: if anything can go wrong, it will.)
On output, the situation is not that bad, but if there were a significant amount of time during which POLL was not called, the output device would effectively be stopped during that period of time. If this were a recurrent phenomenon, you would see the output device starting and stopping in jerks, like a car that loses power.
The easiest way to call POLL often enough from the user's program is to call POLL once in every loop and at least once in every subroutine. (If there is a subroutine call instruction in a loop, we do not need to call POLL explicitly in that loop, since POLL will be called by the called subroutine.) Or, for a more explicitly stated method, call POLL just before every return instruction and at every labeled instruction to which there is a backward jump. (That is, if the label is ALPHA, then somewhere later in the program there must be a jump to ALPHA.) This insures that POLL will be called often enough. [In a system with a real time dock, calling POLL from the interrupt handler for the clock every few milliseconds will accomplish the same end.... CH]
We now discuss the way in which we implement a queue in memory, namely as a "wraparound array." We start with an array IQ (input queue) of characters, together with two 16 bit pointers, or variables whose values are addresses, called FIQ (front of input queue) and EIQ (end of input queue). Figure 1 shows a typical configuration of the input queue.

Figure 1: The "wrap-around" queue. The queue is a method for storing data in the form of a list: the first item into the list becomes the first out of the list, in the same manner as a waiting line of people at a supermarket checkout counter. Figure 1a shows the data for an input queue in memory with two pointers, FIQ (front of input queue) and EIQ (end of input queue). When an item is added to the end of the queue, EIQ is incremented by 1. In removing an item from the queue, FIQ is incremented by one. Note that the queue is "upside-down" here; that is, the end of the queue is on top. When the top of the array in memory is reached, EIQ is altered so it points to the bottom of the array, thus "wrapping" the queue around the array as in figure 1b. Notice also that pointer EIQ points to the location that is one beyond the end of the queue. This enables the program to detect an empty or full array when EIQ = FIQ.

The shaded area shows the characters that are actually in the queue; the unshaded area shows the rest of the array in memory. To take a character off the front of the queue, assuming that FIQ is in register pair HL (which we can bring about by doing LHLD FIQ), we get the character to which FIQ points (by doing MOV A,M) and then increase FIQ by one (by doing INX H). To put a character on the end of the queue, assuming that EIQ is in the HL register pair (by means of LHLD EIQ), we move it to memory at the place where EIQ points (by doing MOV M,A - assuming that the new character is in the A register) and then increase EIQ by one (by doing INX H). Note that, in a sense, the queue is "upside-down" - the end of the queue is on top. If it were "right-side-up" we would have to decrease FIQ and EIQ by one in the above processes (by doing DCX H), rather than increasing them by one. Of course, after either decreasing or increasing, we must put FIQ (or, respectively, EIQ) back in memory (by doing SHLD FIQ or SHLD EIQ).
Of course, we cannot keep increasing FIQ and EIQ forever. Eventually, in figure 1a, EIQ will get to the top of the array in memory. When this happens, we alter it to point to the bottom of this array (this is the "wraparound" feature). After a while, the situation looks like figure 1b. Here again, the shaded area represents the characters actually in the queue. The first one is where FIQ points, the next one is right above that and so on up to the top of the array; then we start at the bottom of the array, and so on up to where EIQ points. We are treating the array as if it were cyclical, and, in fact, on big computers, this setup is often known as a "circular array" or a "ring buffer."
We note that FIQ points to the first character in the queue, but EIQ does not point to the last character in the queue - it points to the position one beyond the last character. To see why this is so, suppose the queue has exactly one character in it. We do not want FIQ and EIQ to be the same, because we want that to happen only when the queue is empty - when there are no characters in it - or else when it is entirely full (since these are the two cases in which special action has to be taken). By adopting the convention illustrated in figure 1, both of these conditions can be sensed by testing for FIQ = EIQ. Of course, the entire setup of figure 1 has to be duplicated for the output queue OQ and its two associated pointers FOQ and EOQ.
Let us make the simplifying assumption that each queue is entirely within one 256 byte page (from hexadecimal addresses xx00 through xxFF for some hexadecimal value of xx). This means that we can compare register pair HL with the address of the top of a queue by simply comparing register L with the low-order eight bits of this address. On equality, we set register L only (register H does not change) to the low-order eight bits of the address of the bottom of the queue. Here the top and the bottom refer to the array in memory, and are distinct from the front and the end as discussed above.
What happens when our queues get full? First of all, let us discuss how big we want the queues to be. The two queues and the four addresses FIQ, EIQ, FOQ, and EOQ must of course be in programmable memory, while the four routines IP, OP, IPOLL, and OPOLL can be in read only memory. So to a certain extent it depends on how much programmable memory is available in your system. An input queue of n characters allows you to type n characters ahead of where the program is at any given moment; an output queue of n characters allows your program to put out n more characters than have actually been output yet by the output device at any given moment. While the device is outputting these n characters, your system can be doing something else simultaneously. There is no reason for the input and the output queues to be the same size, and in a typical application you might be using 10 characters in the input queue and 55 characters in the output queue. A bit of experimentation here will satisfy you as to what is comfortable for your application.
When the output queue gets full, it means that the capacity of the queue for temporarily saving output characters has been used up. In that case we simply go back to what we used to do before we had simultaneous I/O - that is, wait for a character to be actually put out before we do anything else. Whenever the user's program puts a new character into the output queue, we perform our incrementation, as discussed above, and then check to see if the output queue is full (FOQ = EOQ). In that case, we go into a loop, calling IPOLL and OPOLL until OPOLL returns with the carry set. This indicates that OPOLL sensed output ready and put out a character - an operation that reduces the size of the output queue. The result is that, when we enter the output routine OP, the output queue will never be full, and, if FOQ = EOQ, we know that the output queue is not full but empty.
When the input queue becomes full, we are typing too fast. Any further characters which we type will not be read by the user's program. The only thing we can do in this case is to give the user a warning that this has happened, so that he will retype the characters involved. Fortunately we can do this easily, with most output devices, by putting out a control-G (hexadecimal 07, or on some output devices 87) which will either ring a bell or put out a high-pitched beep. A variation on this system, which we use, involves putting out the control-G when the output queue is almost full (let us say seven or fewer spaces remaining) so that the last few characters do not have to be retyped; the user simply stops typing for awhile and waits for a decent interval.
A minor technical point: We cannot sound the bell simply by calling OP. Recall that calling OP simply puts a character on the output queue; it may be a second or longer before that character is actually put out. When we type a character that has to be retyped, however, we need an immediate indication of this fact. We therefore use a single-byte input alarm counter IAC which is normally zero. To specify a bell, as above, we simply increment IAC by one, and then OPOLL checks IAC before it does anything else (if the output device is ready) and outputs a bell if IAC does not equal 0, decrementing IAC by one as it does so.
The complete code for IP, OP, IPOLL, and OPOLL is given in listings 1 through 4, with the data definitions given in listing 5 and the initialization given in listing 6.

FIQ:	DS	2		; FRONT OF INPUT Q (2 BYTES)
EIQ:	DS	2		; END OF INPUT Q (2 BYTES)
FOQ:	DS	2		; FRONT OF OUTPUT Q (2 BYTES)
EOQ:	DS	2		; END OF OUTPUT Q (2 BYTES)
IAC:	DS	1		; INPUT ALARM COUNTER (1 BYTE)
LIQ	EQU	36		; LENGTH OF INPUT Q
LOQ	EQU	36		; LENGTH OF OUTPUT Q
IQ:	DS	LIQ		; INPUT Q (SINGLE PAGE)
OQ:	DS	LOQ		; OUTPUT Q (SINGLE PAGE)
BIQ	EQU	IQ MOD 256	; BOTTOM OF INPUT Q (LO)
BOQ	EQU	OQ MOD 256	; BOTTOM OF OUTPUT Q (LO)
TIQ	EQU	BIQ+LIQ		; TOP OF INPUT Q (LO)
TOQ	EQU	BOQ+LOQ		; TOP OF OUTPUT Q (LO)
ISTAT	EQU	3		; INPUT STATUS PORT
OSTAT	EQU	3		; OUTPUT STATUS PORT
IDATA	EQU	2		; INPUT DATA PORT
ODATA	EQU	2		; OUTPUT DATA PORT
IREADY	EQU	2		; MASK FOR INPUT READY
OREADY	EQU	1		; MASK FOR OUTPUT READY
CTRLG	EQU	7		; CONTROL-G (SOMETIMES 87H)
IFUDGE	EQU	7		; INPUT FUDGE FACTOR
Listing 5: Suggested data definitions.

INIT:	LXI	H,IQ	; BOTTOM OF INPUT Q IS
	SHLD	FIQ	;  INITIAL VALUE OF FRONT
	SHLD	EIQ	;  AND END OF INPUT Q
	LXI	H,OQ	; BOTTOM OF OUTPUT Q IS
	SHLD	FOQ	;  INITIAL VALUE OF FRONT
	SHLD	EOQ	;  AND END OF OUTPUT Q
	XRA	A	; ZERO IS INITIAL VALUE
	STA	IAC	;  OF INPUT ALARM COUNTER
Listing 6: Initialization of the system.

To summarize the steps needed in order to use the system:
  1. Include in your program (kept in either read only memory or programmable memory) the subroutines given in listings 1, 2, 3, and 4.
  2. Include as part of the initialization of your main program the initialization steps given in listing 6.
  3. Include as part of your data (kept in programmable memory) the data definitions of listing 5.
  4. In your program, whenever you need an input character, write CALL IP to put a new character into the A register; whenever you have a character to put out, put it in the A register and then CALL OP.
  5. Have a subroutine POLL in your program, as follows:
    POLL:	PUSH	PSW
    	CALL	IPOLL
    	CALL	OPOLL
    	POP	PSW
    	RET
    
    and have your program call POLL once in each loop and just before each subroutine return.
As soon as you have gotten this much working, it will be possible for you to tinker with this system a bit further. Some suggested ways of doing this are as follows:
  1. The sizes of the input and output queues can be altered. Make sure to alter the entire data structure of listing 5 to insure that all the routines of listings 1, 2, 3 and 4 operate on the same version of the data structure.
  2. There is a section of code in IP that almost duplicates a similar section of code in IPOLL. With a little ingenuity, this can be made into a subroutine called by both IP and IPOLL. (Hint: the first instruction is INX H, and JNZ can be replaced by RNZ.) The same thing happens with OP and OPOLL.
  3. The input alarm logic can be further changed. For example, two kinds of alarms could be given: a single bell when the input queue is almost full, and a long string of bells (say, ten of them) when the queue is actually full.

Eingescanned von Werner Cirsovius
September 2002
© BYTE Publications Inc.