Whenever we call a function we need to create a data structure to hold its data (called an activation record), on the stack. Three things need to go into this record:
We stack the activation records (so we need a stack pointer). Notice that we can't use the stack pointer for indexed addressing, so we also have to have one of the index registers pointing to the current activation record. This is referred to as a frame pointer.
We have a number of registers and instructions to help us manipulate
this stack. First, there is a stack pointer register
(sp
). This register always points to the next location
we'll be pushing to (so it doesn't quite point to the top of the
stack). There are a number of instructions that either manipulate or
make use of sp
. Some of them are
lds
,
sp
psha
, pshb
, pshx
, pshy
pula
, pulb
, pulx
, puly
tsx
, tsy
sp
and copy it into a register
txs
, tys
sp
jsr
, rts
jsr
pushes a return
address and jumps; rts
pops a return address and goes
where it says
The function call itself is a partnership between the procedure doing the calling, and the procedure being called. Each has to execute code involved in setting up an activation record, transferring control, and deleting the activation record. Of the components of an activation record defined above, we can get a picture for who does what based on asking who knows what.
jsr
instruction pushes the return address, but we
have to push the frame pointer "by hand". It turns out that
either the caller or the callee can do this; we just have to keep
straight which is going to! For the example that's going to
follow, we'll assume the callee takes care of it.
Timmy | Lassie |
---|---|
push parameters | |
jsr | |
Push old frame pointer | |
Use des to reserve space on stack for locals | |
Use tsx to create frame pointer | |
Get work done | |
Use ins to get rid of local variables | |
Pop any registers we pushed | |
rts | |
Use ins to remove parameters from stack |
More sophisticated processors will typically have many more registers that might need to be saved and restored as part of the procedure call. HC11 is lucky in this case! Normally a processor and compiler will have a "calling convention", specifying some registers which must be preserved across a procedure call, others which can be corrupted with impunity, and still others that will be used to return results.
Let's do an example of a program that will compute
int summer(int i)
{
if (i == 0)
return i;
else
return i + summer(i - 1);
}
OK... Here's how it looks when we translate this code to HC11:
STACK equ $00ff
EEPROM equ $f800
org EEPROM
main lds #STACK * set up stack pointer
ldaa #5 * push parameter
psha
jsr summer * call procedure
ins * get rid of param
eloop bra eloop
**********************************************
*
* Activation record:
* old <- ix
* x
* ret
* addr
* i
summer pshx * save old frame pointer
tsx * set up new frame pointer
tst 4,x * see if param is 0
bne nonzer * if not, branch to "else" code
clra * if so, we'll return 0
bra ret
nonzer ldaa 4,x * if nonzero param, make a recursive call
deca
psha
jsr summer
ins
* * when we return from the recursive call,
* * summer(i-1) is in the a accumulator
adda 4,x
ret pulx * restore old frame pointer
rts * and return
end main