VectoredInterrupts

Using Vectored Interrupts with Swordfish

Many new 18F devices include a Vectored Interrupt Controller (VIC) module, available on the 18XV core devices in the K and Q families. Vectored Interrupts bring new enhancements to the interrupt scheme used in current 18F designs, improving speed and flexibility. You can identify the 18XV core devices from the device .bas file entry
'#define _xv18 = 1'

Traditional Interrupts

Traditional 18F devices support up to two interrupt vectors: a high priority and a low priority vector. These interrupt vectors are located in low memory at $0008 and $0018, and are fully supported in Swordfish using the Enable, Disable, and Interrupt statements. See the Users Manual/online Help section on Interrupts for more details. Also, check out the article PIC 18F Interrupts in Swordfish by RadioT for some additional tips.

One of the issues with the two-vector approach is once you have more than two interrupts you must begin to add code to detect which peripheral is generating the interrupt request. This adds additional latency and complexity to the ISR.

Here's an example of what a traditional interrupt handler with three different interrupt sources might look like:

interrupt HighPriorityISR()
    // save context

    if (INTCON.TMR0IF = 1) and (INTCON.TMR0IE = 1) then
        // TMR0 isr code
        INTCON.TMR0IF = 0
    endif

    if (PIR1.TMR1IF = 1) and (PIE1.TMR1IE = 1) then
        // TMR1 isr code
        PIR1.TMR1IF = 0
    endif

    if (PIR2.TMR3IF = 1) and (PIE2.TMR3IE = 1) then
        // TMR3 isr code
        PIR2.TMR3IF = 0
    endif

    // restore context
end interrupt

In this case, saving and restoring the interrupt context becomes much more challenging. It also means that you have to move the interrupt routine to outside the .bas module that uses it since you likely have to reference three different source modules, breaking the modular approach used in existing Swordfish modules.

Vectored Interrupts

Vectored interrupt mode departs from the traditional scheme. While it still provides for two interrupt priorities (high and low), the interrupt priority no longer sets the vector address as it does in legacy mode. Instead, each interrupt source is assigned its own individual vector number, from 0 to 255. This allows interrupts to have a unique ISR handler so you no longer need code like the above to check the IF and IE flags. Since ISRs are no longer shared among modules, you can move the interrupt handler code back into the .bas module that uses it.

This is all accomplished through the Interrupt Vector Table (IVT). Vectored interrupts are enabled via the Multi-Vector Enable bit (MVECEN) in the config setting. For compatibility with existing Swordfish modules, the device include file for XV18 core devices sets MVECEN off so by default interrupts operate in traditional legacy mode.

IVT

The IVT is a table of up to 256 entries, one for each IRQ vector number. Each entry is a 16-bit word that contains the address of the interrupt handler for that vector number (more on this later). The IVT resides in ROM so it must be fixed at compile-time, however you can set the base address of the IVT using the IVTBASEU/H/L registers at run-time. This allows the code to setup multiple IVT tables and switch between them if desired.

When an interrupt occurs, the processor uses the interrupt vector number 0-255 as an index into the IVT. It retrieves the word address from the table, shifts it left by 2 (multiplying it by 4), and branches to that address. Why does it shift the address? The IVT holds a 16-bit word, which only allows a value up to 65535. Multiplying the word address from the table by 4 allows the vector to point to up to 256K bytes of code address space.

The downside to this is that the ISR code MUST be located on a 4-byte address boundary, but Swordfish has no built-in mechanism to enforce this restriction. The ENTER_INTR_HANDLER() macro will add padding to the start of the interrupt handler routine if needed to adjust the value used to set the IVT entry point, making it compatible with the word-length address entry requirement.

The number of available interrupt vectors varies with the device, but it typically ranges from 82 to 128. The IVT must include entries for all interrupt vector numbers up to the highest vector used by your program. Since an IVT entry requires a word value, a complete table of 128 entries requires 128*2 = 256 bytes of ROM space. The max number of IRQs and the IRQ vector number definitions themselves can be found in the device .bas file located in the compiler 'Includes' folder.

