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.

ASM > Problem with GBA CPU modes and interrupts

#177793 - WriteASM - Fri Mar 08, 2013 12:18 am

First a little background: I have been working on a BASIC and ASM IDE for the GBA for two months now (called bASMic), and have been having good success. It is coded in 100% ASM, both ARM and THUMB.

Admittedly, I'm having a hard time describing my problem, but here goes: When "BREAKing" a running program via the Keypad interrupt (a la GBBasic), the CPU can be running either ARM or THUMB, and be in System, Supervisor (=BIOS), or possibly even IRQ mode. After confirming the "BREAK" with the user, it currently overwrites the return address on the IRQ stack, pointing it to a short routine that ensures ARM mode. That routine exits out to "Main_FixAll", which basically resets the GBA without clearing EXTRAM. (Reloads font and palette, also calls "isrInit", among other things.) However, if the keypad interrupt occurs in a BIOS call (=Supervisor mode), something goes wrong with the IRQ stack, which causes the auto-sleep routine to put the GBA to sleep. Although I can wake the GBA via joypad interrupt, the BIOS Sleep function never returns, rendering the GBA uselessly locked up in IRQ mode.

If I write a bASMic program that doesn't use BIOS calls (Div and ArcTan2), it does not lock up when "BREAKing".
I also noticed that "gba_crt0.s" in DevKitPRO has the CPU in System mode when jumping to user code; from reading the ARM7TDMI Reference Manual, I'd have expected it to exit in User mode. Any reasons why, besides more privileges?

Below is the code in "irq.s"; TABS=4 on my system. (Source code for the entire project totals about 519KB.)
Code:

         .arm                     @ the ARM subset is used for interrupts.
         .text
         .global isrInit, isrKbdLP, isrKbdBK, isrKbdWK      @ Low-Power, BreaK, WaKe.
         .include "source/defines.h"
         
         .equ BIOS_IF, 0x03007FF8
         .equ ISR_MAIN, 0x03007FFC
         @  For ISR mode the stack is by default initialized to 03007FA0h.
         @NOTE: Only R0-R3, R12 manually PUSHED by the BIOS.  If using others, PUSH them, too.
         @NOTE: Code break (Start+Select) uses a MANUALLY CALCULATED VALUE for the stack offset.
         
         @  Interrupt notes:
         @      Unfortunately, the SWI 2 (Halt) instruction requires that both enable bits be set for an interrupt.  Otherwise, it will do nothing.
         @     That means that we cannot exit the "low-power-mode" without the interrupt handler firing.
         @      The "OR" mask for the keyboard register is for ANY key--it does not "adapt" to the keystate before the SWI call.
         @      You cannot use the "unused bits in IF" for special messages, unfortunately.
         @
         @NOTE: Clearing bits in IE does not prevent IF bits from getting set!  If you would like to disable an interrupt, clear the
         @bit in the respective control register, NOT IE!  Note that there is no secondary interrupt enable for the cartridge interrupt.
         
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
         @                                                              @
         @  isrInit: Interrupt Service Routine Initializer.  Enable interrupts needed for@
         @bASMic's operation.  This includes TMRs, VBlank, Keys (for Break), Serial, etc.@
         @                                                            @
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
isrInit:   ldr r0, =ISR_MAIN         @ISR_MAIN < The hardcode address the BIOS looks to for the ISR handler routine
         ldr r1, =0x03003700         @ Get the address to where our interrupt handler will be.
         str r1, [r0]            @ Set the IRQ handler address.
         @  Copy our ARM interrupt handler to IWRAM for maximum speed.  The shorter the ISR takes, the better for all!
         adr r2, irqHndlr         @ Copy Start Location.  R1 already has the destination
         mov r3, #((irqH_End - irqHndlr) / 4)   @ copying one long at a time.  No LTORGs required
         @  Copy loop.
