← Back to Modules
module
7

Doing the Same Job from Several Places

Introduction

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.

CALL and RET

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.

How it remembers: the stack

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.

Your two values may be a few bytes higher or lower than these. Where the stack starts depends on what the machine was doing when your program was loaded. They will still be two apart.

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.

A subroutine in a loop

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

As before, only A, C and Z need to match yours.

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.

Two mistakes worth making on purpose

Subroutines bring two errors that read perfectly innocently. Both are worth producing deliberately once.

Falling in through the front door

        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.

Testing the wrong result

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.

Conditional calls

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.

Change one thing

  • In the two-asterisk program above, add a RET between the CALL print and the print: label. How many asterisks now, and why?
  • In the alphabet program, delete the 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?
  • Change CALL Z,yes to CALL NZ,yes. Which letters print now, and which of the two calls is taken? Check each condition against the DEC above it before you run it - the answer is not one of the two letters.
  • Change LD A,'A' to LD A,200 and run the alphabet program. Codes above 127 are graphic characters rather than letters - see Appendix I.
  • In the stack program, add a second CALL inside sub to another subroutine, and capture SP at the deepest point. What do you expect it to read?

Exercises

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. Somewhere in Appendix I's graphics range there are corner pieces and straight pieces. Find enough of them to draw a rectangle, then write a program with a draw-a-row subroutine and draw one. Two warnings, both of which are part of the exercise: you need a separate piece for each of the four sides rather than one horizontal and one vertical, because a straight piece sits at the top, bottom, left or right of its character square rather than through the middle of it. And some codes in that range print nothing at all on this screen. If you cannot close the rectangle neatly, the solid block will always do it.

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.

When it goes wrong

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.

Summary

  • CALL label runs a subroutine and remembers where to come back to. RET goes back.
  • The return address lives on the stack. CALL puts two bytes on, RET takes them off, and SP tracks where the top is.
  • Whatever you push, pop before returning, or RET will use it as an address.
  • A subroutine must sit where execution cannot fall into it - after the main program's RET, or jumped over.
  • CALL Z and CALL NZ call only when the condition holds.
  • Factoring code into subroutines changes the structure and not the behaviour. The alphabet program prints the same 26 letters either way.

Next

S8. Reading the Keyboard. Every program so far has been a monologue: it runs, it prints, it stops, and the player has no say in it. Next your programs start listening - which means two quite different ways of reading a key, and only one of them is any use in a game.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.