I/O and Ports
Keyboards send bytes to the CPU, displays receive them and timers report hardware events.
The Z80 handles this through a separate I/O space. In the conventional programming model, devices use an 8-bit port number, giving 256 basic port numbers. The IN and OUT instructions transfer bytes between registers and peripherals over I/O bus cycles. The mapping of port numbers to devices belongs to the target hardware.
The I/O address space
The low byte of an I/O address is an 8-bit port number from 0 to 255. The CPU marks an I/O transaction separately from a memory transaction on its control bus.
The Z80 still drives all sixteen address pins during an I/O transaction. In the (C) forms, C supplies the low 8-bit port number and B appears on the upper address pins. In the immediate (N) forms, N supplies the port number and A appears on the upper pins. Most systems decode only the low eight bits and therefore expose 256 ports. Some hardware also decodes the upper byte. For example, the TEC-1G matrix keyboard uses B to select a row. This upper-byte behaviour is an electrical addressing detail layered on the normal 8-bit port model, so follow the target's hardware documentation when it is used.
The IN and OUT forms in this chapter perform individual byte transfers; the Z80 also has block I/O instructions for repeated transfers.
Writing to a port: OUT
OUT (N), A writes A to the 8-bit port number N:
LD A, $42 ; load value to send
OUT ($10), A ; write $42 to port $10The parentheses around $10 mark a port operand, not a memory address. The instruction encodes as two bytes: the OUT opcode and the port number. Only A can supply the data in the immediate form.
OUT (C), R writes register R to the port number in C. Any standard 8-bit register (B, C, D, E, H, L or A) can supply the data:
LD C, $10 ; 8-bit port number
LD D, $42 ; value to send
OUT (C), D ; write D to port $10
OUT (C), A ; write A to the same portReading from a port: IN
IN A, (N) reads a byte from port N into A:
IN A, ($10) ; read byte from port $10 into AThe immediate form requires A as the destination.
IN R, (C) reads from the port number in C into any standard 8-bit register:
LD C, $10 ; 8-bit port number
IN D, (C) ; read from port $10 into D
IN A, (C) ; read from the same port into AThe register-addressed IN R, (C) form sets flags:
- S is set if the byte read has bit 7 set.
- Z is set if the byte read is zero.
- P/V reflects the parity of the byte.
- H and N are reset.
- C (carry) is unaffected.
IN R, (C) sets flags; the immediate form IN A, (N) does not. When code must branch on a value read with the immediate form, a following OR A sets the flags explicitly before the conditional jump.
Polling a port in a loop
Polling repeatedly reads a status port until a condition is met, then accesses the data port.
STATPORT EQU $11
DATAPORT EQU $10
; READRDY: spin until device is ready, then return the byte read.
; Out: A = received byte
; Clobbers: F
READRDY:
.WAIT:
IN A, (STATPORT) ; read status into A
AND $01 ; test bit 0 (ready flag)
JR Z, .WAIT ; Z set means bit 0 was 0 - not ready yet; loop
IN A, (DATAPORT) ; bit 0 is 1 - device is ready; read data into A
RETAND $01 keeps only bit 0 and sets Z when that bit was 0. JR Z, .WAIT loops back while Z is set (bit 0 still clear).
Both reads use immediate low-byte addresses. These examples assume the target decodes only that low byte, as many small Z80 systems do.
Sending a block of bytes
A counted loop can send a sequence of bytes to a fixed port one at a time. HL points to the data and B holds the count:
OUT_PORT EQU $10
; SENDBLK: send B bytes from (HL) to OUT_PORT.
; In: HL = source address, B = byte count
; Precondition: B > 0
; Clobbers: A, B, HL
SENDBLK:
.SENDLOOP:
LD A, (HL) ; load byte at current address
OUT (OUT_PORT), A
INC HL ; advance source pointer
DJNZ .SENDLOOP ; decrement B; loop until B reaches 0
RETWorked example
; Port demonstration
; Demonstrates Z80 in/out instructions and port forms.
; Port numbers are abstract: inspect the Z80 output, not hardware behavior.
OUT_PORT EQU $10
IN_PORT EQU $11
STATPORT EQU $12
; SENDBYTE: write A to OUT_PORT
; In: A = byte to send
; Clobbers: nothing
SENDBYTE:
OUT (OUT_PORT), A ; immediate port form; A is the source
RET
; RECVBYTE: read IN_PORT into A
; Out: A = byte received
RECVBYTE:
IN A, (IN_PORT) ; immediate port form; reads into A only
RET
; echo_reg: write the byte in D using register-addressed form
; In: D = byte to send
; Clobbers: C
ECHO_REG:
LD C, OUT_PORT ; C holds the 8-bit port number
OUT (C), D ; D is the data source
RET
; POLLRECV: spin on STATPORT until bit 0 is set, then read IN_PORT
; Out: A = byte received
; Clobbers: F
POLLRECV:
.POLLLOOP:
IN A, (STATPORT) ; immediate form; flags unchanged
AND $01 ; test bit 0
JR Z, .POLLLOOP ; Z set: not ready; keep polling
IN A, (IN_PORT) ; ready: read data into A
RET
; SENDBLK: send B bytes from (HL) to OUT_PORT
; In: HL = source address, B = byte count
; Precondition: B > 0
; Clobbers: A, B, HL
SENDBLK:
.BLKLOOP:
LD A, (HL)
OUT (OUT_PORT), A
INC HL
DJNZ .BLKLOOP
RET
PAYLEN EQU 4
ORG $0000
MAIN:
; Demonstrate SENDBYTE
LD A, $AA
CALL SENDBYTE ; sends $AA to OUT_PORT
; Demonstrate RECVBYTE (reads from IN_PORT; result in A)
CALL RECVBYTE
; Demonstrate echo_reg
LD D, $55
CALL ECHO_REG ; sends $55 to OUT_PORT via register-addressed out
; Demonstrate SENDBLK
LD HL, PAYLOAD
LD B, PAYLEN
CALL SENDBLK
HALT
ORG $8000
PAYLOAD: DB $10, $20, $30, $40The key lines work as follows:
OUT (OUT_PORT), A is the immediate port form. OUT_PORT is defined as $10 with EQU; the assembler substitutes $10 at compile time.
IN A, (IN_PORT) reads from port $11 into A and leaves the flags as they were.
OUT (C), D: D supplies the data and C holds the port number.
IN A, (STATPORT) in POLLRECV uses the immediate form, so the flags still hold whatever the previous instruction left. AND $01 isolates bit 0 and sets Z before the branch.
SENDBLK is a DJNZ loop from Chapter 6 applied to output. B counts the bytes and HL steps through source memory. Using the immediate output form keeps B available as the loop counter.
Interrupts
Polling keeps the CPU busy checking the status port until the device is ready.
The Z80 also supports interrupts. A hardware interrupt suspends the current instruction stream, transfers control to a handler and later resumes the interrupted code. Handlers often use IN and OUT to communicate with the device that raised the interrupt.
Interrupts involve DI, EI, IM and RETI, along with stack and register preservation rules. Interrupt-driven code requires the Z80 interrupt-mode documentation for the target platform and its handler conventions.
Exercise
Flag behaviour of IN. The comparison should state whether each form updates Z and whether JR Z, IS_ZERO can follow directly:
IN A, (IN_PORT) ; form A
IN A, (C) ; form BThe shortest correct sequence for each form may assume C already contains IN_PORT for form B. Tests beginning with Z clear and carry set should use input bytes $00 and $80, recording A, Z and carry after each read and any explicit flag-setting instruction.