Loops and dbra

A while loop written out of a compare and two branches, and then dbra, the one instruction that counts and jumps at once. Its off by one, its word sized counter and the db<cc> forms that leave the loop early.

A loop is a comparison, a branch out of it and a jump backwards, which we can now write. The M68K also has one instruction that does the counting and the jumping together, and it is the one you will actually use.

The loop written out

Adding up the numbers from 1 to 10, in C, then flattened, then assembled:

int sum = 0;
for (int i = 1; i <= 10; i++) sum += i;

d0 comes out at 00000037, which is 55, and d1 at 11, one past the last value it used. Four of those seven instructions are the loop machinery and one is the work.

clr.l d0 is move.l #0, d0 written shorter, and addq.l #1, d1 is add.l #1, d1 in a shorter encoding, which is what addq is for: adding a number from 1 to 8, which is what a loop counter does.

dbra does the counting

dbra dn, label subtracts 1 from the low word of dn and branches to the label unless the result is -1. One instruction replaces the subq and the bne, and the loop becomes:

d0 is 55 again. Two things about that counter:

It runs one more time than the number you put in it. dbra stops at -1, not at 0, so a counter of 9 gives ten passes: 9, 8, 7, down to 0, and then the pass that takes it to -1 and falls through. Write move.w #count-1, d1 when you know how many times you want, and let the assembler do the subtraction.

It is a word. dbra reads and writes only the low 16 bits of the register, so the most a single dbra loop can run is 65536 times, and whatever is in the high word is left there and ignored.

The loop runs four times, so d0 comes out at 4, and d1 ends at FFFFFFFF: the low word walked down to FFFF, which is the word -1, and the FFFF above it was never touched. This is why the counter of a dbra loop is set with move.w and read as a word.

After the loop, the counter is $FFFF and not zero, which catches people who then want to reuse the register.

db<cc> leaves the loop early

dbra is the plain member of a family. db<cc> dn, label takes a condition, and it goes round again only when the condition is false and the counter has not run out. So dbeq means "keep going until something is equal or we run out of elements", which is a search.

The 30 is the third element, so the loop goes round three times and stops. d1 comes out at 3, a0 at 0000200C, four bytes past the element that matched, and d3 at 000000FF.

d3 is $FF because db<cc> writes no flags at all: the Z that seq reads is still the one the last cmp left. That is how you tell the two ways out of the loop apart, since dbeq falls through both when it found something and when it ran out. The other way to tell is the counter, which is $FFFF only when the loop ran out.

dbne is the same idea for "keep going while they are equal", and every condition of the branch family has a db version.

Nested loops

Nothing new: an inner loop is a loop between two lines of the outer one, with its own counter in its own register, reset at the top of every outer pass.

d0 comes out at 12, which is 3 times 4. The move.w #3, d2 has to be inside the outer loop: put it above outer: and the inner counter is $FFFF on the second pass, which makes the inner loop run 65536 times. Try moving that line up one and pressing Run to watch it happen.

The Playground stops a program after two million instructions and says so, with "Execution limit of 2000000 instructions reached (maybe an infinite loop?)". That message is what a loop with a broken counter looks like, and it is the reason dbra reads its counter as a word rather than a long: a long counter that starts wrong runs for four billion passes.

Your turn

Add up the numbers from 1 to 10 with a dbra loop and leave 55 in d0. The counter belongs in a data register of your choice, and d0 starts at 0 in the test.

Show solution

The second one starts d0 at 64 and wants to know how many times it can be halved before it reaches

  1. Leave that count in d1, which for 64 is 6.
Show solution