The Heartbeat
Replace the idle loop with a real one. Tell the Z80 to listen for the 50 Hz frame interrupt (IM 1, EI) and HALT until each arrives — so the game beats once per frame, the heartbeat every later unit moves to.
The square is built and the lamplighter stands in it — but the program drew them once and then just sat there, holding the picture with a halt/jr that did nothing useful. That was fine for a still scene. A game is not a still scene. A game runs — frame after frame, reading the player, moving the world, redrawing it — and it never stops until you win or lose.
So the lamplighter needs a loop. And not just any loop: one tied to the screen itself, so the game runs at exactly the same pace on every Spectrum, and so each frame's work lands neatly between one screen-draw and the next. This unit builds that loop — the first of Gloaming's two big techniques, the frame-locked game loop.
Where we start
Unit 2's lamplighter, drawn once and idling on a dead halt/jr loop that holds the picture but does nothing. We replace that idle loop with a living one.
A game is a loop
Every game, on every machine, is the same shape underneath:
forever:
read the INPUT (what is the player doing?)
UPDATE the world (move things, apply the rules)
DRAW the result (show the new state)
Round and round, many times a second. One trip through that cycle is one frame. Our job is to build the forever and the three empty stages inside it; the stages get filled in over the next units (input in Unit 4, movement in Unit 5).
Why lock it to the screen
We could just spin that loop as fast as the Z80 can go. But then the game would run at whatever raw speed the processor manages — and it would do its work at random moments while the screen is being drawn, causing tearing and flicker. Worse, "as fast as possible" is a different speed on a 48K Spectrum, a 128K, and a Next.
The fix is to pace the loop to the screen itself. The Spectrum redraws its display 50 times a second, and every time it finishes one complete screen it raises an interrupt — a precise tap on the shoulder, 50 times a second, the same on every machine. If we run our loop exactly once per tap, the game beats at a rock-steady 50 Hz everywhere. That beat is the heartbeat.
IM 1, EI, and the magic of HALT
Three instructions give us the heartbeat:
IM 1— interrupt mode 1. It tells the Z80 to use the Spectrum ROM's built-in interrupt handler, the one wired to that 50 Hz screen tap. (The ROM routine keeps the clock and scans the keyboard; we get it for free.)EI— enable interrupts. Until we say this, the Z80 ignores the taps entirely. With it, each one gets through.HALT— sleep the processor until the next interrupt arrives.
That last one is the trick. A bare HALT would freeze the machine — but with interrupts enabled, HALT doesn't freeze, it waits: the CPU dozes until the next 50 Hz tap wakes it, then carries straight on. So one HALT is "wait for exactly one frame." Put it at the top of the loop and the loop can never run faster than 50 times a second — it's bolted to the screen.
Milestone — swap the dead loop for a beating one
The change is small and total. Everything that draws the fixed scene — border, cobbles, walls, the lamplighter — stays in setup, drawn once. Then IM 1 / EI start the beat, and the old halt/jr idle becomes a real game_loop: one HALT to wait for the frame, the three (still empty) stages, and round again.
| 1 | 1 | ; Gloaming — Unit 3: The Heartbeat | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 is Unit 2's lamplighter, drawn once and idling on a dead loop. | |
| 3 | + | ; step-01 swaps the dead loop for a 50 Hz heartbeat — IM 1, EI, HALT. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 79 | 79 | inc h ; next screen row down (+256) | |
| 80 | 80 | djnz .draw | |
| 81 | 81 | | |
| 82 | - | .loop: | |
| 83 | - | halt | |
| 84 | - | jr .loop | |
| 82 | + | ; --- start the 50 Hz heartbeat --- | |
| 83 | + | ; IM 1 selects the ROM's interrupt handler (fired once per screen, | |
| 84 | + | ; 50 times a second); EI lets the taps through. From here the loop | |
| 85 | + | ; can never run faster than one pass per frame. | |
| 86 | + | im 1 | |
| 87 | + | ei | |
| 88 | + | | |
| 89 | + | game_loop: | |
| 90 | + | halt ; sleep here until the next frame interrupt | |
| 91 | + | | |
| 92 | + | ; --- INPUT --- read the keys. (Unit 4 fills this in.) | |
| 93 | + | ; --- UPDATE --- move the world on. (Unit 5 fills this in.) | |
| 94 | + | ; --- DRAW --- redraw what changed. (nothing moves yet.) | |
| 95 | + | | |
| 96 | + | jr game_loop ; round again — one pass per frame, forever | |
| 85 | 97 | | |
| 86 | 98 | ; The lamplighter's shape — eight bytes, one per pixel row. A 1 bit is a lit | |
| 87 | 99 | ; pixel (drawn in the cell's INK); a 0 bit shows the PAPER behind. Read the |
The complete program
; Gloaming — Unit 3: The Heartbeat
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 swaps the dead loop for a 50 Hz heartbeat — IM 1, EI, HALT.
org 32768
COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black (0), INK white (7) — the figure
; --- where the lamplighter stands: a cell named by (column 0-31, row 0-23) ---
LAMP_COL equ 15
LAMP_ROW equ 11
; The screen splits top/middle/bottom into THIRDS of 8 character rows. The top
; pixel-row of a cell lives at $4000 + third*$0800 + (row-within-third)*32 + col.
THIRD equ LAMP_ROW / 8
CHARROW equ LAMP_ROW - THIRD * 8
LAMP_SCR equ $4000 + THIRD * $0800 + CHARROW * 32 + LAMP_COL
; The attribute cell for the same (col,row) is the simpler linear address.
LAMP_ATTR_ADDR equ $5800 + LAMP_ROW * 32 + LAMP_COL
start:
; --- the border goes black — the night beyond the square ---
ld a, 0
out ($FE), a
; --- wash the whole grid in cobbles ---
ld hl, $5800
ld de, $5801
ld (hl), COBBLE
ld bc, 767
ldir
; --- top and bottom walls ---
ld hl, $5800
ld b, 32
.top:
ld (hl), WALL
inc hl
djnz .top
ld hl, $5AE0
ld b, 32
.bottom:
ld (hl), WALL
inc hl
djnz .bottom
; --- left and right walls ---
ld hl, $5800
ld b, 24
.sides:
ld (hl), WALL
push hl
ld de, 31
add hl, de
ld (hl), WALL
pop hl
ld de, 32
add hl, de
djnz .sides
; --- give the figure's cell a warm colour so its pixels read ---
ld hl, LAMP_ATTR_ADDR
ld (hl), LAMP_ATTR
; --- draw his eight-byte shape down the eight rows of the cell ---
; HL walks the screen rows (INC H = down one row, +256).
; DE walks the sprite bytes (INC DE = next row of the shape).
ld hl, LAMP_SCR ; top pixel-row of his cell
ld de, lamplighter ; his shape, eight bytes
ld b, 8 ; eight rows
.draw:
ld a, (de) ; one row of the shape
ld (hl), a ; into the screen
inc de ; next shape byte
inc h ; next screen row down (+256)
djnz .draw
; --- start the 50 Hz heartbeat ---
; IM 1 selects the ROM's interrupt handler (fired once per screen,
; 50 times a second); EI lets the taps through. From here the loop
; can never run faster than one pass per frame.
im 1
ei
game_loop:
halt ; sleep here until the next frame interrupt
; --- INPUT --- read the keys. (Unit 4 fills this in.)
; --- UPDATE --- move the world on. (Unit 5 fills this in.)
; --- DRAW --- redraw what changed. (nothing moves yet.)
jr game_loop ; round again — one pass per frame, forever
; The lamplighter's shape — eight bytes, one per pixel row. A 1 bit is a lit
; pixel (drawn in the cell's INK); a 0 bit shows the PAPER behind. Read the
; bytes top-down and the little figure stands up.
lamplighter:
defb %00111100 ; ..XXXX.. head
defb %00111100 ; ..XXXX.. head
defb %00011000 ; ...XX... neck
defb %01111110 ; .XXXXXX. arms
defb %00011000 ; ...XX... body
defb %00011000 ; ...XX... body
defb %00100100 ; ..X..X.. legs
defb %01000010 ; .X....X. feet
end start
On screen, nothing changes:
That steadiness is the result. We've built the engine; the next units give it something to drive.
See the heartbeat
A steady picture is a poor way to show a loop is running. So let's make the beat visible: keep everything, but in the loop, bump a one-byte counter each frame and write it to the border.
game_loop:
halt ; wait one frame
ld a, (tick)
inc a
ld (tick), a
out ($FE), a ; border steps to a new colour every frame
jr game_loop
...
tick:
defb 0
Now the border steps through its eight colours — a full cycle eight times a second — while the square and the lamplighter hold dead still. That flicker is the heartbeat, one step per beat:
The real game keeps a black border — this is just a way to see the beat. (The complete demonstration program is heartbeat-seen.asm alongside the steps.)
When it's wrong, see why
The heartbeat fails in a couple of distinctive ways:
- The screen freezes solid and never settles. Missing
ei, or it runs beforeim 1. With interrupts off, the firsthaltsleeps forever, waiting for a tap that can't arrive. Order itim 1thenei, both before the loop. - It runs for about a second, then garbles or crashes. The ROM's interrupt routine uses the stack; if
SPpoints somewhere bad it corrupts memory each frame. Leave the stack pointer alone for now. - The lamplighter flickers or disappears. His draw drifted into the loop, and something is fighting it. The static scene belongs in setup, drawn once; the loop stays empty until Unit 5.
- It looks exactly like Unit 2. Correct — that's the expected result. Nothing should change on screen yet; the border demo above is how you confirm the loop is alive.
Before and after
You started with a picture that was drawn and abandoned, and finished with the same picture running — fifty beats a second, the same on every Spectrum. The change was three instructions and one relabelled loop, and it bought the single most important thing a game has: a steady, screen-locked place to put everything that happens next. From here on, every new behaviour lives inside this loop.
Try this: slow the beat
Put a second halt just after the first. Now the loop waits two frames before going round, so it beats at 25 Hz. Combine it with the border demo and the colours step half as fast. The number of HALTs per loop sets the pace — a dial you can turn.
Try this: pull the heartbeat out
Delete the ei. Run it. The machine freezes on the first halt — a dead, frozen square — because HALT is now waiting for an interrupt that, with interrupts disabled, can never come. Put ei back and it lives again. That one line is the difference between a sleep you wake from and a sleep you don't.
What you've learnt
- A game is a loop: read input, update the world, draw it — once per frame, forever.
- The Spectrum raises a frame interrupt 50 times a second;
IM 1selects the ROM handler andEIlets the taps through. HALTwaits for the next interrupt — oneHALTper loop locks the game to 50 Hz, the same speed on every machine.- The program splits into setup (the fixed scene, drawn once) and the loop (what changes, every frame).
- A steady screen can still hide a running engine — the border counter is how you prove the beat.
What's next
The loop has an INPUT stage that does nothing. In Unit 4 we fill it in: reading the keyboard with IN A,($FE), watching one half-row of keys for a direction press. It's the first thing the heartbeat reacts to — and the last piece before the lamplighter takes his first step.