For example, here are the first few interrupt vector number entries from the 18F26K42.bas file:

// interrupt vectors
#define _intvectors = 82               // interrupt vector table entries
public const
   IRQ_SWINT = 0,                         // irq 0 - Software Interrupt
   IRQ_HLVD = 1,                          // irq 1 - HLVD Interrupt
   IRQ_OSF = 2,                           // irq 2 - Oscillator Failure Interrupt
   IRQ_CSW = 3,                           // irq 3 - Clock Switch Interrupt
   IRQ_NVM = 4,                           // irq 4 - NVM Interrupt

IVT.bas

This new module contains macros and routines that can be used to setup and manage the IVT and interrupts when using vectored interrupts. Replacements for the standard Enable, Disable, and Interrupt statements are provided and must be used in place of these original functions when operating in VIC mode. Including IVT.bas will automatically set the MVECEN config setting to ON, enabling multi-vectored mode.

Before we look at creating an IVT, let's look at the required format for an interrupt handler ISR.

Interrupt Handler

Interrupt handlers replace the traditional 'interrupt' routines, and must be declared using the 'event' attribute. They should follow the basic format:

    public event isr_XXX_handler()            // user-assigned interrupt event handler name
        ENTER_INTR_HANDLER(isr_XXX_handler)   // parameter must match assigned 'event' name above

        // user code goes here

        // set irq flag to 0 (clears IF)
        INTR_REQ(IRQ_XXX, 0)                  // IRQ_XXX = irq_no from device file list

        EXIT_INTR_HANDLER()
    end event

ENTER_INTR_HANDLER() and EXIT_INTR_HANDLER() are required to mark the beginning and end of the ISR. Each ISR event must have a unique name, and the event name ('isr_XXX_handler' in this example) must be used as the parameter for ENTER_INTR_HANDLER.

ENTER_INTR_HANDLER will make any required adjustments to the code offset and will set the interrupt entry point. This marks the first executable statement in the handler, so you cannot use variable declarations that include an initializer since they would be skipped.

Before exiting the ISR you should clear the source of the interrupt request. This can be done using the INTR_REQ() macro as shown above to set the IF flag to 0.

EXIT_INTR_HANDLER() will perform a 'RETFIE 1', restoring the hardware context and re-enabling interrupts.

Interrupt Priorities

The 18F interrupt mechanism can be set to use a single priority for all interrupts or to allow the use of high and low priority setting through the IPEN register flag. The default setting is IPEN = 0 (all interrupts are high-priority), but this can be set using the ENABLE_INTR_PRIORITY() and DISABLE_INTR_PRIORITY() macros. Unless you have the need to allow a high-priority interrupt to be able to interrupt a running low-priority one, it is recommended to leave the interrupt priority setting at its default setting and just use a single priority.

If interrupt priority is enabled then each interrupt can be assigned as either low (priority=0) or high (priority=1). The individual priorities for each IRQ are set using INTR_PRIORITY(irq_no, priority), which defaults to high-priority if left unchanged. INTR_PRIORITY() sets/clears the respective bit in the IPRx register for the given irq_no.

Interrupt context

There are two levels of hardware shadow registers for saving context: one for high-priority interrupts (priority=1) and one for low-priority interrupts (priority=0) In addition to the program counter (PC), the hardware context automatically saves the registers:

  STATUS, WREG, BSR, FSR0/1/2, PRODL/H and PCLATH/U

The only important registers it does not save are the TBLPTRU/H/L, which you should save/restore if accessing const data or strings in the ISR. This can be done using the SAVE_CONTEXT() and RESTORE_CONTEXT() macros as shown below:

    // IRQ_TMR1
    public event tmr1_handler()
        ENTER_INTR_HANDLER(tmr1_handler)

        // save TABLEPTR (for const data or string access)
        SAVE_CONTEXT()                     // high-priority

        // user code goes here
        INTR_REQ(IRQ_TMR1, 0)           // clear TMR1IF

        RESTORE_CONTEXT()
        EXIT_INTR_HANDLER()
    end event

