Reference: Exceptions & Interrupts

While the words are used interchangeably and the code to handle them is intertwined, exceptions and interrupts have a difference. The easiest way to think about the difference is exceptions happen inside the processor itself, interrupts happen somewhere else on the circuit board and generally toggle an input pin change.

That is the general case, now lets get to the Nintendo 64 implementation details. 

The Exceptions (Inside the MIPS VR4300i):

The Interrupts (Triggered outside the VR4300i):

So what can we use any of these for?

That is an overwhelming list which makes this an 'advanced topic'. With a good code library it's not too bad.

Usage

The code to use an exception/interrupt is fairly clean and easy with the available 'bass' library. The first part to understand is how to use the resulting library code. 

arch n64.cpu

endian msb

fill 1052672

origin $0000'0000

base $8000'0000

include "../LIB/N64.INC"

include "../LIB/N64_INTERRUPT.INC"

include "N64_HEADER.ASM"

insert "../LIB/N64_BOOTCODE.BIN"

Start:

    N64_INIT()

    ScreenNTSC(320, 240, BPP32, $A0100000)

    interrupt_init()

    interrupt_set_vblank($200,interrupt_vblank_handler)

    //

    // Other Initialization Code

    //

Loop:

    //

    // Game Loop

    //

    j Loop

    nop

interrupt_vblank_handler:

    interrupt_register_save()

    //

    // Your Code Here

    //

    interrupt_register_restore()

    jr ra

    nop

include "../LIB/N64_SYS.S"

include "../LIB/N64_INTERRUPT.S"

Many of the lines above are standard code that provide some anchors to understanding what is different. 

The first line of this discussion are:

include "../LIB/N64_INTERRUPT.INC"

The include files is a collection of constants and macros to assist in quickly using these code libraries. Please take a look at the files directly as there are many things supported that are not directly mentioned here. 

interrupt_init()

interrupt_set_vblank($200, interrupt_vblank_handler)

The initialization seems pretty normal, the next line sets an "event handler" or "function pointer" for the event we want our code to respond to. The vblank macro takes the Horizontal Line that should trigger the vblank interrupt. 

interrupt_vblank_handler:

interrupt_register_store()

//

// Your Code Here

//

interrupt_register_load()

jr ra

nop

Just beyond the Game Loop is our custom code handler, fairly normal stuff. Be sure to use the macros shown for saving and restoring the Registers. Since there are a lot of things that are best done every frame, this is a good starter Interrupt to handle. 

Things to do in the VI Interrupt:

include "../LIB/N64_SYS.S"

include "../LIB/N64_INTERRUPT.S"

include "../LIB/N64_INTERRUPT_HANDLER.S"

This last code section is kind of interesting because most of the code performing the work is in these files. These may include several KB of code and therefore it's a little more important about where these are laid out in the final ROM file. Generally I just prefer them near the end. If you actually care where they end up check out the macro fixed_gap(label, 0xdistance) explained below.

Inside the Custom Interrupt Handlers 

Interrupt handlers may not need very many registers, while complex debugging handlers should save all registers. If the handler can be written using just k0 and k1 doesn't touch 'ra', skip using the register store/ load macros. Interrupt handlers may use some or all of the following macros:

The following macros should be used so that when returning from the exception, the desired instruction is executed. Without using these macros, the Branch Delay Slot may be ignored which may cause an infinite loop.

Conclusion - N64 bass Game Developer

The above code pattern should be pretty easy to follow and maintain. It's part of a larger pattern used in the rest of the library being generated for this series. All of the library code is compiled in so there isn't anything hidden or that can't be studied. Continue reading for a deep dive on the guts of the Exception/Interrupt Code. 

Exception/Interrupt - Deep Dive

Creating an Exception/Interrupt Handler (EIH) has a lot of constraints and challenges especially when your trying to include even a little bit of flexibility. The following goals were considered:

To be honest I generally feel that code comments should be used when code is taking advantage of side effects, or other uncommon or non-obvious things are happening. In most cases good variable names and good use of white space are enough. The fact that I've decided to write 1000+ words about this code suggests that the code is probably related to madness whether it's good or evil is to be determined. 

The other advantage to this document is to force me to think through all of the code paths.

MIPS VR4300i Exceptions and Interrupts

Exceptions and interrupts are hardware events that allow software to respond to them. Generally the handler routines are small and return quickly to the main process. It really is a single core processor managing the most basic threading model. 

Out expectation of handler developers is the following:

The following table shows the Memory area that is reserved for our Exception Handlers. For other MIPS processors the Status Register BEV bit = 1 changes these addresses, on the N64 at power on the BEV = 1 and the PIF boot code sets the BEV bit = 0.

Some quick observations about this table:

Also note:

This is small area of memory reserved for the standard exception handlers, but in many cases it's enough for a simple handler or to test a condition and exit. In our case we will attempt to reduce the game developers code and mental overhead in working with the N64's Interrupts/Exceptions.

Code Concept

The goals of the library handler code are:

To achieve this goal with the limited instruction count the easiest way is to use a 'jump table' or 'function pointer' concept. In this case it's easier if we keep the Jump table values inside the Exception block. It helps because the addresses are guaranteed to be in the same range 0x8000 0000 which will minimize instruction counts and also keeps related data near the code, in addition there are some ranges in the exception handler block that are available.

The first step is to write assembly code that will generate an exact 512 byte file.

arch n64.cpu

endian msb

origin $00000000

include "../LIB/N64.INC"

define DEBUG_SHOW_STATIC_VALUE(yes)

align(8)

base $80000000

_inthandler_overlay:

_tlb_handler:

    // tlb_handler code here

fixed_gap(_tlb_handler, 0x80)

_xtlb_handler:

    // xtlb_handler code here

fixed_gap(_xtlb_handler, 0x80)

_cache_handler:

    // cache_handler code here

fixed_gap(_cache_handler, 0x80)

_default_handler:

    // default_handler code here

fixed_gap(_default_handler, 0x80)

fixed_gap(_tlb_handler, 0x200)

At first glance this doesn't look much like assembly language and you would be right, but upon closer inspection the structure is there exactly as described in the table above. The real work is the combination of the define statement and the fixed_gap(label,gap) macro.

Since it's hard to develop optimized code on the first pass, it would be reasonable to change the last 0x200 to 0x300, since there is some available space. There are important values in RAM starting at 0x300 so don't exceed that. When we are done, the hope is to be either at or under the 0x200 value.

This fixed_gap() macro will actually force a compile error if we exceed our specified size. The define shows an alternate byte pattern that makes this type of space more obvious when viewed in a hex editor.

 // fixed_gap(label, 0xdistance)

macro fixed_gap(base, gap) {

    if (pc() > {base} + {gap}) {

        error "Gap is greater than specified size."

    }

    while (pc() < {base} + {gap}) {

if {defined DEBUG_SHOW_STATIC_VALUE} {

            db 0xAB

        } else {

            db 0x0

        }

    }

}

Compiling the code at this point generates a file of exactly 512 bytes of 0xAB not very exciting but helpful as we progress.

Default Handler - Starting at the end

The 'default handler' is the catch all. It can also save us duplicating some code, by sending the other handlers to some of the sections of the default handler. Since it's the last handler it can technically run a little long until we optimize (important system values are written to RAM at 0x8000'0300).

The first and last section of the default handler are setup / clean up then exit from the interrupt handler.

_default_handler:

    lui k0, $8000

    sw ra, _interrupt_ra(k0)

    lui ra, $8000

// More code coming here!

 _interrupt_return_handler:

    mfc0 k0, SR

    andi k0, 0x01

    mtc0 k0, SR

    eret

The MIPS calling convention includes k0 and k1 that are for use in kernel mode (Interrupts/Exceptions run in Kernel mode). This is a critical assumption that we will use to our advantage. 

In this code we can see that k0 is immediately modified without saving it's current value, also since the 'ra' register has hard coded behavior that we can use, we save it's value off before doing anything else. Since we will not be using the 'ra' behavior until the end of the handler, we will use it as a constant. 

Reminder that the 'ra' behavior is when the 'and link' feature of the jump instruction is used (jal, jalr). 

Before we return, it's safe to assume that global interrupts were enabled in order to get here so let's enable them before leaving. The 'eret' instruction does most of the work of restoring the processors state (move EPC to the PC) and restarting execution.

The common standard is to save all of the processor registers to RAM so they can be used by interrupt handlers. In this case I'll use a different approach with the goal of reducing the instruction count, remember only 32 per memory mapped handler (handler developers are unlimited). Basically our handler code will only use the k0, k1 and ra registers. If the handler developers choose to use only those registers they can also save a lot of instructions (approx 384 instructions, i.e. performance improvement) between the save and restore especially if multiple interrupts are triggered per frame. 

Limiting the registers used in our code, can give some flexibility back to the handler developer. See above for optional macros to save off some of the registers.

 _interrupt_custom_handler:

    beq k0, r0, _interrupt_return_handler

    nop

    jalr k0

    nop

The custom handler code block, assumes that the k0 register is populated with a memory address for the function pointer to handle the Exception.  If either are 0 execution should proceed to the _interrupt_return_handler.

Jump Table Entries

The Jump Table Entries can be located in many places through out this code and later we will force them into very specific places but I just want to take a minute and talk about the code and how it works.

The standard way to do this would be:

In our code they are wrapped in a macro like this:

The macro definition:

 jump_entry1:

    dw 0x00

jump_entry1:

    static_value(DEBUG_SHOW_STATIC_VALUE, dw 0x0, db "jpe1")

macro static_value(conditional, default, alternative) {

    if {defined {conditional}} {

        {alternative}

    } else {

        {default}

    }

}

This is very similar to what we saw in our fixed_gap() macro, including using the define 'DEBUG_SHOW_STATIC_VALUE'. Most of the exception handler code is position dependent and it's easier when we can identify the location of each value in our hex editor. When all of the values are 0x00 they all 'run' together and it's hard to tell if they are all where you expected.

