Since the stack is used to store the return address, you must exercise caution when pushing and popping from within a procedure. Check out this defective procedure:
Bollixed: PUSH DE RET
Stack frames are what give procedures the ability to call other procedures (even themselves!) without the CPU getting confused. Before returning, a procedure must clean up the stack frame until just the return address exists.
LD B, 5
Loop:
CALL PrintSpc
b_call(_NewLine)
DJNZ Loop
RET
PrintSpc:
LD A, 'X'
LD B, 6
PrintLoop:
b_call(_PutC)
DJNZ PrintLoop
RET
If a procedure will modify some registers, they should be saved with PUSH/POP. Either the caller or the callee can take responsibility:
LD B, 5
Loop:
CALL PrintSpc
b_call(_NewLine)
DJNZ Loop
RET
PrintSpc:
PUSH BC
PUSH AF
LD A, 'X'
LD B, 6
PrintLoop:
b_call(_PutC)
DJNZ PrintLoop
POP AF
POP BC
RET
LD B, 5
Loop:
PUSH BC
CALL PrintSpc
POP BC
b_call(_NewLine)
DJNZ Loop
RET
PrintSpc:
LD A, 'X'
LD B, 6
PrintLoop:
b_call(_PutC)
DJNZ PrintLoop
RET
What is the practical difference between the two methods? When the procedure saves the registers, then there is only one copy of the pushes and pops. When the caller saves the registers, there must be a set of pushes and pops around each CALL. Not only does this increase program size, it can be difficult to always remember which registers need to be saved.
On the other hand, if the calling code saves the registers, then time doesn't need to be wasted preserving registers that don't need to be preserved. In 14-2, the procedure saves both BC and AF, when there is no real point in saving AF. In 13-3, the caller only saves BC since that is the only register it cares about.
<rant>
Passing Parameters By Value
Passing Parameters By Reference
; Pass the variable val to a procedure by value ; Make a copy of the variable LD A, (val) CALL ByVal RET ByVal: ; A holds the input parameter, since we have no access to the original variable ; (not entirely true, but just play along), it can't be modified AND $0F XOR $07 b_call(_PutC) RET val: .DB 99
The size of the data is a deciding factor in choosing to use By Value. Since a full copy must be made, By Value is only good for small data objects and absolutely abyssmal for larger structures.
Parameters passed by value are input-only. You can pass them to a procedure, but the procedure can't use them for return data.
; Pass the variables val1 and val2 to a procedure by reference ; Create pointers in DE and HL to the variables LD HL, val1 LD DE, val2 CALL ByRef RET val1: .DB 200 val2: .DB 46 ByRef: ; HL and DE hold the address of the parameters and so must be dereferenced. LD A, (DE) ADD A, (HL) LD (DE), A RET
For small amounts of data, pass by reference is usually less efficient than passing by value because the parameters have to be dereferenced each time they are accessed, and dereferencing is slower than using a value.
Parameters passed by reference can be used for input or output.
Passing Parameters Via Registers
Passing Parameters Via Global Variables
Passing Parameters Via The Code Stream
Passing Parameters Via The Stack
Passing Parameters Via A Parameter Block
Now one problem, if you put back the return address, you will return right after the CALL and the data block will be executed as code. This is solved by modifying the return address so that it points to just after the parameters. Not too difficult to do that because you will usually be at the end of the parameter list when you want to return anyway.
LD HL, 0
LD (CurRow), HL
CALL Print_Out
.DB "I ain't not a dorkus", 0
RET
Print_Out:
; Get the return address/address of parameter
POP HL
_Loop:
LD A, (HL)
INC HL
OR A
JR Z, _Done
b_call(_PutC)
JR _Loop
_Done:
; Much better than POP HL \ RET
JP (HL)
You have no excuse for not understanding the code stream mechanism — you've been using it all this time! b_call(xxxx) is macro (you should know at least that much by now) that expands to
RST 28h .DW xxxxRST is the same as CALL, but you can clearly see that the code stream is in use here.
The code stream really is one of the more convenient ways to pass input, and code-stream parameters implement variable-length parameters quite effectively. The string parameter to Print_Out can be any length and the routine will always come off without a hitch.
For all its convenience, the code stream mechanism is not without its disadvantages. First, if you fail to pass exactly the right number of parameters in exactly the right format, the code stream becomes the crash stream. Try leaving off the zero byte, Print_Out prints garbage and returns to god-knows-where. Or you might accidently add in a second zero. Then Print_Out returns in the midst of the string and tries to execute ASCII codes as instruction opcodes. Again, this usually results in a crash (actually most characters will be 8-bit loads that may or may not be harmless).
; $ A48E: LD HL, $1C2A A491: PUSH HL A492: LD HL, $5FC0 A495: PUSH HL A496: LD HL, $44DF A499: PUSH HL A49A: CALL Foo A49D: INC AUpon entry to Foo, its stack frame will look like:
| SP | V |
|||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| $FFE5 | $FFE6 | $FFE7 | $FFE8 | $FFE9 | $FFEA | $FFEB | $FFEC | $FFED | $FFEE | $FFEF | $FFF0 |
| 9D | A4 | DF | 44 | C0 | 5F | 2A | 1C | ||||
| Return Address | Parameter 3 | Paremeter 2 | Paremeter 1 | ||||||||
| EX (SP), HL | Swaps the value of (SP) with the value of L and the value of (SP+1) with the value of H. |
What can now be done is issue a POP HL as the first instruction of Foo. This will deliver the return address in HL and cause SP to point to the third parameter. By a series of pops and exchanges, the arguments can be cleaned off and the return address put back on top.
Foo: POP HL ; HL = return EX DE, HL POP HL ; HL = parameter 3 LD (Par3), HL POP HL ; HL = parameter 2 LD (Par2), HL EX DE, HL EX (SP), HL ; HL = parameter 1If the arguments are to be used more than once they will have to be stored to RAM. This requires space and bogs the system down.
; If you save the machine state, put the number of bytes pushed ; instead of zero in IX LD IX, 0 ADD IX, SPThe parameters to Foo would be as follows:
(IX + 0) LSB of return address
(IX + 1) MSB of return address
(IX + 2) LSB of parameter 3
(IX + 3) MSB of parameter 3
(IX + 4) LSB of parameter 2
(IX + 5) MSB of parameter 2
(IX + 6) LSB of parameter 1
(IX + 7) MSB of parameter 1
The use of indexing offers some glaring disadvantages:
Procedure results can be stored in most of the ways input paramters can be (except the code stream). To use the stack, special considerations have to be made.
EX (SP), HL
JP (HL)
. . .
; Back in the main module
POP BC ; Result in BC
This can be extended for multiple values.
CALL Fetch
POP HL
POP BC
POP DE
RET
Fetch:
LD HL, (Data1)
EX (SP), HL
EX DE, HL
LD HL, (Data2)
PUSH HL
LD HL, (Data3)
PUSH HL
PUSH DE
RET
CALL Fetch
POP HL
POP BC
POP DE
RET
Fetch:
EX (SP), HL
PUSH AF ; Placeholders
PUSH AF
PUSH HL
LD DE, (Data1)
LD (IX + 7), D
LD (IX + 6), E
LD DE, (Data2)
LD (IX + 5), D
LD (IX + 4), E
LD DE, (Data3)
LD (IX + 3), D
LD (IX + 2), E
RET
LD HL, $0000
LD DE, $FFFF
PUSH HL
PUSH DE
CALL Swap
POP DE
POP HL
RET
Swap:
#define p1_lo (IX + 2)
#define p1_hi (IX + 3)
#define p2_lo (IX + 4)
#define p2_hi (IX + 5)
#define temp (IX - 1)
LD IX, $0000
ADD IX, SP
DEC SP ; Allocate one byte of local vars
LD A, p1_lo
LD temp, A
LD A, p2_lo
LD p1_lo, A
LD A, temp
LD p2_lo, A
LD A, p1_hi
LD temp, A
LD A, p2_hi
LD p1_hi, A
LD A, temp
LD p2_hi, A
INC SP ; Deallocate one byte of local vars
RET
b_call(_ClrLCDFull)
b_call(_HomeUp)
LD HL, 8 ; Do not try passing inputs greater than 8,
; it will make the routine unhappy
PUSH HL
CALL Factorial
POP HL
b_call(_DispHL)
b_call(_NewLine)
RET
Factorial:
PUSH IX ; Save previous value of IX for re-entrancy
LD IX, 0 ; Set IX as stack frame pointer
ADD IX, SP
LD A, (IX + 4) ; Get the LSB of the factor
OR A ; The base case is "factor == 0"
JR Z, BaseCase
DEC A ; Subtract one to get next factor
LD E, A ; Push factor onto stack as parameter for next
PUSH DE ; recursion. The value of D is irrelevant
CALL Factorial ; Recurse
POP HL ; Retrieve the result from the previous recursion
LD E, (IX + 4) ; Get the factor from the previous recursion
CALL HL_Times_E ; Multiply 'em
LD (IX + 4), L ; Overwrite the previous factor with running product
LD (IX + 5), H
POP IX ; Restore stack frame pointer from previous recursion
RET ; End of this recursion
BaseCase:
LD (IX + 4), 1 ; Set the $0000 factor to $0001
LD (IX + 5), 0
POP IX ; Restore stack frame pointer
RET ; End of this recursion