There are three different implementations of SAVE_CONTEXT/RESTORE_CONTEXT, and their usage depends on the global INTR_PRIORITY enable setting...

    SAVE_CONTEXT()            // high-priority 
    SAVE_CONTEXT(priority)    // high or low... change 'priority' to match IRQ priority setting (0 or 1)
    SAVE_CONTEXT_ISS()        // use run-time Interrupt State Status from INTCON1 (uses more code)

SAVE_CONTEXT_ISS can determine the priority state at runtime, but it will generate more code and is slower. This might be useful where the interrupt priority for a handler is dynamically changed and not fixed at compilation.

If the ISR uses system library functions or calls other routines you may also need to save the system library variables and/or the frame context. This can be done using 'save/restore' statement blocks just as with traditional interrupts.

The only restriction with using a traditional save/restore block is that it must be used outside of any SAVE_CONTEXT/RESTORE_CONTEXT macros.

Here is a full template for a generic ISR event handler

public event handler_name()
    // << variable declarations here... do NOT use initializers >>

    // define interrupt entry point
    ENTER_INTR_HANDLER(handler_name)    // change 'handler_name' to match 'public event' name

    // save SF system variable context (if req'd, must be before SAVE_CONTEXT)
    save(0)

    // save TABLEPTR registers (if req'd, for const data/string access)
    SAVE_CONTEXT(priority)              // change 'priority' to match IRQ priority setting (0 or 1)

    // << user code goes here >>

    // clear IF
    INTR_REQ(irq_no, 0)                 // chnage 'irq_no' to match intr vector number

    // restore TABLEPTR (if saved above)
    RESTORE_CONTEXT(priority)           // high/low... change 'priority' to match... 0=low, 1=high

    // restore SF system variable context (if saved above)
    restore

    // exit interrupt
    EXIT_INTR_HANDLER()
end event

Creating an IVT

To create an interrupt vector table in program memory you first use CREATE_IVT(), then add a series of SET_IVT_HANDLER() entries for every IRQ used by your program, and finally mark the end of the table using END_IVT().

CREATE_IVT(table_name) is used to initialize the vector table database settings and assign a unique name to the table. Assigning a name to the table does two things: it provides a table name which is used when programming the IVTBASE registers, and also allows you to have multiple tables and switch between them at run-time. The SET_IVTBASE(table_name) macro will load the registers with the address of the specified table_name.

SET_IVT_HANDLER() updates the vector table database for an individual IRQ with the address of an interrupt handler. The format for adding a handler is SET_IVT_HANDLER(irq_no, event_handler), where 'irq_no' is 0-255 (or one of the IRQ_xxx consts from the device file), and 'event_handler' is the name assigned to the event routine by ENTER_INTR_HANDLER (ie 'tmr1_handler').

END_IVT() takes the vector table database information and builds the IVT in program memory.

An example of a simple IVT is shown below:

CREATE_IVT(ivt_table)
SET_IVT_HANDLER(IRQ_TMR1, tmr1_handler)
END_IVT()

This example shows the creation of two tables. In the second table TMR1 and TMR3 share the same interrupt handler routine.

CREATE_IVT(ivt_table1)
SET_IVT_HANDLER(IRQ_TMR1, tmr1_handler)
SET_IVT_HANDLER(IRQ_TMR3, tmr3_handler)
END_IVT()

CREATE_IVT(ivt_table2)
SET_IVT_HANDLER(IRQ_TMR1, tmr_handler)
SET_IVT_HANDLER(IRQ_TMR3, tmr_handler)
END_IVT()

You MUST have a call to SET_IVTBASE() as part of your initialization code to set the IVTBASEU/H/L registers before enabling any interrupts.

Assigning a Default Interrupt Handler

Due to the manner in which the IVT is setup, there must be an entry in the table for every IRQ number, even if the interrupt is unused. To simplify this, the IVT.bas module defines a default_interrupt_handler() routine which is used by CREATE_IVT when the vector table database is initialized. The default_interrupt_handler() will perform a device reset for any unhandled interrupt requests. This is all done automatically, so nothing further needs to be done by the user.

