Skip to content
Game 1 Unit 3 of 20 1 hr learning time

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.

15% of Gloaming

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 1interrupt 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.)
  • EIenable 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.

Step 1: IM 1 / EI, and an idle loop becomes the game loop
+16-4
11 ; Gloaming — Unit 3: The Heartbeat
22 ; 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.
44
55 org 32768
66
...
7979 inc h ; next screen row down (+256)
8080 djnz .draw
8181
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
8597
8698 ; The lamplighter's shape — eight bytes, one per pixel row. A 1 bit is a lit
8799 ; 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:

The walled square and white lamplighter from Unit 2, holding perfectly steady.
Identical to Unit 2 — and that's the proof. The program is no longer running once and idling; it's awake and beating fifty times a second, running a real loop that just has nothing to do yet.

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 same steady square and figure — but the border now steps colour once per frame, driven by a counter in the loop. The cycling border is the 50 Hz heartbeat made visible; the real game keeps the border black.

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 before im 1. With interrupts off, the first halt sleeps forever, waiting for a tap that can't arrive. Order it im 1 then ei, both before the loop.
  • It runs for about a second, then garbles or crashes. The ROM's interrupt routine uses the stack; if SP points 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 1 selects the ROM handler and EI lets the taps through.
  • HALT waits for the next interrupt — one HALT per 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.