Skip to content
Game 0 Unit 13 of 18 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.

72% of Meet The Machine

Your programs are getting long enough that chunks of them want names. "Draw a short bar" 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:

  • jsr draw_barJump to SubRoutine draw_bar, but remember where you came from.
  • rtsReTurn from Subroutine: go back to wherever the matching jsr 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 jsr, the machine writes the return address onto the stack — ordinary memory at page 1, $0100$01FF, with the SP register marking the top. rts reads it back off. The "magic" of how a subroutine knows the way home is just a number parked in RAM. (Your harness has set the stack up since Unit 1: that's what ldx #$ff / txs was doing.)

What you'll see by the end

Two short red bars near the top-left of a light-blue NES screen, one row apart.
Two bars from one subroutine: the bar-drawing code written once and called twice, aimed at a different row each time.

Two short red bars — eight blocks near the top, eight a couple of rows below. The point is that we wrote the bar-drawing code once and called it twice, aiming the PPU window at a different row before each call. One definition, two uses.

One routine, called twice

; ============================================================================
; Meet the Machine (NES) - Unit 13: Call, Return, and a Stack You Can See
;
; Package a job into a subroutine: write it once, jsr to it from anywhere, and
; rts back. draw_bar fills 8 cells from wherever the window is aimed. We aim,
; call, aim, call - two bars on two rows, one routine.
; ============================================================================

.segment "HEADER"
    .byte "NES", $1a
    .byte 2
    .byte 1
    .byte $00, $00

.segment "CODE"

reset:
    sei
    cld
    ldx #$40
    stx $4017
    ldx #$ff
    txs
    inx
    stx $2000
    stx $2001
    stx $4010
warm1:
    bit $2002
    bpl warm1
warm2:
    bit $2002
    bpl warm2

    ; --- palette: backdrop + the bars' colour ---
    bit $2002
    lda #$3f
    sta $2006
    lda #$00
    sta $2006
    lda #$21                ; backdrop light blue
    sta $2007
    lda #$16                ; colour 1 red
    sta $2007

    ; --- first bar: aim at the top-left cell, then call ---
    bit $2002
    lda #$20
    sta $2006
    lda #$00
    sta $2006
    jsr draw_bar

    ; --- second bar: aim at the start of row 2 ($2040), then call ---
    bit $2002
    lda #$20
    sta $2006
    lda #$40
    sta $2006
    jsr draw_bar

    ; --- square up and turn the picture on ---
    bit $2002
    lda #$00
    sta $2006
    sta $2006
    sta $2005
    sta $2005
    lda #$1e
    sta $2001

forever:
    jmp forever

; --- draw_bar: pour 8 blocks from wherever the window is currently aimed ------
draw_bar:
    ldx #0
db_loop:
    lda #$01
    sta $2007
    inx
    cpx #8
    bne db_loop
    rts

nmi:
    rti
irq:
    rti

.segment "VECTORS"
    .word nmi
    .word reset
    .word irq

.segment "CHARS"
    .byte $00,$00,$00,$00,$00,$00,$00,$00
    .byte $00,$00,$00,$00,$00,$00,$00,$00
    .byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff
    .byte $00,$00,$00,$00,$00,$00,$00,$00
    .res 8192 - 32, $00

draw_bar is last unit's loop — pour eight blocks through the window — given a name and an rts at the end. It draws from wherever the window is currently aimed, so we set up its input by aiming first: $2000 for the top row, then $2040 for two rows down. Aim, call, return; aim, call, return. The same eight-block routine, two places on screen.

The stack, in plain sight

Here's what jsr does: it writes the two-byte return address onto the stack — SP drops, and those two bytes of page-1 RAM now hold "come back here" — then jumps to the routine. rts reads those two bytes back, bumps SP up, and jumps to them. Open the memory view at page 1 ($0100$01FF) in your emulator and you can watch the address go on at each jsr and come off at each rts.

This is worth seeing once, because the stack is real and therefore breakable — push something on and forget to take it off, and rts reads the wrong number and jumps somewhere wild. For now draw_bar is balanced and well-behaved; just know the safety rail is made of plain RAM.

Assemble and run

ca65 call.asm -o call.o && ld65 -C nes.cfg call.o -o call.nes

Two short red bars, both painted by the same draw_bar.

Try this: a third bar

Add a third aim-and-call before the last block — $2080 (two rows further down), then jsr draw_bar. Three bars, and you didn't write the drawing 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 and open the memory view at page 1. Step through, and you'll see two bytes appear near the top of the stack at each jsr — the return address — and disappear at each rts. The thing BASIC hid inside GOSUB is right there in memory.

If it doesn't work

  • The screen garbles after the bars draw. An rts is missing, so draw_bar runs straight on past its end into whatever bytes follow. Every subroutine needs its rts.
  • Both bars land on the same row. You forgot to change the window's aim between the calls, or both aims use the same address. The second needs the low byte $40.
  • It returns to the wrong place / crashes. Something inside the routine left the stack unbalanced — for now, draw_bar should only jsr/rts cleanly and not push anything it doesn't pull back.

What you've learnt

jsr runs a named subroutine and pushes the return address onto the page-1 stack; rts pulls it and comes back — so you write a job once and call it from anywhere, passing what it needs (here, the window's aim) before the call.

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, adc and sbc, and meeting the carry flag the 6502 makes you mind.