.png)
S13's tables held values: five numbers, four letters, one byte each. Indexing worked because every element was the same size, so element n was n bytes along.
Now try it with these:
message1: DEFB "Ready",0
message2: DEFB "Set",0
message3: DEFB "Go!",0
Six bytes, four bytes, four bytes. Where does the second one start? You would have to count the first one to find out, and count both to find the third. There is no arithmetic that gets you there.
The way out is to stop storing the things in a table and store where they are instead. Addresses are all the same size - two bytes, always - so a table of addresses can be indexed even when the things they point at cannot.
DEFW puts a 16-bit value in your program, and if you give it a label it puts
that label's address:
data: DEFB 42
ptr: DEFW data ; two bytes: the address of data
ptr does not contain 42. It contains the address where 42 lives. That is a
pointer, and it takes two bytes because addresses are 16-bit - the low
byte first, the little-endian order from S4.
If data happened to sit at C000, then ptr holds 00 C0.
To get from the pointer to the data, load the pointer into a register pair and then read through it:
LD HL,(ptr) ; HL = the address stored at ptr
LD A,(HL) ; A = 42
Two instructions. The first is the one from S10 that reads a 16-bit value out of memory - and what it reads happens to be an address, which is the whole trick. Following a pointer to the thing it points at is called dereferencing it.
You will also see it done like this:
LD HL,ptr ; HL points AT the pointer
LD E,(HL) ; low byte of the address
INC HL
LD D,(HL) ; high byte
EX DE,HL ; HL = the address
LD A,(HL) ; A = 42
Five instructions for the same result, and both give 42 - I checked.
So why would anyone write it? Because LD HL,(nn) needs the address written
into the instruction. It can only load from a place you knew about when you
assembled the program. The moment you calculate which pointer you want -
which is the entire point of a table - you have the pointer's address in a
register, and there is no instruction that loads HL from an address held in
a register.
So: LD HL,(ptr) for a pointer you can name. The long form for one you had to
work out. Which is the next section of this page.
EX DE,HL is doing real work there - it is the one-byte swap from S10, used
because the two bytes had to be assembled in DE and the result is wanted in
HL.
table: DEFW message1
DEFW message2
DEFW message3
Three entries, two bytes each. Entry n is at table + n*2.
Doubling a small index is one instruction, because adding a number to itself doubles it:
ADD A,A ; A = A * 2
So the full job - pick entry 1, follow it, print what it points at:
ORG 256
LD A,1 ; which message
ADD A,A ; times two - each entry is two bytes
LD L,A
LD H,0 ; HL = the offset
LD DE,table
ADD HL,DE ; HL = the address OF the pointer
LD E,(HL) ; now follow it
INC HL
LD D,(HL)
EX DE,HL ; HL = the address of the message
CALL puts
RET
puts: LD A,(HL)
CP 0
RET Z
RST 8
DEFB 158 ; ZOUTC
INC HL
JR puts
message1: DEFB "Ready",0
message2: DEFB "Set",0
message3: DEFB "Go!",0
table: DEFW message1
DEFW message2
DEFW message3
What you should see
>Set
Change the LD A,1 to 0 or 2 and you get Ready or Go!. Nothing else
in the program changes - and it would still be one line to change with thirty
messages in the table.
There are two levels of indirection here and it is easy to lose track. Say them out loud as you read the code:
HL = the address of the pointer - after the ADD HL,DE.HL = the address of the message - after the EX DE,HL.A = a character from the message - after the LD A,(HL).Every pointer table in every program you write is those same three steps.
Once you can index a table of addresses, a lot of things become one lookup.
A message by number. Above. Error messages, prompts, level names.
A menu. The player presses 2; you index a table and print the third
entry, with no comparisons at all.
Sprite frames. Animation frames are rarely the same size. A table of addresses handles them; a table of data cannot.
And routines, not just data. DEFW an address of code rather than
data, and JP (HL) jumps to whatever HL holds. That turns a table of
addresses into a table of behaviours - press a key, index a table, jump to the
routine for it. That is how a game's control scheme is usually built, and it
replaces the longest chain of comparisons you would otherwise write.
LD A,1 to LD A,2. Then to LD A,3, which is past the end of the
table. What gets printed, and where did the address come from?ADD A,A. Which message prints, and can you explain the result
from the byte layout rather than by guessing?LD HL,(table). Which message do
you get, and why does it not respond to LD A,1 any more?DEFW lines without touching anything else.
What changes? What does that tell you about where the messages live?14.1 - Follow one pointer. Store a byte, make a DEFW pointer to it, and
load the byte into A through the pointer. Do it both ways - one instruction
and five - and satisfy yourself they agree.
14.2 - Pick and print. Three messages, a table, and code that prints the
second. Then make the index come from a byte in memory rather than from LD A,
so that changing one stored value changes the output.
14.3 - Ask which. Read a key with S8's call 156, subtract '1' from it to
turn 1, 2, 3 into 0, 1, 2, and print the matching message. That is a
working menu in about fifteen instructions.
14.4 - Bounds. Extend 14.3 so that any key outside 1 to 3 prints
nothing instead of following a pointer that is not there. Nothing in the
machine will do this for you.
14.5 - Two levels deep. Make a table of addresses of tables, and follow
it twice to reach a message. Say out loud what HL holds after each step -
that is the only way to keep it straight.
14.6 - A table of routines. DEFW the addresses of three subroutines
instead of three messages, and use JP (HL) to jump to the one you index.
Each routine can just print a different letter. You have now written the
dispatch mechanism that a game's input handling is built on.
| What you see | What it means |
|---|---|
| The wrong message, one entry out | The index was not doubled, or was doubled twice. Each entry is two bytes. |
| Rubbish printed, then more rubbish | The pointer was not followed - HL still held the address of the pointer, not the address it contained. |
| The right message but the program does not return | The puts terminator. Check every message has its ,0. |
| A message you did not put in the table | An index past the end. The bytes there are not a pointer, but the machine will follow them anyway. |
| Nothing at all | EX DE,HL missing, so HL held something unrelated. Read HL in the register line and compare it with the listing. |
DEFW label creates one, two bytes,
low byte first.LD HL,(ptr) follows a pointer whose location you can name. For one you
calculated, read the two bytes with LD E,(HL) / INC HL / LD D,(HL) and
EX DE,HL.base + n*2. ADD A,A doubles the
index in one instruction.DEFW an address of code and JP (HL) jumps to it, which turns the same
pattern into a table of routines.S15. How the screen actually works. Everything you have put on screen so far went through a ROM routine. Next you find out what is behind it: a second chip, with its own memory that the Z80 cannot address, holding a picture it redraws every frame.