Home | New Tutorials | Programs | ||
---|---|---|---|---|
1-R4DS Setup | 2-Software | Space Invaders | ||
Old Tutorials | DSEmu | |||
1-Setup | 4-Sound | 7-FIFO | ||
2-Framebuffer | 5-SRAM | 8-Interrupts | ||
3-Keys | 6-Filesystems | 9-Microphone | ||
10-Extended Rotation Background |
The mechanism for interrupts has changed in libnds and this tutorial is out of date. It is still useful as an introduction to how interrupts work at a low level. Unfortunately the example does not fully run using libnds dated 2005-12-12. The SWI counter shown on the screen is not incremented. I'm looking into it.
An 'Interrupt' is a way of stopping the current execution of a CPU in the Nintendo DS to run an important function. When that function returns the DS will continue executing the piece of code it was executing before it was interrupted.
Interrupts are important because they ensure that certain things happen immediately rather than when the currently executing program 'gets around to it'. It prevents important things from being missed altogether. One very common interrupt that was briefly discussed in Tutorial Two is the Vertical Blank Interrupt.
Another useful aspect of interrupts is that the CPU can be told to go into a low power mode until an interrupt occurs. In low power mode the CPU is effectively stopped until an interrupt occurs. If most of your processing is done in interrupt handlers then the CPU can spend most of its time in low power mode. This extends the battery life of the DS.
There are three very important registers for dealing with interrupts and their handlers. They are:
Name | Address | Size | Description |
---|---|---|---|
REG_IME | 0x04000208 | 16 bits | Interrupt Master Enable Register |
REG_IE | 0x04000210 | 32 bits | Interrupt Enable Register |
REG_IF | 0x04000214 | 32 bits | Interrupt Flags Register |
Each CPU has their own copy of these registers, and will have their own interrupt handlers.
The REG_IME register provides the ability to turn on and off the handling of all interrupts. If bit 0 is clear then all interrupts are prevented from running. If bit 0 is set then interrupts will occur.
This is useful for running blocks of code that must complete without interruption. An example is changing the value of the interrupt register themselves. If you want to change the values of all the interrupt registers you might want to turn interrupts off to prevent an interrupt from occurring half way through and causing undersirable effects. Code to do this would look like:
// Turn interrupts off REG_IME=0; [...do some code...] // Turn interrupts on REG_IME=1
The REG_IE register enables turning on and off individual interrupts. Each bit in the register corresponds to a particular interrupt that can occur. These are documented at DSTek and for the ARM9 REG_IE register they are:
Bit | libnds define | Description |
---|---|---|
0 | IRQ_VBLANK | in vertical blank period |
1 | IRQ_HBLANK | in horizontal blank period |
2 | IRQ_YTRIGGER | REG_VCOUNT scanline reached |
3 | IRQ_TIMER0 | Timer 0 triggered |
4 | IRQ_TIMER1 | Timer 1 triggered |
5 | IRQ_TIMER2 | Timer 2 triggered |
6 | IRQ_TIMER3 | Timer 3 triggered |
7 | IRQ_NETWORK | Unknown |
8 | IRQ_DMA0 | DMA 0 |
9 | IRQ_DMA1 | DMA 1 |
10 | IRQ_DMA2 | DMA 2 |
11 | IRQ_DMA3 | DMA 3 |
12 | IRQ_KEYS | Key pressed |
13 | IRQ_CART | GBA Flashcard is being pulled out |
16 | ARM7 triggered an IPC interrupt | |
17 | Receive FIFO is not empty | |
18 | Send FIFO is not empty | |
19 | IRQ_CARD | DS card data completed |
20 | IRQ_CARD_LINE | DS card interrupt |
21 | GFX FIFO interrupt |
The ARM7 REG_IE register has a few other interrupts according to DSTek:
Bit | libnds define | Description |
---|---|---|
22 | power management interrupt | |
23 | SPI interrupt | |
24 | WIFI interrupt |
Using the REG_IE register is a simple matter of setting it to the OR'd value of the interrupts you want to handle. The example in this tutorial handles the vertical blank interrupt and the keys interrupt:
REG_IE = IRQ_VBLANK | IRQ_KEYS;
The REG_IF register is set by the DS when the interrupt occurs. It contains a bitmask indicating which interrupt occurred. Note that multiple interrupts may have occurred so REG_IF may have more than one bit set. An interrupt handler should check the REG_IF register to see what interrupt to process.
REG_IF also has another purpose. At the end of the interrupt routine it should be set to the bitmask of the interrupts that have actually been handled. So on entry to the handler it will have set bits indicating what interrupts occured, and on exit you need to set the bits indicating what interrupts you've handled. You have to explicitly do the write even if the bits are already set. The action of writing is what signals to the DS to record the handled interrupts.
if(REG_IF & IRQ_VBLANK) { on_irq_vblank(); VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK; REG_IF |= IRQ_VBLANK; } if(REG_IF & IRQ_KEYS) { on_irq_keys(); VBLANK_INTR_WAIT_FLAGS |= IRQ_KEYS; REG_IF |= IRQ_KEYS; } [...more code...]
In this case we set REG_IF to the value of itself or'd with the vertical blank and keys interrupts. We 'or' it to the value of itself so we don't overwrite any other modifications to REG_IF that may have been done during subroutines of the interrupt handler or nested interrupts.
When an interrupt occurs an interrupt handler is called. The standard interrupt handler is located in the Nintendo DS BIOS.
For the ARM9 the code is located at 0xFFFF0018. When an interrupt occurs the processor saves some information so it knows where to go back to and immediately jumps to 0xFFFF0018.
The instruction at 0xFFFF0018 is a branch that goes to the standard BIOS interrupt routine. This routine jumps to a function stored in a special memory address which we are able to write to. By storing a pointer to our own function at this memory address we can cause the interrupt to be processed by our own function.
The memory address used is technically known as DTCM+0x3FFC. DTCM is a special memory area in the ARM9 which can be 'mapped' to reside at various actual physical address. By default, in the DevkitPRO setup, DTCM is located at 0x00800000. So the interrupt handlers address is stored at 0x00803FFC. This is defined in libnds as:
#define IRQ_HANDLER (*(VoidFunctionPointer *)0x00803FFC)
Although libnds has hardcoded the physical address of the ARM9 IRQ_HANDLER, it can be different if DTCM is remapped to a different address. This is unlikely but something to be aware of if using a different setup.
The ARM7 does not have DTCM memory. Its interrupt handler address is stored in memory location 0x03FFFFFC. It cannot be moved. The ARM7 definition in libnds is:
#define IRQ_HANDLER (*(VoidFunctionPointer *)(0x04000000-4))
By assiging a pointer to a function to IRQ_HANDLER you can set your own interrupt handler. Here's the setup code to do it in out sample program:
REG_IME = 0;
IRQ_HANDLER = on_irq;
REG_IE = IRQ_VBLANK | IRQ_KEYS;
REG_IF = ~0;
REG_IME = 1;
First we turn interrupts off by assigning '0' to REG_IME. Then we assigned 'on_irq' to IRQ_HANDLER. 'on_irq' is our interrupt handler function:
void on_irq() { if(REG_IF & IRQ_VBLANK) { on_irq_vblank(); VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK; REG_IF |= IRQ_VBLANK; } if(REG_IF & IRQ_KEYS) { on_irq_keys(); VBLANK_INTR_WAIT_FLAGS |= IRQ_KEYS; REG_IF |= IRQ_KEYS; } }
When we turn interrupts back on our 'on_irq' will be called whenever an interrupt occurs. Following this the 'Interrupt Enable' register is set to process vertical blank interrupts and key interrupts. REG_IF is set to a default value and then interrupts are turned back on.
As the DS screen is updated by the graphics hardware it progresses from the top left of the screen to the bottom right, line by line. If you were to write to the screen memory while it is drawing the same line you get strange visual artifacts. This is discussed in Tutorial Two.
To prevent this from happening the hardware fires a horizontal blank interrupt at the end of each line. You get a short time to do things here. At the end of the last line, after the horizontal blank interrupt, a vertical blank interrupt is fired. This allows you to execute code while the hardware moves from the last line back up to the first line, ready to redraw the screen.
It is here in the vertical blank interrupt you'll want to do your writes to memory. In this sample program we do exactly as we've done before in the framebuffer tutorial. A box is drawn moving across and down the screen.
Vertical Blank Interrupts also require an additional setup step. If you want to handle VBI's you not only have to set the correct REG_IE bit as we've shown, but you need to set a bit in the DISP_SR register. This register also allows setting the horizontal blank interrupt amongst others, but for now, the following code sets the correct bit using libnds defines:
DISP_SR = DISP_VBLANK_IRQ;
Our vertical blank interrupt will just draw the screen as per the framebuffer example. It will also increment the X and Y coordinates so the block moves across and down the screen:
void on_irq_vblank() { old_x = shape_x; old_y = shape_y; shape_x++; if(shape_x + shape_width >= SCREEN_WIDTH) { shape_x = 0; shape_y += shape_height; if(shape_y + shape_height >= SCREEN_HEIGHT) { shape_y = 0; } } draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0)); draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0)); printf("\x1b[2J"); printf("Interrupt Count: %d\n", interrupt_count); printf("SWI Count : %d\n", swi_return_count); }
The handler will also display some text to a 'console' which is displayed on the second DS screen. It will display a count of the number of times the key interrupt has occurred (interrupt_count), and the number of times we have returned from an SWI BIOS call (swi_return_count) which will be explained later in this tutorial.
Our demo program also wants to respond to a key press as an interrupt. Although you generally don't handle all key routines in an interrupt some keys are usefully handled there. Because an interrupt is called immediately it makes a useful place for a 'pause' key or 'reset' key handler.
A disadvantage of the key interrupt is that it is only called when a key is pressed, not when it is released. This prevents handling 'key held down' scenarios so, as far as I can see, there is always a place for key handling code in the main application loop.
Like VBI's, you need a bit of special setup code to handle key interrupts. There is a register, KEYS_CR, which needs bits set to identify which keys will invoke the interrupt. The bits are:
Bit | libnds define | Description |
---|---|---|
0 | KEY_A | A button |
1 | KEY_B | B button |
2 | KEY_SELECT | Select button |
3 | KEY_START | Start button |
4 | KEY_RIGHT | Right shoulder button |
5 | KEY_LEFT | Left shoulder button |
6 | KEY_UP | Up directional key |
7 | KEY_DOWN | Down directional key |
8 | KEY_R | Right directional key |
9 | KEY_L | Left directional key |
10 | Unused | |
11 | Unused | |
12 | Unused | |
13 | Unused | |
14 | Enable KEY interrupt (0=disable, 1=enable) | |
15 | Interrupt condition (0=or, 1=and) |
Bit 14 must be set to enable the key interrupts. Bit 15, if clear, means that any of they keys in the bitmask may be pressed to cause the interrupt. If it is set then all keys must be pressed to cause the interrupt. In our example program we want the interrupt triggered when the users presses both the A and B keys:
// The interrupt will fire when both the A and B key are // pressed. Bit 15 being set is what makes the requirement that both // keys must be pressed. KEYS_CR = KEY_A | KEY_B | (1<<14) | (1<<15);
The key handler itself will just increment a count to show how many times the interrupt has been called. This count is displayed on the second DS screen during the vertical blank interrupt:
void on_irq_keys() { // Key interrupt occurred. interrupt_count++; }
There is a BIOS routine that allows us to wait for an interrupt to occur. This routine will switch the ARM9 to a 'low power' mode where it stops processing instructions and powers down some memory banks.
You've seen this routine indirectly used in other tutorials with the 'swiWaitForVBlank' call. This powers the ARM9 down waiting for the VBI. After the interrupt handler is called as the result of a VBI the ARM9 is powered back up and processing resumes. The good things about calling this routine is it saves power and therefore extends the battery life of the DS. It is known as 'SWI 5'. Fortunately libnds provides a C wrapper function, swiWaitForVBlank, to allow us to call this BIOS routine from C.
An 'SWI' is an ARM machine instruction that causes the processor to jump to a special location in memory (the BIOS). It indexes into an array of addresses using the number provided following the SWI and jumps to that routine.
A more general BIOS routine exists that lets us wait for any interrupts matching a bitmask. This allows us to power down waiting for any particular interrupts to occur. This is known as 'SWI 4'.
In this demo I'm going to show you how to call the more general BIOS function to wait for the key interrupt. It uses inline ARM assembly:
void swiWaitForKeys() { asm("mov r0, #1"); asm("mov r1, #4096"); asm("swi #262144"); }
The first two instructions set up the ARM registers R0 and R1. The '1' in R0 says to wait for the next interrupt. The R1 value of 4096 is the value of IRQ_KEYS. The '262144' passed to SWI is the decimal value of '4' left shifted 16 times. '4' is the SWI number for the wait for interrupt BIOS routine.
By calling this function the wait for interrupt routine in the BIOS is called. The actual interrupt routine does code like this:
while(!(WAIT_FOR_INTR & R1)) { ; Switch the ARM9 to low power mode and ; wait for an interrupt MCR p15, 0, LR,c7,c0, 4 ; We got an interrupt, returned from the interrupt ; handler so go back to loop condition. } ; Return from SWI
WAIT_FOR_INTR is a memory address that holds a bitmask containing bits set for interrupts that were handled. It is located at DTCM+0x3FF8. As per the DTCM discussion previously, on the ARM9 with DevKitPRO, this is 0x00803FF8. libnds has a define for this, confusingly called VBLANK_INTR_WAIT_FLAGS. It is not specific to the vblank interrupt even though the name suggest that.
The important thing to get out of this is you MUST set the correct bit in VBLANK_INTR_WAIT_FLAGS if you use the SWI routine to wait for the interrupt. If you don't your ARM9 process will seem to 'hang'.
So Vertical blank interrupt handling code must contain something similar to the following if it uses swiWaitForVBlank:
VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
Because we are going to use our swiWaitForKeys, we must do similar for keys:
VBLANK_INTR_WAIT_FLAGS |= IRQ_KEYS;
I've seen reports on forums about 'swiWaitForVBlank' being broken and causing code to hang. Not setting the VBLANK_INTR_WAIT_FLAGS is very likely the reason for that.
libnds has routines for setting up interrupt handlers a bit easier. I've not used these routines here for two reasons. The first is to show how to do it using the underlying registers so you understand how things work. The second is there is a subtle problem with the libnds routines if you are using the BIOS wait for interrupt SWI.
The 'libnds' code does the following in its default interrupt handler:
void irqDefaultHandler(void)
{
int i = 0;
for (i = 0; i < 32; i++)
{
if(REG_IF & (1 << i) )irqTable[i]();
}
VBLANK_INTR_WAIT_FLAGS = REG_IF | REG_IE;
}
Note the assign to 'VBLANK_INTR_WAIT_FLAGS'. It or's the value of the REG_IE register along with the REG_IF register. Recall that the REG_IE register contains a bitmask of all interrupts you want to handle. So on any interrupt this code will inform the BIOS wait for interrupt SWI that all interrupts have been handled.
The effect of this is if a VBI occurs, but no keys interrupt, then the BIOS will still think our key interrupt occurred and exit out of our 'swiWaitForKeys' routine. Ideally the libnds code should be something like:
VBLANK_INTR_WAIT_FLAGS |= REG_IF;
If you use the libnds default interrupt handler you probably won't be able to rely on using the BIOS wait for interrupt routine for anything but the VBI unfortunately.
There is one more thing to be careful of when setting REG_IF and VBLANK_INTR_WAIT_FLAGS. If you are assigning 'REG_IF' to VBLANK_INTR_WAIT_FLAGS to say you handled all interrupts that occured in the handler you need to do it in the following order:
VBLANK_INTR_WAIT_FLAGS = REG_IF; REG_IF = REG_IF;
A Write to 'REG_IF' resets it. So if you first write to REG_IF and then assign REG_IF to VBLANK_INTR_WAIT_FLAGS, you will store the wrong value in VBLANK_INTR_WAIT_FLAGS resulting in the BIOS wait for interrupt SWI probably hanging.
To demonstrate waiting for the key interrupt the main loop will use our 'swiWaitForKeys' routine:
while(1) { swiWaitForKeys(); ++swi_return_count; }
On return from that call I increment a counter which is displayed on the second screen during the VBI. This is to demonstrate when the SWI call actually returns.
Most main loops I've used previously use 'swiWaitForVBlank'. This is called 60 times per second. I'm not actually doing anything in the main loop so I don't care about when the VBI returns. So instead I wait on the keys interrupt. This happens only when a key pressed. Perhaps once every few seconds.
This demo uses the second screen to display some debug output, relating to whether keys were pressed and how often the SWI call is returned. This is set up using the following code:
videoSetModeSub(MODE_0_2D | DISPLAY_BG0_ACTIVE); vramSetBankC(VRAM_C_SUB_BG); SUB_BG0_CR = BG_MAP_BASE(31); BG_PALETTE_SUB[255] = RGB15(31,31,31); consoleInitDefault((u16*)SCREEN_BASE_BLOCK_SUB(31), (u16*)CHAR_BASE_BLOCK_SUB(0), 16);
This should look very similar to other screen setup code for console usage (for example, tutorial one). The different is we use 'videoSetModeSub' insteoad of 'videoSetMode'. The sub-screen is the second screen. You'll notice the various registers use the 'SUB' versions instead of the 'MAIN' versions. I'll cover much more on setting up and using the second screen in an upcoming tutorial but this should give you a basic idea.
The example application I used to test this out is called interrupt_demo1. Pressing the 'A' and 'B' buttons at the same time will trigger the keys interrupt. This interrupt increments a count which is displayed on the second screen to show it happened. It will also cause the BIOS routine which we call to wait for the interrupt to return, incrementing a second count which is also displayed on the second screen.
Throughout this a small red square traverses across and down the screen. For most of the execution of the program the ARM9 is in low power mode. Only during the interrupts is the ARM9 really active.
The complete example program is 'interrupts_demo1'. The ARM9 code is in arm9_main.cpp. It uses the console routines to print information similar to the first tutorial. The ARM7 code is in arm7_main.cpp. A Makefile file is supplied to build everything.
The complete source code is supplied in interrupts_demo1.zip and you can download the interrupts_demo1.nds and interrupts_demo1.nds.gba files for running on the emulators or hardware. DSEmu 0.4.4 and above will run this demo without problems.
This tutorial covered how to use interrupts and the BIOS calls to efficiently wait for the interrupt. The approach shown here is friendly on the battery life and it also has the advantage of emulators being able to more efficiently use CPU resources as it knows when the ARM9 can be switched off rather than busy looping.
Future tutorials will probably use interrupts more which is why I decided to cover them at this stage. Although keys aren't often handled via interrupts, things like the FIFO, IPC and Timers use them so they're important to learn about.
As always, any comments or suggestions are welcomed. See my contact details below.
Copyright (c) 2005, Chris Double. All Rights Reserved.