isrInitLp:   ldr r0, [r2], #4         @ Get a long
         str r0, [r1], #4         @ Write it back
         subs r3, r3, #1
         bne isrInitLp
         
         mov r4, #0x03000000         @ Address to IWRAM for setting the ISR handler routines.
         @================================
         @  Enable all interrupts we plan to support.
         ldr r0, =IE
         ldr r1, =0b0001000011111001   @13:cartridge, 12:keypad, 7:port, 6-3:timers, 0:VBlank
      @   ldr r1, =0b0001000000000001
         strh r1, [r0]            @ Set the interrupt enable bits.
         
         @  Set the ISR for VBlank interrupts.
         adr r0, ihVBlank         @ Address to the VBlank handler
         str r0, [r4, #0xC0]         @ ISR routine offset.
         @  Set the ISR for keypad interrupts
         adr r0, ihKeypad
         str r0, [r4, #0xF0]         @ Keypad handler offset.
         
         @  Turn on the master interrupt enable.
         ldr r0, =IME
         mov r1, #0x1
         str r1, [r0]            @ Master Enable bit. ($4000208 = IME)
         bx lr                  @ Return
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
         
         
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
         @                                                              @
         @  isrKbdLP: Set the keyboard interrupt for the lower-power keyboard mode.      @
         @  isrKbdBK: Set the keyboard interrupt for the "break" AND mask mode, ISR.      @
         @  isrKbdWK: Set the interrupt for the "wake" mode, ISR disabled.            @
         @                                                            @
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
         @  Initialize the keyboard interrupt for the BREAK combination of START+SELECT.
isrKbdLP:   @ldr r1, =0b0100001111111111   @ All keys, OR mask (15)
         mvn r1, #0b1011110000000000   @ All keys, OR mask (15)--remember than an MVN inverts everything!
         mov r2, #0               @ No ISR
         b iK_WR
         @--------------------------------
isrKbdBK:   mov r2, #59               @ Keypad ISR "Break" Enabled.  Security code to try to prevent lockups.
         b iK_AndMsk
         @--------------------------------
isrKbdWK:   mov r2, #0               @ No "Break"
iK_AndMsk:   mov r1, #0xC000            @ Use the Logical AND method (15) and interrupt enable (14) for wakeup.
         add r1, #0x000C            @ Give the mask as START+SELECT.
         
         @--------------------------------
         @  Write to the actual registers.
iK_WR:      mov r0, #0x3000000         @ Hardcode address to IWRAM. 
         strb r2, [r0, #255]         @ 0x030000FF.  "KeypadISR Flags Register" (our doing, not the GBA's)
         
         ldr r0, =0x4000132         @ pointer to KeyControl register
         strh r1, [r0]            @ Set the keyboard interrupt's flags
         bx lr
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
         
         
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
         @                                                              @
         @  isrHndlr: Interrupt Service Routine Handler.  An interrupt occured; determine@
         @what it is, and take the appropriate action.                           @
         @                                                            @
         @   THE SHORTER THIS ROUTINE, THE BETTER FOR EVERYONE.                     @
         @  Note: The BIOS interrupt handler PUSHes R0-R3, R12                     @
         @                                                            @
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
irqHndlr:   @push {lr}
         @ldr r0, =BIOS_IF         @BIOS_IF < If you would like to use BIOS VBlank wait SWI, this code must be here.
         @ldr r1, [r0]            @ Otherwise, just comment it out!
         @orr r1, #0x1
         @str r1, [r0]
         
         @  "Check loop" to see what caused the interrupt
ihInters:   mov r3, #0x04000000         @ Going for IF, 0x04000202
         add r3, r3, #0x200         @ MOV/ADD/ADD much faster than an LDR
         
         @  No SWPH, unfortunately--and a SWP (32-bit) on $4000202 wouldn't work very well.
         ldrh r12, [r3, #2]         @ What interrupt flags are set?
         @  DON'T reset IF here--as easy as that is to do.  That can result in nested interrupts, and those can cause unexplained actions.
                  
         mov r2, #0x03000000         @ Root address to IWRAM
         add r2, r2, #0xC0         @ Refine to interrupt handler address.  This is much faster than an LDR, and also requires no .ltorg
         mov r1, #14               @ #of interrupt handlers.
         @  Loop to find out what caused the interrupt
ihLoop:      lsrs r12, r12, #1         @ Check the next IE bit.
         bcs ihJump
         beq ihClear               @ If no set interrupt bits, reset IF ourselves, and exit.  If no handler for an interrupt, it is skipped.
         @  Point to the next interrupt handler address with each iteration.
         add r2, r2, #4
         b ihLoop
         
         @  An interrupt bit was set.
ihJump:      ldr r0, [r2]
         movs r0, r0               @ Is a handler specified?
         beq ihLoop               @ No handler.
         bx r0                  @ There you go.
         @----------------------------
ihClear:   mvn r1, #0
         strh r1, [r3, #2]         @ Reset all the interrupt flags.
         bx lr
irqH_End:   @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
         
         
         
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
         @                                                              @
         @  ihVBlank: Interrupt Service Routine for the VBlank Interrupt.            @
         @Make Sprite #0 blink on and off at ~2Hz, also handle a 3-minute timeout.      @
         @                                                            @
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
ihVBlank:   ldr r0, [sp, #-32]         @ Get the "static" value from the IRQ Stack, containing a "VBlank Count"
         add r0, r0, #1            @ Increment it
         str r0, [sp, #-32]         @ Save it again.
         @  Provide a 3-minute timeout.  "Keyboard" will reset this if a key pressed.
         cmp r0, #10816            @ barely fits in the CMP, but it does fit.
         @DEBUG: This a weak Band-Aid patch to fix an infinite hang after a BREAK.  (Should be a BGT.)
         @In other words, a BEQ doesn't solve the problem, but merely reduces the chances you'll hit it.
         @The problem seems to be when we try to BREAK from a SWI.  Perhaps we need to reset the Supervisor stack?
         beq ihVB_PSave            @ Power save...DEBUG: Only if an EXACT match.
         
         @  Make the cursor sprite blink.  MODding by binary powers is relatively easy.
         @--------------------------------
         tst r0, #0x1F            @ 32 counts, toggle on-and-off once per second.
         bne ihVB_Done            @ Reset the VBlank Interrupt Flag.
         
         mov r1, #0x7000000         @ start address for OAM fits nicely in a MOV...!
         ldrh r2, [r1]            @ INT(0) has the Y position and the "Visible" bit
         eoreq r2, #0b1000000000      @ 9th bit is "Disable"
         strh r2, [r1]            @ Write the bit back.
         @--------------------------------
ihVB_Done:   mov r1, #1               @ Reset the VBlank interrupt flag
         strh r1, [r3, #2]
         bx lr                  @ Done.
         @================================
         @  Timeout!  The keyboard mode should be set to interrupt on any key press, so just sleep.
ihVB_PSave:   mov r0, #0               @ Reset the timeout
         str r0, [sp, #-32]
         @  There are some bugs with BREAKing that cause catastrophic "sleep hangs" here.  You can wake it just fine, but it's locked up.
         @VisualBoyAdvance points to the SWI below as being the problem.  However, VBA also has some bugs with interrupt emulation...
         
         mvn r1, #0               @ We have to reset flags so the SWI can go to sleep!
         strh r1, [r3, #2]
         @  Sleeping in IRQ mode /shouldn't/ be a problem.
         swi #0x30000            @ Full Stop.  Code execution halts here until the interrupt.  As the display is off, there is no VBL interrupt.
         b ihVB_Done               @ Reset the VBlank interrupt flag, although that won't be what woke it.
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
         
         
datKeyBrk:   .ascii "USER BREAK.\x80","Exit Interpreter?\x00"
         .align
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
         @                                                              @
         @  ihKeypad: Interrupt Service Routine for the Keypad Interrupt.            @
         @If the Interpreter is running, we have to BREAK it.  Unfortunately, the CPU can@
         @be in ARM -or- THUMB mode, as well as in System, IRQ and Supervisor modes!      @
         @                                                            @
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
ihKeypad:   mov r0, #0x03000000
         ldrb r0, [r0, #255]         @ 0x030000FF
         cmp r0, #59               @ Security code
         bne ihKeyRet            @ If NE, clear the interrupt flag and exit.
         @--------------------------------
         @  Keypad Break interrupt enabled.  That means we're in the THUMB Interpreter.  (But could be running ARM routines)
         push {r3, lr}            @ One more stack level--and save the ~address to the IF register (R3=0x04000200)
         
         adr r0, datKeyBrk
         mov r3, #s_Text            @ on the "run" text screen.
         bl RMsgBox               @ Red MsgBox...
         
         pop {r3, lr}            @ Get the stack back to just what the BIOS did.
         @- - - - - - - - - - - - - -
         @  If OK, we have to quit the Interpreter, and set the return address of our own call stack to Main_FixAll.
         cmp r0, #msgCancel
         beq ihKeyRet            @ Cancel selected, so continue running the program.
         
         @  OK selected.  Disable interrupts, and return to Main_FixAll.  R3 = 0x04000200
         @NOTE: The BIOS return is by "SUB PC, LR+4": the subset will return to what was last set there by a BX.
         adr r0, ihThumbRet +4      @ Counteract the BIOS's 4-byte offset.
         str r0, [sp, #4*5]         @ The BIOS PUSHed R0,R1,R2,R3,R12,--LR--.  R0 is first on the stack, LR is last
         
         @  Clear the keypad interrupt flag, return.
ihKeyRet:   mov r1, #4096            @ 1<<12 = Keypad flag.
         strh r1, [r3, #2]         @ Reset the keypad interrupt flag.
         bx lr                  @ Return...or BREAK, depending on whether we messed with the return address.
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
         .thumb
         @  The ARM specification indicates that upon the return from IRQ, FIQ, UNDEF, ABORT, SWI modes,
         @the destination language will be THUMB if that was what it was before the event.  Consequently, we must have
         @our own BX mode-changer.  BX is the ONLY instruction that can switch between ARM and THUMB
         @
         @  But sometimes, the interrupted routine will have been in ARM mode (MSGBOX, INPUTBOX, PSET).
         @The next three lines assemble in ARM to "andge r4, r1, r0, ASR #13", "andeq r4, r0, r0, LSL #14"...quite harmless.
ihThumbRet:   nop                     @ for ARM "runover" upon return.  Without this, it's "STRMI R10, [r0, -r0]"
         adr r0, ihTR_Ret
         bx r0                  @ Switch to ARM mode.  Or, if we're already there, do some nonsense ANDs, and fall through to the Branch.
         @////////////////////////////
         .arm
         .align
         @DEBUG: Make sure we're in System mode.  A hacked return to IRQ or Supervisor mode wouldn't work too well.
         @Seems this code doesn't exist--it doesn't work /with/ it, and it doesn't work /without/ it!
ihTR_Ret:   mov   r0, #0x1F            @DEBUG: Switch to System Mode, all flags cleared.
         msr   cpsr, r0            @ When changing system mode, it technically should be a R-M-W. (ARM Reference Manual.) 
         b Main_FixAll
         @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@