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 program memory 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 program memory 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 priorities 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 or by setting '#option _IVT_DEFAULT_IPEN = 1'. 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. Where used, priority settings can be specified using the values 0, 1, or the constants HIGH_PRIORITY/LOW_PRIORITY.

Interrupt context

New in IVT v2.1, '#option _IVT_SAVE_CONTEXT' allows you to tailor the level of context support. The option setting is a bit-field value with the meanings as shown below-

_IVT_SAVE_CONTEXT bit field values:
  %0000         // no context support (default setting)
  %0001         // ---1 high-priority TBLPTR
  %0010         // --1- high-priority SF SYSTEM variables
  %0100         // -1-- low priority TBLPTR (requires IPEN = 1) 
  %1000         // 1--- low-priority SF SYSTEM variables (requires IPEN = 1)
  %0011         // high-priority TBLPTR + SYSTEM
  %1100         // low-priority TBLPTR + SYSTEM
  %1111         // high and low priority TBLPTR + SYSTEM

Note: IVT v2.2 adds saving the TABLAT register to TBLPTR context operations.

_IVT_SAVE_CONTEXT must be set to the proper value to allow the module to include the required functionality, remove unneeded code, and minimize variable usage. The option may have multiple bits set. For example '#option _IVT_SAVE_CONTEXT = %0101' would enable saving the low priority TBLPTR and high-priority TBLPTR 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 improved 18XV hardware context automatically saves all CPU 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 and TABLAT, which you should save/restore if the ISR accesses const data arrays or strings. 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 which save the hardware TBLPTRU/H/L registers, and usage depends on the INTR_PRIORITY setting of the ISR...

    SAVE_CONTEXT()            // high-priority only 
    SAVE_CONTEXT(priority)    // high or low... change 'priority' to match ISR INTR_PRIORITY
    SAVE_CONTEXT_ISS()        // use run-time Interrupt State Status from INTCON1

In addition to the TBLPTR registers, you may also need to save/restore the state of the SF system variables. This is required if the interrupt handler uses system library support functions that reference 'SB_SVxx' variables, such as multiply or divide

    SAVE_SYSTEM_CONTEXT()           // high-priority only 
    SAVE_SYSTEM_CONTEXT(priority)   // high or low... change 'priority' to match ISR INTR_PRIORITY
    SAVE_SYSTEM_CONTEXT_ISS()       // use run-time Interrupt State Status from INTCON1

The SYSTEM_CONTEXT macros take the place of the compiler's equivalent built-in 'save(0)/restore' functions, and if enabled by the '#option _IVT_SAVE_CONTEXT' setting will also save the TBLPTRU/H/L registers.

SAVE_SYSTEM_CONTEXT allows multiple isr handlers to share the context storage for SF system variables. Since ISR handlers are defined as events, using the traditional 'save(0)' statement in multiple handlers would result in each handler allocating an additional 31 bytes of ram for the system context and hdw registers which is unnecessary. SAVE_SYSTEM_CONTEXT is also slightly faster since it does not have to save the additional registers which are saved automatically as part of the hardware shadow registers. The SAVE_xxxx_ISS() versions can determine the priority state at runtime, but will generate more code and are slower. They are for use in rare situations where an ISR has a dynamically changing priority and is not fixed at compile time.

Along with the above macros, you can also use the compiler's traditional built-in 'save()/restore' functions to save the state of any local variables used by subroutines and functions that are called by their IRQ event handler. If used, the order of save()/SAVE_SYSTEM_CONTEXT is unimportant, but the order of 'saves' must match the order of 'restores' otherwise a system error will occur.

Here is a full template for a generic ISR event handler

