Z80 Assembly - Simple Structures

You could ask why I don’t speak about structuring the programs. The answer is that there is no structuring in assembly! The language is entirely made up of instructions, and it’s up to you to create structuring. Such abstract structural elements as loops or subroutines are not provided by the language; you have to code them manually. This might sound scary, but it’s quite easy to get used to. Moreover, this is a very powerful feature, since it gives you the complete freedom of creation.

Working with arrays

Arrays provide a good opportunity to demonstrate this process in an example. Let’s make a routine that creates an array of ten elements containing the even integers from 2 to 20. There will be lots of new concepts to explain.

MakeArray:                       ; this is the label indicating the beginning of the routine
  ld a,2                         ; A holds the value of the first element
  ld b,10                        ; B is the number of times we want our loop to execute
  ld hl,Numbers                  ; HL now holds the address of the first element
ArrayLoop:                       ; the label marking the beginning of the loop
  ld (hl),a                      ; storing the current element at its proper place
  inc hl                         ; increasing HL by 1, so that it points to the next element
  add a,2                        ; increasing A by 2, giving the value of the next element
  djnz ArrayLoop                 ; the end of the loop; this instruction decreases B, checks
                                 ; if it is zero, and jumps to the label given if it isn’t;
                                 ; when B is zero, execution continues after DJNZ
  ret                            ; returning from the subroutine; explained a bit later

Numbers:                         ; this is the label that identifies our array
  .byte 0,0,0,0,0,0,0,0,0,0      ; initially the array will be full of zeroes

OtherStuff:                      ; just some kind of other data not used now
  .byte 100                      ; this is the byte that immediately follows our array

Let’s go through the code step by step. The three ld’s at the beginning are the initial values of the loop. Keep in mind that Numbers is just a memory address for the computer, i. e. an ordinary 16-bit integer. The same goes with ArrayLoop, but it is of course a different address. It is very important to note: assembly does NOT distinguish between variable names and labels that mark certain parts of the code. They are all the same kind of stuff. You could use ArrayLoop as a variable as well, but then you would overwrite the instructions of the loop. (This is actually an advanced programming technique called Self Modifying Code or SMC, but it is too early for you to talk about at this point.) Be aware that the computer cannot distinguish between data and code, because both are just bunches of bits. It is easy to screw things up if you are not careful enough. This is one of the reasons why you should first test your code on an emulator.

Okay, that said, I would go on discussing the loop itself. The first instruction after the label is ld (hl),a, which copies the value of A into the byte pointed by HL, as we already know. When the program first enters the loop, A contains 2, the value of the first element we want to set, and HL points to the first element. So this instruction loads the proper value into the element. After this, you can see inc hl. This instruction advances HL by 1, so that it will point to the next element to be processed. Then we have add a,2 that calculates the value to be put into the next element. The most important instruction this time is djnz. It always works with register B, that’s just one special register role I was talking about in the Registers section. djnz is very useful to create loops, but it is essential to memorise that it is not a “loop instruction”, as such things don’t exist in assembly. It is however a built-in conditional branch instruction that first decrements B by one, then it jumps to the address given if the result is not zero. If B is zero when the instruction is executed, it will overflow and take the value of 255, and since this is not zero, the jump will be taken. If I had written 0 instead of 10, the core of the loop would have been executed 256 times for this reason. To sum up, djnz is equivalent to “jump if B is not equal to one or continue if it is, and decrease it anyway”.

After the loop, what do we have in the registers? B will be zero, as this is the condition of leaving the loop. A will hold 22, which would be the value of the 11th element of the array, if there was such a thing. HL also points to this virtual 11th element, which is actually the same as OtherStuff. If we were to do an ld c,(hl) for instance, C would take the value of 100. Note that we have easily entered the area of another variable, and if I had loaded 11 into B initially, this byte would also have been overwritten. This is just another source of error: there is nothing to prevent you from corrupting other variables. The price of freedom is responsibility. Take that seriously.

Forget ret for a while, I will get back to it later.

Conditional branches

Most of the programs do not simply consist of a series of instructions that are consecutively executed, but there are many places where you need to decide which way to take. This is where the flags register comes into sight. Just as always, we will look at a simple example (actually the loop above is already one). We have a signed number in A. We want to take its absolute value and write it back into A. Let’s start with the code:

  cp $80                         ; comparing the unsigned A to 128
  jr c,A_Is_Positive             ; if it is less, then jump to the label given
  neg                            ; multiplying A by -1
A_Is_Positive:                   ; after this label, A is between 0 and 128

