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.

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.

Show solution