7
votes

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
2
Exactly what device are you targeting? On some AVRs, registers 0x3d and 0x3e are the stack pointer regs. The ISR prologue might be 'hard coded' to stash the stack pointer in callee-saved registers r28 and r29 in case the ISR code swaps to an interrupt stack? But I'm just guessing.Michael Burr
as i said above the MEGA328user1816847
i'm amazed that there still isn't any good answer as to why gcc does this (I know, 'fixed/scratch registers, blah blah blah') and why, for over a decade now, when people wonder why, they're just told to write the ISRs 'naked' or in assembly. how hard would it be, really, to get gcc to track and eliminate the redundant code here?Spongman

2 Answers

4
votes

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.

4
votes

Keep in mind that ISRs interrupt whatever code is currently executing on the CPU. They must be careful to save the current CPU state on entry (prologue) and restore it on exit (epilogue) so that the interrupted code can continue normally.

On the AVR, there are a couple registers that do not need to be saved/restored in normal function prologues/epilogues but do need to be saved by ISRs:

  • r0 - a scratch register. Normal functions can do whatever they want with it, but ISRs must preserve it.
  • r1 - a zero register, but can be written to by instructions in normal functions. Again, must be preserved by ISRs.

This explains why r0 and r1 are saved. GPIOR0 is apparently just another general-purpose register that a function might be using, so it needs to be saved as well. Possibly some of this could be optimized out if the optimizer could tell that certain registers aren't actually being modified by the ISR, but the optimizer might not be that smart or you might need to try a higher optimization level.

More information is at http://gcc.gnu.org/wiki/avr-gcc