Arrays, strings and ix
Walking an array with hl, indexing one by a register when the Z80 has no mode for it, scaling for 16 bit elements, zero terminated strings, and ix parked on a record so the fields have names.
The overview of this topic is in Assembly basics. The same topic in M68K, MIPS, RISC-V.
Seven registers hold seven bytes, and a program has more than seven bytes to keep. Everything else lives in the 64 KB, and the instructions that reach it are the ones from the addressing lecture. Let's now use them on the three shapes that turn up in every program: an array, a string and a record.
An array is a run of bytes
In C an array is elements of the same size laid end to end, and a[i] is the address of a[0] plus
i times the size of an element. Assembly has exactly that without the brackets: a pointer in hl,
and inc hl to move it on.
Type 9000 into the memory panel and press Run. The six bytes go from 00 00 00 00 00 00 to
01 02 03 04 05 06, and hl ends at 9006, one past the last one it wrote. They start at zero
because .ds reserved the room and put nothing in it.
inc hl is the p++ of C written out. C hides the size of what a pointer points at and assembly does
not, so a pointer into an array of bytes moves by 1 and a pointer into an array of words moves by 2.
Indexing by a register
Walking is one thing, reaching straight for numbers[i] with i in a register is another, and the
addressing lecture said why: the Z80 has no base plus index mode. The index is widened into a pair
and added.
Elements bigger than a byte need the index scaled first, by the size of an element. For 16 bit
words that is a doubling, which add hl, hl does in one instruction. Both are here, an array of
bytes and an array of words:
a comes out at 1E, which is 30, and bc at 012C, which is 300. Four instructions of address
arithmetic for one read, which is why a program that touches every element walks a pointer instead,
and only computes an address when it has to jump straight to one.
The two loads at the end are the little endian order from the memory lecture: the low byte is the one
at the lower address, so it goes into c.
Try changing the second ld a, 2 to ld a, 3 and bc comes out at 0190, which is 400.
Strings
A string is an array of bytes holding character codes, and nothing marks a byte as a letter. 'H' is
the number 72, or 0x48, which is what ASCII assigns to that letter, and the same byte is 72 to
every instruction that reads it.
Nothing records how long a string is either, so the convention says where it ends: a byte of zero
after the last character, which is what C does and what .asciz writes for you.
| address | byte | as a character |
|---|---|---|
text | 68 | h |
text + 1 | 69 | i |
text + 2 | 20 | a space |
text + 3 | 7A | z |
text + 4 | 21 | ! |
text + 5 | 00 | the terminator |
So every loop over a string is "read a byte, is it zero, if not do the work and step on", and a string of five characters takes six bytes.
Press the text button in the memory panel's corner and the bytes are drawn as characters. Before the
run 0x9000 reads hi z! and afterwards it reads HI Z!, with the space and the exclamation mark
untouched because neither is between a and z.
cp 'z' + 1 is the assembler doing the arithmetic: 'z' is 122, so the instruction that ends up in
memory compares against 123, and jr nc is "greater or equal to 123", which is "greater than z".
Records, and ix
A record is a fixed set of fields at fixed offsets, which is a struct in C. ix and the (ix+dd)
mode were made for it: park ix at the start and reach each field by a name defined with equ.
Stepping through an array of records means adding the record's size to ix, and since the
displacement in the instruction is a constant, that addition is add ix, de with the size in de.
a comes out at 3C, which is 60, the three y fields added up. ix ends at 9009, one record
past the last one.
Changing the layout is now one place: add a SCORE equ 3, change SIZE to 4, and give each record a
fourth byte. Nothing else in the loop moves.
The block instructions
Copying and searching are common enough that the Z80 does each in one instruction, using hl for the
source, de for the destination and bc for the count.
After the ldir the six bytes appear at dest. After the cpir, hl holds 9003, which is one
past the byte that matched, and Z is 1 to say it was found; a cpir that runs out of bytes
leaves Z at 0. The address of the match is hl minus 1, so a program that wants it writes
dec hl next.
lddr is the same copy going backwards, from the last byte to the first, and it is the one to use
when the two blocks overlap and the destination is higher. Copying 0x9000 to 0x9001 forwards
would read a byte the copy has already overwritten; going backwards reads each byte before anything
lands on it.
Your turn
Find the largest of the six bytes at numbers and leave it in a. They are unsigned, the largest is
90, and a cp (hl) next to a jr is the comparison.
Show solution
The second one copies a string. text holds hello and copy has room for six bytes: copy the five
characters and the zero after them into copy, in one instruction.