CPC I/O - The Expansion Port

Expansion port pinout diagram
The expansion port (viewed from the rear of the computer)

1 SOUND 18 A0 35 INT*
2 GND 19 D7 36 NMI*
3 A15 20 D6 37 BUSRQ*
4 A14 21 D5 38 BUSAK*
5 A13 22 D4 39 READY
6 A12 23 D3 40 BUS RESET*
7 A11 24 D2 41 RESET*
8 A10 25 D1 42 ROMEN*
9 A9 26 D0 43 ROMDIS
10 A8 27 +5V 44 RAMRD*
11 A7 28 MREQ* 45 RAMDIS
12 A6 29 M1* 46 CURSOR
13 A5 30 RFSH* 47 L PEN
14 A4 31 IORQ* 48 EXP*
15 A3 32 RD* 49 GND
16 A2 33 WR* 50 CLOCK
17 A1 34 HALT*

Using the expansion port is a bit more awkward than using the printer or joystick ports. This is because hardware on the expansion port needs a 16-bit numerical address, just like a memory address. This means that your hardware must include logic circuitry to decode the address lines (A0 to A15) so that it only acts on instructions that are intended for it. A lot of the possible addresses are already being used by the CPC's internal hardware, but there are ranges that are reserved for user peripherals, namely:

  • &F8E0 to &F8FE
  • &F9E0 to &F9FE
  • &FAE0 to &FAFE
  • &FBE0 to &FBFE

The addresses &F8FF, &F9FF, &FAFF, and &FBFF are used to reset peripherals on the expansion bus.

The CPC firmware guide (SOFT 968) has the full list of I/O addresses and the hardware that uses them.

Reading and writing data using the expansion port


The BASIC command for reading a byte from an I/O address is:

INP (&nnnn)

where &nnnn is the 16-bit I/O address to read from.

To send a byte to an I/O address, use:

OUT &nnnn,&xx

where &nnnn is the 16-bit I/O address, and &xx is the byte to send.

There's also the WAIT command, which will wait until the specified I/O address returns a particular value:

WAIT &nnnn,mask,inversion

&nnnn is a 16-bit I/O address. mask and inversion (optional parameter) are 8 bit values.

This command reads a byte from the I/O port specified as &nnnn, XORs the byte with inversion then ANDs it with mask. It loops until this operation returns a non-zero result.

Using Machine Code

The following opcodes are available for reading and writing to and from I/O ports:

IN A, (C)
IN B, (C)
IN C, (C)
IN D, (C)
IN E, (C)
IN H, (C)
IN L, (C)
IN A, &nnnn

OUT (C), A
OUT (C), B
OUT (C), C
OUT (C), D
OUT (C), E
OUT (C), H
OUT (C), L
OUT (C), &nnnn

Despite the (C) in the opcode, these operations actually use the full 16-bit value of the BC register.

The Z80 processor has block-copy instructions for I/O operations, equivalent to the LDIR instructions for copying memory. However, the design of the CPC's hardware means that these instructions don't work on the CPC.

The Z80 also has two undocumented I/O instructions. Most assemblers won't recognise the opcodes, so you'd probably have to insert them as hexadecimal values if you wanted to use them.

OpcodeHex bytes
IN F, (C)ED 70
OUT (C), 0ED 71

How the hardware responds to I/O commands

When you issue an IN/INP or OUT command (from BASIC or machine code), the following things happen:

  • The IORQ (IO ReQuest) line goes low. This distinguishes it from an attempt to access RAM, for which the MREQ line would go low.
  • WR goes low if it's a write request, RD goes low if it's a read request.
  • The value of the BC register gets put on the address bus.
  • For an OUT command, the value of the appropriate register gets put on the data bus.

So, to give an example, if you made a device that would ignore all IN or OUT commands except those to address &F8F0, you could send a byte to it by doing something like OUT &F8F0, &FF (or the machine code equivalent). The IORQ and WR lines would go low, the value &F8F0 would be put on the address bus, and the value &FF would be put on the data bus. The address decoding logic on your expansion device would recognise that this I/O operation was directed at it, and the device would then do whatever it was supposed to with the data on the data bus.

Back to: