AVR Procedures

Read time: 32 minutes (8245 words)

We definitely want to be able to set up procedures (or functions, if you prefer). Fortunately, doing so is not too hard for simple procedures with no parameters. In that case, the procedure will get all of the information it needs to run through global variables. In a small system, this is not a bad approach, but we will explore parameters here, after we get things going.

Let’s start off with a simple example of a function that simply references a global variable:

tstproc1.c
1
2
3
4
5
6
7
8
9
int x = 0;

void func(void) {
    x = 5;
}

int main(void) {
    func();
}

Here is the compiler’s output:

testproc1.s
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
	.file	"testproc1.c"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
.global	x
	.section .bss
	.type	x, @object
	.size	x, 2
x:
	.zero	2
	.text
.global	func
	.type	func, @function
func:
	push r28
	push r29
	in r28,__SP_L__
	in r29,__SP_H__
/* prologue: function */
/* frame size = 0 */
/* stack size = 2 */
.L__stack_usage = 2
	ldi r24,lo8(5)
	ldi r25,0
	sts x+1,r25
	sts x,r24
	nop
/* epilogue start */
	pop r29
	pop r28
	ret
	.size	func, .-func
.global	main
	.type	main, @function
main:
	push r28
	push r29
	in r28,__SP_L__
	in r29,__SP_H__
/* prologue: function */
/* frame size = 0 */
/* stack size = 2 */
.L__stack_usage = 2
	rcall func
	ldi r24,0
	ldi r25,0
/* epilogue start */
	pop r29
	pop r28
	ret
	.size	main, .-main
	.ident	"GCC: (GNU) 7.2.0"
.global __do_clear_bss

Once again, we see a lot of code we will not worry about here.

We can see that the compiler set up our two functions, one named main and one named func. Furthermore, I can see a rcall to func in line 51. So, it look like calling a procedure is pretty simple.

Note

This processor actually has two instructions that “call” a procedure. One is simply call which specifies the procedure to call by providing the address of that procedure as an operand. The second form, shown here, is a “relative call”. In this form, the operand can be a small number to be added to the current value of the program counter,. In that case the operand is the distance from the call to the procedure, which is a small number.

The compiler’s code has a lot of stuff that is actually not needed in this case. However, compiler’s are basically following simple rules thta use patterns of code for every action in your program. In this case, the compiler actually set things up the same way it would if you used parameters. (That is all those lines with push and pop. We will explain those in a bit!)

The actual work in the procedure looks exactly the same as code we have seen earlier. We simply reference the global variable by name and work with it.

Introducing the Stack

Before we start using parameters in our function calls, we need to understand a basic memory management structure called the “Stack”. Those of you taking a data structures course probably already know about this kind of structure.

Basically, a stack is a “last-in first-out” data store. Think of it like a stack of trays in a cafeteria. You add new trays onto the top of the stack, and you remove trays from the top of the stack as well. You are not allowed to pull a tray from the middle of the stack, and you cannot push a new tray into the middle either.

we can build such a thing using a simple array:

stack.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>  // defines printf
#include <stdlib.h> // defines exit

#define STACKSIZE   100

unsigned char stack[STACKSIZE];
int SP = -1;

void push(unsigned char item) {
    SP++;
    if(SP>=STACKSIZE) {
        printf("Error: STack overflow\n");
        exit(1);
    }
    stack[SP] = item;
}

unsigned char pop(void) {
    if(SP<0) {
        printf("Error: Stack underflow\n");
        exit(1);
    }
    SP--;
    return stack[SP+1];
}

int main(void) {
    unsigned char data;

    push(5);
    data = pop();
    printf("data = %d\n", data);
    data = pop();
}


RUnning this code give this output:

$ gcc -o stack stack.c
$ ./stack
data = 5
Error: Stack underflow

We can use the stack as a convenient way to temporarily store data, and retrieve it later. This is what the compiler is doing when the generated function code starts up.

Warning

When you use a stack, it is vital that you manage it properly. Any time you use a push operation, make sure you immediately add a matching pop operation. Failure to do this will cause program crashes later, as we shall see!

