.png)
Look back at the alphabet program from S6. The three lines that actually put a character on screen are:
RST 8
DEFB 158 ; ZOUTC
Two lines, and they appear in every program you have written. In a real game they would appear thirty times - and the day you want to change how printing works, you would have thirty places to edit and one of them you would miss.
A subroutine is a block of code you can run from anywhere and come back from. Write the printing once, use it everywhere.
Two instructions.
CALL label goes to the label, like a jump - but it remembers where it came
from.
RET goes back to wherever the last CALL came from.
ORG 256
LD A,'!'
CALL print ; go and print
LD A,'?'
CALL print ; go and print again
RET ; finish the program
print: RST 8
DEFB 158 ; ZOUTC
RET ; go back to whichever CALL sent us here
The clever part is that print does not need to know who called it. The
first CALL comes back to the LD A,'?' line; the second comes back to the
RET that ends the program. Same subroutine, two different destinations,
worked out for you.
If you have used BASIC, this is GOSUB and RETURN.
RET has to get its destination from somewhere, and that somewhere is the
stack.
The stack is a region of memory the processor uses as a scratch pad, with one
rule: the last thing put on it is the first thing taken off. CALL puts the
address of the next instruction on it. RET takes an address off and jumps
there.
There is a register that tracks it - SP, the stack pointer - and you can
watch this happen. This program captures SP inside a subroutine and again
after returning:
ORG 256
CALL sub
LD HL,0
ADD HL,SP ; HL = SP back in the caller
RET
sub: LD HL,0
ADD HL,SP ; HL = SP inside the subroutine
LD D,H
LD E,L ; keep a copy in DE
RET
What you should see
A BC DE HL PC SZ-H-PNC
0F 3D41 FCEB FCED 0769 10101000
DE is FCEB - the stack pointer inside the subroutine. HL is FCED -
the stack pointer after the return. Two apart, which is the size of the
address CALL put there and RET took away.
ADD HL,SP is the trick used here: SP cannot be printed directly, so the
program zeroes HL, adds SP to it, and reads it out of HL instead.
Two things follow from this that matter later.
The stack is why RET works at all, including the RET at the end of
your programs. Something put a return address on the stack before your program
started, and your final RET uses it to hand control back. You have been
relying on the stack since S3 without knowing it.
Anything you push, you must pop. The stack is shared. If a subroutine
leaves something on it, the RET will pick that up instead of the return
address and jump somewhere meaningless.
Put a subroutine together with S6's counted loop and you get the alphabet again, with the printing factored out:
ORG 256
LD C,26
LD A,'A'
next: CALL print
INC A
DEC C ; the last thing before the jump
JR NZ,next
RET
print: RST 8
DEFB 158 ; ZOUTC
RET
What you should see
>ABCDEFGHIJKLMNOPQRSTUVWXYZ
A BC DE HL PC SZ-H-PNC
5B 3D00 1EFF FB3E 0769 01000010
A finishes at 5B, one past Z, because the last INC A ran after the
final letter. C is 00. Exactly the same output as S6's version - the
subroutine changed the structure, not the behaviour.
Note where DEC C sits: immediately before the jump. INC A is above it, so
DEC C is the last thing to set the flags and the jump tests the counter.
Swap those two lines and the loop tests the letter instead, which is a
different program that happens to run.
Subroutines bring two errors that read perfectly innocently. Both are worth producing deliberately once.
ORG 256
LD A,'*'
CALL print
print: RST 8
DEFB 158 ; ZOUTC
RET
This prints two asterisks.
CALL print runs the subroutine and comes back to the line after it - which
is print. So it runs again, and this time the RET takes the address that
was on the stack before your program started, and the program ends.
A subroutine has to sit somewhere execution cannot wander into. Put your
subroutines after the RET that ends the main program, or jump over them.
next: CALL print
DEC C ; sets the flags...
INC A ; ...and then this overwrites them
JR NZ,next ; so the jump tests INC A, not the counter
This is the S6 lesson in a new costume. The loop still runs, still prints, and still finishes - so it looks fine. It just ends when the character wraps to zero rather than when the counter runs out, and the count you asked for is never consulted.
The rule is unchanged: whatever you are testing goes immediately before the jump.
CALL takes conditions, exactly like JR and JP:
CALL Z,label ; call only if the last result was zero
CALL NZ,label ; call only if it was not
Here both are exercised - the first condition is true, the second is not:
ORG 256
LD A,1
DEC A ; A = 0, so Z is set
CALL Z,yes ; taken
LD A,2
DEC A ; A = 1, so Z is clear
CALL Z,no ; not taken
RET
yes: LD A,'Y'
RST 8
DEFB 158 ; ZOUTC
RET
no: LD A,'N'
RST 8
DEFB 158 ; ZOUTC
RET
What you should see
>Y
One Y, and no N. This is the point at which your programs can start making
decisions rather than just counting.
RET between the CALL print and
the print: label. How many asterisks now, and why?RET from the end of print. Predict
what happens before you run it - and note that "it crashes" is a guess, not
a prediction. What is actually on the other side of that subroutine?CALL Z,yes to CALL NZ,yes. Which letter prints now?LD A,'A' to LD A,200 and run the alphabet program. Codes above
127 are graphic characters rather than letters - see Appendix I.CALL inside sub to another
subroutine, and capture SP at the deepest point. What do you expect it to
read?7.1 - A newline subroutine. Write a subroutine that prints a line break - 13 then 10 - and use it between three printed words. Notice how much shorter the main program gets.
7.2 - Print a digit. Write a subroutine that takes a number 0-9 in A,
converts it to its character, and prints it. Call it three times with
different numbers. What does it leave in A, and does that matter to the
caller?
7.3 - A box. Using Appendix I's graphics range, find four corner
characters and two line characters, then write a program with a
draw-a-row subroutine and use it to draw a rectangle on screen.
7.4 - Watch the stack. Write a subroutine that calls a second subroutine,
which calls a third. Capture SP at each level and work out how much stack
one call costs. Then predict SP at the fourth level before measuring it.
7.5 - Break it deliberately. Write a subroutine that pushes something onto
the stack with PUSH BC and returns without a matching POP. You will need
to look those two instructions up. Predict what RET will do before you run
it, then check PC in the register line to see where it actually went.
| What you see | What it means |
|---|---|
| Everything printed twice | Execution is falling into a subroutine from above. Move it below the main program's RET, or jump over it. |
| The program ends early, after one call | A RET reached with nothing of yours on the stack hands control back to the system. Usually a subroutine placed where execution runs into it. |
| The loop runs the wrong number of times | Something between the DEC and the jump reset the flags. |
PC in the register line is an address nowhere in your program |
RET took something off the stack that was not a return address. Check for a PUSH without a POP. |
error: unable to resolve reference: print |
The label and the CALL do not match - check spelling and capitals. |
CALL label runs a subroutine and remembers where to come back to. RET
goes back.CALL puts two bytes on, RET takes
them off, and SP tracks where the top is.RET will use it as an address.RET, or jumped over.CALL Z and CALL NZ call only when the condition holds.S8. Keeping things in memory. Subroutines let you reuse code. The next problem is data - a message longer than one character, a score, a position that has to survive from one frame to the next - which means storing values somewhere and getting them back.