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
) orhigh_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 eithervoid
or onechar
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, usinglow_priority
(or__low_priority
) orhigh_priority
(or__high_priority
); and optionally, specify the base address of the IVT in which to place the function’s address, usingbase()
(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 eithervoid
or onechar
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 eitherirq()
or__irq()
; specify the interrupt priority assigned to the function’s source, using eitherlow_priority
(or__low_priority
) orhigh_priority
(or__high_priority
); and optionally, specify the base address of the IVT in which to place the function’s address, using eitherbase()
(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.
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)
{...}
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.