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:

Interrupts are things which cause code execution to jump to somewhere else ("interrupting" the program flow) to handle something

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:

.org $38
.section "Interrupt handler" force
  push af
    in a,(VDPStatus) ; satisfy interrupt
    push hl
      ld hl,(VBlankRoutine)
      call CallHL
    pop hl
  pop af
  ei
  reti

CallHL:
  jp (hl)
.ends

Let's go through this one step at a time, because it's quite complicated.

  1. .org $38 tells WLA DX that this code has to go at offset $0038.
  2. .section ... helps this code to cohabit nicely with other section-using code.
  3. We 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.
  4. 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:
.define VDPStatus $bf
You may notice that this is the same as the VDPData definition used earlier; this one is read-only and VDPData is write-only, so they can share the port number.
  1. ld hl,(VBlankRoutine) is retrieving a function pointer from RAM. (VBlankRoutine must have been defined in RAM somewhere before this code.)
  2. 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.
  3. It cleans up the pushed registers...
  4. 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...
  5. 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 interrupt 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.

  • 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 VBlankRoutine to 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

  • Use halt

Example program

  • Palette fade?

Work in progress

Be patient...


< More good coding practices | Lesson 2 | Drawing an extra line? >