Path: Home => AVR overview => Interrupt programming => Interrupts and Resources
Interrupt AVR assembler tutorial logo

Interrupts and Resources

Interrupts are effective because they only require action when those are really necessary. In most cases it is not necessary to predict when exactly this will happen. The mechanism even allows that several events overlap.

But: they need some specific resources to work well:
  1. They need a functioning stack.
  2. As they can occur at any time, and if the service routine alters flags in the status register, they have to save and, at the end, to restore the status register.
  3. All other used resources, such as registers, SRAM content, internal hardware etc., all need to be exclusive for the interrupt: whenever you change something, you'll have to make sure that these changes are fine and do not interact with other program uses of such resources.

The stack

Interrupts can occur at any time. They change the controller's program counter and so execute different code. In order to return back to the original place, to before the event occurred, they need to store that address somewhere. In a few older AVR types (AT90S1200, ATtiny12) they had an internal storage place for those 16-bit addresses. All newer devices use the stack for this purpose.

The stack is located in the SRAM, more specific: in its last bytes. A stackpointer is used to point to that location, and this stackpointer is automatically manipulated. Whenever an interrupt occurs, the two address bytes are written to the stack, and the stackpointer advances two steps backwards. At the end of the interrupt service routine, when the instruction RETI is executed, these two address bytes are read back to the program counter. Reading automatically advances the stackpointer, so its original value is reached after RETI.

So, any interrupt-driven program has to init the stackpointer, once following its RESET. The following assembler code is to be used for that:

  ldi R16,Low(RAMEND) ; Point to the end of SRAM, LSB address
  out SPL,R16 ; Init the LSB of the stackpointer
  .ifdef SPH ; Does the device have an MSB stackpointer?
    ldi R16,High(RAMEND) ; Point to the end of SRAM, MSB address
    out SPH,R16 ; Init the MSB of the stackpointer

The first two instructions have to be executed in any case. SPL is a port register and holds the stackpointer's lower eight bits. The function LOW returns the lower 8 bits of the constant RAMEND, which is defined in the of the device (to be included with the .include directive. That is the reason why you need the RAMEND depends from the device's SRAM extend.

If your device has more than 32 + (port register extent) + (SRAM-size) > 256 bytes, it has an additional SPH port register that holds the upper 8 bits of the stackpointer address. The two following instructions only execute in cases that port register SPH exists as constant (as defined in the file). If that symbol name does not exist, those two instructions are not exceuted.

To demonstrate the mechanism, the following pictures show an interrupt execution in the simulator avr_sim. Simulated is an overflow of TC0 in an ATtiny13.

Stackpointer after init In the first two instructions the stack pointer of the device is initiated. It points to the last location in the SRAM of the device.

Timer started,  overflow int enabled With the next four instructions the timer is started in normal mode and with a prescaler value of one. The timer's overflow int has been enabled.

Timer requests overflow interrupt When the timer reaches 256 (and restarts with zero) he requests an interrupt by setting its respective flag bit. If no higher-priority interrupt request is activated, the overflow interrupt will be executed as next step.

Timer overflow interrupt executes Indeed, the overflow interrupt is now executed.

The return address has been written to the stack Stackpointer decreased The stackpointer has been decreased from 0x9F down to 0x9D, two bytes return address have been written to the SRAM's last two positions, and the program counter is on the timer-overflow jump address executing the instruction there.

The interrupt is terminated, waiting for the next The stackpointer has increased After the RETI instruction has been executed the stackpointer returned to its previous value, the execution address has been restored and the timer waits for the next overflow.

Once the stack has been initiated for handling interrupts, you can use it for other purposes as well. The instrcution (R)CALL uses it for calls to subroutines: just like in an interrupt the calling address is pushed to the stack and the calling address is written to the program counter. Whenever your subroutine is completed, you can return to the calling address with the instruction RET. And, like RETI, the return address is read back from the stack to the program counter.

