Subroutines and The Stack Pointer

 

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.

MnemonicDescription
pushpush register on stack
poppop 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.



Pushing a register to the stack does not erase the value of the register, it simply copies its contents to SRAM. Similarly, popping a value from the stack does not erase the contents of that address in the stack.


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.

MnemonicDescription
calllong call to subroutine
icallindirect call to subroutine
rcallrelative call to subroutine
retreturn 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 addReg. 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


Post a Comment

Post a Comment (0)

Previous Post Next Post