Timers | Contents | BIOS calls |
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.
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
.
F E | D | C | B A 9 8 | 7 | 6 5 4 3 | 2 | 1 | 0 |
- | C | K | Dma | Com | Tm | Vct | Hbl | Vbl |
bits | name | define | description |
---|---|---|---|
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-6 | Tm | 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-B | Dma | 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. |
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.
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.
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.
r0-r3, r12, lr
) are pushed onto the stack.
0300:7FFC
and
branches to that address.
0300:7FFC
is run. Since we're
in ARM-state now, this must to be ARM code!
REG_IF
, then return from
the isr by issuing a bx lr
instruction.
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.
REG_INTMAIN
jumps to must
be ARM code! If you compile with the -mthumb
flag, the
whole thing comes to a screeching halt.
REG_IME
is not the only thing that
allows interrupts, there's a bit for irqs in the program status
register (PSR) as well. When an interrupt is raised, the
CPU disables interrupts there until the whole thing is over and done
with.
hbl_pal_invert()
doesn't check whether it has been
activated by an HBlank interrupt. Now, in this case it doesn't
really matter because it's the only one enabled, but when you use
different types of interrupts, sorting them out is essential. That's
why we'll create an interrupt switchboard in
the next section.
REG_IFBIOS
(== 0300:7FF8
). The use is the
same as REG_IF
.
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.
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.
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.
__isr_table[]
.
An interrupt table. This is a table of function pointers to the
different isr's. Because the interrupts should be prioritized, the
table should also indicate which interrupt the pointers belong to.
For this, we'll use an IRQ_REC
struct.
irq_init()
/
irq_set_master()
.
Set master isr. irq_init()
initializes the interrupt
table and interrupts themselves as well.
irq_enable()
/
irq_disable()
.
Functions to enable and disable interrupts. These will take care of
both REG_IE
and whatever register the sender bit is on.
I'm keeping these bits in an internal table called
__irq_senders[]
and to be able to use these, the input
parameter of these functions need to be the index of the
interrupt, not the interrupt flag itself. Which is why I have
II_foo
counterparts for the
IRQ_foo
flags.
irq_set()
/
irq_add()
/
irq_delete()
.
Function to add/delete interrupt service routines. The first allows
full prioritization of isr's; irq_add()
will replace
the current irs for a given interrupt, or add one at the end of the
list; irq_delete()
will delete one and correct the
list for the empty space.
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; }
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, ®_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, {®_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, {®_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
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.
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.
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.
Today's demo shows a little bit of everything described above:
isr_master_nest
which routes
the program flow to an HBlank isr, and an isr in C that
handles the HBlank interrupt directly. For the latter to work, we'll
need to use ARM-compiled code, of course, and I'll also show you
how in a minute.
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.
![]() Fig 16.1a: Gradient; nested HBlank has priority. |
![]() Fig 16.1b: Gradient; nested VCount has priority. |
![]() 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.
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; }
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.
Prev | Contents | Next |
Timers | BIOS calls |