gbadev.org forum archive

This is a read-only mirror of the content originally found on forum.gbadev.org (now offline), salvaged from Wayback machine copies. A new forum can be found here.

Coding > Multiple interrupts handler.

#26311 - Lord Graga - Sun Sep 12, 2004 6:43 pm

Heya. I am looking for an interrupt handler that supports multiple interrupts.

I have tried to use the one from jeff's crt0, but it doesn't seem like that I can get it right.


I am using DevkitARM.

#26328 - Krakken - Mon Sep 13, 2004 1:00 am

TONC has a good one.
http://user.chem.tue.nl/jakvijn/tonc/interrupts.htm

#26332 - col - Mon Sep 13, 2004 2:45 am

Lord Graga wrote:
Heya. I am looking for an interrupt handler that supports multiple interrupts.


Do you mean a re-entrant handler?

Quote:

I have tried to use the one from jeff's crt0, but it doesn't seem like that I can get it right.

what are the symptoms? is it just not working at all?
or is it working, but causing instability?

cheers

Col

#26484 - Lord Graga - Thu Sep 16, 2004 10:38 pm

col wrote:
Do you mean a re-entrant handler?


Yes, I probably do. I do not know much about interrupts, and until not, I have just used the handler that came with libgba, which is a single interrupts handler.

col wrote:
what are the symptoms? is it just not working at all?


With screen, which is a sign of something that has fucked up badly, which probably means that some registers has been changed on a time where they should not be changed/etc.


LG

#26490 - col - Fri Sep 17, 2004 12:33 am

That's not a whole lot to go on :)

Anyway, here's another question:
When you tried Jeff Fs multiple interrupt code, did you set it to switch back to the user stack, or to use the irq stack ?

cheers

Col

#27057 - abilyk - Sat Oct 02, 2004 7:18 am

Lord Graga,

You say you're looking for an interrupt handler that supports multiple interrupts. By that do you mean nested interrupts? I've seen a lot of information on handlers that can support more than one interrupt... you just use a series of if statements and process all interrupts that are both enabled in REG_IE and requested in REG_IF. In contrast, I haven't seen much information on how to deal with nested interrupts (an interrupt can be interrupted by another interrupt).

I needed to support nested interrupts because I plan to do some scanline tricks using the HBlank interrupt. If some random interrupt is currently being processed when an HBlank interrupt fires, it's imperative that I halt the current interrupt and deal with the HBlank; if I wait for the first interrupt to finish, I may miss the HBlank window completely. Using info I found in GBATEK, the Cowbite Spec, TONC, and on this forum, I've written my own nested interrupt handler in ARM assembly.

I've spent the last week or so researching and programming, and I believe I understand how the whole process works and that my code does what it should. Following is the relevant info I've found in my research and the code for my interrupt handler. If I have all my facts straight here, hopefully some people will benefit from it. If I'm incorrect in places, or if there are problems with my code (I'm a newbie to ARM assembly), I'd be happy for someone to set the record straight.

----------------------

In order for an interrupt to be requested (its bit in REG_IF to be set to 1), the interrupt enable flag in the appropriate hardware register must be set. For example, in order for HBlank interrupts to be requested, bit 4 of REG_DISPSTAT must be set to 1. Once this bit in REG_DISPSTAT is set, the HBlank request bit in REG_IF will be set to 1 every time an HBlank occurs, even if interrupts are disabled in REG_IME, REG_IE, and the CPSR (current program state register).

If interrupts are disabled in either REG_IME (bit 0 cleared to 0) or in the CPSR (bit 7 set to 1), no interrupts can occur. Interrupts must be enabled in both REG_IME and the CPSR in order for interrupts to be able to occur.

If interrupts are enabled in both REG_IME and the CPSR, an interrupt will occur when an interrupt's flag is set in both REG_IE and REG_IF. In most cases, interrupts will be enabled in both REG_IME and the CPSR, a particular interrupt will be enabled in REG_IE, and it is not until that particular interrupt is requested and its bit set in REG_IF that the interrupt actually occurs. However, it is possible that a particular interrupt will be enabled in REG_IE and requested in REG_IF, but it cannot yet occur because interrupts are disabled in either (or both) REG_IME and/or the CPSR. Once interrupts are enabled in both REG_IME and the CPSR, the interrupt will immediately occur, even if it was requested a number of cycles/frames/minutes ago.



When an interrupt occurs, the following events happen, in order:

