Skip to content
Game 0 Unit 11 of 16 1 hr learning time

Call, Return, and a Stack You Can See

Package a job into a subroutine you can call from anywhere — and watch where the machine remembers its way back: the stack, sitting in plain memory you can read.

69% of Meet The Machine

Your programs are getting long enough that chunks of them want names. "Fill a row" is a job you'll want again and again — so write it once, give it a name, and call it whenever you need it.

That's a subroutine, and two instructions run it:

  • call fill_row — jump to fill_row, but remember where you came from.
  • ret — go back to wherever the matching call was.

It's BASIC's GOSUB / RETURN — with one difference that matters for the rest of your life as a programmer: here you can see how it works. When you call, the machine writes the return address onto the stack — ordinary memory, pointed at by the register SP. ret reads it back off. The "magic" of how a subroutine knows the way home is just a number parked in RAM.

What you'll see by the end

A red bar above a blue bar, both full width, near the top of the screen.
Two rows — red above blue — both painted by the same fill_row routine, written once and called twice with a different row and colour each time.

Two bars — a red row above a blue one. The point is that we wrote the row-filling code once and called it twice, with a different row and colour each time. One definition, two uses.

One routine, called twice

; ============================================================================
; PRIMER — Beat 11: Call, Return, and a Stack You Can See
; ============================================================================
; When a job is worth a name, package it into a SUBROUTINE:
;
;   call fill_row  -- jump to fill_row, but REMEMBER where we came from
;   ret            -- go back to wherever the matching call was
;
; That's BASIC's GOSUB / RETURN -- except you can see how it works. CALL
; pushes the return address onto the STACK (real memory, pointed at by SP);
; RET pops it back off. Open the memory view at SP and you can watch the
; address go on and come off.
;
; We write fill_row ONCE, then call it twice -- different row, different
; colour each time. One definition, two uses.
; ============================================================================

            org     32768

start:
            ld      hl, $5800        ; row 0 (top)
            ld      a, $17           ; red   (PAPER 2, INK 7)
            call    fill_row         ; paint row 0 red

            ld      hl, $5820        ; row 1 (one cell-row down, +32)
            ld      a, $0F           ; blue  (PAPER 1, INK 7)
            call    fill_row         ; paint row 1 blue

.loop:
            halt
            jr      .loop

; --- fill_row: colour 32 cells from HL with the attribute in A -------------
fill_row:
            ld      b, 32            ; 32 cells in a row
.fr:
            ld      (hl), a          ; colour the cell with A
            inc     hl               ; step along
            djnz    .fr              ; ...32 times
            ret                      ; back to whoever called us

            end     start

fill_row is the loop from Beat 10, given a name and a ret at the end. Before each call we set up its inputs in registers — HL for where, A for which colour — then call fill_row runs it and ret brings us back to the line after the call. Set up, call, return; set up, call, return.

The stack, in plain sight

Here's what call does under the hood: it pushes the two-byte return address onto the stack — SP drops by two, and those two bytes of RAM now hold "come back here" — then jumps to the routine. ret reads those two bytes back, bumps SP up by two, and jumps to them. Open your emulator's memory view at SP and you can watch the address go on at each call and come off at each ret.

This is worth seeing once, because the stack is real and therefore breakable — which is exactly where the next unit goes.

Assemble and run

pasmonext --sna call-and-return.asm primer.sna

A red row and a blue row, both painted by the same four lines of fill_row.

Try this: a third row

Add a third set-up-and-call before the loop — ld hl, $5840 (row 2), a colour in A, then call fill_row. Three bars, and you didn't write the filling code again. That's the whole point of a subroutine: name it once, use it freely.

Try this: watch the stack work

Load the program with your emulator's memory view open at SP. Step through (or just watch) and you'll see two bytes appear on the stack at each call — the return address — and vanish at each ret. The thing BASIC hid inside GOSUB is right there in memory.

When it's wrong, see why

  • The screen garbles after the bars draw. A ret is missing, so fill_row runs straight on past its end into whatever bytes follow. Every subroutine needs its ret.
  • It returns to the wrong place / crashes. Something unbalanced the stack inside the routine — for now, fill_row should only call/ret cleanly and not leave anything on the stack.
  • Only one bar appears. Check both call fill_row lines are present and each has its own ld hl, / ld a, set-up before it.

What you've learnt

call runs a named subroutine and pushes the return address onto the stack; ret pops it and comes back — so you write a job once and call it from anywhere.

What's next

Your programs can now decide, repeat, and break into named pieces. A few pieces of everyday fluency remain before you build a game — and the first is the most ordinary thing of all. Next — Adding and Taking Away — doing maths on a value with inc, add and sub, and meeting the carry flag for when a byte overflows.