Stacking the deck

1. Encapsulating function calls

Subroutines with zero inputs and zero outputs (besides side-effects) are nice for doing repetitive actions, but we want to call functions with input arguments and receive the results of the routine to the calling code.

  • input arguments via a convention for register usage

  • inputs on the stack

  • instructions using relative addressing modes

  • passing results back from a function

    • C can only return one value

    • Pointers for input arguments

  • Open the GCC Wiki page for avr-gcc

    ABI

    Application Binary Interface. A definition of the low-level ordering of bits, bytes, and other structures required for programs (at the machine code level) to interoperate. How arguments are set up before calling a subroutine and conventions for register usage are part of an ABI.

    API

    Application Programmer Interface. A software interface definition to connect between programs from source code. #include-ing a header file for a library is an example of using an API in C/C++.

Glance at the Type Layout table to see the size definitions of various C types. int is the same 16-bits as short and only long is 32-bits. On the AVR, only char is the native “8-bit processor” size, which has implications for speed and code space.

1.1. Register Layout

Skip down to section Register Layout.

R0

When the compiler wants to use a register for some temporary purpose, it first uses R0.

R1

This one may seem weird. However, some may recall that the MIPS architecture has a hardware register that is always zero, $r0. A register guaranteed to always hold a zero is useful for many purposes. The selection of R1 was arbitrary and nowadays just a fact of life with AVR-GCC.

T

That special “1-bit register”.

since it’s part of the Status Register, it is frequently messed with — be wary of your assumptions about its current value.

1.1.1. Call-Saved Registers

R2 — R17, R28, R29

Put it back the way you found it after you’re finished.

Good life advice, but also how you should deal with these registers.

IF you wish to use these registers to hold local variables or results inside your function, you MUST:

  • Save the value of the affected register (to the stack: push r5)

  • … do your thing …

  • Restore the value of the affected register (from the stack: pop r5)

  • Then and only then can you ret or reti

SomeFunction:
    ; save register values
    push r2
    push r3
    push r4

    ; some random code that touches these registers
    ldi r2, 0x4A
    add r3, r2
    mov r4, r1      ; set r4 to zero!
    subi r4, r3

    ; restore register values
    : (note the reversed order, like "unwinding" or "undoing" a thing)
    pop r4
    pop r3
    pop r2
    ret

1.1.2. Call-Used Registers

R18 — R27, R30, R31

An example in C

// saved
uint8_t r15;   // result

// clobbered
uint8_t r18;   // multiplier
uint8_t r19;   // multiplicand
uint8_t r20;   // loop counter


// compute 7 * 6
r15 = 0;   // for the result
r18 = 7;   // the multiplier
r19 = 6;   // multiplicand

// grade school style multiplication
for (r20=0, r20 < r19, r20++) {
    r15 = adder(r15, r18);
}


uint8_t adder(a, b) {
    r20 = a;
    return r20 + b;
}

Notice how R20 is used inside of the adder() function?

The compiler is under no obligation whatsoever to remember what value R20 held before it stored the value of a in it. The result is that, after returning from the function, the loop counter has changed value.

Think about the consequences of that last sentence.
Therefore an interrupt service routine (ISR) must save and restore these registers, if used.

Also note that the pair of registers R31 : R30 is known as Z on the AVR and has sometimes special uses.

1.2. (Stack) Frame Layout

avrgcc stack frame
Figure 1. Stack frame layout in avr-gcc. Addresses decrease downwards.

avr-gcc decides which specific registers to use near the end of the compilation process, hence this talk about “pseudo registers” — it’s a CS thing.

Notice how R29 : R28 are assigned — i.e. don’t use these registers!

2. Interrupt Service Routines

Introduction to avr-libc’s interrupt handling

It’s nearly impossible to find compilers that agree on how to handle interrupt code.

Even though we are presently using assembly for programming our AVR-based ATtiny85, it is useful to be aware of how a C/C++ compiler deals with interrupts.

Open up the AVR libc documentation about interrupts:
https://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html

3. References

Jaywalking Around the Compiler — Jason Sachs, EmbeddedRelated.com. Mixing C and assembly requires understanding the compiler’s assumptions it uses.