4.8.1 Writing an Interrupt Service Routine

Observe the following guidelines when writing an ordinary ISR.

  • Write each ISR prototype by enabling the CCI (3.6.3.4 Ext Option) and using the __interrupt() specifier. This will create a function with the appropriate name, prototype, and attributes.
  • If necessary, clear the relevant interrupt flag once the source has been processed, although typically this is not required.
  • Only re-enable interrupts inside the ISR body if absolutely necessary. Interrupt are re-enabled 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.

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

The hardware globally disables interrupts when an interrupt is executed.

Usually, each interrupt source has a corresponding interrupt flag bit, accessible in a control register. When set, these flags indicate that the specified interrupt condition has been met. Interrupt flags are often cleared in the course of processing the interrupt, either when the handler is invoked or by reading a particular hardware register; however, there are a few instances when the flag must be cleared manually by code. Failure to do so might result in the interrupt triggering again as soon as the current ISR returns.

The flag bits in the SFRs have a unique property whereby they are cleared by writing a logic one to them. To take advantage of this property, you should write directly to the register rather than use any instruction sequence that might perform a read-modify-write. Thus to clear the TOV0 timer overflow flag in the TC0 interrupt flag register, use the following code:
TIFR = _BV(TOV0);

which is guaranteed to clear the TOV0 bit and leave the remaining bits untouched.

An example of an interrupt function is shown below.
void __interrupt(SPI_STC_vect_num) spi_Isr(void) {
  process(SPI_ClientReceive());
  return;
}

Note that the argument to the __interrupt() specifier is a vector number, which are available as macros ending with vect_num once you have included <xc.h> in your program.

More complex interrupt function definitions can be created using macros and attributes defined by <avr/interrupt.h>, which are shown in the following examples.

If there is no code to be executed for an interrupt source but you want to ensure that the program will continue normal operation should the interrupt unexpectedly trigger, then you can create an empty ISR using the EMPTY_INTERRUPT() macro and an interrupt source argument.
#include <avr/interrupt.h>
EMPTY_INTERRUPT(INT2_vect);
The special interrupt source symbol, BADISR_vect, can be used with the ISR() macro to define a function that can process any otherwise undefined interrupts. Without this function defined, an undefined interrupt will trigger a device reset. For example:
ISR(BADISR_vect) {
  // place code to process undefined interrupts here
  return;
}
If you wish to allow nested interrupts you can manually add an in-line sei instruction to your ISR to re-enable the global interrupt flag; however, there is an argument you can use with the ISR() macro to have this instruction added by the compiler to the beginning of the interrupt routine. For example:
ISR(IO_PINS_vect, ISR_NOBLOCK)
{ ... }
The ISR_NOBLOCK expands to __attribute__((interrupt)), which can be used instead.
If one ISR is to be used with more than one interrupt vector, then you can define that ISR in the usual way (using __interrupt()) for one vector then reuse that ISR for other vector definitions using the ISR_ALIASOF() argument.
void __interrupt(PCINT0_vect_num)
{ ... }
ISR(PCINT1_vect, ISR_ALIASOF(PCINT0_vect));
In some circumstances, the compiler-generated context switch code executed by the ISR might not be optimal. In such situations, you can request that the compiler omit this context switch code and supply this yourself. This can be done using the ISR_NAKED argument, as shown in this example.
ISR(TIMER1_OVF_vect, ISR_NAKED)
{
  PORTB |= _BV(0); // results in SBI which does not affect SREG
  reti();
}

Note that the compiler will not generate any context switch code, including the final return from interrupt instruction, so you must write any relevant switching code and the reti instruction. The SREG register must be manually saved if it is modified by the ISR, and the compiler-implied assumption of __zero_reg__ always being 0 could be wrong, for example when an interrupt occurs right after a mul instruction.