To take the absolute value, we first have to find out whether the number is negative or not. If it is, then it must be multiplied by -1. The first instruction does this with a little trick. As we know, all negative numbers in binary representation start with 1. In other words, if we consider them to be unsigned integers, they are all greater than 127. The cp instruction does the following thing: it subtracts the value of the operand – either a 8-bit constant, a 8-bit register or (HL), (IX+n), (IY+n) – from A, but does not write the result anywhere. However, it modifies the flags register (F). If the virtual subtraction results in zero, i. e. A is equal to the operand given, the Z (zero) flag is set. If A is less than the operand, the subtraction results in a step over zero, i. e. the C (carry) is set. If A is greater, then both C and Z are reset (set to zero). For these relations, both numbers are considered as unsigned integers.

In the end, if A is negative upon entering the piece of code above, the cp instruction sets the carry flag. The next one, jr is a jump instruction. It can have either one or two operands. If there is only one operand, then it is a label, and the program jumps to this label when the instruction is executed. However, in our case there are two operands. The first is always the condition, and the second is the label to jump to if the condition is met. This time the jump will be taken if the carry flag is set. All the possible conditions are listed in the next section.

neg is an instruction that negates the value of A. Note that -128=128 when 8 bits are used to represent a number.

Relative and absolute jumps

You could see a jump instruction in the previous example, so it’s time to look at them more closely. The fact whether a jump is absolute or relative depends on how you calculate the address of the destination. In the case of absolute jumps (jp instruction), address is always given with respect to the beginning of the memory, while the relative jumps (jr) only know where to jump with respect to themselves. It is easier to list the differences between the two in a table.

PropertyAbsoluteRelative

Instruction

jp

jr

Length of address

16 bits

8 bits

Length of instruction

3 bytes

2 bytes

Speed of execution

faster

slower

Possible destination

anywhere

the vicinity (+/- 128 bytes)
of the jump instruction

Possible conditions

c, nc, z, nz, pe, po, m, p

c, nc, z, nz

Both kinds of jumps can be either conditional or unconditional, and the conditions work the same way. They can be one of the following: c (C flag set), nc (C flag reset), z (Z set), nz (Z reset), and only in the case of absolute jumps: pe (P set), po (P reset), m (S set) or p (S reset). At this point I think I should mention djnz Label again, because as we already know how it works, we can see that it is equivalent to:

  dec b                          ; decrementing B by 1
  jr nz,Label                    ; if it did not become zero, jump to Label

This pair of instructions and djnz are actually interchangeable; the only differences are that djnz is smaller and faster, and it preserves the flags. Here you can see that the destination of djnz is also limited to its vicinity. Therefore, if you want to take a jump that points farther than 128 bytes (one instruction can be 1, 2, 3 or 4 bytes long), you must use a jp nz combined with a dec b instead of it. Although it is somewhat larger in size, it is almost as fast as the original djnz. The assembler will always tell you if a relative jump cannot be taken, because it is the one that converts the address of the label into a relative address. When such an error is generated, you have to use jp.

Subroutines

Another fundamental element of programming is the use of subroutines. In assembly, they are handled by two instructions: call and ret. The former is used to enter a subroutine, and the latter for returning from the subroutine and continue where we left off. This is achieved with the help of the stack. That’s why you should not play around with SP, unless you really know what you are doing. Just as with djnz, we can express call and ret with the help of virtual instructions we can already interpret.

Call is basically a push pc+3 followed by a jp Label. (The +3 is needed to jump over the call—always 3 bytes long—after returning.) ret is even simpler, it is equivalent to a pop pc. Of course, this decomposition is not entirely true, since every time an instruction is executed PC is altered as well. What’s important to keep in mind that each time a call occurs the address of the next instruction is pushed onto the stack, and ret will always continue execution from the address that is stored on the top of the stack. Hopefully I have confused you enough, so here is a working example:

  call MakeArray                 ; calling the subroutine presented in the first example
  ld a,(Numbers)                 ; loading the first element into A (i. e. 2)
  ld c,(hl)                      ; if everything went right, C will hold 100 after this

To be short: the call pushes the address of the ld a,(Numbers) instruction on the top of the stack and jumps to MakeArray. MakeArray, as we know, fills the 10 bytes beginning at Numbers with the first 10 even numbers. When the program reaches the ret I put at the end of the subroutine, the return address will be retrieved from the stack. After returning, we continue from the address saved by call. As I explained above, C should be loaded with 100, because the loop in the subroutine causes HL to point at that value.

To ease our lives, both call and ret can be used with conditions in the very same manner as absolute jumps, i. e. all the eight conditions can be investigated. If you write for instance call c,Label, the call only occurs if the carry is set. Similarly, ret z will only return if the zero flag is set. This way the subroutines can be combined with conditions in a compact way.

Finally, here is a bit advanced example. With the help of call, you can also determine the value of PC:

  call NextInstr                 ; calling the NEXT instruction
NextInstr:                       ; the label we jump to
  pop hl                         ; popping the address of the instruction into HL

This is equivalent to the otherwise impossible ld hl,pc. Although there is no ret, the code will not cause any problem, because the only important thing, the stack is properly handled. I just showed this example to you in order to demonstrate again the freedom assembly gives to you.

Back to the index