5.9.1.2 Writing Dual-priority or Legacy Mode ISRs

If your project is using dual priority vectors, observe the following guidelines when writing interrupt service routines (ISRs).

Write one interrupt function using the __interrupt() specifier to process each priority being used. You can define at most two interrupt functions. Use void as the ISR return type and for the parameter list. Consider implementing both interrupt functions, even if they are not needed, to handle accidental triggering of unused interrupts, or use the -mundefints option to provide a default action (see Undefints Option).

Specify the priority each ISR handles, using any one of low_priority or __low_priority; or high_priority or __high_priority as argument to the __interrupt() specifier. If no specifier argument is present, the ISR will default to being high priority. It is recommended that you always specify the ISR priority to ensure your code is readable.

Inside the ISR body, determine the source of the interrupt by checking the interrupt flag and the interrupt enable for each source that is to be processed and make the relevant interrupt code conditional on those being set. These checks can be placed in any order, but those appearing earlier in the ISR will be processed sooner, potentially improving response times.

At the appropriate point in your main-line code, assign the required priority to each interrupt source by writing the appropriate bits in the SFRs. Additionally, enable the interrupt sources required and the global interrupt enable.

Examples of dual-priority interrupt functions are shown below. Notice that both functions check for the source of the interrupt by looking at the interrupt enable bit (e.g., TMR0IE) and the interrupt flag bit (e.g., TMR0IF). Checking the interrupt enable flag is required since interrupt flags associated with a peripheral can be asserted even if the peripheral is not configured to generate an interrupt.
void __interrupt(high_priority) tcInt_high(void) {
    if (TMR0IE && TMR0IF) {  // any timer 0 interrupts?
        TMR0IF=0;
        ++tick_count;
    }
    if (TMR1IE && TMR1IF) {  // any timer 1 interrupts?
        TMR1IF=0;
        tick_count += 100;
    }
    // process other interrupt sources here, if required
    return;
}

void __interrupt(low_priority) tcInt_low(void) {
    if (TMR2IE && TMR2IF) {
        TMR2IF = 0;
        tick_count -= 10;
    }
}

For devices with the VIC module that are operating in Legacy mode, some features of the hardware interrupt vector tables are still usable. The above information applies to these ISRs, however, the following options are also available.

Consider how many hardware IVTs you require. Write at most two interrupt functions per vector table to link to the high- and low-priority vectors, using any one of low_priority or __low_priority; or high_priority or __high_priority. Consider implementing both interrupt functions for each table, to handle accidental triggering of unused interrupts, or use the -mundefints option to provide a default action (see Undefints Option).

If desired, define a single char parameter instead of using a void parameter list with the ISR. Inside the interrupt function, determine the source of the interrupt using that parameter. It is recommended, however, that the parameter list be set to void if you want to ensure portability with devices that do not have the VIC module.

In your project, set the MVECEN configuration bit to OFF. If required, at the appropriate points in your code, select the required set of dual vectors by writing to the IVTBASE registers.

The vector table for devices operating in legacy mode will have only two vectors, the high and low priority interrupt vectors, rather than the complete set. The offsets of these vectors from the base of the table are 0x8 and 0x18, respectively, which match the vector addresses used by devices without the VIC module. The base address of the dual vectors can still be moved at runtime, and the base() or __base() argument allows the ISR's base address to be specified. Note that this specifies the base address of the vector table, not the address of a vector within that table. Thus, the following shortened example shows the above function prototypes redefined for a table base address of 0x2000. These ISRs will be linked to vector addresses 0x2008 and 0x2018.
void __interrupt(__base(0x2000), __high_priority) tcInt_high(void)
{...}
 
void __interrupt(__base(0x2000), __low_priority) tcInt_low(void)
{...}
If required, more than one runtime IVT can be used in a program. The following ISRs could be defined in addition to those above and linked to a different IVT at address 0x1000.
void __interrupt(__base(0x1000), __high_priority) tcInt_high_alt(void)
{...}
 
void __interrupt(__base(0x1000), __low_priority) tcInt_low_alt(void)
{...}
To make use of the ISRs above, the IVTBASE register must be written with either 0x1000 or 0x2000 at runtime to select which IVT is currently active. Never write the IVTBASE registers if interrupts are enabled. The initial vector table can also be selected by using the -mivt option (see Ivt Option).
The following example rewrites the high priority interrupt function from the first example to include a parameter. The value of this parameter is supplied by the device and can be used to check for the source of the interrupt rather than using more complex expressions that check the interrupt flags. Note the use of the special symbols to check for the interrupt source, for example, IRQ_TMR0. To see the symbols applicable to your device, open the or pic18_chipinfo.html file (located in the docs directory of your compiler’s installation directory) in your web browser and select your target device.
void __interrupt(__high_priority) tcInt_high(unsigned char src) {
    switch(src) {
    case IRQ_TMR0:
        TMR0IF=0;
        ++tick_count;
        break;
    case IRQ_TMR1:
        TMR1IF=0;
        tick_count += 100;
        break;
    // process other interrupt sources in cases here, if required
    }
    return;
}