// if priorities are used you may want to define a const that can be used wherever
// specifying the irq 'priority' is required
public const handler_name_PRIORITY = HIGH_PRIORITY   // or LOW_PRIORITY

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

    // define interrupt entry point (this MUST be the first executable statement)
    ENTER_INTR_HANDLER(handler_name)        // ** change 'handler_name' to match 'public event' name

    // save context (optional, depends on user code)
    // select one or more of the following:
    'SAVE_CONTEXT()                     // save TABLEPTR registers (for const data array/string access)
    'SAVE_CONTEXT(priority)             // same as above... change 'priority' to match IRQ priority
    'SAVE_SYSTEM_CONTEXT()              // saves SF system variables (and TABLEPTR if enabled) 
    'SAVE_SYSTEM_CONTEXT(priority)      // same as above... change 'priority' to match IRQ priority
    'save(mysub)                        // save local frame variables used by mysub

    // << user code goes here >>

    // clear IF interrupt request flag
    INTR_REQ(irq_no, 0)                     // ** change 'irq_no' to match intr vector number

    // restore context (if saved above)
    'RESTORE_CONTEXT()                  // restore TABLEPTR registers
    'RESTORE_CONTEXT(priority)          // same as above... change 'priority' to match IRQ priority
    'RESTORE_SYSTEM_CONTEXT()           // restores SF system variables (and TABLEPTR if enabled) 
    'RESTORE_SYSTEM_CONTEXT(priority)   // same as above... change 'priority' to match IRQ priority
    'restore                            // restore local frame variables

    // 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_interrupt_handler() routine
// 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)
#option _IVT_DEFAULT_HANDLER = false
include "ivt.bas"


// our local default_interrupt_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 IPEN=1 and 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, high and low priority with save_context

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

device = 18F27K42
clock = 64

include "intosc.bas"

// enable both priorities by default
#option _IVT_DEFAULT_IPEN = 1
// save_context support - low-priority TBLPTR + SYSTEM context, high-priority TBLPTR context
#option _IVT_SAVE_CONTEXT = %1101
include "ivt.bas"

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

//----------------------------------------------------------------------------
// IRQ_TMR1 (high-priority w/TBLPTR context save example)
//----------------------------------------------------------------------------
public event tmr1_handler()
    ENTER_INTR_HANDLER(tmr1_handler)

    // save TABLEPTR registers
    SAVE_CONTEXT(HIGH_PRIORITY)

    // 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

    // restore 
    RESTORE_CONTEXT(HIGH_PRIORITY)

    EXIT_INTR_HANDLER()
end event

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

    // save SF system variables and TABLEPTR registers
    SAVE_SYSTEM_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 
    RESTORE_SYSTEM_CONTEXT(LOW_PRIORITY)

    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()      // not req'd if '#option _IVT_DEFAULT_IPEN = 1'
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 swapped at runtime
// - replace default_interrupt_handler
program example3

device = 18F27K42
clock = 64

include "intosc.bas"

// ivt.bas contains a default_interrupt_handler() routine
// set '#option _IVT_DEFAULT_HANDLER = false' to override the default routine
// and use our own local routine
#option _IVT_DEFAULT_HANDLER = false
include "ivt.bas"

// set this option true to test using the default_interrupt_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_interrupt_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                 // turn on LED
    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                 // turn off LED
    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 ivt table 2)
//----------------------------------------------------------------------------
public event tmr_handler()
    dim irq_no as byte

    ENTER_INTR_HANDLER(tmr_handler)

    // on entry 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 an IRQ=UNUSED_IRQ
INTR_ENABLE(UNUSED_IRQ, 1)   // enable IRQ = UNUSED_IRQ
INTR_REQ(UNUSED_IRQ, 1)      // generate intr (should go to default_interrupt_handler)
#endif

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

// test TMR1 and TMR3 interrupts using ivt table1
// - 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

// let it run for 10 secs... LED should go ON for 1/2 sec then OFF for 1 sec
delayms(10000)

//
// now stop everything and swap runtime IVT tables...
// both TMR1 and TMR3 interrupts should now be handled by common tmr_handler()
//
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... LED should go ON for 1 sec and then OFF for 1/2 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(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