Remember: before executing this code, comment out the 'define DEBUG_SHOW_STATIC_VALUE' line.

TLB (refill) Handler

The next step is to create the TLB (refill) Handler, other TLB Exceptions are handled by the default handler. In our case we are going to check if the developer wants to handle this exception themselves or ignore it. It could also be handled by a debug handler which is assigned by the developer.

    lui k0, $8000

    lw k0, tlb_handler_ptr(k0)

    lui k1, $8000

    sw ra, _interrupt_save_ra(k1)

    j _interrupt_custom_handler

    nop

tlb_handler_ptr:

        static_value(DEBUG_SHOW_STATIC_VALUE, dw 0x0, db "TLBH")

_interrupt_save_ra:

        static_value(DEBUG_SHOW_STATIC_VALUE, dw 0x0, db "ra  ")

This code starts to get interesting because of the use of a side effect of the bass assembler. The normal way to write the first 2 lines would be:

At first glance this code doesn't look that different? The difference is that 'la' is a psuedo instruction that becomes 2 instructions something like:

Unfortunately we don't have the extra instruction space, Technically we could count the actual position of the variable _interrupt_save_ra and create our own 2 line version like the above code:

BUT we would loose the flexibility of moving or adjusting that variables location. Instead we take advantage of the 'bass' assemblers truncation behavior. We loaded the high bytes of k0 then let the compiler truncate the _interrupt_save_ra variable to the last 2 bytes for us. If bass were to change it's behavior we would have a problem, but at this time bass is a very stable program so we can trust this (at least for now). We can see the same technique is used to get the _interrupt_save_ra location.

Since the beq depends on the value of k0 and that value is in RAM, we need to wait 2 instruction cycles before using it. It works nicely that we can save off 'ra' while waiting for k0 to be loaded.

With 'ra' saved and k0 loaded with our function pointer jump to the handler code covered above.

XTLB (refill) Handler

The XTLB comes into play in 64-bit situations in other words not applicable on the N64, but since it's easy we'll do essentially the same handler so copy and paste the same code, remember to rename the tlb_handler_ptr to start with an 'x' and change the "TLB " to "XTLB" in the macro. Also we don't need the 'ra' memory location duplicated.

    la k0, tlb_handler_ptr

    sw ra, 0(k0)

    lui k0, $8000

    ori k0, $0040

    sw ra, 0(k0)

     lui k0, $8000

    sw ra, $0040(k0)

    lui k0, $8000

    lw k0, xtlb_handler_ptr(k0)

    lui k1, $8000

    sw ra, _interrupt_save_ra(k1)

    j _interrupt_custom_handler

    nop

xtlb_handler_ptr:

        static_value(DEBUG_SHOW_STATIC_VALUE, dw 0x0, db "XTLB")

Cache Error Handler

For now we will treat this one identical to the TLB and XTLB so we can get to the much harder 'other handler'.

    lui k0, $8000

    lw k0, xtlb_handler_ptr(k0)

    lui k1, $8000

    sw ra, _interrupt_save_ra(k1)

    j _interrupt_custom_handler

    nop

xtlb_handler_ptr:

        static_value(DEBUG_SHOW_STATIC_VALUE, dw 0x0, db "XTLB")

Function Pointers

There are 17 Exception Types, 2 software interrupts, 6 hardware interrupts, 6 MI Interrupts and 3 function pointers already created.

First let's recognize we have 31 function pointers to store in our space, let's start by grouping them.

The first item that stands out is the largest 'linear' group also has a huge gap in the Exception Types (missing 16-22). If we count carefully the range 16-22 inclusive, is 7 and our "standard" exception handler is 6 instructions. Can we wrap this list around an exception handler?

This process is a personal preference, compromising readability/maintainability with efficiency.

While the process above is fairly detailed, after a few tries it's not to bad and the macro will output errors if there is a mistake. 

Next I took the Int0-Int7 function pointers and placed them after the _tlb_handler code and before the Exception pointers. Using the macro I placed a 4 byte gap on either side of them.

Next I moved the tlb_handler_ptr, xtlb, cache and ra save locations to after the Exception 23 function pointer.

All that's left is the six MI Interrupts, these should fit in the same _xtlb_handler block, I suggest using the fixed_gap() macro to create some "blank" space between the different types, or use it to make 'labels' in the compiled code.

Other Handler

This one has a lot going on! We need to handle 31 different situations, using only 32 instructions of which 10 are already used!

I love to write tight optimized code the first time but reality is you have to prove that it works first, then make it small. To do that we need more "breathing room". Looking in the Hex Editor we can see there is still some space in the _cache_handler code block. Let's move our "common" handlers (_interrupt_custom_handler, _interrupt_return_handler) there.

The Memory Value Map for the Exception address space is as follows

ASM Instruction

 Function Pointer

  Unused

Interrupt/Exception Causes