Your game will want to do certain things regularly - e.g. updating the screen, updating the music, reading the controller inputs. It will want to do these things:
The VBlank interrupt satisfies all of these goals. It is triggered once per frame (so 60 times per second on an NTSC system, 50 times per second on a PAL system), which is a fast enough but not too fast, and it is guaranteed to be evenly spaced.
It also has the advantage that it signals the start of the VBlank, which is a very special time...
The VBlank is the period of time during which the game screen is not being drawn. It starts as soon as the last line is drawn and ends just before the first line of the next frame is drawn. Because the screen is not being drawn during this time, we are free to access VRAM as fast as we like, the same as if the screen were turned off. This is of course very important - because we want to update the screen, otherwise our program would be very boring.
Because it's so important, there's some special handling of the VBlank in the SMS: it can be configured to produce an interrupt.
Back in lesson one I said:
They effectively allow the program to be doing two things at once. It can be executing one bit of code, and then something will trigger an interrupt. The CPU will pause what it was doing, then go and execute some special code for the interrupt. Then it will resume where it left off. Since these things happen pretty quickly, it looks like they're happening at the same time (even though they aren't).
(It's worth noting that things only work this way if the code is written correctly. A bad interrupt handler can completely screw things up. We won't be writing a bad interrupt handler, hopefully.)
There are many different ways of doing this. I will only cover one, in the interests of simplicity: the switchable do-everything-in-the-interrupt system. Unfortunately, it's rather complicated - but it could be worse.
On the SMS, we only have one kind of (hardware) interrupt, compared to other Z80 systems. We only use Interrupt Mode 1, so all interrupts are handled at $0038
. We are also only going to use one kind of interrupt (VBlank interrupts - HBlank interrupts are the only alternative and will will leave them for another lesson).
That means we have to put some code at offset $0038
to handle the interrupts. Here goes:
Let's go through this one step at a time, because it's quite complicated.
.org $38
tells WLA DX that this code has to go at offset $0038
.
.section ...
helps this code to cohabit nicely with other section-using code.
push
/pop
all the registers we use. This is important because this interrupt handler could be interrupting any piece of code in the program. We therefore must preserve the contents of these registers as we can't know if it is important or not.
in a,(VDPStatus)
satisfies the interrupt. It lets the VDP know that we have handled the interrupt, which means the VDP will produce another interrupt for the next VBlank. If we didn't do this, interrupts would grind to a halt.
VDPStatus
is a .define
. We came across these in Enhancing Our Program, but this one is a new one. In general, you'll build up a small library of these definitions and use them where needed. So, somewhere before this code, there must have been:
VDPData
definition used earlier; this one is read-only and VDPData
is write-only, so they can share the port number.
ld hl,(VBlankRoutine)
is retrieving a function pointer from RAM. (VBlankRoutine
must have been defined in RAM somewhere before this code.)
call CallHL
is then doing a special trick. It will call the CallHL function. But this function simply executes jp (hl)
. (Some disassemblers might call this instruction ld pc,hl
. It's the same instruction with a different name.) That means that, so long as hl
is the address of a valid function, that function will be executed. When that function gets to its end, it should ret
. Because CallHL didn't call the function, it just jumped to it, that means that this ret
will return back to the point after the call CallHL
.
ei
. This means enable interrupts. Interrupts are automatically disabled when they happen, to allow the interrupt handler to execute without itself being interrupted by more interrupts (!). So, our code has to re-enable them. You can re-enable them inside the interrupt handler or outside it; we'll do it inside. However, it's important to re-enable them right at the end, before...
reti
returns from the interrupt handler and execution continues on from the point at which it was interrupted. It's pretty much the same as the ret
opcode, but we use reti
because it signifies the end of the i
nterrupt handler.
(Technical note: ei
is specially designed so it will enable interrupts a short time after it is called; in fact, just long enough to allow reti
to execute. This allows the interrupt handler to be totally finished before interrupts are enabled. If it didn't do this, a barrage of interrupts could overload the system, as the reti
would never execute and thus the stack would fill up with return addresses.)
All this is fairly technical and confusing. I've chosen to jump into a fairly advanced interrupt handler rather than dumb it down because I've had to learn from experience that if you don't do things properly from the start, you struggle with your mistakes for a long time... so trust me.
VBlankRoutine
to some suitable function.
VBlank interrupts are doubly enabled on the SMS. That means:
halt
Be patient...