AVR Naked Interrupt Service Routine
Using naked interrupt service routine during AVR dev
AVR, ISR, interrupt service routine, ISR_NAKED, Assembly, avr-asm
--by Captdam @ 2022-11-12 10:09 GMTNon-naked ISR
In MCU, interrupt service routine (ISR) is a mini program executed at the middle of executing the main routine. When an interrupt request rised, the hardware will stop the current process, remember the current process (by recording some information in the stack), and then pass the control to the ISR. The ISR finished by executing a RETI
instruction in the ISR, which let the hardware to pass the control back to the previous process.
The current process does not expect any change made by the ISR except variables marked volatile
. Therefore, most application binary interfaces (ABI) require the ISR to restore any register modified by the ISR. For some MCU like the 68HC11, registers are pushed into stack by the hardware before entering the ISR; however, this is not the case for AVR. For AVR, the hardware only push the program counter (PC) into the stack; everything eles must be saved manually if it will be modified by the ISR, those are:
- Registers R0 to R31, if will be used.
- Status register SREG, which represents flags (e.g. Zero flag, overflow, global interrupt enable...).
- Stack pointer, if the ISR dose push/pop data into stack.
The GNU AVR C compiler will always generate a prologue and an epilogue.
Following is an empty ISR:
ISR (TIMER0_COMPA_vect) {
}
The GNU AVR C compiler will generete the machine code even with optimization on (-O2 or -O3):
00000276 <__vector_14>:
276: 1f 92 push r1
278: 0f 92 push r0
27a: 0f b6 in r0, 0x3f ; 63
27c: 0f 92 push r0
27e: 11 24 eor r1, r1
280: 0f 90 pop r0
282: 0f be out 0x3f, r0 ; 63
284: 0f 90 pop r0
286: 1f 90 pop r1
288: 18 95 reti
In the generated machien code, the following tasks are preformed:
- Save R0 and R1. R0 can be used as a temp register; R1 can be used as a zero register.
- Save SREG by copy it into register and then push that register. SREG may be modified by some instructions such as
EOR
,ADD
. - Clear R1, so the content in R1 become zero.
- Execute actual ISR code.
- Restore SREG, R0 and R1.
- Execute
RETI
to return from ISR.
If the ISR uses any register other than R0 and R1, the compiler will generate code to save and restore those registers as well.
Naked ISR
It is possible to prevent the compiler from generate the prologue and the epilogue by adding ISR_NAKED
flag.
In most case, we should not use the ISR_NAKED
flag because the prologue and the epilogue are here to help us write functionality correct code; however, we sometimes may run into performance issue, or we just want to have some fun, play some trick.
For example, there is an ISR. In the ISR, we reset a value when the timer compare matches:
volatile uint8_t value;
ISR (TIMER0_COMPA_vect) {
value = 20;
}
Now, let's compile this code. We get the following code with -O3 or -O2 flag:
00000276 <__vector_14>:
276: 1f 92 push r1
278: 0f 92 push r0
27a: 0f b6 in r0, 0x3f ; 63
27c: 0f 92 push r0
27e: 11 24 eor r1, r1
280: 8f 93 push r24
282: 84 e1 ldi r24, 0x14 ; 20
284: 80 93 a0 01 sts 0x01A0, r24 ; 0x8001a0
288: 8f 91 pop r24
28a: 0f 90 pop r0
28c: 0f be out 0x3f, r0 ; 63
28e: 0f 90 pop r0
290: 1f 90 pop r1
292: 18 95 reti
We get 14 instructions which consumes 15 words in this ISR. This code does all the job correctly, but we are not happy with the preformance.
- The compiler saved and restored temp register (R0), but not used.
- The compiler saved, cleared and restored the zero register (R1), but not used.
- Because of the
EOR R1, R1
instruction used to clear zero register, SREG is modified; therefore, SREG has to be saved and restored.
Since we are just load a constant value into the variable value
, we can rewrite this ISR in assembly code to save on performace:
volatile uint8_t value;
ISR (TIMER0_COMPA_vect, ISR_NAKED) {
asm volatile (
" PUSH R16 \n"
" LDI R16, 20 \n"
" STS %[var], %[dat] \n"
" POP R16 \n"
" RETI \n"
:
: [var] "m"(value)
, [dat] "I"(20)
);
}
Now, let's compile this code. We get the following code:
00000276 <__vector_14>:
276: 0f 93 push r16
278: 04 e1 ldi r16, 0x14 ; 20
27a: 40 93 a0 01 sts 0x01A0, r20 ; 0x8001a0
27e: 0f 91 pop r16
280: 18 95 reti
Here is what we did:
- We saved a upper register so we can use that register int our ISR. For AVR, we can only assign constant value to the upper 16 registers (R16 to R31).
- We load the value into that register, then write that register into the memory where we save our variable.
- We restore that register and pass the control back.
In the new naked ISR, we perform the task in only 5 instruction and 6 words, that's more than 100% performance boost.
When to use naked ISR and naked function?
Generally speaking, we should avoid naked ISR and naked function as much as possible. Life is so good with the automatically generated code. But, who don't want to have some fun?
Disclaimer: This is my oppion.
For performance issue, I will sometimes use naked ISR. But for naked function, I will say no in most case.
Because ISR needs to be small and fast. Therefore, I will need to make the ISR naked and write some assembly code. On the other hand, functions in the main routine do not have the strict timing requirement like ISRs do; therefore, I am happy with lower performance.
The ABI for ISR is simple: restore whatever you used in ISR. So, house-keeping in ISR is simple. For function call, the ABI is complex, there are call-saved registers, there are call-used registers, there are calling argument registers and stack, there are return registers and stack. Different compilers get different ABI, it is a mess. So, I say no to naked function, unless I have to.