email sol follow sol rss feed of the blog wishlist Sol::Speccy

Speccy, Vol 6: Some Little Details

January 23rd, 2016

Over the new year's I decided that I wouldn't try to make "the best thing ever" for speccy and instead would make and release smaller things. To get things rolling, I decided to create a simple shoot-em-up, and while doing that I covered a couple of little details my earlier experiments had not, so let's look at these.

First bit is interrupts. The z80 has three interrupt modes; IM 1, which is on by default, hops into fixed address in ROM, IM 0 executes opcode from data bus (which may be whatever) rendering it completely useless, and IM 2 hops to an address in a lookup table indexed by a byte from the data bus (which may be whatever), which is awkward but not completely useless.

We could leave the interrupt to point at ROM, but since it does things like scanning the keyboard which takes variable amounts of time depending on what keys are pressed, and since we'd rather not have the ROM poking at RAM addresses, it's better to turn it off.

We could just disable interrupts and leave them disabled, but then we'd lose frame sync which isn't so nice. Okay, we could count instruction clock-cycles for the whole frame, but that would be insane.

So we're down to using the third interrupt mode.

The way the third mode works is that you create a table of addresses where the interrupt should jump, set one register to say where the table is, and then change the interrupt mode. Whenever the interrupt happens (near beginning of vertical retrace, or 50Hz), the z80 reads a byte from the bus, uses that as index to the table, and hops there. Another slight problem is that the addresses are 16 bit, but the index may be odd or even, meaning that in practise your interrupt address must be of form 0xXYXY - both bytes being the same.

The zx spectrum 48k ROM has a large chunk of 0xff:s in it. I presume they didn't need the whole 16k of ROM, and the unused bit was left to 0xff. Whatever the reason, there's a fun little trick we can do on the 48k speccy.


        ld a, #0x39 ; 0x39xx is a rom address with lots of ff's in 48k speccy
        ld i, a     ; set i
        ld hl, #0xffff
        ld (hl), #0xc9 ; 'ret'. Should be reti, but that's 2 bytes, and we aint got room for that.
        im 2 ; interrupt mode 2, which should now hop to 0xffff which has our ret instruction.

First we set the interrupt jump table address to point at the large chunk of 0xff:s, causing the interrupt to always jump to the address 0xffff. Then we poke the opcode for 'ret' to 0xffff, and finally we set the interrupt mode to 2. After this, whenever the interrupt happens, it just does 'ret'.

What we should be using is 'reti'. Unfortunately, opcode for 'reti' is 0xed4d, so it won't fit in our single byte. Another sneaky thing that could be done is relying on the fact that the memory loops, and put a partial opcode at the 0xffff address and use the first byte of ROM as the second part. If the resulting opcode is a relative jump backwards, we can then put whatever we'd want there.

The difference between 'ret' and 'reti' opcodes is that 'reti' is also decoded by a bunch of z80 peripheral chips, but that doesn't seem to matter on the zx spectrum, so we're fine.

Now that the interrupt handling is done, the next bit that was missing was user input.

If you look at how the speccy keyboard works, there's a 5-wire connector and a 8-wire connector to the keyboard membrane, and there are exactly 40 keys on the keyboard, split into eight 5-key groups.

These wires are connected to the port memory space on the z80, meaning we have eight i/o ports from which to read, and the bottom 5 bits of each of those ports represents a key on the keyboard. So, to read the whole keyboard state, we need to read from 8 ports.

If a key is down, its bit is zero. If a key is up, the bit is one.

PortBit0Bit1Bit2Bit3Bit4
0xFEFEShiftZXCV
0xFDFEASDFG
0xFBFEQWERT
0xF7FE12345
0xEFFE09876
0xDFFEPOIUY
0xBFFEEnterLKJH
0x7FFESpaceSymMNB

Note that the bottom 8 bits of the port address is 0xFE, or 254 - yes, the same port where we write audio and border color.. everything is basically hooked to the same i/o port in the spectrum.

I wrote a simple inline assembly function that reads all those ports and puts the data into a byte array, and then through some C preprocessor trickery my keyboard use boils down to:


    if (KEYDOWN(SPACE)) fire();

But wouldn't it be nice to be able to control a game with a joystick instead? Unfortunately, because spectrum didn't come with a built in joystick port, there are at least four joystick standards for the spectrum. Two of these are the most prominent: the "interface-2" joystick and the "Kempston" joystick.

The interface-2 joystick was Sinclair's definition of the joystick port. Its input maps to keyboard ports directly (more specifically the numeric keys), so it's relatively easy to support via the keyboard code. The joysticks themselves used the same connector as the very widely used "Atari" standard, but the pins are wired differently because Sinclair wanted some vendor lock-in. Sigh. Anyway, the later "big" model speccys also have these ports built in.

If I ever get to fixing the 128k speccy I bought off ebay, I'll have to make a atari-speccy joystick port converter.

The Kempston interface uses the standard Atari pinout, but doesn't map to the keyboard. Instead, the Kempston state is read from port 0x1f, again five bits for left/right/up/down/fire. To support Kempston, I simply added another byte to read in my keyboard update function, and reading the joystick is just a matter of, for example, checking if "KEMPL" key is "down".

Since reading from random unconnected ports may result in any kind of result, one can't simply just assume that there's a joystick there and read all of them (and some incompatible joystick standards actually overlap, if a completionist wants to support everything out there), so the game needs to prompt for input preference from the user. A slight bummer, but users are totally used to that.

Site design & Copyright © 2017 Jari Komppa
Possibly modified around: June 23 2016