GameBoy Timer
The GameBoy timer is an excellent way to set and dynamically change the speed of your application's main loop. Here's a tutorial on how to use the timer effectively for this purpose. The techniques here are demonstrated in both hello-window.asm and hello-angle.asm.
notes on the timer
- the timer is fully described in gbspec.txt lines 848 - 872
- the timer counts incrementally at a rate specified by the rTAC hardware register. The choices of speed are defined gbhw.inc lines 217 to 220:
- TACF_4KHZ
- TACF_16KHZ
- TACF_65KHZ
- TACF_262KHZ
- The counter will start when the bits in rTAC corresponding with TAC_START are set. Likewise, the counter will stop when the bits in rTAC corresponding with TAC_STOP are set.
- Whenever the timer overflows (counts from $ff to 0), it:
- generates an interrupt if the timer interrupt is enabled
loads the timer with the value set in register rTMA
- examples:
- if
- rTAC = TACF_START | TACF_4KHZ ; start the timer at 4K
- rTMA = $ff = 256 - 1 ; timer division = 1
rIE = EIF_TIMER | <other stuff maybe>
- then the timer interrupt will be called 4K/second
- if
- rTAC = TACF_START | TACF_4KHZ ; start the timer at 4K
- rTMA = $fe = 256 - 2 ; timer division = 2
rIE = EIF_TIMER | <other stuff maybe>
- then the timer interrupt will be called 2K/second
- if
Application
A good way to use the timer is to have the timer interrupt trigger the main loop to cycle one iteration. Then you can adjust the speed between timer interrupts to set the number of iterations in your main loop:
add a call to a timer interrupt routine where the timer interrupt is called:
SECTION "Timer_Overflow",HOME[$0050] jp TimerInterrupt ; flag the timer interrupt
add some constants which describe the number of times you wish for the interrupt to be generated:
TimerHertz EQU TACF_START|TACF_4KHZ TimerClockDiv EQU 256-30 ; divide clock by 30
- in this example, the timer interrupt will be generated 4096/30 times each second
whereever you already load rIE and enable interrupts in your code, add IEF_TIMER to the list of interrupts you are enabling. For example, if you wanted to enable the vblank and timer interrupts, you would have in your initialization code:
ld a, IEF_VBLANK|IEF_TIMER ld [rIE],a ; ENABLE VBLANK AND TIMER INTERRUPT ei ; LET THE INTS FLY
if you don't already have one, add a variable in your code whose bits can be used as flags to know what interrupts have been generated:
LoByteVar IFlags ; flag var for interrupts
add your timer interrupt routine to your code. The routine's only purpose is to set a flag in IFlags and to reset the timer:
; TimerInterrupt is the routine called when a timer interrupt occurs. TimerInterrupt: push af ; save a ld a,TimerClockDiv ; load number of counts of timer ld [rTMA],a ld a,TimerHertz ; load timer speed ld [rTAC],a ld a,IEF_TIMER ld [IFlags],a pop af ; restore a. Everything has been preserved. reti
add a manual call to the timer interrupt routine after you have enabled interrupts and before your main loop. This will turn the timer on:
call TimerInterrupt ; turn on the timer
- start your application's main loop with logic to look at the flag to know whether or not the timer interrupt occurred. The loop:
- halts, until an interrupt occurs
- when an interrupt does occur, it looks at the IFlags variable to see if the interrupt the occurred was the timer interrupt
- if the interrupt was not a timer interrupt, it loops back to halt, waiting again for an interrupt.
- if the interrupt was a timer interrupt, it resets the timer flag in IFlags variable and continues with the main loop.
the code for all of this might look like:
MainLoop: halt nop ld a,[IFlags] ; sit and wait cp IEF_TIMER ; unless the timer caused the interrupt jr nz,MainLoop and ~IEF_TIMER ; reset the timer flag to catch the next ld [IFlags],a ; timer interrupt ...the rest of your loop goes here. We only get here if there has been a timer interrupt
Tips on the Main Loop
- Sometimes you want something to happen every 2nd, 4th, 8th, 16th, 128th time through the main loop. These easiest way to do this is to
define this macro in your code:
CALL_IF_NTH_TIME: MACRO push af push bc ld a,[\1] ld b,a and 256-\2 cp b call z,\3 pop bc pop af ENDM
create a routine which contains the code you wish to execute every 2nd, 4th, 8th, or (put a power of 2 here) time. For this example, we'll imagine we created a routine called ChangeWindow.
create a variable which counts the number of iterations through your loop:
LoByteVar LoopCount ; count iterations through MainLoop
add code to increment the variable in your main loop. So the beginning of your main loop, complete with timer interrupt, might look like:
MainLoop: halt nop ld a,[IFlags] ; sit and wait cp IEF_TIMER ; unless the timer caused the interrupt jr nz,MainLoop xor a ; reset the timer flag to catch the next ld [IFlags],a ; timer interrupt ld a,[LoopCount] inc a ld [LoopCount],a ; inc LoopCount timer
add a call to your macro in your main loop to do the magic. The call could be right below the main loop code example above. For example:
CALL_IF_NTH_TIME LoopCount,16,ChangeWindow
This example will call the routine ChangeWindow every 16th time through the loop:
- 1st parameter: name of variable being iterated through the loop
- 2nd parameter: how often to call the routine. In this case, the routine will be called every 16th time through the loop. This parameter must be a power of 2 and max 256.
- 3rd parameter: routine to call.
The Big Lie
Unless the timer interrupt is the only interrupt you have enabled, it isn't actually true that you can be sure how many timer interrupts will be generated in a second. This is because when the GameBoy is servicing an interrupt, interrupts are disabled. I think if the timer overflows while another interrupt (such as VBLANK) is being serviced, the timer interrupt is "lost" but I have not actually confirmed this. The alternative would be that perhaps the timer interrupt is generated immediately on return from the VBLANK interrupt...but...I doubt it. Anyway, for our non-time-critical uses (we aren't doing precision timing of digital electronics here) we can think of the timer interrupt as being consistent and predictable.