You can override the default interrupt handler and provide your own routine if desired. To do this, set '#option _IVT_DEFAULT_HANDLER=false' and create a public event routine named 'default_interrupt_handler()'. When you create the IVT, you must register the routine using SET_IVT_HANDLER. Note: If you list the default_interrupt_handler first then the irq_no used is unimportant, and your routine will now be used for all unspecified interrupts.

Example of overriding the Default Interrupt handler

// ivt.bas contains a default_intr_handler() routine which is defined by default
// set '#option _IVT_DEFAULT_HANDLER = false' to override the default routine in ivt.bas 
// and use our own local routine
#option _IVT_DEFAULT_HANDLER = false
include "ivt.bas"


// our local default_intr_handler()
#if (_IVT_DEFAULT_HANDLER = false)
public event default_interrupt_handler()
    dim irq_no as byte

    // define interrupt entry point
    ENTER_INTR_HANDLER(default_interrupt_handler)

    // on entry, WREG contains the interrupt number
    irq_no = WREG

    // clear IF in PIRx
    // this is an example of using 'irq_no' to determine
    // which PIRx register and bit to clear
    FSR0 = addressof(PIR0)
    FSR0 = FSR0 + (irq_no >> 3)             // get PIRx reg addr
    PRODL = byte(1 << ((irq_no) mod 8))     // create bit mask
    INDF0 = INDF0 and not(PRODL)            // and clear the bit

    // exit interrupt
    EXIT_INTR_HANDLER()
end event
#endif      // _IVT_DEFAULT_HANDLER


CREATE_IVT(ivt_table)
SET_IVT_HANDLER(0, default_interrupt_handler)        // register our default handler
SET_IVT_HANDLER(IRQ_TMR1, tmr1_handler)
END_IVT()

Enabling and Disabling Interrupts

The PIC18 interrupt structure is managed using two settings: each peripheral has its own interrupt enable bit located in the PIEx registers, and the global interrupt enable bit GIE (or GIE and GIEL if using interrupt priorities).

INTR_ENABLE(vector_no, val) is used to set/clear the individual PIEx interrupt enable mask register bit, where 'vector_no' is the number of the IRQ (can be one of the constants from the device file), and 'val' is the PIRx bit setting... '0' disables the interrupt, and '1' enables the interrupt.

The 'enable()/disable()' keywords used in traditional interrupts are replaced by the ENABLE_INTERRUPT() and DISABLE_INTERRUPT() macros.

The global/high-priority GIE bit can be set/cleared using ENABLE_INTERRUPT()/DISABLE_INTERRUPT(), while ENABLE_INTERRUPT(priority)/DISABLE_INTERRUPT(priority) can be used for either GIE or GIEL depending on the 'priority' value setting... 0=GIEL (low-priority) and 1=GIE (high-priority). You can also use the LOW_PRIORITY/HIGH_PRIORITY constant values.

If using interrupt priorities you must enable both GIE and GIEL, and note that setting GIE=0 will disable both priorities.

ENABLE_INTERRUPT(0)        // sets GIEL=1 to enable low-priority interrupts
ENABLE_INTERRUPT(1)        // sets GIE=1, enabling both high and low priorities since GIEL is set

DISABLE_INTERRUPT()     // sets GIE=0, disabling all interrupts

Wrapping it up - Some examples

Example 1 - single interrupt

// simple VIC example using TMR1
program example1

device = 18F27K42
clock = 64

include "intosc.bas"

include "ivt.bas"

dim LED as PORTC.3        // user LED, active high

//----------------------------------------------------------------------------
// IRQ_TMR1
//----------------------------------------------------------------------------
public event tmr1_handler()
    ENTER_INTR_HANDLER(tmr1_handler)

    // user code goes here
    toggle(LED)

    INTR_REQ(IRQ_TMR1, 0)   // clear TMR1IF

    EXIT_INTR_HANDLER()
end event

