Timers Contents BIOS calls

16. Interrupts

16.1. Introduction

Under certain conditions, you can make the CPU drop whatever it's doing, go run another function instead, and continue with the original process afterwards. This process is known as an interrupt (two ‘r’s, please). The function that handles the interrupt is an interrupt service routine, or just interrupt; triggering one is called raising an interrupt.

Interrupts are often attached to certain hardware events: pressing a key on a PC keyboard, for example, raises one. Another PC example is the VBlank (yes, PCs have them too). The GBA has similar interrupts and others for the HBlank, DMA and more. This last one in particular can be used for a great deal of nifty effects. I'll give a full list of interrupts shortly.

Interrupts halt the current process, quickly do ‘something’, and pass control back again. Stress the word “quickly”: interrupts are supposed to be short routines.

16.2. Interrupts registers

There are three registers specifically for interrupts: REG_IE (0400:0200h), REG_IF (0400:0202h) and REG_IME (0400:0208h). REG_IME is the master interrupt control; unless this is set to ‘1’, interrupts will be ignored completely. To enable a specific interrupt you need to set the appropriate bit in REG_IE. When an interrupt occurs, the corresponding bit in REG_IF will be set. To acknowledge that you've handled an interrupt, the bit needs to be cleared again, but the way to do that is a little counter-intuitive to say the least. To acknowledge the interrupt, you actually have to set the bit again. That's right, you have to write 1 to that bit (which is already 1) in order to clear it.

Apart from setting the bits in REG_IE, you also need to set a bit in other registers that deal with the subject. For example, the HBlank interrupt also requires a bit in REG_DISPSTAT. I think (but please correct me if I'm wrong) that you need both a sender and receiver of interrupts; REG_IE controls the receiver and registers like REG_DISPSTAT control the sender. With that in mind, let's check out the bit layout for REG_IE and REG_IF.

REG_IE @ 0400:0200 and REG_IF @ 0400:0202
F EDCB A 9 876 5 4 3210
- C K Dma Com Tm Vct Hbl Vbl

bitsnamedefinedescription
0 Vbl IRQ_VBLANK VBlank interrupt. Also requires REG_DISPSTAT{3}
1 Hbl IRQ_HBLANK HBlank interrupt. Also requires REG_DISPSTAT{4} Occurs after the HDraw, so that things done here take effect in the next line.
2 Vct IRQ_VCOUNT VCount interrupt. Also requires REG_DISPSTAT{5}. The high byte of REG_DISPSTAT gives the VCount at which to raise the interrupt. Occurs at the beginning of a scanline.
3-6Tm IRQ_TMx Timer interrupt, 1 bit per timer. Also requires REG_TMxCNT{6}. The interrupt will be raised when the timer overflows.
7 Com IRQ_COM Serial communication interrupt. Apparently, also requires REG_SCCNT{E}. To be raised when the transfer is complete. Or so I'm told, I really don't know squat about serial communication.
8-BDma IRQ_DMAx DMA interrupt, 1 bit per channel. Also requires REG_DMAxCNT{1E}. Interrupt will be raised when the full transfer is complete.
C K IRQ_KEYS Keypad interrupt. Also requires REG_KEYCNT{E}. Raised when any or all or the keys specified in REG_KEYCNT are down.
D C IRQ_CART Cartridge interrupt. Raised when the cart is removed from the GBA.

16.3. Interrupt Service Routines

You use the interrupt registers described above to indicate which interrupts you want to use. The next step is writing an interrupt service routine. This is just a typeless function (void func(void)); a C-function like many others. Here's an example of an HBlank interrupt.

void hbl_pal_invert()
{
    pal_bg_mem[0] ^= 0x7FFF;
    REG_IF = IRQ_HBLANK;
}

The first line inverts the color of the first entry of the palette memory. The second line resets the HBlank bit of REG_IF indicating the interrupt has been dealt with. Since this is an HBlank interrupt, the end-result is that that the color changes every scanline. This shouldn't be too hard to imagine.

If you simply add this function to an existing program, nothing would change. How come? Well, though you have an isr now, you still need to tell the GBA where to find it. For that, we will need to take a closer look at the interrupt process as a whole.

On acknowledging interrupts correctly

To acknowledge that an interrupt has been dealt with, you have to set the bit of that interrupt in REG_IF, and only that bit. That means that ‘REG_IF = IRQ_x’ is usually the correct course of action, and not ‘REG_IF |= IRQ_x’. The |= version acknowledges all interrupts that have been raised, even if you haven't dealt with them yet.

Usually, these two result in the same thing, but if multiple interrupts come in at the same time things will go bad. Just pay attention to what you're doing.


16.3.1. The interrupt process

The complete interrupt process is kind of tricky and part of it is completely beyond your control. What follows now is a list of things that you, the programmer, need to know. For the full story, see GBATek : irq control.

  1. Interrupt occurs. Some black magic deep within the deepest dungeons of BIOS happens and the CPU is switched to IRQ mode and ARM state. A number of registers (r0-r3, r12, lr) are pushed onto the stack.
  2. BIOS loads the address located at 0300:7FFC and branches to that address.
  3. The code pointed to by 0300:7FFC is run. Since we're in ARM-state now, this must to be ARM code!
  4. After the isr is done, acknowledge that the interrupt has been dealt with by writing to REG_IF, then return from the isr by issuing a bx lr instruction.
  5. The previously saved registers are popped from stack and program state is restored to normal.

Steps 1, 2 and 5 are done by BIOS; 3 and 4 are yours. Now, in principle all you need to do is place the address of your isr into address 0300:7FFC. To make our job a little easier, we will first create ourselves a function pointer type.

typedef void (*fnptr)(void);
#define REG_INTMAIN *(fnptr*)(0x03007FFC)

// Be careful when using it like this, see notes below
void foo()
{
    REG_INTMAIN= hbl_pal_invert;  // tell the GBA where my isr is
    REG_DISPSTAT |= VID_HBL_IRQ;  // Tell the display to fire HBlank interrupts
    REG_IE |= IRQ_HBLANK;          // Tell the GBA to catch HBlank interrupts
    REG_IME= 1;                   // Tell the GBA to enable interrupts;
}

Now, this will probably work, but as usual there's more to the story.

On section mirroring

GBA's memory sections are mirrored ever so many bytes. For example IWRAM (0300:0000) is mirrored every 8000h bytes, so that 0300:7FFC is also 03FF:FFFC, or 0400:0000−4. While this is faster, I'm not quite sure if this should be taken advantage of. no$gba v2.2b marks it as an error, even though this was apparently a small oversight and fixed in v2.2c. Nevertheless, consider yourself warned.

16.4. Creating an interrupt switchboard

The hbl_pal_invert() function is an example of a single interrupt, but you may have to deal with multiple interrupts. You may also want to be able to use different isr's depending on circumstances, in which case stuffing it all into one function may not be the best way to go. Instead, we'll create an interrupt switchboard.

An interrupt switchboard works a little like a telephone switchboard: you have a call (i.e., an interrupt, in REG_IF) coming in, the operator checks if it is an active number (compares it with REG_IE) and if so, connects the call to the right receiver (your isr).

This particular switchboard will come with a number of additional features as well. It will acknowledge the call in both REG_IF and REG_IFBIOS), and make sure this happens when the isr is absent as well. It'll also allow for nested interrupts and even prioritized nested interrupts.


