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.

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), a writes a to port n.
  • in a, (n) reads port n into a.

There is a second form where the port number comes from a register:

  • out (c), r writes register r to the port whose number is in c.
  • in r, (c) reads that port into r, 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.

portwriting printsreading gives
0x00the byte as a characterthe next character of the input line
0x01the byte as an unsigned number, 0 to 255a line parsed as a decimal number
0x02the byte as a signed number, -128 to 127the same as 0x01
0x03the byte as two upper case hexadecimal digitsa line parsed as hexadecimal
0x04a 16 bit number, the high byte from the address busthe 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.

Show solution