//----------------------------------------------------------------------------
// setup TMR1 to use 500KHz MFINTOSC
//----------------------------------------------------------------------------
sub init_timer()
    // OSCEN register MFINTOSC bit
    const MFOEN = 5

    // enable MFINTOSC 500khz for TMR1 and TMR3
    OSCEN.bits(MFOEN) = 1

    // setup TMR1
    // at 500KHz w/1:8 prescaler TMR wraps in 2us * 8 * 65536 = 1048576us (approx 1 sec)  
    T1CON = %00110010       // 1:8, RD16
    T1CLK = %00000101       // MFINTOSC (500khz)
    TMR1H = 0
    TMR1L = 0
end sub

//----------------------------------------------------------------------------
// main program entry begins here
//----------------------------------------------------------------------------
main:

low(LED)
init_timer()

// set IVTBASE registers 
SET_IVTBASE(ivt_table)

// enable PIE bits
INTR_ENABLE(IRQ_TMR1, 1)

// enable high-priority interrupts
ENABLE_INTERRUPT()

// start TMR1
T1CON.bits(0) = 1

// repeat forever... LED should toggle every sec
while (true)
end while

//----------------------------------------------------------------------------
// IVT table
//----------------------------------------------------------------------------

// create the primary IVT and add interrupt handlers to the table
CREATE_IVT(ivt_table)
SET_IVT_HANDLER(IRQ_TMR1, tmr1_handler)
END_IVT()

end program

Example 2 - two interrupts, low and high priority w/context save

// VIC example using TMR1 and TMR3, low and high priority
program example2

device = 18F27K42
clock = 64

include "intosc.bas"

include "ivt.bas"

dim LED as PORTC.3        // user LED, active high

//----------------------------------------------------------------------------
// IRQ_TMR1
//----------------------------------------------------------------------------
public event tmr1_handler()
    ENTER_INTR_HANDLER(tmr1_handler)

    // user code goes here
    LED = 1
    T1CON.bits(0) = 0       // stop TMR1
    T3CON.bits(0) = 1       // start TMR3

    INTR_REQ(IRQ_TMR1, 0)   // clear TMR1IF

    EXIT_INTR_HANDLER()
end event

//----------------------------------------------------------------------------
// IRQ_TMR3 (low-priority w/context save example)
//----------------------------------------------------------------------------
public event tmr3_handler()
    ENTER_INTR_HANDLER(tmr3_handler)

    // save SF system variable context (must be before SAVE_CONTEXT)
    save(0)
    // save TABLEPTR registers
    SAVE_CONTEXT(LOW_PRIORITY)

    // user code goes here
    LED = 0
    T3CON.bits(0) = 0       // stop TMR3
    T1CON.bits(0) = 1       // start TMR1

    INTR_REQ(IRQ_TMR3, 0)   // clear TMR3IF

    // restore TABLEPTR registers
    RESTORE_CONTEXT(LOW_PRIORITY)
    // restore SF system variable context
    restore

    EXIT_INTR_HANDLER()
end event

//----------------------------------------------------------------------------
// setup TMR1 and TMR to use 500KHz MFINTOSC
//----------------------------------------------------------------------------
sub init_timers()
    // OSCEN register MFINTOSC bit
    const MFOEN = 5

    // enable MFINTOSC 500khz for TMR1 and TMR3
    OSCEN.bits(MFOEN) = 1

    // setup TMR1
    // at 500KHz w/1:8 prescaler TMR wraps in 2us * 8 * 65536 = 1048576us (approx 1 sec)  
    T1CON = %00110010       // 1:8, RD16
    T1CLK = %00000101       // MFINTOSC (500khz)
    TMR1H = 0
    TMR1L = 0

    // setup TMR3
    // at 500KHz w/1:4 prescaler TMR wraps in 2us * 4 * 65536 = 524288us (approx 1/2 sec)  
    T3CON = %00100010       // 1:4, RD16
    T3CLK = %00000101       // MFINTOSC (500khz)
    TMR3H = 0
    TMR3L = 0
end sub

//----------------------------------------------------------------------------
// main program entry begins here
//----------------------------------------------------------------------------
main:

low(LED)
init_timers()

// set IVTBASE registers 
SET_IVTBASE(ivt_table)

ENABLE_INTR_PRIORITY()
INTR_PRIORITY(IRQ_TMR1, HIGH_PRIORITY)
INTR_PRIORITY(IRQ_TMR3, LOW_PRIORITY)