16.4.1. Design and interface considerations

The actual switchboard is only one part of the whole; I also need a couple of structs, variables and functions. The basic items I require are these.

All of these functions do something like this: disable interrupts (REG_IME=0), do their stuff and then re-enable interrupts. It's a good idea to do this because being interrupted while mucking about with interrupts is not pretty. The functions concerned with service routines will also take a function pointer (the fnptr type), and also return a function pointer indicating the previous isr. This may be useful if you want to try to chain them.

Below you can see the structs, tables, and the implementation of irq_enable() and irq_add(). In both functions, the __irq_senders[] array is used to determine which bit to set in which register to make sure things send interrupt requests. The irq_add() function goes on to finding either the requested interrupt in the current table to replace, or an empty slot to fill. The other routines are similar. If you need to see more, look in tonc_irq.h/.c in tonclib.

//! Interrups Indices
typedef enum eIrqIndex
{
    II_VBLANK=0, II_HBLANK, II_VCOUNT, II_TM0,
    II_TM1,      II_TM2,    II_TM3,    II_COM,
    II_DMA0,     II_DMA1,   II_DMA2,   II_DMA3,
    II_KEYS,     II_CART,   II_MAX
} eIrqIndex;

//! Struct for prioritized irq table
typedef struct IRQ_REC  
{
    u32 flag;   //!< Flag for interrupt in REG_IF, etc
    fnptr isr;  //!< Pointer to interrupt routine
} IRQ_REC;

// === PROTOTYPES =====================================================

CODE_IN_IWRAM void isr_master_multi();

void irq_init(fnptr isr);
fnptr irq_set_master(fnptr isr);

