Loops and djnz
A while loop written out of a compare and two jumps, then djnz, the one instruction that counts and jumps at once. Its counter in b, its 256 iterations when b starts at zero, and what a nested loop has to do about it.
The overview of this topic is in Assembly basics. The same topic in M68K, MIPS, RISC-V.
A loop is a jump backwards, and we already have jumps. What the Z80 adds is one instruction that does
the counting and the jumping together, and it is the reason b is the register it is.
A while loop, written out
In C:
int i = 0;
while (i < 10) {
i++;
}
Flattened into gotos, the way the general course did it, that is a test at the top and a jump back
at the bottom:
int i = 0;
while_start:
if (i >= 10) goto while_end;
i++;
goto while_start;
while_end:
And every line of that is an instruction. i lives in b, and the comparison has to go through a,
since cp compares against the accumulator and nothing else.
b comes out at 0A, which is 10. Three of those instructions are the loop and one is the body,
which is three quarters of the program spent on counting.
Try changing cp 10 to cp 200: the loop runs 200 times instead, and the run finishes just as fast,
because 200 iterations of four instructions is nothing.
djnz
djnz label means decrement b and jump if it is not zero. One instruction, two bytes, and it
replaces the dec, the test and the jump.
a comes out at 0F, which is 15, five threes. The counter is always b, there is no form that
counts in another register, and the jump is always relative, so the loop body has to fit in the
128 bytes a jr reaches.
Two things follow from b being the counter. A loop counts down, from the number of iterations
to zero, so if the body needs to know which iteration it is on it has to work it out or keep a second
counter. And b is not available to the body, which is what the nested loop below is about.
The third thing is the edge case, which is the second loop in that program. djnz decrements first
and then tests, so b at 0 gives 256 iterations, not none: the first dec takes 0 down to 255
and the loop runs the whole way round. c comes out at 00, because it went from 0 all the way
round to 0 again, which is what a byte does after 256 increments. Put ld c, 1 in that body instead
of the inc and step it if you want to watch b count down from FF.
A loop that might have to run zero times therefore needs a test before it, which is a do while in C
turned into a while: ld a, b, or a, jr z, skip.
Walking memory
The loop that turns up most is one pointer stepping through bytes, and it is hl and inc hl next
to a djnz.
a comes out at 0F, which is 15, and hl at 9005, one past the last byte it read. Three
instructions in the loop, one of which is the work.
Try changing add a, (hl) to add a, a and ld a, 1, which doubles a five times: the array is
never read and a comes out at 20, which is 32.
Looping on something other than a count
A loop that stops on a value instead of a count has no counter at all. Scanning a string for its zero terminator is the standard one:
b comes out at 05. or a is the "is a zero" idiom from the flags lecture: it leaves a alone
and sets Z from it, and it is one byte where cp 0 is two.
Counting past 255, and nesting
b is a byte, so djnz counts to 256 and no further. A longer loop counts in a pair, and here the
Z80 gets awkward: dec bc sets no flags, so there is nothing to jump on afterwards. The way
round it is to or the two halves together, since b | c is zero exactly when both are.
djnz also owns b, so an inner djnz destroys the outer loop's counter, and the usual fix is to
push it. Both problems are in this one, the long count first and the nested pair second.
hl reaches 012C, which is 300, at the end of the first loop, and comes out at 000C, which is
12, three times four, at the end of the second. Step through it to see both.
The three instructions in the middle of the first loop are the price of a 16 bit counter, and they
destroy a, which is why a loop like this keeps its working value in hl or in memory. For copying
and searching there is a better answer, which the instruction set lecture already showed: ldir and
cpir count in bc themselves and cost one instruction.
push bc and pop bc in the second loop are two bytes and eleven clock cycles each on a real Z80,
which is cheap next to a loop body.
The other way is to keep the outer counter somewhere djnz cannot reach: c, d, e, the shadow
set through exx, or a byte in memory. Counting down c with dec c and jr nz costs three bytes
in the loop instead of two pushes outside it, so which is cheaper depends on how often the outer loop
goes round.
Your turn
Add up the numbers from 1 to 10 and leave the total in a, which is 55, or 37 in hexadecimal. A
djnz loop counts down from 10 to 1, and adding the counter itself each time round is the whole
program.
Show solution
The second one has the string already written for you and hl already pointing at it. Count its
characters, not counting the zero at the end, and leave the count in b. The answer is 5.