Ports: in and out
The Z80 has no system call instruction. It has a second address space of 256 ports reached with in and out, and this editor puts one console port per output format in it. Printing, reading and what an unconnected port answers.
The overview of this topic is in Assembly basics. The same topic in M68K, MIPS, RISC-V.
Every program so far left its answer in a register or in memory. To print a line or read what you
typed, an M68K program runs trap #15, a MIPS program runs syscall and a RISC-V program runs
ecall. The Z80 has no such instruction. There is nothing in the instruction set that means "ask
the environment for something", and there never was.
What it has instead is a second address space.
The I/O address space
Alongside the 64 KB of memory the Z80 has 256 I/O ports, numbered 0x00 to 0xFF, and they are
not memory: no ld reaches them, no address in the 64 KB overlaps with them, and the CPU raises a
different signal on the bus when it talks to one. Two instructions reach them and nothing else does.
out (n), awritesato portn.in a, (n)reads portnintoa.
There is a second form where the port number comes from a register:
out (c), rwrites registerrto the port whose number is inc.in r, (c)reads that port intor, and any 8 bit register can be the destination.
The number in the parentheses is the low byte of the address bus. The high byte is a in the
first form and b in the second, and this editor uses that: a port that needs a parameter takes
it in b, and one port takes a whole 16 bit value that way.
That is port-mapped I/O, the sibling of memory-mapped I/O from the general course, and it is what x86 uses too. Which device sits behind which number is decided by whoever built the machine, and this editor's map is what the rest of this module is about.
Why ports and not a system call convention
Real Z80 machines did have printing routines, in ROM, and every machine had a different one at a
different address: the ZX Spectrum's is not the Amstrad's and neither is CP/M's. Inventing one here
would mean loading a ROM into the 64 KB, which would then collide with wherever your .org put your
program. Ports cost no memory, and in and out belong to the CPU itself, so the whole convention
lives outside your address space. That is the reasoning written down in
ADR 0002.
The console ports
Five ports carry the console, and there is one port per output format, because an out carries
exactly one byte of payload and the port number is the only other thing the instruction encodes.
| port | writing prints | reading gives |
|---|---|---|
0x00 | the byte as a character | the next character of the input line |
0x01 | the byte as an unsigned number, 0 to 255 | a line parsed as a decimal number |
0x02 | the byte as a signed number, -128 to 127 | the same as 0x01 |
0x03 | the byte as two upper case hexadecimal digits | a line parsed as hexadecimal |
0x04 | a 16 bit number, the high byte from the address bus | the same as 0x01 |
The whole table, with a runnable example for every port, is on the Z80 I/O documentation page.
Printing a string
A string is bytes and the character port takes one byte, so printing is the string loop from the
"Arrays, strings and ix" lecture with an out in the middle.
The console panel below the editor shows Hello, world! and a newline. The 10 at the end of the
.asciz line is that newline written as its character code, and the terminating zero goes after it,
so the loop prints the 10 and then stops.
There is no "print a string" port, because a port carries one byte. Every string this machine prints is printed a character at a time by a loop you wrote.
Printing numbers
The same byte written to four different ports prints four different things.
The console reads 200 -56 C8 1000. One byte, three ports, three answers, and choosing the port is
choosing how the bits are read, which is the same choice the numbers lecture made about C and
P/V.
Try changing the three ld a, 200 to ld a, 100 and the second one prints 100 as well, because
100 has its top bit clear and reads the same either way.
The last four instructions are port 0x04, which is where the high byte of the address bus earns its
keep. In the out (c), r form the address bus carries b on the high half, so b and the byte
written are the two halves of one 16 bit number, and three instructions and one out print anything
up to 65535. It is the only port that reads the high byte of the address.
Reading
in a, (1) asks for a whole line, parses it as a decimal number and gives back its low byte. When no
input is waiting the program stops inside the in and waits: press Run, type a number in the box
under the console, press Enter, and the run carries on inside that one instruction.
Press Run and type 10, Enter, 32, Enter. The console shows 42.
A line that is not a number stops the program with an error, and a number over 255 comes back as its
low byte, because a is one byte.
The character port reads differently. It hands back the input line one byte at a time, and the
line ends with a newline character, 0x0A, so a program reads until it sees one:
Type hi there and press Enter: the console shows 8, the eight characters before the newline.
Ports nobody is behind
Only the numbers this editor maps do anything. Every other port is an empty bus, which is exactly
what a real Z80 sees when no device answers: a write goes nowhere and a read comes back FF, because
that is what an undriven bus reads as.
The console reads FF. So a program written for a real machine will run here with its unsupported
I/O quietly doing nothing, instead of stopping with an error.
Ending a program
The Z80's four endings from the first lecture are still the only ones: halt, a top level ret,
ei and halt, or running off the end of the code. There is no "terminate" port and no task 9 to
call, because ending a program is not something a device does.
Your turn
Print The answer is 42 with no newline after it. The string is written for you at 0x9000, and the
number is not part of it: print the string a character at a time, then the 42 with the unsigned
number port.
Show solution
The second one reads a number and prints it back in hexadecimal, with nothing else in the output. The
test types 255, so the console reads FF.