Writing an Interrupt Service Routine

The prototype and content of an ISR will vary based on the target device and the project being compiled. Observe the following guidelines when writing an ISR.

For devices that do not have the VIC module:

For devices operating in legacy mode:

For devices which are using the VIC module:

For all devices:

If interrupt priorities are being used but an ISR does not specify a priority, it will default to being high priority. It is recommended that you always specify the ISR priority to ensure your code is readable.

If you supply an irq() or base() argument to the __interrupt() specifier with a device that does not have the VIC module, an error will be issued by the compiler. If you use this specifier with a device that is configured for legacy mode, supplying an irq() argument will result in an error from the compiler; however, you may continue to use the base() argument if required.

Devices that have the VIC module identify each interrupt with a number. This number can be specified with the irq() argument to __interrupt() if the vector table is enabled, or you can use a compiler-defined symbol that equates to that number. You can see a list of all interrupt numbers, symbols and descriptions by opening the files pic_chipinfo.html or pic18_chipinfo.html in your favorite web browser, and selecting your target device. Both these files are located in the docs directory under your compiler’s installation directory.

Interrupt functions always use the non-reentrant function model. These functions ignore any option or function specifier that might otherwise specify reentrancy.

The compiler processes interrupt functions differently to other functions, generating code to save and restore any registers used by the function and a special return instruction.

An example of an interrupt function written for code not using the IVT is shown below. Notice that the interrupt function checks 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.

int tick_count;
void __interrupt(high_priority) tcInt(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;
}

Here is the same function code, split and modified for a device using vector tables. Since only one interrupt source is associated with each ISR, the interrupt code does not need to determine the source and is therefore faster.

void __interrupt(irq(TMR0),high_priority) tc0Int(void)
{
    TMR0IF=0;
    ++tick_count;
    return;
}
void __interrupt(irq(TMR1),high_priority) tc1Int(void)
{
    TMR1IF=0;
    tick_count += 100;
    return;
}

If you prefer to process multiple interrupt sources in one function, that can be done by specifying more than one interrupt source in the irq() argument and using a function parameter to hold the source number, such as in the following example.

void __interrupt(irq(TMR0,TMR1),high_priority) tInt(unsigned char src)
{
    switch(src) {
    case IRQ_TMR0:
        TMR0IF=0;
        ++tick_count;
        break;
    case IRQ_TMR1:
        TMR1IF=0;
        tick_count += 100;
        break;
    }
    return;
}

The VIC module will load the parameter, in this example, src, with the interrupt source number when the interrupt occurs.

The special interrupt source symbol, default, can be used to indicate that the ISR will be linked to any interrupt vector not already explicitly specified using irq(). You can also populate unused vector locations by using the -mundefints option (see Undefints Option).

By default, the interrupt vector table will be located at an address equal to the reset value of the IVTBASE register, which is the legacy address of 0x8. The base() argument to __interrupt() can be used to specify a different table base address for that function. This argument can take one or more comma-separated addresses. The base address cannot be set to an address lower than the reset value of the IVTBASE register.

By default and if required, the compiler will initialize the IVTBASE register in the runtime startup code. You can disable this functionality by turning off the -mivt option (see Ivt Option). This option also allows you to specify an initial address for this register, for the initial vector table that will be used. If vectored interrupts are enabled but you do not specify an address using this option, the vector table location will be set to the lowest table address used in the program, as specified by the base() arguments to __interrupt().

If you use the base() argument to implement more than one table of interrupt vectors, you must ensure that you allocate sufficient memory for each table. The compiler will emit an error message if it detects an overlap of any interrupt vectors.

The following examples show the interrupt function prototypes for two ISRs which handle the timer 0 and 1 interrupt sources. These are configured to reside in independent vector tables whose base addresses are 0x100 and 0x200. All other interrupt sources are handled by a low-priority ISR, defIsr(), which appears in both vector tables. For these ISRs to become active, the IVTBASE register must first be loaded either 0x100 or 0x200. Changing the address in this register allows you to select which vector table is active.

void __interrupt(irq(TMR0,TMR1),base(0x100)) timerIsr(void)
{...}
void __interrupt(irq(TMR0,TMR1),base(0x200)) altTimerIsr(void)
{...}
void __interrupt(irq(default),base(0x100,0x200),low_priority) defIsr(void)
{...}