Another use of the stack: you can PUSH the content of a register on the stack, and later restore this (or any other) register with POP. This even works with interrupts: whenever those occur they use the next two stack locations for the address to jump back on RETI, no matter where the stackpointer points to when the interrupt starts. As the interrupt return address is always on the bottom of the stack (the stackpointer advances backwards), after RETI the program finds its pushed stack content at the right place, even if in between an interrupt has been executed.

The stack size can require a few bytes at the end of the SRAM. As only one interrupt is executed at a time, while others wait for their execution, this size is very small. Even ten interrupts in a row need only two bytes of stack space. If you use many additional PUSHs and POPs you might need a bit more stack space. If you program in C, your stack might be unnecessarily filled with several useless PUSHs by the compiler, but not so in assembler.

Forgetting to init the stack with interrupts enabled yields some funny results. As the stackpointer points to zero by default, and as there is no SRAM space where something can be stored, the interrupt will not be able to store and remember the return address. It therefore executes, but only once. And never returns back, to where it was before the interrupt occurred. All subsequent interrupts also execute correctly, but also never return. Nice example for a simulator (like avr_sim): this should recognize the missing stackpointer.

Saving/restoring the status register

One of the resources you have to take care of in case of an interrupt is the status register. This is implemented in AVRs only once, so interrupts and normal program execution both use this resource.

As interrupts are assynchroneous to normal program execution and can execute at any time, even if normal execution is executing something else than the sleep instruction, you'll have to save the status register at the beginning of any interrupt service routine and restore its content at the end. This applies only if your service routine changes flags. So a typical ISR looks like that:

  in R15,SREG ; Save status register content in a register
  ; ... execute interrupt service
  out SREG,R15 ; Restore status register content
  reti ; Return from interrupt

In one case this is a difficult choice: if you want to change the T flag in SREG. The T flag in the status register can be used as a multipurpose flag. It can be set with SET and cleared with CLT or can be loaded with a single bit in a register with BST or a single bit in a register can be overwritten with the T flag by BLD. If you have saved SREG and if you want to restore it afterwards, change this bit in the register you have saved SREG in. Otherwise your change in SREG will be overwritten at the end of the ISR. Or: if nothing else in that routine changes flags: just do not save and restore SREG. The T flag is the reason why AVRs do not automatically save and restore the SREG during interrupts.

There are different methods to save and restore SREG.
Nr.Save inCodeTime required
Clock cycles
1Registerin R15,SREG
out SREG,R15
2 FastRegister
2Stackpush R0
in R0,SREG
out SREG,R0
pop R0
6No register
3SRAMsts $0060,R0
in R0,SREG
out SREG,R0
lds R0,$0060
Therefore, method selection and priority is clear: if you have enough registers available, select (1), if not (2) or (3).

Use of other resources

As interrupts can occur at any time, be sure to avoid interferences with normal program execution. A register or an SRAM location that is used by an ISR cannot be used by any other program part. Be sure that you do not mix appliances here without being aware what the consequences could be. If you need one register or a register pair in an interrupt, handle this as exclusive and note that in the register defintion section as "Used in interrupt X". If you need it only temporarily, name it something with an "i", such as rimp for "Register Interrupt Multi Purpose" or as riTemp. As no interrupt can occur during an ISR execution the same register can be used temporarily in different ISRs.

If you need to change 16-bit values that are used in an interrupt from outside the ISR: disable interrupts temporarily with CLI and enable those if you have written both registers or SRAM locations. Otherwise you risk than an interrupt in between works with a partial or incomplete and false value. An example: An interrupt counts down a value in the register pair R25:R24 with SBIW R24,1, e. g. for the duration of a tone. If you change either R25 or R24 from outside, make sure that no interrupt occurs in between those two. You would end up with a completely false duration if that would happen.

A special case can occur if you have a fast and a slow interrupt generation mixed or if your flag handling routine is very long. The two or more handling routines then have to ensure that all flag settings are treated and that no flag is missed. That means you have to carefully select the what-comes-first and, if necessary, to insert further flag handling routine calls inside your lengthy routine. Just consider your timings and check whether something can go wrong or not.

Some basic recommendations

To the top of that page

©2009-2019 by