Parameters

The puzzle we have is how did the calling code (main) provide the parameter, and how did the function (func) retrieve that parameter?

The answer is simple, the compiler will use a register to hold the parameter!

Here is our new code:

tstproc2.c
1
2
3
4
5
6
7
8
9
int x = 0;

void func(unsigned char data) {
    x = data;
}

int main(void) {
    func(5);
}

Here is the compiler’s output:

testproc2.s
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
	.file	"testproc2.c"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
.global	x
	.section .bss
	.type	x, @object
	.size	x, 2
x:
	.zero	2
	.text
.global	func
	.type	func, @function
func:
	push r28
	push r29
	push __zero_reg__
	in r28,__SP_L__
	in r29,__SP_H__
/* prologue: function */
/* frame size = 1 */
/* stack size = 3 */
.L__stack_usage = 3
	std Y+1,r24
	ldd r24,Y+1
	mov r24,r24
	ldi r25,0
	sts x+1,r25
	sts x,r24
	nop
/* epilogue start */
	pop __tmp_reg__
	pop r29
	pop r28
	ret
	.size	func, .-func
.global	main
	.type	main, @function
main:
	push r28
	push r29
	in r28,__SP_L__
	in r29,__SP_H__
/* prologue: function */
/* frame size = 0 */
/* stack size = 2 */
.L__stack_usage = 2
	ldi r24,lo8(5)
	rcall func
	ldi r24,0
	ldi r25,0
/* epilogue start */
	pop r29
	pop r28
	ret
	.size	main, .-main
	.ident	"GCC: (GNU) 7.2.0"
.global __do_clear_bss

In this example, we still see the stack code at the top of the function. This time, before the call in main, we see the compiler copying the data from the variable into a register, and then making a call to the function.

Function Stack Usage

We still have not discussed why the compiler added all that stack code to the function. It was not needed for this simple function, so why is it there.

The answer is simple. The pattern the compiler used to set up this function assumed that you might call another function while inside this first one If so, the pattern says to use the same register for a parameter for the second call. But you are using that same register in this first function. If you take that one over, your valuable parameter data will be lost when you try to use it after the call to the second function.

What a mess.

But, there is a simple solution.

Before the second call, protect your current register contents by “pushing” the data onto the stack, and immediately after the call, restore the register by using a pop.

We do not see that action here, but we can set up a simple experiment to see it

tstproc3.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>
int x = 0;

unsigned char func(unsigned char data) {
    if(data>1) return func(data-1)*data;
    return 1;
}

int main(void) {
    x = func(5);
    printf("Result: %d\n", x);
}

Here is the compiler’s output:

testproc3.s
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
	.file	"testproc3.c"
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__SREG__ = 0x3f
__tmp_reg__ = 0
__zero_reg__ = 1
.global	x
	.section .bss
	.type	x, @object
	.size	x, 2
x:
	.zero	2
	.text
.global	func
	.type	func, @function
func:
	push r28
	push r29
	push __zero_reg__
	in r28,__SP_L__
	in r29,__SP_H__
/* prologue: function */
/* frame size = 1 */
/* stack size = 3 */
.L__stack_usage = 3
	std Y+1,r24
	ldd r24,Y+1
	cpi r24,lo8(2)
	brlo .L2
	ldd r24,Y+1
	subi r24,lo8(-(-1))
	rcall func
	ldd r25,Y+1
	mov r22,r25
	rcall __mulqi3
	rjmp .L3
.L2:
	ldi r24,lo8(1)
.L3:
/* epilogue start */
	pop __tmp_reg__
	pop r29
	pop r28
	ret
	.size	func, .-func
	.section	.rodata
.LC0:
	.string	"Result: %d\n"
	.text
.global	main
	.type	main, @function
main:
	push r28
	push r29
	in r28,__SP_L__
	in r29,__SP_H__
