Why do I need timing?
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:
- rapidly, else it won’t be smooth
- at even intervals, else things will move at different speeds at different times
- not too fast, else it will be unwatchable/unplayable
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...
What’s the VBlank?
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.
What’s 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.)
How do I use VBlank interrupts?
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.
1. Set up an interrupt handler
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:
.section "Interrupt handler" force
in a,(VDPStatus) ; satisfy interrupt
Let’s go through this one step at a time, because it’s quite complicated.
.org $38tells WLA DX that this code has to go at offset
.section ...helps this code to cohabit nicely with other section-using code.
popall 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.
.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:
VDPDatadefinition used earlier; this one is read-only and
VDPDatais write-only, so they can share the port number.
ld hl,(VBlankRoutine)is retrieving a function pointer from RAM. (
VBlankRoutinemust have been defined in RAM somewhere before this code.)
call CallHLis 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
hlis 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
retwill return back to the point after the
- It cleans up the pushed registers...
- Next is
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...
retireturns from the interrupt handler and execution continues on from the point at which it was interrupted. It’s pretty much the same as the
retopcode, but we use
retibecause it signifies the end of the
interrupt handler. (Technical note:
eiis specially designed so it will enable interrupts a short time after it is called; in fact, just long enough to allow
retito 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
retiwould 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.
- This interrupt handler works pretty well (for VBlank interrupts - HBlank interrupts are an added complication we’ll ignore for now)
- To use it, you just set
VBlankRoutineto some suitable function.
2. Turn on VBlank interrupts
VBlank interrupts are doubly enabled on the SMS. That means:
- You have to turn on interrupts in the VDP
- You have to enable interrupts in the CPU
3. Make something for the non-VBlank code to do
- Palette fade?
Work in progress