I have a very simple Interrupt Service Routine(ISR) written for the atmega328 and compiled with avrgcc (using -Os) using AVR studio.
ISR (TIMER0_OVF_vect) {
txofcnt++; //count overflows and store in uint16_t
}
If you note the assembly generated (below), it uses r24, r25 to get the job incrementing the volatile uint16_t txofcnt, but it also push-write-pop r1, r28, r29 without ever reading them. It also has an extra push/pop of r0 without ever using it in between.
I flat out don't understand why r1 is pushed, cleared and then finally poped. But also why does gcc feel the need to load EIMSK and GPIOR0 into registers and then not use them. Bonus points if you can tell me what GPIOR0 is even for, the datasheet says it exists but has no description.
00000258 <__vector_16>:
ISR (TIMER0_OVF_vect) {
258: 1f 92 push r1
25a: 0f 92 push r0
25c: 00 90 5f 00 lds r0, 0x005F
260: 0f 92 push r0
262: 11 24 eor r1, r1
264: 8f 93 push r24
266: 9f 93 push r25
268: cf 93 push r28
26a: df 93 push r29
26c: cd b7 in r28, 0x3d ; 61 reads register EIMSK
26e: de b7 in r29, 0x3e ; 62 reads register GPIOR0
txofcnt++;
270: 80 91 0a 01 lds r24, 0x010A
274: 90 91 0b 01 lds r25, 0x010B
278: 01 96 adiw r24, 0x01 ; 1
27a: 90 93 0b 01 sts 0x010B, r25
27e: 80 93 0a 01 sts 0x010A, r24
}
282: df 91 pop r29
284: cf 91 pop r28
286: 9f 91 pop r25
288: 8f 91 pop r24
28a: 0f 90 pop r0
28c: 00 92 5f 00 sts 0x005F, r0
290: 0f 90 pop r0
292: 1f 90 pop r1
294: 18 95 reti
There is some documentation on GCC's register usage for AVR at https://gcc.gnu.org/wiki/avr-gcc
Some passages relevant to your question:
Fixed Registers
Fixed Registers are registers that won't be allocated by GCC's register allocator. Registers R0 and R1 are fixed and used implicitly while printing out assembler instructions:
R0
is used as scratch register that need not to be restored after its usage. It must be saved and restored in interrupt service routine's (ISR) prologue and epilogue. In inline assembler you can use
__tmp_reg__
for the scratch register.R1
always contains zero. During an insn the content might be destroyed, e.g. by a MUL instruction that uses R0/R1 as implicit output register. If an insn destroys R1, the insn must restore R1 to zero afterwards. This register must be saved in ISR prologues and must then be set to zero because R1 might contain values other than zero. The ISR epilogue restores the value. In inline assembler you can use
__zero_reg__
for the zero register....
Call-Used Registers
The call-used or call-clobbered general purpose registers (GPRs) are registers that might be destroyed (clobbered) by a function call.
R18–R27, R30, R31
These GPRs are call clobbered. An ordinary function may use them without restoring the contents. Interrupt service routines (ISRs) must save and restore each register they use.
...
Call-Saved Registers
R2–R17, R28, R29
The remaining GPRs are call-saved, i.e. a function that uses such a registers must restore its original content. This applies even if the register is used to pass a function argument.
What follows is my speculation on why the compiler performs some apparently unnecessary register save/restores in the ISR prologue/epilogue:
r0
and r1
are saved/restored because code that the compiler generates or calls will make the assumptions outlined about them above. Since they aren't tracked by GCC's register allocator, the prologue must make sure they're saved (and in r1
's case initialized to 0).
r28
and r29
are used to save the stack pointer (0x3d
/SPL
and 0x3e
/SPH
). I'm guessing (and I want to stress that this is a guess) that the compiler writers assume that it might be common for an interrupt handler to swap stacks, and this makes sure that the ISR can restore that stack that was in use when the interrupt occurred. The compiler can assume that these registers won't be altered by called functions since they are "call-saved" registers.
Also, you should note that the apparently "extra" push & pop of r0
are to save the SREG
status register on the stack. Even though r0
isn't used between those push
and pop
instructions, remember that the r0
register is a scratch register that isn't tracked by the register allocator, so the compiler won't assume that r0
will not have changed after it loads SREG
into it.
As a side note, the reads of 0x3d
and 0x3e
are the SPL
and SPH
stack pointer registers, not the EIMSK
and GPIOR0
registers. See Note 4 of the Register Summary table on page 625 in the reference manual here for detail on how the register addressing differs when using the IN
/OUT
instructions instead of a load or store instruction.
And for the bonus points regarding GPIOR0
:
8.5.1 General Purpose I/O Registers
The ATmega48A/PA/88A/PA/168A/PA/328/P contains three General Purpose I/O Registers. These registers can be used for storing any information, and they are particularly useful for storing global variables and Status Flags. General Purpose I/O Registers within the address range 0x00 - 0x1F are directly bit-accessible using the SBI, CBI, SBIS, and SBIC instructions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With