/* prologue: function */
/* frame size = 0 */
/* stack size = 2 */
.L__stack_usage = 2
	ldi r24,lo8(5)
	rcall func
	mov r24,r24
	ldi r25,0
	sts x+1,r25
	sts x,r24
	lds r24,x
	lds r25,x+1
	mov r18,r25
	push r18
	push r24
	ldi r24,lo8(.LC0)
	ldi r25,hi8(.LC0)
	mov r24,r25
	push r24
	ldi r24,lo8(.LC0)
	ldi r25,hi8(.LC0)
	push r24
	rcall printf
	pop __tmp_reg__
	pop __tmp_reg__
	pop __tmp_reg__
	pop __tmp_reg__
	ldi r24,0
	ldi r25,0
/* epilogue start */
	pop r29
	pop r28
	ret
	.size	main, .-main
	.ident	"GCC: (GNU) 7.2.0"
.global __do_copy_data
.global __do_clear_bss

What do you think this code is doing?

Here is the output:

$ gcc -o testproc3 testproc3.c
$ ./testproc3
Result is 120

Note

Obviously, this did not appear on the Arduino! The code we will examine is AVR assembly, though!

We need to be a bit careful in reading this code, the compiler is using a register named Y here. The AVR has three 16-bit registers, which are simply aliases for the top six 8-bit registers in the register “file”. Those aliases are X, Y, and Z.

Let’s start off by examining the call to the function in main.

Line 62 is that call. Right above that line we see the parameter being loaded into the r24 register. I bet we see that setup again inside the func function!

Look at line 32. THis is the inner (Recursive) call to func. Right above that call, we see a reference to r24 again, bu this one is not so simple to understand. We will get to that.

Recursion!

Of all the scary concepts in programming, this one is right at the top.

A function that calls itself to do something! Yikes!

This is safe to do as long as you do not do it the wrong way.

In this case, the code is calling another copy of itself, but with a smaller parameter. Keep doing that and eventually the last function will wake up and find that the parameter matches zero. The conditional call will not happen, and all those pending functions, waiting for a return value, will get their value and the call sequence will “unwind! Phew!

Function “Prologue”

The code generated by the compiler for any function follows a common pattern. First it will save whatever is currently in the r28 and r29 registers on the stack. If you look at the bottom of the function, you will see code restoring those two registers using pop instructions. This is how the compiler “frees up” those two registers for use in this function. If the calling code was using those registers for something, it would be unaware that we used the same registers here!

The next thing the compiler does is load those two registers with the system stack pointer (those funny __SP_L__ and __SP_H__`` names refer to the low and high bytes of that 16-bit register. What that does is mark a spot in the stack where the stack was as this function was activated. What is stored there is pretty important. It turns out that all active functions (those that have not “returned” yet, have this register pair set t point to the value of the stack pointer at the moment they were called. This is a pointer into the stack that can help locate data later. Formally, this pointer is pointing to a “stack frame” of temporary that is available to that function while it runs.

Guess what we can put in the “frame”!

Local Variables!

Note

Do you remember that you were told that “local variables” go “poof” when you leave a function, and it is not safe to assume they will have the same values the next time you call that same function? The reason why is simple. Those variables are allocated to memory on the “stack” and that memory is used by all functions. As functions come and go, the stack grows and shrinks. You never really now where on the stack your “frame” will be found.

Calling a Function

When the call instruction is executed, it is just a fancy branch instruction. The difference is critical. In a simple branch, we overwrite the PC with a new address, so the next fetch will happen at the new location. When we call a function, we do not want to lose the address calculated during the decode step. That is the address we want to return to when the function ends.

So, call “pushed the next address (the one calculated during decode) onto the stack. Then it overwrites the PC with the new address, safe in the knowledge that the place we are supposed to return to is stored on the stack.

Returning From a Function

The companion ret instruction simply “pops” the return address off of the stack, and updates the PC, so we fetch from the right spot.

Simple.

However, there is one critical thing to remember!

Warning

Remember that warning about matching every “push” with a “pop”? If you fail to do that in the function body, when you get to the ret instruction, you may pop an address that lives in “never-never-land”! Say goodbye to your program at that point!

Accessing the Parameter

That weird code inside of the function needs a look.

Our parameter is in r24. That much is clean.