5.9.1 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:

  • Write each ISR prototype using the __interrupt() specifier.
  • Use void as the return type and for the parameter specification.
  • If your device supports interrupt priorities, with each function use the low_priority (or __low_priority) or high_priority (or __high_priority) arguments to __interrupt().
  • 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.

For devices operating in legacy mode:

  • Write each ISR prototype using the __interrupt() specifier.
  • Use void as the return type and specify a parameter list of either void or one char argument if you need to identify the interrupt source. It is recommended that the parameter list be set to void if you want to ensure portability with devices that do not have the VIC module.
  • As arguments to the __interrupt() specifier in the ISR prototype, specify the interrupt priority assigned to the function’s source, using low_priority (or __low_priority) or high_priority (or __high_priority); and optionally, specify the base address of the IVT in which to place the function’s address, using base() (or __base()). It is recommended that the base address be left as the default if you want to ensure portability with devices that do not have the VIC module.
  • If the ISR processes more than one source, determine the source of the interrupt from the function’s parameter, if specified, or by checking the interrupt flag and the interrupt enable for each source that is to be processed.

For devices which are using the VIC module:

  • Write each ISR prototype using only the __interrupt() specifier.
  • Use void as the return type and specify a parameter list of either void or one char argument if you need to identify the interrupt source.
  • As arguments to the __interrupt() specifier in the ISR prototype, specify which sources each interrupt function should handle, using either irq() or __irq(); specify the interrupt priority assigned to the function’s source, using either low_priority (or __low_priority) or high_priority (or __high_priority); and optionally, specify the base address of the IVT in which to place the function’s address, using either base() (or __base()).
  • If the ISR processes more than one source, determine the source of the interrupt from the function’s parameter, if specified, or by checking the interrupt flag and the interrupt enable for each source that is to be processed.

For all devices:

  • Inside the ISR body, clear the relevant interrupt flag once the source has been processed.
  • Do not re-enable interrupts inside the ISR body. This is performed automatically when the ISR returns.
  • Keep the ISR as short and as simple as possible. Complex code will typically use more registers that will increase the size of the context switch code.

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 using a special return instruction. It is recommended that interrupt functions be placed in C source files in the project; however, as they are marked as a “root” function in the program's call graph, they will always be linked into a program, even if they are placed in a library archive.

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 4.6.1.26 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 4.6.1.12 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)
{...}
The vector table for devices operating in legacy mode will only have two vectors, those being the high and low priority interrupt vectors. The offsets of these vectors from the base of the table are 0x8 and 0x18, respectively. As it does in other instances, the base() argument, if it is required, specifies the base address of the vector table, not the address of a vector within that table. Thus, the following code defines both priority ISRs for a device in legacy mode and where the table base address has been moved to address 0x2000. These ISRs will appear at addresses 0x2008 and 0x2018.
void __interrupt(base(0x2000), high_priority) highIsr(void)
{...}
 
void __interrupt(base(0x2000), low_priority) lowIsr(void)
{...}

Devices operating in legacy mode still allow you to define more than one interrupt vector table. For each table, use a different base address and define at most two interrupt functions for the high and low priority interrupts.