// enable PIE bits
INTR_ENABLE(IRQ_TMR1, 1)
INTR_ENABLE(IRQ_TMR3, 1)

// enable interrupts
ENABLE_INTERRUPT(LOW_PRIORITY)
ENABLE_INTERRUPT()

// start TMR1
T1CON.bits(0) = 1

// repeat forever...
while (true)
end while

//----------------------------------------------------------------------------
// IVT table
//----------------------------------------------------------------------------

// create the primary IVT and add interrupt handlers to the table
CREATE_IVT(ivt_table)
SET_IVT_HANDLER(IRQ_TMR1, tmr1_handler)
SET_IVT_HANDLER(IRQ_TMR3, tmr3_handler)
END_IVT()

end program

Example 3 - two interrupts, two IVT tables, local default interrupt handler

// VIC example using TMR1 and TMR3
// - two IVT tables
// - replace default_intr_handler
program example3

device = 18F27K42
clock = 64

include "intosc.bas"

// ivt.bas contains a default_intr_handler() routine which is enabled by default
// if you wish to override that routine with your own handler, then:
//  - set '#option _IVT_DEFAULT_HANDLER = false'
//  - define a replacement event 'public event default_interrupt_handler()' routine
//  - add at least one reference to the new routine between CREATE_IVT/END_IVT using
//      SET_IVT_HANDLER(<unused irq_no>, default_interrupt_handler)
//    if you add it first then it doesn't matter what irq_no you use
#option _IVT_DEFAULT_HANDLER = false
include "ivt.bas"

// set this option true to test using the default handler
#option TEST_DEFAULT_HANDLER = true
const UNUSED_IRQ = NUM_INT_VECTORS-1    // set an unused IRQ number


dim LED as PORTC.3        // user LED, active high

//----------------------------------------------------------------------------
// override the default_intr_handler() in ivt.bas with our own local routine
//----------------------------------------------------------------------------
#if (_IVT_DEFAULT_HANDLER = false)
// default interrupt handler
public event default_interrupt_handler()
    dim irq_no as byte

    // define interrupt entry point
    ENTER_INTR_HANDLER(default_interrupt_handler)

    // on entry, WREG contains the interrupt number
    irq_no = WREG

    // clear IF in PIRx
    // this is an example of using 'irq_no' to determine
    // which PIRx register and bit to clear
    FSR0 = addressof(PIR0)
    FSR0 = FSR0 + (irq_no >> 3)             // get PIRx reg addr
    PRODL = byte(1 << ((irq_no) mod 8))     // create bit mask
    INDF0 = INDF0 and not(PRODL)            // and clear the bit

    // turn on led
    LED = 1

    // exit interrupt
    EXIT_INTR_HANDLER()
end event
#endif      // _IVT_DEFAULT_HANDLER

//----------------------------------------------------------------------------
// IRQ_TMR1
//----------------------------------------------------------------------------
public event tmr1_handler()
    ENTER_INTR_HANDLER(tmr1_handler)

    // user code goes here
    LED = 1
    T1CON.bits(0) = 0       // stop TMR1
    T3CON.bits(0) = 1       // start TMR3

    INTR_REQ(IRQ_TMR1, 0)   // clear TMR1IF

    EXIT_INTR_HANDLER()
end event

//----------------------------------------------------------------------------
// IRQ_TMR3
//----------------------------------------------------------------------------
public event tmr3_handler()
    ENTER_INTR_HANDLER(tmr3_handler)

    // user code goes here
    LED = 0
    T3CON.bits(0) = 0       // stop TMR3
    T1CON.bits(0) = 1       // start TMR1

    INTR_REQ(IRQ_TMR3, 0)   // clear TMR3IF

    EXIT_INTR_HANDLER()
end event

