Subroutines and The Stack Pointer
AVR Assembly supports the reuse of code through subroutines. Subroutines are one of the most effective ways of making your life easier as you can write code that does what you want once and reuse it again and again.
However, before learning how to call subroutines, you must understand the concept of The Stack Pointer.
The Stack Pointer
The Stack Pointer is a special register in I/O Memory that points to space allocated in SRAM, referred to as The Stack. The Stack is used to temporarily store register values and return addresses when subroutines are called.
The Stack Pointer is a 16-bit register defined in include files as SPH and SPL. In microcontrollers with very small amounts of SRAM, SPH is not needed and only SPL is used.
Typically, the stack begins at the end of SRAM, and will grow from higher to lower address values when data is stored in it. The stack pointer always points to the top of the stack.
Initializing The Stack Pointer
In order to use the stack, the stack pointer must be initialized to an address in SRAM. Since the stack pointer is in I/O memory, values can be loaded to it using the out instruction. On newer AVRs, the stack pointer will initialize to the last value of SRAM on power up, but on older ones it must be setup manually at the start of any program. An example of how to do this is shown below
ldi r16,LOW(RAMEND) ; load low byte of RAMEND into r16
out SPL,r16 ; store r16 in stack pointer low
ldi r16,HIGH(RAMEND) ; load high byte of RAMEND into r16
out SPH,r16 ; store r16 in stack pointer high
The constant RAMEND is defined in the include file as the last address in SRAM. For many microcontrollers, RAMEND is a 16-bit address so it must be broken into 8-bit components with the HIGH and LOW functions to be loaded into a working register. On small microcontrollers, RAMEND may be less than 16-bits in which case SPH is not used and SPL is the only register that needs to be initialized.
ldi r16,RAMEND ; load RAMEND into r16
out SPL,r16 ; store r16 in stack pointer
Note: Although newer micrcontrollers automatically initialize the stack pointer to RAMEND on power up, it is good practice to always initialize it at the start of a program anyways. This protects you from the stack pointer starting from the wrong location in the case of a software reset.
Storing Data In The Stack
Once the stack pointer is initialized, registers can be saved or loaded to the stack - referred to as pushing or popping, respectively. The instructions for this are shown in the table below.
Mnemonic | Description |
---|---|
push | push register on stack |
pop | pop register from stack |
The push and pop instructions are simple to use:
push r0 ; push r0 to the stack
pop r0 ; restore r0 from stack
When push is called with a register, the contents of that register are stored to the top of the stack, i.e. the address loaded in the stack pointer. The stack pointer is automatically decremented when push is called (remember that stack grows from higher to lower addresses).
When the instruction pop is called with a register, that register is loaded with the contents of the top of the stack. The stack pointer is automatically incremented when pop is called.
When pushing multiple registers to the stack, pop instructions must be called in reverse order to restore values to their original registers, i.e.
push r0 ; push contents of r0 to stack
push r1 ; push contents of r1 to stack
push r2 ; push contents of r2 to stack
pop r2 ; restore contents of r2
pop r1 ; restore contents of r1
pop r0 ; restore contents of r0
Pay careful attention to this as calling pop instructions in the wrong order will result in values being restored to the wrong registers. For example
ldi r16,0x01 ; load r16 with 0x01
ldi r17,0x02 ; load r17 with 0x02
push r16 ; save r16 to the stack
push r17 ; save r17 to the stack
pop r16 ; restore r16 (result = 0x02)
pop r17 ; restore r17 (result = 0x01)
The above results in the contents of r16 and r17 being swapped because the pop instructions are not called in the reverse order of what push was called in (of course, if you really want to swap the contents of two registers without using a third, this is a great way to do it!).
Remember: Registers must be popped from the stack in the reverse order that they were pushed to restore their original values.
Now that we have an understanding of the stack pointer we can move on to subroutines.
Subroutines
Subroutines are sequences of code that can be reused at any point in a program. When a subroutine is called, the microcontroller will push a return address to the stack. It will then jump to the location of the subroutine and execute the code there. When a return statement is reached, the return address will be popped from the stack and the microcontroller will jump to the instruction immediately following the subroutine call.
The instructions used for calling and returning from subroutines are shown below.
Mnemonic | Description |
---|---|
call | long call to subroutine |
icall | indirect call to subroutine |
rcall | relative call to subroutine |
ret | return from subroutine |
Calling Subroutines
The following shows some simple code which calls a subroutine.
ldi r16,0x01 ; load r16 with 0x01
ldi r17,0x02 ; load r17 with 0x02
call addReg ; call subroutine
loop: rjmp loop ; infinite loop
addReg:
add r16,r17 ; add r16 and r17
ret ; return from subroutine
The instruction call is used with the label of our subroutine . Using the call instruction will force the microcontroller to jump to the label given, execute the code there, and when the instruction ret is reached, jump back to the instruction immidiately following call - in this case the infinite loop we have setup with rjmp.
Note that we cannot explicitly pass parameters to a subroutine like with functions in C. In the example above, the subroutine expects its parameters to already be in registers r16 and r17.
Subroutines can be called from the entire program space using call, or relatively over distances of up to 4K words using rcall.
rcall doSomething ; call subroutine doSomething
... ; other program code
doSomething:
... ; subroutine code
ret ; return from subroutine
The last method of calling subroutines, icall is a bit more advanced than is necessary here, but for completeness, it jumps to the address of the subroutine loaded in the Z pointer.
ldi ZL,LOW(doSomething) ; load address of doSomething
ldi ZH,HIGH(doSomething) ; in Z pointer
icall ; indirect call to doSomething
doSomething:
... ; subroutine code
ret ; return from subroutine
Indirect calls are useful when different subroutines must be called depending on runtime parameters. This would usually be implemented with a lookup table, rather than explicitly loading parameters with ldi as shown above.
Saving Registers
When a subroutine is called, it may modify registers that you need later in the program. To prevent this from happening, registers can be pushed to the stack at the beginning of the subroutine, and popped back again at the end.
For example, if your subroutine modifies r16 and r17, their values can be preserved by including the following in your subroutine
rcall doSomething ; call subroutine
... ; rest of program
doSomething:
push r16 ; save r16 to the stack
push r17 ; save r17 to the stack
... ; subroutine code
pop r17 ; restore r17 from the stack
pop r16 ; restore r16 from the stack
ret ; return from subroutine
إرسال تعليق