This is not my original work it has been copied from the following address because facebook would not let me share the original URL
About the Author
Adrian Brown has been programming computers since the early 80’s. he has built up a good knowledge of many languages over the years, ranging from Z80/6502 all the way through to higher level languages such as C/C++/C# and beyond
His blog and the original article.
What are interrupts?
Interrupts, as the name suggests, are when the normal running of a computer program are interrupted so something else can happen. On modern computers there can be various interrupts, but on the ZX Spectrum there were very few, most times you will hear of only 3 different interrupt modes 0, 1 and 2. While the main focus of this document is interrupt mode 2, we will quickly cover modes 0 and 1.These three interrupts are all maskable, that is to say we can stop the effect of them by using the DI (Disable interrupts) instruction. If we want them to start happening again we use the EI (Enable interrupts) instruction. For completeness I will point out that when we use the DI instruction, the interrupts still trigger, that is the Z80 is still aware of them, it just ignores them and carries on doing what it was doing anyway.
Interrupt Mode 0
This interrupt mode is not really used. The reason for this is that it is triggered by external hardware. The devices must place instructions (such as RST or CALL) onto the data bus when it sets the interrupt request on the Z80. It is this interrupt mode however that is running when the spectrum first powered up, luckily the Spectrum ROM soon setups up Interrupt Mode 1.
Interrupt Mode 1
This interrupt is very similar to IM2 (Interrupt Mode 2) in that it is triggered by the vertical blanking of the screen refresh that happens roughly 50 times a second. The vertical blank is the time when the beam that draws the screen has reached the bottom and is turned off while it moves back to the top, ready to draw the next frame. Mode 1 is the standard interrupt used by the Spectrum while running basic and, unless you alter it, while your programs are running.
When the vertical blank occurs the current contents of the program counter (PC) are pushed onto the stack (SP). The address $0038 is then loaded into PC and the Spectrum will start running the program stored at this location. As you may notice by the address, this is in the ROM and as such we have no way of altering what this does. The ROM routine mainly scans the keyboard and stores details of what is being pressed.
Interrupt Mode 2
As mentioned IM2 is very similar to IM1 above except it is known as a vectored interrupt. Like IM1 it is triggered roughly 50 times a second on the vertical blank, it pushes the current PC onto the stack but then rather than jumping to a specific address it uses a vector table to find out what value to load into PC. There is a lot of confusion over the exact operation of this vector table, and as different emulators deal with it in different ways, it is best to go with the method that works for all.
The vector table is simple a list of memory addresses, 128 of them in total. The table sits on a 256 byte boundary and the high byte of its address is loaded into the special I register on the Z80. The confusion seems to occur with what fills in the bottom 8 bits. The value comes from the BUS and general feeling is (and as such the safest rule to follow) that any value can occur. Some documentation such as ZAKS (Programming the Z80 book) will say that only the higher 7 bits will be used from the BUS, the least significant bit will always be 0. While to me this seems the most logical given the vector table, as mentioned not all emulators follow this rule and as such I would strongly advise against assuming this bottom bit is 0. Why does it make a difference you may ask? To answer that lets look at what happens when the interrupt is triggered.
When the interrupt is triggered, and they are enabled, the contents of the PC are pushed onto the stack. The contents of the I register are loaded into the upper 8 bits of the address bus, the BUS supplies the lower 8 bits, as we are not using any devices these bits can be any value. . Given this address ((I * 256) + BUS bits) a two byte address is read and loaded into the PC. The Z80 will then run the code stored at this address as per normal. First lets assume that the I register is set to $40 and that the bottom bit is always 0. When the interrupt is triggered the memory address will we calculated as $40xx where xx is any even 8 but value (0, 2, 4… 254). We can simply store the address of our interrupt routine at all 128 locations $4000, $4002, $4004 …… $40fe. This way when the interrupt occurs the same routine address will be loaded into the program counter no matter what the BUS value is. The code to set this up would be something like
ORG $8000 ; Setup the 128 entry vector table di ld hl, VectorTable ld de, IM2Routine ld b, 128 ; Setup the I register (the high byte of the table) ld a, h ld i, a ; Loop to set all 128 entries in the table _Setup: ld (hl), e inc hl ld (hl), d inc hl djnz _Setup ; Setup IM2 mode im 2 ei ret ; Basically nothing IM2Routine: ei reti ; Make sure this is on a 256 byte boundary ORG $F000 VectorTable: defs 256
Hopefully the code shows that the vector table stores the 128 entries pointing to the routine IM2Routine. When the interrupt is triggered the byte address at (I * 256) + Buswill be read along with the following byte to form the address. The routine at this address will be called, in our example no matter what the Bus is it will always call IM2Routine. But what happens if we don’t assume bit 0 will always be 0?In our example above IM2Routine will have the address $8016, so the first eight bytes of our vector table will be
defb $16, $80, $16, $80, $16, $80, $16, $80
The rest of the table will follow the same pattern. When bit 0 was always 0 the low byte would always be pulled out as $16 and the following high byte of the address would be $80 (assuming Bus was 0 the low byte would be pulled out from the start of the table and the high byte would be the next byte), the next option would be Bus was 2 which would give the same result just reading from 2 bytes into the table. If however bit 0 was not always 0 we could end up reading starting from byte 1 in the table giving our low byte of $80 and our high byte of $16. This gives us the address $1680 which certainly isn’t where we want the Z80 to jump to.
Given the reasons behind how the vectored interrupt is designed I would speculate that the hardware does indeed always reset bit 0 to 0 however as mentioned not all emulators follow this rule (The hardware ive tested this on does mind you). To be safe its best to work on the theory it can do a read from any memory address. So, how do you deal with that? Simply make sure all the bytes in the table are the same value.
One other thing to note with the issue of reading from any byte is what happens if the Bus value is 255 when the interrupt triggers. This will be used as the low end byte of the address on the vector table, but as we need to read two bytes it will read the last entry in the 256 byte table as well as the byte following it. To allow for this we must use a 257 byte table when allowing for any Bus value. So with that in mind lets take a look at the code to setup the table.
ORG $8000 ; Setup the 128 entry vector table di ld hl, IM2Table ld de, IM2Table+1 ld bc, 256 ; Setup the I register (the high byte of the table) ld a, h ld i, a ; Set the first entries in the table to $FC ld a, $FC ld (hl), a ; Copy to all the remaining 256 bytes ldir ; Setup IM2 mode im 2 ei ret ; This routine now needs to be at a specific address (remember we only have from $FCFC to $FE00 else we will overwrite our table) ORG $FCFC ; Basically nothing IM2Routine: ei reti ; Make sure this is on a 256 byte boundary ORG $FE00 IM2Table: defs 257
Why do we need an interrupt routine?
Having looked at how we setup an interrupt routine, you may find yourself asking why you need one. Well there are a few good reasons to set one up, firstly why let the ROM waste good cpu cycles doing something that you may not need? The main reason however often comes down to timing, especially with things like music.A music player will usually require that you call an update function every frame, which is 50 times a second. If you don’t do this then the music will start to sound very strange indeed as frequencies etc will all go wrong. While it might seem easy to do this if you know your program will always run in a frame, when you start to write more complex programs and games this might not always be the case. In a game you may have an unknown number of enemies on the screen firing an unknown number of bullets. While your code might happily run in a frame if there are 5 enemies, what happens when the 6th one appears?
To solve this we can put the music update routine into the interrupt, then no matter how long the game takes to update the music will continue to be updated at a constant 50 frames a second. To do this will simply add a call to the music player update in the IM2Routine.
//Update a music player IM2Routine: call Music_Update reti
Seems easy enough right? Well yes and no, the chances are this would crash your computer horribly. The reason is that the interrupt can occur at absolutely any time and it doesn’t care what the state of your registers are. When we just had EI/RETI it was fine, unfortunately the music update function will most likely alter registers, and even if it handles storing and restoring them all itself, if it very good practice to store and restore them yourself to be sure.
; Update a music player with register storing / restore IM2Routine: push af push hl push bc push de push ix push iy exx ex af, af’ push af push hl push bc push de call Music_Update pop de pop bc pop hl pop af ex af, af’ exx pop iy pop ix pop de pop bc pop hl pop af ei reti
Just to clear up a few things…
The Z80 provides 2 other interrupts that are not maskable, that is you cannot disable them with the DI instruction, they are the NMI and BUSRQ. To my knowledge the BUSRQ was never used as this was designed to allow DMA systems to work along side the Z80, something the spectrum never had. The NMI (Non-Maskable Interrupt) was used on the Spectrum but was only available via additional hardware.
Certain hardware devices had a button that would trigger a NMI which would cause the current program to stop and the routine at address $0066 to be executed. As this address is fixed and in the ROM it is of very little use for software programmers.
RET / RETI
You may have noticed that the interrupt routine ends with RETI rather than the standard RET, this is for completeness and not specifically required, you can end with RET if you prefer. The main reason RETI was added to the instruction set is to allow hardware to detect when the end of the interrupt routine and to pass control onto the next interrupt in a dasiy chained system. The hardware could specifically check the opcode for RETI. If it checked RET it could get confused when coming to the end of a function rather than the entire interrupt routine. To my knowledge no devices for the spectrum use this feature.
EI but not DI?
There is another oddity in the interrupt routine, we have an EI at the end but we never do a DI. This is due to the fact the Z80 will do an internal DI when it starts processing the interrupts. The reason for this is to stop multiple interrupts triggering on top of each other and overrunning the system stack. You can do an EI before the end of your interrupt routine if you wish but general practice is to wait until you have finished then enable them, that way there is no chance of you starting a new interrupt routine before the old one has finished.
Follow up from Peter Ped Helcmanovsky
Nice and thorough with regard to classic ZX. There’s one tiny technical detail about EI omitted.
It will enable interrupts from the next instruction onward, i.e.
ei + ret (while in
di state) => the interrupt can’t happen before the
ret is finished.
And you may want also to mention importance of
IY register content for classic ZX48/128 IM1 routine (it does use the IY to access sysvars and if it is modified, it will modify other memory area)
And also the most visible effect is keyboard reading (mentioned), and 24bit frame counter (may cause unexpected memory values changes if you forget to reconfigure IM1 mode in your code while using the sysvars area for own data or changing
And there’s one more technical detail which may surprise newcomers. Not all RAM in ZX48/128 is equal, some banks/areas are contended. If the
I register points to contended area of RAM, you will get the “snow” effect on display, as the ULA is clashing for access to VRAM data. On ZX48 it means you want to have vector table at $8000+ addresses.
In the source code of correct IM2 setup I tend to add after reti the ASSERT $ <= IM2Table to make sure I will not add too much code into handler, leaking into the table defined afterward.
And there’s nothing about the new possibilities of Next, so I guess that’s like reservoir for next article.