Skip to content

Atom Book 2 — Z80 Programming04

Memory Access and Data Representation ​

Scanning a table, processing a string or reading from hardware all require reaching into memory, and the Z80 has several specific ways to do it.


Memory access through HL ​

(HL) means the byte at the address HL holds. You can read or write it directly.

asm
LD A, (HL)     ; A = byte at address HL
LD (HL), A     ; byte at address HL = A
LD B, (HL)     ; B = byte at address HL
LD (HL), 19    ; byte at address HL = 19

Any of A, B, C, D, E, H, L can appear on either side when the other side is (HL). To process consecutive bytes, load an address into HL, read or write with (HL), increment HL and repeat. Chapter 7 applies this sequence to byte tables.

IX and IY support displaced addressing: (IX+D) reads the byte at address IX + d while IX keeps its value. Chapter 7 covers this in full when the use case makes it concrete.

Parentheses in LD memory operands

In these LD forms, parentheses mean "use memory at this address."

LD A, B copies register B into A, no memory involved. LD A, (HL) reads the byte at the address held in HL from memory.

Adding or removing parentheses may select a different legal instruction, so the operand form is worth checking whenever memory is involved. In indirect jump and I/O forms, parentheses mark a jump target or a port number.


Memory access through BC or DE ​

Only A works with (BC) or (DE):

asm
LD A, (BC)     ; A = byte at address BC
LD (DE), A     ; byte at address DE = A

These are compact single-byte opcodes with A hardcoded in the instruction encoding, and the assembler rejects any other register in those forms.


Direct memory address ​

You can load A from a fixed 16-bit address, or store it to one. Register pairs can also transfer both bytes in one instruction (little-endian, as always):

asm
LD A, ($8000)      ; A = byte at $8000
LD ($8001), A      ; byte at $8001 = A
LD HL, ($8002)     ; HL = word at $8002-$8003
LD ($8004), BC     ; word at $8004-$8005 = BC

When you write LD A, (COUNT), the assembler substitutes the address that COUNT was assigned and emits a direct-address load.


Memory to memory goes through a register ​

A register has to carry the value from one memory location to another; the Z80 has no direct memory-to-memory LD:

asm
; NO SUCH INSTRUCTION: LD ($8001), ($8000)

; Do this instead:
LD A, ($8000)
LD ($8001), A

Both this and the (BC)/(DE) restriction follow from the specific operand combinations encoded by the Z80 instruction set. Z80 Instruction Reference has the complete searchable list.


Summary of LD forms ​

FormExampleNotes
reg8 ← reg8LD A, BAny 8-bit register to any other
reg8 ← nLD B, $FFImmediate 8-bit constant
reg16 ← nnLD HL, $8000Immediate 16-bit constant
reg8 ← (HL)LD C, (HL)Read byte at address HL
(HL) ← reg8LD (HL), DWrite byte to address HL
(HL) ← nLD (HL), 0Write immediate to address HL
A ← (BC)LD A, (BC)Read byte at address BC; A only
(DE) ← ALD (DE), AWrite A to address DE; A only
A ← (nn)LD A, ($8000)Read byte from fixed address
(nn) ← ALD ($8001), AWrite A to fixed address
reg16 ← (nn)LD HL, ($8002)Read 16-bit word from memory
(nn) ← reg16LD ($8004), HLWrite 16-bit word to memory
SP ← reg16LD SP, HLSP = HL (or IX or IY)

For a compact LD quick table and the full addressing-shape reference, see Addressing, Prefixes and Forms.

Where each form finds its data. The first two differ by a pair of brackets: one loads the number, the other loads whatever is at that address.


Signed and Unsigned Values ​

As an unsigned value, the byte holds 0 to 255. The bit pattern $FF is 255.

As a signed value using two's complement, bit 7 is the sign bit. If bit 7 is 0 the value is positive (0 to 127). If bit 7 is 1 the value is negative (−128 to −1). The bit pattern $FF is −1. The bit pattern $80 is −128.

Two's complement negation inverts every bit and adds one. The two's complement of $01 (%00000001) is %11111110 + 1 = %11111111 = $FF, which is −1.

ADD A, B performs the same bitwise addition regardless. The result byte is identical whether the inputs are treated as signed or unsigned. The difference surfaces with $80 + $01, which gives $81: its unsigned meaning is 128 + 1 = 129, while its signed meaning is −128 + 1 = −127. The bug appears when one part of a program writes a value as signed and another reads it as unsigned. The common landmark values ($00, $7F, $80, $FF) and their signed and unsigned meanings are in Registers, Flags and Conditions.


Worked example ​

asm
MAXCOUNT EQU 10

ORG $0000
MAIN:
  LD A, MAXCOUNT
  LD (COUNT), A

  LD HL, $1234
  LD (SCRATCH), HL

  LD HL, (SCRATCH)
  HALT

ORG $8000
COUNT:   DB 0
SCRATCH: DW 0

With LD A, MAXCOUNT, the assembler substitutes the value 10 from the EQU definition. This is an immediate load: the 10 travels inside the instruction bytes.

LD (COUNT), A stores A at the address of COUNT. This is a direct-address write: the (NN) ← A form from the table above. COUNT resolves to $8000.

LD (SCRATCH), HL stores the two-byte value in HL into SCRATCH. DW 0 emitted two initialized bytes for SCRATCH: $8001 and $8002. This uses the (NN) ← REG16 form.

LD HL, (SCRATCH) reads the word back from SCRATCH. After this instruction, HL holds $1234 again. This uses the REG16 ← (NN) form.

After the program runs: $8000 holds 10 ($0A) and $8001–$8002 hold $1234 (little-endian: $34 at $8001, $12 at $8002).


Exercise ​

Memory form identification. Each instruction should be matched to a row in the LD forms table, with the memory action and the register or address that selects the location.

asm
LD A, (HL)
LD (HL), B
LD A, (BC)
LD ($8010), A
LD DE, ($8020)

Exercise notes