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.
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_bar— Jump to SubRoutinedraw_bar, but remember where you came from.rts— ReTurn from Subroutine: go back to wherever the matchingjsrwas.
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 — 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
rtsis missing, sodraw_barruns straight on past its end into whatever bytes follow. Every subroutine needs itsrts. - 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_barshould onlyjsr/rtscleanly 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.