Performed immediately when a CPU IRQ exception occurs:
  • The PC (program counter), which points to the next instruction of your interrupted process, is saved to LR (link register).
  • The CPSR is saved to SPSR_irq (saved program state register for IRQ mode).
  • The CPSR is updated to ARM state and IRQ mode.
  • Interrupts are disabled in the CPSR.
  • The PC is set to 0x18 (the exception vector for IRQs).


Interrupt exception vector:
  • Branch to 0x128 (the interrupt handler in the BIOS).


BIOS interrupt handler:
  • Registers r0-r3,r12,r14 are pushed onto the IRQ stack (not the system/user mode stack).
  • The PC, which points to the BIOS instruction after the upcoming branch, is saved to LR_irq.
  • Branch to the address stored in 0x03007FFC (should be a pointer to your interrupt handler, must be compiled as ARM).


My nested interrupt handler:
  • Push any registers that must not be trashed. I had to push r4-r7, LR_irq.
  • Determine which interrupts to service by &-ing (bitwise AND-ing) the values of REG_IE and REG_IF.
  • Acknowledge in REG_IF the interrupts you are going to service by setting the appropriate bits to 1. Yes, they were 1 to begin with, but setting them to 1 clears them to 0. Do it, it works. Note that writing a 0 to a bit does nothing. If REG_IF has two bits set, but you only want to acknowledge and clear one of the bits, writing a 1 to that bit does what you want. The bit that you wrote a 1 to will be cleared to 0, and the other bit, which you wrote a 0 to, will still be set to 1. Also note that if you do not acknowledge an occurred interrupt in REG_IF, the interrupt will erroneously occur again as soon as interrupts are re-enabled in the CSPR.
  • Acknowledge in IWRAM address 0x03007FF8 (I call this ISR_RAM_IF, but I've seen it called BIOS_IF) the interrupts you are going to service by setting the appropriate bits to 1. The memory at this address is not a special-case register like the funky REG_IF explained above. If you write 0 to all the bits, all the bits will become 0, and vice-versa. You must use bitwise OR (C code would be ISR_RAM_IF = ISR_RAM_IF | flags_to_clear;) in order to not overwrite any bits that should be maintained. Note that if you do not acknowledge an occurred interrupt in ISR_RAM_IF, SWI (software interrupt) calls which deal with interrupts (like SWI 4, IntrWait) will not work correctly.
  • Save contents of SPSR_irq. If a nested interrupt occurs, SPSR_irq will be overwritten, so we must preserve it.
  • Disable interrupts in REG_IME. They're already disabled in the CPSR, but since we want interrupts to remain disabled for the time being and the next step is to enable interrupts in the CPSR, we must disable interrupts here so they remain disabled.
  • Enable interrupts in the CPSR. Interrupts will remain disabled because they are disabled in REG_IME.
  • For each possible interrupt (in order of priority),
    • Test to see if it is one of the interrupts to be serviced. If not, move on to the next interrupt. If so, continue processing this interrupt.
    • If the interrupt is highest priority and cannot be interrupted (like HBlank is in my case), just call that interrupt's ISR (interrupt service routine).
    • If the interrupt is medium priority -- it can be interrupted by some interrupts but not by others -- disable the specific lower-priority interrupts in REG_IE, enable interrupts in REG_IME, and call the interrupt's ISR. It can be interrupted only by the higher-priority interrupts that you did not disable in REG_IME. After the interrupt's ISR returns, disable interrupts in REG_IME and re-enable the lower-priority interrupts you had disabled in REG_IE.
    • Otherwise, if the interrupt is low priority -- it can be interrupted by any interrupt -- enable interrupts in REG_IME, call the interrupt's ISR, and then disable interrupts in REG_IME again.

  • After all interrupts have been processed, restore the contents of SPSR_irq.
  • Enable interrupts in REG_IME.
  • Pop the registers you pushed in the beginning.
  • Return to caller.


BIOS interrupt handler (again):
  • Registers r0-r3,r12,r14 are popped off the IRQ stack.
  • Restore the contents of CPSR from SPSR_irq.
  • The PC is set to the next instruction of your initially-interrupted process, at which point everything is back to pre-interrupt state (barring any changes your ISR made, of course).



Here's my code:

Code:
/*
    ------------------------------------------------------------------------------------
     interrupt.S
     andrew p. bilyk
     dungeon monkey studios
     September 21, 2004
    ------------------------------------------------------------------------------------
   
   
    ------------------------------------------------------------------------------------
     Unused registers
    ------------------------------------------------------------------------------------
    r1
    r2
    r3
    r8
    r9
    r10
    r11 = FP (frame pointer)
    r12 = IP
    r13 = SP (stack pointer)
    r15 = PC (program counter)
   
   
    ------------------------------------------------------------------------------------
     Used by MainInterruptServiceRoutine
    ------------------------------------------------------------------------------------
    r0  = scratch data
    r4  = interrupts_to_be_serviced
    r5  = enabled_interrupts
    r6  = saved_spsr_irq
    r7  = REG_BASE_ADDRESS
    r14 = LR (link register)
*/


    .section  .iwram
    .align    2
    .global   MainInterruptServiceRoutine
    .type     MainInterruptServiceRoutine,function
   
   
@ void MainInterruptServiceRoutine(void)
@----------------------------------------
MainInterruptServiceRoutine:

    @ save contents of r4-r7 and link register
    @ we'll be using r4-r7, and a function call to an IRQ handler will overwrite lr
    stmfd   sp!, {r4-r7, lr}      @ push r4-r7 & link register
   
    @ store base address used to refer to REG_IE, REG_IF, REG_IME, & ISR_RAM_IF
    mov     r7,    #0x4000000     @ r7 = 0x4000000
    add     r7, r7, #0x200        @ r7 = REG_BASE_ADDRESS = 0x4000200
   
    @ determine which interrupts to service (enabled_interrupts & requested_interrupts)
    ldr     r5, [r7]              @ r5_lo = enabled_interrupts = REG_IE;  r5_hi = requested_interrupts = REG_IF
    and     r4, r5, r5,lsr#16     @ r4 = interrupts_to_be_serviced = REG_IE & REG_IF
   
    @ acknowledge interrupts_to_be_serviced in REG_IF
    strh    r4, [r7, #0x2]        @ REG_IF = r4 = interrupts_to_be_serviced
   
    @ acknowledge interrupts_to_be_serviced in ISR_RAM_IF
    ldr     r0, [r7, #-0x208]     @ r0 = ISR_RAM_IF
    orr     r0, r0, r4            @ r0 = ISR_RAM_IF | interrupts_to_be_serviced
    str     r0, [r7, #-0x208]     @ ISR_RAM_IF = r0 = ISR_RAM_IF | interrupts_to_be_serviced
   
    @ save contents of SPSR_irq;  if a nested interrupt occurs, it will overwrite the register
    mrs     r6, spsr              @ r6 = saved_spsr_irq = SPSR_irq
   
    @ disable interrupts via REG_IME
    mov     r0, #0                @ r0 = 0
    strh    r0, [r7, #0x8]        @ REG_IME = r0 = 0
   
    @ enable interrupts via CPSR (but they are still disabled due to REG_IME)
    mrs     r0, cpsr              @ r0 = CPSR
    bic     r0, r0, #0x80         @ set CPSR's IRQ flag to enable by clearing bit 7
    msr     cpsr, r0              @ CPSR = r0


.L_HBLANK:
    @ if((interrupts_to_be_serviced & HBlank flag) != 0), handle interrupt
    @ otherwise, branch to next test
    tst     r4, #0x0002           @ test interrupts_to_be_serviced & HBlank flag
    beq     .L_TIMER_0            @ if result of test == 0, branch to next test
   
    @ call specific ISR
    ldr     r0, .L_HBLANK_ISR     @ load address of function pointer into r0
    ldr     r0, [r0]              @ load function pointer into r0
    mov     lr, pc                @ save program counter in link register
    bx      r0                    @ call ISR pointed to by the function pointer
   

.L_TIMER_0:
    @ if((interrupts_to_be_serviced & Timer_0 flag) != 0), handle interrupt
    @ otherwise, branch to next test
    tst     r4, #0x0008           @ test interrupts_to_be_serviced & Timer_0 flag
    beq     .L_KEYPAD             @ if result of test == 0, branch to next test
   
    @ temporarily disable lower-priority interrupts via REG_IE
    bic     r0, r5, #0x1000       @ r0 = enabled_interrupts & ~(key input flag)
    bic     r0, r0, #0x0001       @ r0 = r0 & ~(vblank flag)
    strh    r0, [r7]              @ REG_IE = r0 = enabled_interrupts & ~lower_priority_flags
   
    @ re-enable interrupts via REG_IME
    mov     r0, #1                @ r0 = 1
    strh    r0, [r7, #0x8]        @ REG_IME = r0 = 1
   
    @ call Timer 0 ISR
    ldr     r0, .L_TIMER_0_ISR    @ load address of function pointer into r0
    ldr     r0, [r0]              @ load function pointer into r0
    mov     lr, pc                @ save program counter in link register
    bx      r0                    @ call ISR pointed to by the function pointer
   
    @ disable interrupts via REG_IME
    mov     r0, #0                @ r0 = 0
    strh    r0, [r7, #0x8]        @ REG_IME = r0 = 0
   
    @ re-enable lower-priority interrupts via REG_IE
    strh    r5, [r7]              @ REG_IE = r5 = enabled_interrupts
   

.L_KEYPAD:
    @ if((interrupts_to_be_serviced & Keypad flag) != 0), handle interrupt
    @ otherwise, branch to next test
    tst     r4, #0x1000           @ test interrupts_to_be_serviced & Keypad flag
    beq     .L_VBLANK             @ if result of test == 0, branch to next test

    @ re-enable interrupts via REG_IME
    mov     r0, #1                @ r0 = 1
    strh    r0, [r7, #0x8]        @ REG_IME = r0 = 1
   
    @ call Keypad ISR
    ldr     r0, .L_KEYPAD_ISR     @ load address of function pointer into r0
    ldr     r0, [r0]              @ load function pointer into r0
    mov     lr, pc                @ save program counter in link register
    bx      r0                    @ call ISR pointed to by the function pointer
   
    @ disable interrupts via REG_IME
    mov     r0, #0                @ r0 = 0
    strh    r0, [r7, #0x8]        @ REG_IME = r0 = 0


.L_VBLANK:
    @ if((interrupts_to_be_serviced & VBlank flag) != 0), handle interrupt
    @ otherwise, branch to next test
    tst     r4, #0x0001           @ test interrupts_to_be_serviced & VBlank flag
    beq     .L_END                @ if result of test == 0, branch to next test
   
    @ re-enable interrupts via REG_IME
    mov     r0, #1                @ r0 = 1
    strh    r0, [r7, #0x8]        @ REG_IME = r0 = 1
   
    @ call Vblank ISR
    ldr     r0, .L_VBLANK_ISR     @ load address of function pointer into r0
    ldr     r0, [r0]              @ load function pointer into r0
    mov     lr, pc                @ save program counter in link register
    bx      r0                    @ call ISR pointed to by the function pointer
   
    @ disable interrupts via REG_IME
    mov     r0, #0                @ r0 = 0
    strh    r0, [r7, #0x8]        @ REG_IME = r0 = 0

   
.L_END:
    @ restore contents of SPSR_irq
    msr     spsr, r6              @ SPSR_irq = r6
   
    @ re-enable interrupts via REG_IME
    mov     r0, #1                @ r0 = 1
    strh    r0, [r7, #0x8]        @ REG_IME = r0 = 1
   
    @ restore contents of r4-r7 and link register
    ldmfd   sp!, {r4-r7, lr}      @ pop r4-r7 & link register
   
    @ return to caller
    bx      lr                    @ return to caller


.L_HBLANK_ISR:
    .word    hblank_isr


.L_TIMER_0_ISR:
    .word    timer_0_isr


.L_KEYPAD_ISR:
    .word    keypad_isr


.L_VBLANK_ISR:
    .word    vblank_isr


.Lfe1:
    .size    MainInterruptServiceRoutine,.Lfe1-MainInterruptServiceRoutine

#27442 - col - Wed Oct 13, 2004 2:39 am

Code:

@ save contents of SPSR_irq;  if a nested interrupt occurs, it will overwrite the register
    mrs     r6, spsr              @ r6 = saved_spsr_irq = SPSR_irq

what happens to the 'saved' SPSR_irq in r6 if a nested interrupt occurs ?



col

#27448 - abilyk - Wed Oct 13, 2004 3:32 am

col wrote:

what happens to the 'saved' SPSR_irq in r6 if a nested interrupt occurs ?


When a nested interrupt occurs, the current interrupt-handling process is put on hold and the whole interrupt procedure starts again for the new interrupt, starting with saving the PC to LR. r6 is not accessed until the process reaches my interrupt handler. The first instruction pushes r6 and other registers to the stack, and shortly afterwards r6 is overwritten (by the new interrupt's saved SPSR_irq). But after this new interrupt is processed, and right before my interrupt handler returns to the caller, the previous contents of r6 and other registers are restored by being popped from the stack. After a few instructions from the BIOS interrupt handler, none of which touch r6, control returns to our initial interrupt-handling process with the correct contents of r6 in place.

#28664 - DekuTree64 - Thu Nov 04, 2004 4:46 am

Hi Andrew,

I was doing some testing on this idea, and I think I ran across a problem. When you have a meduim/low priority interrupt, enable interrupts, and then mov lr, pc to branch to the handler, then at any time another interrupt could occurr, overwriting LR and leaving that handler to return to the wrong function. As long as the handler saves LR right away, there would only be a few cycle window, but it certainly can happen.

I've only come up with two possible solutions so far. One would be to use a different register for the return address in all your interrupt handlers (but then you'd have to do them all in assembly, and never use lr for anything), and the other is to switch back to system mode and save the system SPSR/LR as well before reenabling interrupts in IME. Switching to system mode seems to work, but I haven't gotten it to run 100% reliably yet, so I may have overlooked some other case as well.
What do you think, any other good solutions?
_________________
___________
The best optimization is to do nothing at all.
Therefore a fully optimized program doesn't exist.
-Deku

#28679 - abilyk - Thu Nov 04, 2004 6:49 am

Deku, you're right. Going through all the steps I have listed above, there is potential for error if a nested interrupt occurs at that precise moment. However, if this is actually the case, couldn't the same issue occur when an initial interrupt is triggered? In user mode, you branch from function A to function B, but before you can push LR onto the stack, an interrupt occurs, and you would run into the problem you just described.

Instead, I think that LR will be actually be preserved in both these instances. Upon some more research, I believe that the steps in my "Performed immediately when a CPU IRQ exception occurs" and "BIOS interrupt handler (again)" sections are incomplete, or perhaps out-of-order. I'll try to correct this within the next day or two, so we can get to the bottom of the issue, one way or another.

Thanks for bringing this to my attention.

#28681 - DekuTree64 - Thu Nov 04, 2004 8:28 am

Yes, the LR_IRQ register is there to prevent that very problem. Normally the system mode LR is used, so when the initial interrupt occurrs, it gets bank-swapped wth LR_IRQ before the old PC is saved.
The problem is when the interrupt occurrs while you're in IRQ mode already. No state switch happens so the 'current' LR_IRQ gets overwritten first thing before branching to the IRQ exception vector.

After a little more thought, it does make sense that you should never allow an interrupt to happen while in IRQ mode. The hardware just wasn't designed for it.
The only restriction that switching to system mode puts on the rest of your program is that the stack pointer always has to be valid, but unless you go doing crazy assembly stuff using it as a general register, that should always be the case.

I'll have another go at it tomorrow morning. I think my real problem lies elsewhere, but at least that little bug is caught before it caused any trouble.
_________________
___________
The best optimization is to do nothing at all.
Therefore a fully optimized program doesn't exist.
-Deku

#30534 - abilyk - Wed Dec 01, 2004 10:17 pm

I haven't had much time at all lately to devote to this problem, but I'm about to get back to it and have been thinking about some potential solutions. The simplest idea I've had, though it seems like a hack, is to avoid using LR altogether when calling each specific ISR. Instead of saving the PC to LR, I could save it to r8, which I'm not using and is guaranteed not to be overwritten. When returning from each specific ISR, I'd have to pop the return address from r8 instead of LR. Since GCC wouldn't know to do this, I'd have to write each specific ISR in assembly (which is probably in my best interest anyway) or at least modify the compiled C code. See any holes in this approach?

#30559 - DekuTree64 - Thu Dec 02, 2004 5:14 am

Yes, I believe that will be 100% reliable, just forces you to use assembly. But while you're already doing a non-standard return sequence, you could make it even simpler and just have your handler functions branch directly to the return location in the main interrupt handler, rather than storing the address in r8 or any other register, since it will always be the same anyway. Only a couple cycles difference to mov r8, pc and then store/load it though.

Switching to system mode does indeed work too. The procedure is as follows:

Disable interrupts in IME
Load SPSR and save it somewhere
Load CPSR and save it somewhere
Set mode in your CPSR copy to SYS (0x1f), and clear the IRQ/FIQ disable bits (FIQ isn't really necessary, but is normally enabled)
Move to CSPR
Enable interrupts in IME

Call handler, do anything you want

Disable interrupts in IME
Load your saved copy of the original CPSR and SPSR
Move those two back into current CPSR/SPSR
Enable interrupts in IME
Return to BIOS

Either way has its disadvantages, but personally I would use the system mode switch, because it doesn't force you into writing non-standard assembly code.
_________________
___________
The best optimization is to do nothing at all.
Therefore a fully optimized program doesn't exist.
-Deku

#30561 - abilyk - Thu Dec 02, 2004 5:37 am

Ha. This "new solution" I thought I stumbled upon, you suggested it yourself almost a month ago when you first found the problem. It's been so long that I had completely forgotten about it and thought it up again myself. I probably will do the system mode switch, as I tend to be a perfectionist and having those non-standard returns just... bothers me. Thanks for the sequence of events there, that'll help. Once I get it completely nailed down I'll compile all this ISR info into a doc and send it off to Simon -- it's still on the to-do list.