fnptr irq_add(enum eIrqIndex irq_id, fnptr isr);
fnptr irq_delete(enum eIrqIndex irq_id);

fnptr irq_set(enum eIrqIndex irq_id, fnptr isr, int prio);
void irq_enable(enum eIrqIndex irq_id);
void irq_disable(enum eIrqIndex irq_id);
// IRQ Sender information
typedef struct IRQ_SENDER
{
    u16 reg_ofs;    //!< sender reg - REG_BASE
    u16 flag;       //!< irq-bit in sender reg
} ALIGN4 IRQ_SENDER;

// === GLOBALS ========================================================

// One extra entry for guaranteed zero
IRQ_REC __isr_table[II_MAX+1];

static const IRQ_SENDER __irq_senders[] =
{
    { 0x0004, 0x0008 },     // REG_DISPSTAT,    DSTAT_VBL_IRQ
    { 0x0004, 0x0010 },     // REG_DISPSTAT,    DSTAT_VHB_IRQ
    { 0x0004, 0x0020 },     // REG_DISPSTAT,    DSTAT_VCT_IRQ
    { 0x0102, 0x0040 },     // REG_TM0CNT,      TM_IRQ
    { 0x0106, 0x0040 },     // REG_TM1CNT,      TM_IRQ
    { 0x010A, 0x0040 },     // REG_TM2CNT,      TM_IRQ
    { 0x010E, 0x0040 },     // REG_TM3CNT,      TM_IRQ
    { 0x0128, 0x4000 },     // REG_SCCNT_L      BIT(14) // not sure
    { 0x00BA, 0x4000 },     // REG_DMA0CNT_H,   DMA_IRQ>>16
    { 0x00C6, 0x4000 },     // REG_DMA1CNT_H,   DMA_IRQ>>16
    { 0x00D2, 0x4000 },     // REG_DMA2CNT_H,   DMA_IRQ>>16
    { 0x00DE, 0x4000 },     // REG_DMA3CNT_H,   DMA_IRQ>>16
    { 0x0132, 0x4000 },     // REG_KEYCNT,      KCNT_IRQ
    { 0x0000, 0x0000 },     // cart: none
};


// === FUNCTIONS ======================================================

//! Enable irq bits in REG_IE and sender bits elsewhere
void irq_enable(enum eIrqIndex irq_id)
{
    u16 ime= REG_IME;
    REG_IME= 0;

    const IRQ_SENDER *is= &__irq_senders[irq_id];
    *(u16*)(REG_BASE+is->reg_ofs) |= is->flag;

    REG_IE |= BIT(irq_id);
    REG_IME= ime;
}

//! Add a specific isr
fnptr irq_add(enum eIrqIndex irq_id, fnptr isr)
{
    u16 ime= REG_IME;
    REG_IME= 0;

    int ii;
    u16 irq_flag= BIT(irq_id);
    fnptr old_isr;
    IRQ_REC *pir= __isr_table;

    // Enable irq
    const IRQ_SENDER *is= &__irq_senders[irq_id];
    *(u16*)(REG_BASE+is->reg_ofs) |= is->flag;
    REG_IE |= irq_flag;

    // Search for previous occurance, or empty slot
    for(ii=0; pir[ii].flag; ii++)
        if(pir[ii].flag == irq_flag)
            break;
    
    old_isr= pir[ii].isr;
    pir[ii].isr= isr;
    pir[ii].flag= irq_flag;

    REG_IME= ime;
    return old_isr;
}

16.4.2. The master interrupt service routine

The master isr itself needs to seek out the raised interrupt(s) and run down ___isr_table to find it. If it can't find the interrupt or if it doesn't have a service routine, just acknowledge it and quit the routine. If it does have an isr, try to run it. The nesting part of the switchboard means primarily that the IRQ-disable bit the status register needs to be cleared, though there are other considerations as well. To allow only higher-priority irqs to interrupt the current isr we keep track of the earlier irq-flags when searching through the list and mask out the other bits in REG_IE. The C code would look something like this:

// This is mostly what tonclib's isr_master_nest does, but
// you really need asm for the full functionality
CODE_IN_IWRAM void isr_master_nest_c()
{
    u32 ie= REG_IE;
    u32 ieif= ie & REG_IF;
    u32 irq_prio= 0;
    IRQ_REC *pir;

    REG_IFBIOS |= ieif; // (1) Ack for BIOS routines

    // --- (2) Find raised irq ---
    for(pir= __isr_table; pir->flag; pir++)
    {
        if(pir->flag & ieif)
            break;
        irq_prio |= pir->flag;
    }
    // (3) Irq not recognized or has no isr:
    // Just ack in REG_IF and return
    if(pir->flag == 0 || pir->isr == NULL)
    {
        REG_IF= ieif;   
        return;
    }

    REG_IME= 0;

    // --- (4) CPU back to system mode ---
    //> *--sp_irq= lr_irq;
	//> *--sp_irq= spsr
    //> cpsr &= ~(CPU_MODE_MASK | CPU_IRQ_OFF);
    //> cpsr |= CPU_MODE_SYS;
    //> *--sp_sys = lr_sys;

    REG_IE= ie & irq_prio; // (5) Allow only irqs of higher priority
    REG_IME= 1;

    pir->isr();

    u32 ime= REG_IME;
    REG_IME= 0;

    // --- (6) Back to irq mode ---
    //> lr_sys = *sp_sys++;
    //> cpsr &= ~(CPU_MODE_MASK | CPU_IRQ_OFF);
    //> cpsr |= CPU_MODE_IRQ | CPU_IRQ_OFF;
    //> spsr = *sp_irq++
    //> lr_irq = *sp_irq++;

    // --- (7) Restore ie and ack handled irq ---
    REG_IE= ie;
    REG_IF= pir->flag;

    REG_IME= ime;
}

Most of these points have been discussed already, and don't need repeating yet again. Do note the difference is acknowledging REG_IFBIOS and REG_IF: the former uses an OR and the latter an assignment. The variable irq_prio keeps track of interrupts of higher priorities, and is masked over REG_IE at point 5 (the original ie is restored at point 7).

This routine would work, were it not for items 4 and 6. There should be code to set/restore the CPU mode to system/irq mode, but you can't do that in C. Another problem is that the link registers (these are used to hold the return addresses of functions) have to be saved somehow. Note: registers, plural. Each CPU mode has its own link register and stack, even though the names are the same: lr and sp; Usually a C routine will save lr on its own, but since you need it twice now it's very unsafe to leave this up to the compiler. Aside from that, you need to save the saved program status register spsr, which indicates the program status when the interrupt occurred. This is another thing that C can't really do. These items pretty much force you to use assembly for nested interrupts.


So, assembly it is then. Below is the assembly equivalent of the function described above; I've even added the numbered points in the comments here, although the order is a little shuffled sometimes. I don't expect you to really understand everything written here, but with some imagination you should be able to follow most of it. Teaching assembly is way beyond the scope of this chapter, but worth the effort in my view. Tonc's assembly chapter should give you the necessary information to understand most of it, and shows where you can learn more.

    .file   "tonc_isr_nest.s"
    .extern __isr_table;

/*! \fn CODE_IN_IWRAM void isr_master_nest()
*   \brief  Main isr for using prioritized nested interrupts
*/
    .section .iwram, "ax", %progbits
    .arm
    .align
    .global isr_master_nest
