Captdam

AVR Naked Interrupt Service Routine

Using naked interrupt service routine during AVR dev

--by Captdam @ 2022-11-12 10:09 GMT

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

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:

  1. Save R0 and R1. R0 can be used as a temp register; R1 can be used as a zero register.
  2. Save SREG by copy it into register and then push that register. SREG may be modified by some instructions such as EOR, ADD.
  3. Clear R1, so the content in R1 become zero.
  4. Execute actual ISR code.
  5. Restore SREG, R0 and R1.
  6. 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.

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:

  1. 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).
  2. We load the value into that register, then write that register into the memory where we save our variable.
  3. 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.