The Counted Loop
Hand the repetition to the machine. Load a count, run the body, step on, and branch back until you've done them all — and the PPU's auto-stepping port turns one short loop into a whole row across the screen.
Back in Units 5 to 7 you wrote a single tile to the screen, by hand, one cell at a time. Filling a whole row that way would be thirty-two near-identical writes — absurd. So you don't write it thirty-two times. You tell the machine to.
That's a loop, built from pieces you already have: a counter, a step, and the test-then-branch from Unit 8.
ldx #0— start the counter at 0.inx— step it on after each pass.cpx #32— compare with 32: have we done them all?bne fill— if not equal yet, branch back and go round again.
And the NES hands you a gift that makes the loop body tiny: the PPU's data port steps itself. Every time you write a tile through $2007, the window automatically moves to the next cell. So the loop body doesn't even have to work out where to write — it just pours another tile, and the port advances. One short loop, a whole row.
What you'll see by the end
A red bar clear across the top — the whole 32-cell top row, painted by a loop that ran the same body thirty-two times. You wrote that body once.
The loop
; ============================================================================
; Meet the Machine (NES) - Unit 12: The Counted Loop
;
; Hand the repetition to the machine. The PPU's data port auto-steps to the
; next cell after every write, so a counted loop pouring tile 1 thirty-two
; times paints a whole row across the top of the screen.
; ============================================================================
.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 row's 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
; --- aim the window at the top-left cell of the screen ---
bit $2002
lda #$20
sta $2006
lda #$00
sta $2006
; --- the loop: write tile 1 to 32 cells. The port auto-steps each time. ---
ldx #0 ; the counter, and how many we have done
fill:
lda #$01
sta $2007 ; pour a block; the PPU steps to the next cell
inx
cpx #32 ; a row is 32 cells wide - done them all?
bne fill ; if not, go round again
; --- 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
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
We aim the window at the top-left cell once, before the loop. Then the body — lda #$01 / sta $2007 — pours a block and lets the port step on; inx counts it; cpx #32 / bne fill asks "done yet?" each pass and goes round if not. Thirty-two passes, thirty-two cells, one row. It's BASIC's FOR I = 0 TO 31 … NEXT, assembled from a counter and a branch you already know.
(There's a leaner way the CPU quietly prefers — counting down to zero, where the test comes free and you can drop the cpx. It takes one small trick to line up, so we'll save it for later, once the plain loop is second nature. Counting up is the clearer place to start.)
Assemble and run
ca65 loop.asm -o loop.o && ld65 -C nes.cfg loop.o -o loop.nes
One loop, a full red row.
Try this: change the count
Set cpx #16 for half a row, cpx #8 for eight cells. The number in the cpx is your FOR I = 0 TO N-1 — it's the loop's whole story. Predict how much of the row fills before you run it.
Try this: fill the whole screen
A row is 32 cells; the whole screen is 960 (32 × 30). That's more than fits in X (which stops at 255), so a full-screen fill needs a bigger count — and that's the cliffhanger for Unit 16, where numbers grow past a single byte. For now, push cpx up to #192 and watch six rows fill: the loop doesn't care how many, only when to stop.
If it doesn't work
- Only one cell fills. The
inxis missing or sits outside the loop, so the count never climbs. The step belongs inside, before thecpx. - The whole screen fills, or garbage appears. Your
cpxvalue is wrong or missing, so the loop runs too long. Check it readscpx #32. ca65can't findfill. The label and thebne fillmust match exactly, case and all — labels are the assembler's rule, from Unit 8.
What you've learnt
ldx #0, a body, inx, then cpx #N / bne runs the loop body N times. Paired with the PPU port that auto-steps to the next cell on every write, one short loop paints a whole row — what would have been thirty-two writes by hand.
What's next
Your programs are growing, and chunks of them want names. Next — Call, Return, and a Stack You Can See — we package a job into a subroutine you can jsr to from anywhere and rts from, and watch where the machine remembers its way back: the stack, sitting in plain memory.