Skip to content

LD Is Not LET

Meet the registers — the handful of physical, 8-bit stores inside the CPU. Move a value from A into B and back again, and watch the border prove it made the trip.

  • Sinclair ZX Spectrum
  • Assembly

Last time you sent a number to the border and met the build-run loop. You used a register — A — without properly meeting it. Let’s meet it now, because this is the first place the machine stops behaving like the language you came from.

In BASIC, LET a=5 gave you a variable, and you could have as many as you liked — score, lives, name$, on and on. The CPU is not so generous. It has a handful of registers — small stores inside the chip itself — and that’s all. Their names are fixed: A, B, C, D, E, H, L. You don’t declare new ones. You learn to orchestrate the few you have.

And each one holds a single 8-bit number: 0 to 255, nothing larger. A register isn’t a roomy named box; it’s a tiny slot with a hard limit. That constraint shapes everything you’ll write.

What you’ll see by the end

The Spectrum's default screen surrounded by a solid cyan border.
A cyan border — colour 5, brought home through B. The border is the visible proof; the real show is the registers changing step by step.

A cyan border — colour 5. But the colour isn’t the point this time. The point is how the 5 got there: it travelled from A into B, survived us wiping A, and came back. The border is just the proof you can see. The real show is in the register view — open it in your emulator and watch A and B change step by step.

LD just moves a number

There’s one new idea here, and it’s LD — “load”. Despite the name, it doesn’t “load” in any grand sense; all it does is move a number. Into a register from a literal (ld a, 5), or from one register into another (ld b, a). That’s the whole instruction.

To prove a value moves between registers — instead of just trusting it — we send 5 on a round trip:

registers.asm
; ============================================================================
; PRIMER — Beat 2: LD Is Not LET
; ============================================================================
; In BASIC, LET a = 5 gave you as many named variables as you liked. The CPU
; doesn't work that way. It has a *handful* of registers — physical stores
; inside the chip, each holding one 8-bit number (0 to 255). Their names are
; fixed: A, B, C, D, E, H, L. You don't make more; you orchestrate these few.
;
; LD means "load", and all it ever does is move a number — into a register,
; or from one register to another. To prove a value really moves, we take it
; on a round trip and watch it come back:
;
;   1. Put 5 into A.            (5 = cyan)
;   2. Copy A into B.           (now B holds 5 too)
;   3. Wipe A back to 0.        (A = 0; if we stopped here the border'd be black)
;   4. Copy B back into A.      (A = 5 again — the 5 was safe in B all along)
;   5. Show A on the border.    (cyan proves B kept our 5)
;
; Open the register view in Emu198x and watch A and B change at each step.
; The border is just the visible proof; the registers are the lesson.
; ============================================================================

            org     32768

start:
            ld      a, 5             ; put 5 into A           (5 = cyan)
            ld      b, a             ; copy A into B          (B now holds 5 too)
            ld      a, 0             ; wipe A                 (A = 0; B still holds 5)
            ld      a, b             ; copy B back into A     (A = 5 again, from B)
            out     ($FE), a         ; show A on the border   (only A can do OUT)

.loop:
            halt
            jr      .loop

            end     start

This is last unit’s program with three lines added in the middle. Follow A and B down the steps:

After… A B
ld a, 5 5 —
ld b, a 5 5
ld a, 0 0 5
ld a, b 5 5

We deliberately wipe A to 0, then bring the value back from B. The border ends up cyan — so the 5 must have been safe in B the whole time. That’s the proof the copy was real.

Why copy back into A just to show it? Because out ($FE), a — the form of out with the port number written into the instruction — only ever sends A, never B or the others. It’s one of the chip’s fixed rules. (The Z80 has a second form of out that can send other registers; you won’t need it here.) It’s also what makes the round trip prove anything: the 5 had to come home through B.

Assemble and run

Same loop as before — change, build, run:

Code
asm198x --dialect pasmo --cpu z80 --tapbas registers.asm -o primer.tap

Load primer.tap in your emulator with a register view open, and watch A and B move through the table above before the border settles on cyan.

Try this: too big to fit

Change ld a, 5 to ld a, 300, assemble, and run. Now look at A in the register view: it reads 44, not 300. A register holds 8 bits — 0 to 255 — and 300 doesn’t fit, so the machine keeps only the low 8 bits (300 − 256 = 44). And notice what didn’t happen: no error, no warning. The assembler let it through; the machine kept the bits that fit and carried on. The tools won’t catch this for you — a theme we’ll return to.

Try this: a longer trip

Send a value through three registers before showing it — A into B, B into C, then C back into A and out it. It’s the round trip with one more hop (and a different colour to tell them apart):

One more register in the chain: A → B → C → A+10 added−23 removed
; ============================================================================
Removed: ; PRIMER — Beat 2: LD Is Not LET
Added: ; PRIMER — Beat 2, "Try this: a longer trip"
; ============================================================================
Removed: ; In BASIC, LET a = 5 gave you as many named variables as you liked. The CPU
Removed: ; doesn't work that way. It has a *handful* of registers — physical stores
Removed: ; inside the chip, each holding one 8-bit number (0 to 255). Their names are
Removed: ; fixed: A, B, C, D, E, H, L. You don't make more; you orchestrate these few.
Removed: ;
Removed: ; LD means "load", and all it ever does is move a number — into a register,
Removed: ; or from one register to another. To prove a value really moves, we take it
Removed: ; on a round trip and watch it come back:
Removed: ;
Removed: ;   1. Put 5 into A.            (5 = cyan)
Removed: ;   2. Copy A into B.           (now B holds 5 too)
Removed: ;   3. Wipe A back to 0.        (A = 0; if we stopped here the border'd be black)
Removed: ;   4. Copy B back into A.      (A = 5 again — the 5 was safe in B all along)
Removed: ;   5. Show A on the border.    (cyan proves B kept our 5)
Removed: ;
Removed: ; Open the register view in Emu198x and watch A and B change at each step.
Removed: ; The border is just the visible proof; the registers are the lesson.
Added: ; Send a value through THREE registers before showing it:
Added: ;   A -> B -> C -> back to A -> border.
Added: ; Predict A, B and C at each step, then watch them in the register view.
Added: ; (6 is yellow.)
; ============================================================================

…
start:
Removed:             ld      a, 5             ; put 5 into A           (5 = cyan)
Removed:             ld      b, a             ; copy A into B          (B now holds 5 too)
Removed:             ld      a, 0             ; wipe A                 (A = 0; B still holds 5)
Removed:             ld      a, b             ; copy B back into A     (A = 5 again, from B)
Removed:             out     ($FE), a         ; show A on the border   (only A can do OUT)
Added:             ld      a, 6             ; put 6 into A         (6 = yellow)
Added:             ld      b, a             ; copy A into B
Added:             ld      c, b             ; copy B into C
Added:             ld      a, c             ; copy C back into A
Added:             out     ($FE), a         ; show A on the border (only A can do OUT)

.loop:

Before you run it, predict A, B and C at each step — then open your emulator’s register view and check. (6 is yellow.)

When it’s wrong, see why

  • The assembler says “Expected ‘A’ but…” on the out line. You tried to out a register other than A. Only A can go out through out ($FE), a — route the value back into A first.
  • The border is black, not cyan. The ld a, b step is missing or mistyped, so A was still 0 (from the wipe) when you showed it. Put the round trip back exactly as shown.
  • The border’s the wrong colour. Check the number in ld a, … — and remember the border reads only the low 3 bits, so values above 7 repeat the eight colours.

What you’ve learnt

A register is a physical, finite 8-bit store — the CPU has only a handful (A B C D E H L) — and LD moves numbers between them.

What’s next

This isn’t a Spectrum quirk: nearly every CPU is built around a small set of registers — some have more, some fewer. You’re learning how machines work, not a Spectrum trick. Next — Everything Is a Number — we look at what those bytes you’re moving around are, and discover the machine doesn’t know a letter from a colour from a number. They’re all just bytes.

Sources

Zilog, Z80 CPU User Manual, UM008011-0816: LD r, r' and LD r, n for register loads; OUT (n), A, p. 306, which sends only A, and OUT (C), r, p. 307. Sinclair Research, ZX Spectrum BASIC Programming, chapter 23, IN and OUT: the border colour from bits 0–2 of port 254.

Run it here

This unit's program, built for you.

Program
primer.tap
Machine
ZX Spectrum 48K (PAL)

Download primer.tap