isr_master_nest:
    mov     r3, #0x04000000
    ldr     r2, [r3, #0x200]!
    and     r2, r2, r2, lsr #16     @ irq_curr= IE & IF
    
    @ (1) REG_IFBIOS |= irq_curr
    ldr     r1, [r3, #-0x208]
    orr     r1, r1, r2
    str     r1, [r3, #-0x208]

    @ --- (2) Find raised irq in __isr_table ---
    @ r0 := IRQ_REC *pir= __isr_table
    @ r12:= irq_prio (higher priority irqs)
    ldr     r0, =__isr_table
    mov     r12, #0
.Lirq_search:
    ldr     r1, [r0], #8 
    tst     r1, r2
    bne     .Lirq_found     @ Found one, break off search
    orr     r12, r12, r1
    cmp     r1, #0
    bne     .Lirq_search    @ Not here; try next irq_rec
    
    @ --- (3a) No irq or isr: just ack and return ---
.Lirq_none:
    strh    r2, [r3, #2]    @ REG_IF= irq_curr (is this right?)
    bx      lr

    @ (3b) If we're here, we found the irq; check for isr
.Lirq_found:
    ldr     r0, [r0, #-4]   @ isr= pir[ii-1].isr
    cmp     r0, #0
    streqh  r1, [r3, #2]    @ No isr: ack and return
    bxeq    lr

    @ --- isr found, prep for nested irqs ---
    @ {r0,r1,r3,r12} == { isr(), irq_flag, &REG_IE, irq_prio }

    @ (5) r2 := ieif= REG_IE|(irq_flag<<16); REG_IE &= irq_prio;
    ldrh    r2, [r3]
    and     r12, r12, r2
    strh    r12, [r3]
    orr     r2, r2, r1, lsl #16

    mrs     r12, spsr
    stmfd   sp!, {r2, r12, lr}  @ sp_irq,{ieif, spsr, lr_irq}
    str     r3, [r3, #8]    @ REG_IME=0 (yeah ugly, I know)

    @ (4) Set CPU to SYS-mode, re-enable IRQ
    mrs     r2, cpsr
    bic     r2, r2, #0xDF
    orr     r2, r2, #0x1F
    msr     cpsr, r2

    stmfd   sp!, {r3, lr}   @ sp_sys, {&REG_IE, lr_sys}
    mov     r2, #1
    str     r2, [r3, #8]    @ REG_IME= 1
    adr     lr, .Lpost_isr
    bx      r0

    @ --- Unroll preparation ---
.Lpost_isr:
    ldmfd   sp!, {r3, lr}   @ sp_sys, {&REG_IE, lr_sys}
    ldr     r0, [r3, #8]
    str     r3, [r3, #8]    @ REG_IME=0 again

    @ (6) Restore CPU to IRQ-mode, disable IRQ
    mrs     r2, cpsr
    bic     r2, r2, #0xDF
    orr     r2, r2, #0x92
    msr     cpsr, r2
    
    ldmfd   sp!, {r2, r12, lr}  @ sp_irq,{ieif, spsr, lr_irq}
    msr     spsr, r12

    str     r2, [r3]        @ (7) REG_IE/REG_IF= ieif
    str     r0, [r3, #8]    @ Restore REG_IME
    bx      lr
Nested irqs are nasty

Making a nested interrupt routine work is not a pleasant exercise when you only partially know what you're doing. For example, that different CPU modes used different stacks took me a while to figure out, and it took me quite a while to realize that the reason my nested isrs didn't work was because there are different lr's too.

The isr_master_nest is largely based on libgba's interrupt dispatcher, but also borrows information from GBATek and A. Bilyk and DekuTree's analysis of the whole thing as described in forum:4063. Also invaluable was the home-use debugger version of no$gba, hurray for breakpoints.

If you want to develop your own interrupt routine, these sources will help you immensely and will keep the loss of sanity down to somewhat acceptable levels.

Now, the next section is an old way of doing it and is completely optional. The real story continues here.

16.5. The dreaded crt0.S file (obsolete)

Toolchain specific

This section is very compiler dependent, so I can't say it'll be true for your set-up. Everything said here is true for DKA r4, partially true for r5b3, and perhaps even incorrect for STD or HAM. Caveat emptor.


This is just a legacy section so you'll know what people mean when you see it used in other people's code and not the recommended procedure.

Many times, the switchboard mentioned above is incorporated inside boot code. Exactly what constitutes the boot code depends on the compiler (GCC has crt0.S and the ARM STD has boot.asm or start.asm). The method of dispatching interrupts is the same: there is the switchboard code, only this time it's buried deep within lines upon lines of assembly and comments. Like before, this will also use an external IntrTable which you need to define somewhere lest you get link errors. But even after defining it, that still wouldn't get us anywhere because by default the interrupts inside the boot-file are disabled. To activate them you have to go into crt0.S and uncomment a few lines:

[ uncomment this line]
@ .equ __InterruptSupport, 1

[ And _one_ of these (SingleInterrupts wil do).]
@ .equ __FastInterrupts, 1
@ .equ __SingleInterrupts, 1
@ .equ __MultipleInterrupts, 1

This in itself shouldn't be difficult either. However, you will find it quite hard for one simple reason. In DKA v4, the file crt0.S is nowhere to be found. There's a crt0.o alright (in fact there are four of them!), but no crt0.S. This is probably just an oversight but the end result remains: no crt0.S, no interrupts. The newer revision 5 beta 3 does have a crt0.S and you can enable interrupts by setting the configuration in config-crt0.h and config-isr.h and recompiling the devkit. At least I think that's how it works; I don't consider myself an expert on this stuff so I try to stay the fsck away from messing with system files.

This is why you will often see a stand-alone crt0.S that you can use without fussing. Most, if not all, crt0.S files you may come across will be based on Jeff Frohwein's crt0.S+linkscript combo, which you can find on his site (www.devrs.com). You can find the lines for interrupt supports buried in the comments somewhere.


Unfortunately, this is only half the problem. To use the file, you first need to assemble it with the DKA's assembler as; this will give you a crt0.o. Which you already have in no less than four varieties scattered across the directories of devkit/arm-agb-elf/lib. These files are linked automatically, so simply adding your own crt0.o to the list would result in multiple definitions. Of course, you could replace the standard versions, but which one(s)? All of them? Pick one and hope for the best? To be honest I have no idea, but there is another way. In fact, there are two ways.


The first way is probably the recommended way but I'm not going to use it. Instead of using gcc to link the files, you could invoke the linker (ld) directly. This is actually what gcc does for linking as well, but it'll add a directory list and the boot-code files automatically; using ld directly is linking in its purest form. And also its trickiest. First, you have to specify the boot-code files yourself. But then, that was the whole point. You also have to specify the default C-libraries such as libc.a and libgcc.a and where to find them (Note there are also four of these. Each.). You also need to tell the linker how to link everything; that's what the linkscript lnkscript is for. An example of linking like this is the following, and this could replace the linking command in your makefile (note: it is vital that crt0.o is the first in the object list. Trust me on this).

DEVDIR  = e:/gbadev/kit
LIBDIR  = $(DEVDIR)/lib/gcc-lib/arm-agb-elf/3.0.2
LIBDIR2 = $(DEVDIR)/arm-agb-elf/lib

LDFLAGS = -L$(LIBDIR) -L$(LIBDIR2) -T lnkscript -lstdc++ -lgcc -lc

# PROJ, OBJS defines omitted for clarity

$(PROJ).elf : $(OBJS)
	ld -o $@ crt0.o $^ $(LDFLAGS)

The reason I'm not going to use this is way of linking is that it won't accept the mathematical library if I try to link it as well; I get all kinds of weird “undefined reference to `__negdf2'” messages. This is probably ignorance of proper procedure on my part (using the wrong libgcc.a perhaps?), but it does mean that the project won't compile and I'm dead in the water. Fortunately, there is another way.


It so happens that there is a way of linking with gcc without using the standard boot-code using the -nostartfiles option. This is buried somewhere is the GCC manual and mentioned in the Dev'rs faq right about here. This option will remove the standard crt0.o from the list so you can use yours instead, but still includes the proper default libraries. The makefile lines to use this strategy are

LDFLAGS = -nostartfiles -T lnkscript

# PROJ, OBJS defines omitted for clarity

$(PROJ).elf : $(OBJS)
	gcc -o $@ crt0.o $^ $(LDFLAGS) 

No messing with extra directories and libraries and such, just the extra option and you're done. Easy, eh? Oh, crt0.o should still go first, by the way. And whether or not you actually need the linkscript as well is compiler dependent; DKA r4 can do without it, but r5b3 requires it.


16.5.2. Side effects

Doing your own linking can/does have some other side effects. First of all, if you were planning to use C++ instead of pure C you should also link crtbegin.o and crtend.o. These files take care of the default constructor and destructor business of C++ classes. This is 5500 bytes of additional code (one of the reasons I'm using C rather than C++). Secondly, the main C function is now called AgbMain instead of the usual main. Unless you use C++, in which case it's still just plain ol' main. This is actually particular to which crt0.S you use, because that's the file that does the jump into C-code. I suppose I could have altered it but I try to keep my meddling with boot-code to a minimum until I learn more about how it works.


But, like I said, using a custom crt0+linkscript is neither required nor advised for interrupts. Simply using the switchboard code given earlier will work just as well, with the added benefit of better readability and less hassle when linking. Still, you need to be aware of the existence of these files because demo code found at older sites may still use this method.

16.6. Finally, an interrupt demo!

Today's demo shows a little bit of everything described above:

The controls are as follows:

A Toggles between asm switchboard and C direct isr.
B Toggles HBlank and VCount priorities.
L,R Toggles VCount and HBlank irqs on and off.
#include <stdio.h>
#include <tonc.h>

CODE_IN_IWRAM void hbl_grad_direct();

const char *strings[]= 
{
    "asm/nested", "c/direct  ", 
    "HBlank", "VCount"
};

// Function pointers to master isrs
const fnptr master_isrs[2]= 
{
    (fnptr)isr_master_nest,
    (fnptr)hbl_grad_direct 
};


// (1) Uses tonc_isr_nest.s  isr_master_multi() as a switchboard
void hbl_grad_routed()
{
    u32 clr= REG_VCOUNT/8;
    pal_bg_mem[0]= RGB15(clr, 0, 31-clr);
}

// (2) VCT is triggered at line 80; this waits 40 scanlines
void vct_wait()
{
    pal_bg_mem[0]= CLR_RED;
    while(REG_VCOUNT<120);
}

int main()
{
    u32 bDirect=0, bVctPrio= 0;
    char str[32];

    txt_init_std();
    txt_init_se(0, BG_CBB(0)|BG_SBB(31), 0, CLR_ORANGE, 0);
    se_puts(8, 8, "ISR : \nPrio: \nIE  : ", 0);
    se_puts(56,  8, strings[bDirect], 0);
    se_puts(56, 16, strings[2+bVctPrio], 0);

    REG_DISPCNT= DCNT_MODE0 | DCNT_BG0;

    // (3) Initialise irqs; add HBL and VCT isrs 
    // and set VCT to trigger at 80
    irq_init(master_isrs[0]);
    irq_add(II_HBLANK, hbl_grad_routed);
    irq_add(II_VCOUNT, vct_wait);
    BF_SET(REG_DISPSTAT, 80, DSTAT_VCT);

    while(1)
    {
        vid_vsync();
        key_poll();

        // Toggle HBlank irq
        if(key_hit(KEY_R))
            REG_IE ^= IRQ_HBLANK;

        // Toggle Vcount irq
        if(key_hit(KEY_L))
            REG_IE ^= IRQ_VCOUNT;

        siprintf(str, "%04X", REG_IE);
        se_puts(56, 24, str, 0);

        // (/4) Toggle between 
        // asm switchblock + hbl_gradient (blue->red)
        // or hbl_grad_direct (black->green)
        if(key_hit(KEY_A))
        {
            bDirect ^= 1;
            irq_set_master(master_isrs[bDirect]);
            se_puts(56,  8, strings[bDirect], 0);
        }

        // (5) Switch priorities of HBlank and VCount
        if(key_hit(KEY_B))
        {
            irq_set(II_VCOUNT, vct_wait, bVctPrio);
            bVctPrio ^= 1;
            se_puts(56, 16, strings[2+bVctPrio], 0);
        }
    }

    return 0;
}

The code listing above contains the main demo code, the HBlank, and VCount isrs that will be routed and some sundry items for convenience. The C master isr called hbl_grad_direct() is in another file, which will be discussed later.

First, the contents of the interrupt service routines (points 1 and 2). Both routines are pretty simple: the HBlank routine (hbl_grad_routed()) uses the value of the scanline counter to set a color for the backdrop. At the top, REG_VCOUNT is 0, so the color will be blue; at the bottom, it'll be 160/8=20, so it's somewhere between blue and red: purple. Now, you may notice that the first scanline is actually red and not blue: this is because a) the HBlank interrupt occurs after the scanline (which has caused trouble before in the DMA demo) and b) because HBlanks happen during the VBlank as well, so that the color for line 0 is set at REG_VCOUNT=227, which will give a bright red color.

The VCount routine sets the color to red and them waits until scanline 120. At point 3, the VCount irq is set up to be triggered at 80, so that this routine will wait for 40 scanlines until moving out of the service routine again. In the mean time, HBlank irqs happen as they always did. Or not, of course. This is where you see what prioritized and nested interrupts actually do. Allowing them to nest means that the HBlank interrupts can still act even though the VCount routine isn't finished yet; the result is a single red line (fig 16.1a). Prioritized means that you can disallow the HBlank from breaking into the VCount if the VCount interrupt has a higher priority, resulting in a red block because the next HBlank interrupt isn't handled until the 120th line (fig 16.1b). Now, it is possible to do the prioritizing inside the isr itself, but it's and probably faster to do it in the master isr, which is what I've done with isr_master_nest().

Point 3 is where the interrupts are set up in the first place. The call to irq_init() clears the isr table and sets up the master isr. Its argument can be NULL, in which case the tonc's default master isr is used. The calls to irq_add() initialize the HBlank and VCount interrupts and their service routines. If you don't supply a service routine, the switchboard will just acknowledge the interrupt and return. There are times when this is useful, as we'll see in the next chapter. irq_add() already takes care of both REG_IE and the IRQ bits in REG_DISPSTAT; what it doesn't do yet is set the VCount at which the interrupt should be triggered, so this is done separately. Note that the oder in which irq_add() is called matters: the first irq called has the highest priority.

You can switch between master service routines with irq_set_master(), as is done at point 4. While irq_add() just fills the isr table and doesn't really select a priority, irq_set() does (point 5). The last parameter allows you to add the isr at that priority and moves the rest out of the way. There are more options for it, but this will do for now. Note that changing the priorities wouldn't happen often, but it's still useful to have something around that can do that.

Gradient hbl>vct
Fig 16.1a: Gradient; nested HBlank has priority.
Gradient vct>hbl
Fig 16.1b: Gradient; nested VCount has priority.
Gradient hbl in C
Fig 16.1c: Gradient; HBlank in master ISR in C.

This explains most of what the demo can show. For Real Life use, irq_init() and irq_add() are pretty much all you need, but the demo shows some other interesting things as well. Also interesting is that the result is actually a little different for VBA, no$gba and hardware, which brings up another point: interrupts are time-critical routines, and emulating timing is rather tricky. If something works on an emulator but not hardware, interrupts are a good place to start looking.

This almost concludes demo section, except for one thing: the direct HBlank isr in C. But to do that, we need it in ARM code and to make it efficient, it should be in IWRAM as well. And here's how we do that.


16.6.1. Using ARM + IWRAM code

The master interrupt routines have to be ARM code. As we've always compiled to THUMB code, this would be something new. The reason that we've always compiled to THUMB code is that the 16bit buses of the normal code sections make ARM-code slow there. However, what we could do is put the ARM code in IWRAM, which has a 32bit bus (and no waitstates) so that it's actually beneficial to use ARM code there.

Compiling as ARM code is actually quite simple: use -marm instead of -mthumb. The IWRAM part is what causes the most problems. There are GCC extensions that let you specify which section a function should be in. Tonclib has the following macros for them:

#define DATA_IN_EWRAM __attribute__((section(".ewram")))
#define DATA_IN_IWRAM __attribute__((section(".iwram")))
#define  BSS_IN_EWRAM __attribute__((section(".sbss")))

#define CODE_IN_EWRAM __attribute__((section(".ewram"), long_call))
#define CODE_IN_IWRAM __attribute__((section(".iwram"), long_call))


// --- Examples of use: ---
// Declarations
extern DATA_IN_EWRAM u8 data[];
CODE_IN_IWRAM void foo();


// Definitions
DATA_IN_EWRAM u8 data[8]= { ... };

CODE_IN_IWRAM void foo()
{
    ....
}

The EWRAM/IWRAM things should be self-explanatory. The DATA_IN_x things allow global data to be put in those sections. Note that the default section for data is IWRAM anyway, so that may be a little redundant. BSS_IN_EWRAM concerns uninitialized globals. The difference with initialized globals is that they don't have to take up space in ROM: all you need to know is how much space you need to reserve in RAM for the array.

The function variants also need the long_call attribute. Code branches have a limited range and section branches are usually too far to happen by normal means and this is what makes it work. You can compare them with ‘far’ and ‘near’ that used to be present in PC programming.

It should be noted that these extensions can be somewhat fickle. For one thing, the placement of the attributes in the declarations and definitions seems to matter. I think the examples given work, but if they don't try to move them around a bit and see if that helps. A bigger problem is that the long_call attribute doesn't always want to work. Previous experience has led me to believe that the long_call is ignored unless the definition of the function is in another file. If it's in the same file as the calling function, you'll get a ‘relocation error’, which basically means that the jump is too far. The upshot of this is that you have to separate your code depending on section as far as functions are concerned. Which works out nicely, as you'll want to separate ARM code anyway.

So, for ARM/IWRAM code, you need to have a separate file with the routines, use the CODE_IN_IWRAM macro to indicate the section, and use -marm in compilation. It is also a good idea to add -mlong-calls too, in case you ever want to call ROM functions from IWRAM. This option makes every call a long call. Some toolchains (including DKP) have set up their linkscripts so that files with the extension .iwram.c automatically go into IWRAM, so that CODE_IN_IWRAM is only needed for the declaration.


In this case, that'd be the file called isr.iwram.c. This contains a simple master isr in C, and only takes care of the HBlank and acknowledging the interrupts.

#include <tonc.h>

CODE_IN_IWRAM void hbl_grad_direct();

// an interrupt routine purely in C
// (make SURE you compile in ARM mode!!)
void hbl_grad_direct()
{
    u32 irqs= REG_IF & REG_IE;

    REG_IFBIOS |= irqs;
    if(irqs & IRQ_HBLANK)
    {
        u32 clr= REG_VCOUNT/8;
        pal_bg_mem[0]= RGB15(0, clr, 0);
    }

    REG_IF= irqs;
}
Flags for ARM+IWRAM compilation

Replace the ‘-mthumb’ in your compilation flags by ‘-marm -mlong-calls’. For example:

CBASE   := $(INCDIR) -O2 -Wall

# ROM flags
RCFLAGS := $(CBASE) -mthumb-interwork -mthumb
# IWRAM flags
ICFLAGS := $(CBASE) -mthumb-interwork -marm -mlong-calls

For more details, look at the makefile for this project.


Modified Feb 8, 2007, J Vijn. Get all Tonc files here