//----------------------------------------------------------------------------
// common handler for both tmr1 and tmr3 (using vector table 2)
//----------------------------------------------------------------------------
public event tmr_handler()
    dim irq_no as byte

    ENTER_INTR_HANDLER(tmr_handler)

    // WREG contains the interrupt number
    irq_no = WREG 

    if (irq_no = IRQ_TMR1) then
        INTR_REQ(IRQ_TMR1, 0)   // clear TMR1IF

        LED = 0                 // turn off led
        T1CON.bits(0) = 0       // stop TMR1
        T3CON.bits(0) = 1       // start TMR3
    elseif (irq_no = IRQ_TMR3) then        
        INTR_REQ(IRQ_TMR3, 0)   // clear TMR3IF

        LED = 1                 // turn on led
        T3CON.bits(0) = 0       // stop TMR3
        T1CON.bits(0) = 1       // start TMR1
    endif

    EXIT_INTR_HANDLER()
end event

//----------------------------------------------------------------------------
// setup TMR1 and TMR to use 500KHz MFINTOSC
//----------------------------------------------------------------------------
sub init_timers()
    // OSCEN register MFINTOSC bit
    const MFOEN = 5

    // enable MFINTOSC 500khz for TMR1 and TMR3
    OSCEN.bits(MFOEN) = 1

    // setup TMR1
    // at 500KHz w/1:8 prescaler TMR wraps in 2us * 8 * 65536 = 1048576us (approx 1 sec)  
    T1CON = %00110010       // 1:8, RD16
    T1CLK = %00000101       // MFINTOSC (500khz)
    TMR1H = 0
    TMR1L = 0

    // setup TMR3
    // at 500KHz w/1:4 prescaler TMR wraps in 2us * 4 * 65536 = 524288us (approx 1/2 sec)  
    T3CON = %00100010       // 1:4, RD16
    T3CLK = %00000101       // MFINTOSC (500khz)
    TMR3H = 0
    TMR3L = 0
end sub

//----------------------------------------------------------------------------
// main program entry begins here
//----------------------------------------------------------------------------
main:

low(LED)
init_timers()

// set IVTBASE registers 
SET_IVTBASE(ivt_table)

// enable PIE bits
INTR_ENABLE(IRQ_TMR1, 1)
INTR_ENABLE(IRQ_TMR3, 1)

// enable high-priority interrupts
ENABLE_INTERRUPT()

// test default handler
#if (TEST_DEFAULT_HANDLER)
// enable and generate IRQ=UNUSED_IRQ
INTR_ENABLE(UNUSED_IRQ, 1)   // enable IRQ = UNUSED_IRQ
INTR_REQ(UNUSED_IRQ, 1)      // generate intr (should go to default_handler)
#endif

// start TMR1
T1CON.bits(0) = 1

// test TMR1 and TMR3 interrupts
// - start TMR1 (1 sec rollover)
// - TMR1 intr turns on LED and starts TMR3 (1/2 sec rollover)
// - TMR3 intr turns off LED and restarts TMR1
// - LED should go ON for 1/2 sec then OFF for 1 sec
// let it run for 10 secs
delayms(10000)

DISABLE_INTERRUPT()
// stop timers
T1CON.bits(0) = 0
T3CON.bits(0) = 0

// set TMR1 and TMR3 to use a single common handler
SET_IVTBASE(ivt_table2)

// clear IF bits
INTR_REQ(IRQ_TMR1, 0)
INTR_REQ(IRQ_TMR3, 0)

// enable high-priority interrupts
ENABLE_INTERRUPT()

// start TMR1
T1CON.bits(0) = 1

// repeat forever...
while (true)
end while

//----------------------------------------------------------------------------
// IVT table
//----------------------------------------------------------------------------

// create the primary IVT and add interrupt handlers to the table
CREATE_IVT(ivt_table)
SET_IVT_HANDLER(0, default_interrupt_handler)   // assign local default handler
SET_IVT_HANDLER(IRQ_TMR1, tmr1_handler)
SET_IVT_HANDLER(IRQ_TMR3, tmr3_handler)
END_IVT()

// create a second ivt table
// this one uses a single handler to alternate TMR1 and TMR3
CREATE_IVT(ivt_table2)
SET_IVT_HANDLER(IRQ_TMR1, tmr_handler)
SET_IVT_HANDLER(IRQ_TMR3, tmr_handler)
END_IVT()

end program