Introduction To Z80 Assembler

Please note this content is from the original WoS site, and may no longer be relevant. If you have any queries, please contact us.

Written by James Hollidge

Foreword

The Z80 is one of the most popular microprocessors of the 80’s having been used in many home computers systems of that era. This document will give an introduction to all aspects of the Z80 assuming no knowledge of programming.

This guide is only intended as an introduction to the concepts and language of machine code and assembler – it doesn’t give a complete examination of the instruction set nor does it attempt to deal with the particulars of any one Z80 system. However I will be covering that sort of stuff in my next Z80 article when I deal with the actual specific Spectrum stuff and some more of the instructions that are only useful in context.

Contents

  1. Binary vs Decimal vs Hexadecimal
  2. The Registers, Memory and Machine Language vs Assembler
  3. Addition and Subtraction
  4. Bit Manipulation
  5. Program Flow, more on Flags and The Stack
  6. Memory Manipulation

1. Binary vs Decimal vs Hexadecimal ^

The way we count things in every day life is based on the number ten. This is an Arabic convention that has been widely adopted by the west. However it’s not the only way of counting things. Ancient cultures have used systems based on 5, 12 and even 60. Computers use a counting system based on 2. Why? Quite simply it’s for electronic simplicity – it is far easier to determine whether something is on (1) or off (0) (known as digital) than if it is at a range of voltages (known as analogue).

It’s really very easy to work with binary. In decimal each extra digit represents numbers ten times greater than the last digit. For example take 55. The first 5 represents 5 tens, the second 5 represents 5 ones. The first digit hence represents numbers ten times larger than the second. In 555 the first digit represents 5 hundreds, the second 5 tens and the third 5 ones. The first digit represents numbers ten times bigger than the second, in turn ten times bigger than the third. The magnitude of the digits are 10x10x1, 10×1, 1. For thousands the magnitude of the digits is 10x10x10x1 and so on. Each extra digit is worth ten times more the last. Why do we work in these columns? Well when we get to 9 in one column then adding 1 will produce a number too large to represent (we can only represent 0,1,2,3,4,5,6,7,8 and 9). Unless we invent a symbol for each number we have to show the numbers larger than 9 in a different way. We do this by creating a column of digits that do this: the tens. Hence when we add 1 to 9 we write 10 to show there’s one 10 and zero 1s.

Binary works in the same way except as we only have two digits we have to represent the 2’s in a different way; by adding an extra column. For example when we add 1 to 1 we add an extra column of digits to represent the 2 leaving us with a number 10 in binary: One 2 and zero 1s. Each extra digit (or more correctly bit, for binary digit) we add represents numbers twice as large as the last bit. Hence:

1=1, 1×2=2, 1x2x2=4, 1x2x2x2=8, 1x2x2x2x2=16 etc….

The Z80 is an 8 bit system which means it uses 8 bits to represent its numbers, which look like this:

76543210 – Bit Number
01001000 – Binary Number

Now to convert this to decimal we take each bit that is set (1) and look at it’s bit number (shown above the binary number), take 2 to the power of that number and add it to the total. (Taking a power of a number says take that number and multiply it by itself a number of times, 2 to the power of 2 (or 2^2) is the same as 2x2x1 or 4).

26 (2x2x2x2x2x2x1 = 64 ) + 23 (2x2x2x1 = 8 ) = 64 + 8 = 72

The maximum value we can represent with 8 bits is therefore 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 = 255, practice your binary mathematics as it’s going to be essential here.

Now alongside binary in computing we use hexadecimal, a number system based on 16. So this time each digit we write represents 16 times the magnitude of the last digit. We use the letters A to F to represent the numbers 10 to 15 in decimal.

So a hexadecimal number like A5 would be 10 x 16 + 6 in decimal or 166. However hexadecimal is far more useful when converting binary because it just so happens that because 16 is a power of 2 (24) that conversion between these number systems is very straightforward. All we need do is take each group of four bits in the number and replace them with the hexadecimal equivalent. Why does this work? Well each group of four bits can represent 16 different numbers and each hexadecimal digit can represent 16 different numbers. For example:

76543210
11101101

The first four bits (0-3) are 23 + 22 + 2= 13 which is D, the second four bits (4-7) are 23 + 22 + 21 = 14 which is E. Hence this number is ED in hexadecimal (and 14×16 + 13 = 237 in decimal). As you might see hexadecimal is very much more convenient for writing binary numbers down quickly without having to think too much about conversion as going back from hexadecimal is just as easy: you simply replace each digit with it’s equivalent binary number. For example C9 would become 12 and 9, or 1100 and 1001: 11001001 or 201. Hexadecimal makes working with binary a bit easier and converting to decimal a bit more straightforward. If you can, learn the first 16 binary numbers so you can convert quickly:

Binary Decimal Hexadecimal
0000 0 0
0001 1 1
0010 2 2
0011 3 3
0100 4 4
0101 5 5
0110 6 6
0111 7 7
1000 8 8
1001 9 9
1010 10 A
1011 11 B
1100 12 C
1101 13 D
1110 14 E
1111 15 F

In this article b, d and h after a number represent binary, decimal and hexadecimal respectively, if there is any possibility of misinterpretation.

2. The Registers, Memory and Machine Language vs Assembler ^

The Z80 stores its numbers and does all its maths (most of the time) using registers – these are a set of 8 bit numbers that are stored internally (actually it’s a bit more complicated by we’ll get to that in a bit).

At the moment we’ll deal with the registers you’re going to use pretty much all the time. They are A,B,C,D,E,F,H,L and A’,B’,C’,D’,E’,F’,H’,L’.

Each register stores an 8 bit number (that’s 0-255). A and F are special registers. A is known as the accumulator. It does most of the maths work. F is known as the flag register. It works a bit differently to other registers in that its purpose is to store the results of other operations depending on whether they are zero, negative, carry etc… but that’ll be covered later. The registers B,C,D,E,H and L are a set of general purpose registers and can also be used in pairs to form 16 bit numbers (which can represent numbers up to 65535, test your binary understanding to figure out why). The valid pairs are BC, DE and HL.

The other registers are known as shadow registers, They can’t be accessed directly but instead can be swapped around with the normal registers. They are incredibly useful for temporarily storing values. The way we load values into these registers is by way of the LD instruction, which is short for load. For example LD A,0 will load A with 0. LD BC,0 will load B and C with 0. LD A,B will load A with B and so on. There are many forms of LD but you can’t do things like LD HL,BC – you just have to learn what is valid.

Now back to shadow registers. We access those using the exchange instructions. EXX will swap BC with B’C’, DE with D’E’ and HL with H’L’. EX AF,AF’ will swap A with A’ and F with F’. You won’t see them for awhile but you should know about them.

In addition to those exchange instructions there are a few others, EX DE,HL will swap DE with HL. You’ll see its uses later. The other instructions you need not know about for now.

Before we actually move on to assembler proper you should also know about machine language. Assembler is basically one step away from machine language. The Z80 gets instructions from the memory as numbers. Assembler is the English representation of these numbers. Machine language or machine code are the numbers represented by the assembler. So to clarify when we write an instruction LD A,0 the Z80 reads 3E 00. 3E is the opcode or machine code and 00 is the operand, or the value that tells LD A,number what number to load. For the most part we don’t need to concern ourselves with these numbers but for advanced code we can use the knowledge of these opcodes to manipulate the memory and actually alter the operation of a program by altering the opcodes stored!

Now for those of you wondering what memory is exactly it’s best thought of as a huge storage area. It stores opcodes and data side by side. The Z80 doesn’t understand the difference so you need to look after it and make sure you don’t try to execute graphic data or display program code! The Z80 uses a 16bit register called PC, or program counter, to keep track of where it’s getting instructions from. As 16 bit numbers can represent 65536 different numbers the Z80 can access 65536 different memory locations to execute code. Each memory location stores an 8 bit number or byte, giving 65536 bytes of memory or 64 kilobytes (a kilobyte being 1024 bytes) or 64kb. You’ll see more about PC later but for now all you need to understand is that memory stores all the information the Z80 can use and it’s where we place instructions and data so we can make a computer useful!

3. Addition and Subtraction ^

Now we are actually getting somewhere. Addition and subtraction are the most basic mathematical tools available to the Z80 and you’ll probably use them a lot.

Addition in binary works the same way as in decimal. We add the ones, twos, fours etc… together and write down the result for each column, carrying to the next if it’s too large. For example:

00000110
00000011 +
00001001 Result
0 00001100 Carry

The carry is treated as a third number to add. If the result of the addition of one column of bits is too large to fit into one column (i.e. if both bits are 1) then we place a 1 in the carry in the next column and include that in the next addition. The carry usually starts with a zero in the first (rightmost) column.

Now what if we add up two values with a result greater than 255?

11111111
11111111 +
11111110 Result
1 11111110 Carry

Or 255+255 = 254?!? No, in fact we use the F register to store the extra carry in one of its bits, strangely enough called the CARRY bit, or flag. The CARRY bit in essence represents 256, making the result of the addition 510. There’s more on the carry flag later.

Now, when we do subtraction we do things in a slightly different way. The way negative numbers are represented is though a system called ‘twos complement’. The reason it is called a complement is that we represent negative numbers by inverting each bit of a positive number. It’s twos complement because we then add one to this inversion (ones complement). Why twos complement? Well, it’s quite simple really.

Inverting all the bits to form ones complement seems a reasonable way to represent negative numbers, if we have 00001011, or 11, -11 is simply 11110100. Obviously this means numbers above 127 will be negative (01111111 is 127, thus -127 is 10000000, or 128 in positive representations). However look at zero. If we invert the bits in zero we get 11111111 for -0! As there is no positive or negative sense for zero clearly we have a problem here. We get around this in twos complement by adding 1. 11111111 + 1 = 0 plus a carry out – which we ignore. 00001011 will now be 11110100 + 1, or 11110101. We now have a range from -128, 10000000, to +127 01111111. The leftmost (or most significant bit) thus determines whether or not the number is negative or positive.

Twos complement also makes subtraction easy – as adding two numbers represented in twos complement together will always give the right result. For example, 11111111 + 00000001 (-1 + 1), which we’ve already seen equals zero as expected. In ones complement we’d have 11111110 + 00000001 – which is 11111111 and still right as we have two zeros. If we went on to add 1 twos complement would give us the correct answer but ones complement would not. – it would remain zero – positive zero! The Z80 cannot know whether or not a number is positive or signed in this system and handle the two zeros correctly for subsequent instructions.

Hence due to the fact ones complement has two zeros and would make the Z80 work harder we use twos complement to perform subtraction – always treating both numbers as twos complement.

Here we can do 100 – 10 by adding 100 and 10 in twos complement.

01100100 100
11110110 + -10
01011010 Result 90
1 11001000 Carry

What about something like 255 – 128? -128 doesn’t have a positive representation in twos complement. Well the Z80 doesn’t make the distinction between twos complement and unsigned numbers so what we’re actually doing is -1 + 128 which is still 127. So when adding 255 + 128 this is also -1 + 128 and again gives us 127. So 128 is treated as both positive and negative at the same time – it’s the context with which you use it that gives it a sign.

In essence don’t worry about the way the Z80 actually performs the operation – (you won’t get the carry working as it does here with subtraction for example) – it’ll always give the correct answer when subtracting or adding. What you as a programmer need to worry about is whether or not you treat the number as signed or unsigned.

There are four flags which will help you working with addition and subtraction operations:

  • The carry flag you have met. It will be set if an addition has a result greater than 255 or a subtraction has a result less than 0.
  • The sign flag will be set if an operation on a register leaves a negative result (i.e. bit 7 is set).
  • The parity/overflow flag – which has two uses; the parity part we’ll see later. In dealing with addition and subtraction an overflow occurs if an operation causes a result larger than can be represented in twos complement, i.e. something less than -128 or more than 127. So 127 + 1 or -128 – 1 would both cause overflows.
  • Finally there is the zero flag which is set if an operation results in a zero result – no surprises there.

Along with carry flag these flags are helpful in dealing with the results of operations as will be seen later.

The instructions for carrying out these operations are ADD and SUB, no prizes for guessing what they do. For example:

LD A,23
LD B,100
LD C,53
ADD A,B
SUB C

Would leave A with 70, sign unset, carry .

ADD A, can work with any of the normal registers (excluding F of course) or an 8 bit number, so ADD A,D or ADD A,65 are fine. ADD A,HL or ADD A,1000 are invalid.

SUB works in pretty much the same way, with any of the normal registers or an 8 bit number but not with pairs or numbers larger than 255.

However, ADD can also work with register pairs. There are three ADD HL, instructions that work with BC,DE or HL. SUB cannot do this however, it’s always A take away another number and hence the reason why A is not mentioned in the SUB C operation in the above example. Note that when adding register pairs if carry is set it will represent 65536 (use your understanding of binary to figure out why).

As seen above these operations can often produce a result that leads to carry being set. In this case there are two further instructions that take heed of this, ADC and SBC, or add with carry and subtract with carry.

ADC will treat the carry as 1, hence if carry is set ADC A,0 will actually add 1, not 0. SBC works again in basically the same way, treating carry as 1. So SBC A,0 with carry set will subtract 1. Notice that SBC is written with A, like ADD and ADC.

This is because SBC has some 16 bit subtraction instructions: SBC HL, and any valid register pair. Remember EX DE,HL? Well say instead of HL-DE you want to do DE-HL. Using EX DE,HL SBC HL,DE EX DE,HL will achieve this. Very useful.

There’s one final subtraction instruction you should know about, NEG. NEG is short for negate and performs 0-A. In effect it will convert twos complement number from positive to negative. For example if A is 1 NEG will be FF, or -1. If A is FF NEG will be 1 or +1. Remember however that NEG -128 will be -128 still.

There are two final instructions for addition and subtraction which are very useful indeed. They are INC and DEC and stand for increment and decrement. These basically add 1 or take away 1. You’ll find yourself using them all the time as they are much more convenient than the above instructions. They work on any valid register pair or single register. Both instructions completely ignore the carry flag and won’t affect it at all. When you get to 255 or 65535 and increment you get zero, if you’re at zero and decrement you get 255 or 65535. Carry plays no part at all.

4. Bit Manipulation ^

Beyond addition and subtraction there are numerous other ways in which you can affect registers. They are called bitwise instruction because they work at the level of bits.

AND is short for, erm, and and works by comparing each bit and setting the result bit to 1 if they are both 1. For example:

11000011 AND 10011001 = 10000001

OR is short for, well or, and works in a similar way to AND but will set the result but to 1 if either bit being compared is 1. For example:

11000011 OR 10011001 = 11011011

XOR is short for exclusive or and works a bit like OR and a bit like AND. It will set the result bit to 1 if either bit being compared is 1 but *NOT* if both bits are 1. For example:

11000011 XOR 10011001 = 01011010

AND, OR and XOR all work on A and either a number or a single register, XOR B or AND 45 for example. Note that XOR A will set A to 0 and is a very useful optimization (try to figure out why).

CPL is short for complement and basically inverts each bit, so a 1 is 0 and a 0 is 1. For example:

CPL 10010010 = 01101101

CPL only works on A and hence takes no operand. XOR FF is equivalent to CPL, can you see why?

The next set of instructions all work by shifting and rotating bits rather than comparing them.

The rotation group works by moving all the bits either left or right and then moving the leftmost or rightmost bit to the beginning or to the carry and the carry or the rightmost or leftmost bit to the end. So starting from:

Register C
10000001 0

 

    • with RL (rotate left with carry)

00000010 1

    • with RR (rotate right with carry)

01000000 1

    • with RLC (rotate left without carry)

00000011 0

    • with RRC (rotate right without carry)

11000000 0

Can you see why RL and RR nine times and RLC and RRC eight times returns the register to its initial value? These instructions take any normal single register. There are also some special instructions involving the A register only. They are RLA, RRA, RRCA and RLCA. Note the lack of a space is important – these are different to RL A, RR A, RRC A and RLC A but work very similarly. The difference in these instructions is that they only affect the carry flag. There is another flag called the parity/overflow flag which will be explained after the shift instructions.

The shift instructions work in a very similar way, either shifting bits right or left. SLA, or shift left arithmetic, will shift each bit left with the leftmost bit being moved into the carry but the rightmost bit is *always* reset to zero. In effect it will multiply by two. (Why? Well adding a zero in decimal to the end of the number makes it ten times bigger, here we’re basically doing the same thing with the carry representing 256 and hence adding a zero here makes it twice as big).

Now there are two right shifting instructions that have one subtle difference. SRL, or shift right logical, will do much the opposite of SLA, but with the leftmost bit being set to zero and the rightmost being moved into the carry. SRA, or shift right arithmetic on the other hand will leave the leftmost bit unchanged. So if it was one it remains one and if it was zero it remains zero. This is because SRA treats the number as being signed, and hence leaves bit 7 alone as it signifies the signing. All three instructions work on any single register but don’t have any special instruction for A or excluding carry.

Right, back to flags. These instructions have a special use for the parity/overflow flag (or PV). Basically if the result of the instruction leaves a register with an even number of bits the parity is even, and if it’s left with an odd number of bits the parity is odd. You’ll see in the next topic how we can use PE (parity even) and PO (parity odd).

The final group of instructions are the SET, RES and BIT instructions. These take the form bit,register and will SET, RESet or test the BIT of the register in question. For example SET 4,D will make bit 4 of D 1, RES 0,H will make bit 0 of H 0 and BIT 7,C will test bit 7 of C and set the zero flag depending on the result.

5 Program Flow, more on Flags and The Stack ^

What is program flow? Essentially it refers to the way we navigate a program’s instructions. So far you’ve only see things move linearly: that is to say when you execute one instruction you move onto the next in memory straight after it and any operands. As you’ve heard PC keeps track of where code is being fetched from the memory. For every instruction and operand fetched from memory PC is incremented by one. However why should PC always being moving forward? If we change it completely we could move to an entirely different part of memory to execute instructions. Why we would want to do this and how we do this are covered here. Firstly: why? Well we’ve been talking a lot about flags but so far only carry has been of any use. However say you want to execute one bit of code depending on whether or not A-B is zero and another if it isn’t it might look like this:

SUB B
JP Z,somewhere
JP NZ,elsewhere

JP is short for jump. It basically reads two bytes of information and sets PC to them. You could think of it as LD PC,xxxxh. JP Z is a conditional jump, and this is where flags start to make more sense. JP Z will only jump is the zero flag is set. That is to say IF Z=1 LD PC,xxxxh. JP NZ is another conditional jump but will only jump if the zero flag isn’t set. Can you see now why the above program does what it’s supposed to? Conditions form the backbone of most programs – you’ll find it next to impossible to make useful programs without them. The conditions we can use with JP are:

Z, jump if the zero flag is set.
NZ, jump if the zero flag is reset.
C, jump if the carry flag is set.
NC, jump if the carry flag is reset.
PO, jump if parity odd or there's no overflow.
PE, jump if parity even or there's an overflow.
P, jump if positive.
M, jump if negative.

Let’s look at PO, PE and P and M a bit more closely. As you already know 00 to 7F can represent positive numbers and 80 to FF can represent negative numbers. P and M work with this convention as you might expect – using the sign flag – depending on whetherr the result of an operation if positive or negative using this convention. PO and PE have two uses. As outlined with the shift and rotate instructions they stand for parity even and parity odd. However the flag is called the parity/overflow flag and it also detects something called an overflow. What is an overflow? Well as stated in chapter 3 if the result of an operation in two’s complement produces a result that’s signed incorrectly then there’s an overflow. For example 127+127 is 254, but in twos complement 254 is -2. Two positive numbers added together don’t form a negative so there’s an overflow.

Now along side JP is the CALL instruction that works in a very similar way. The CALL instruction also takes the conditions outlined above but it will first store a copy of the current PC value in a special place called the stack. Why does it do this and what is the stack?

Well the stack is a section of memory where registers can be stored temporarily. Think of it like a bit spike where you can shove bits of paper on and take bits of paper off but only ever work with the top of the spike. The Z80 uses a 16 bit register called SP, or the stack pointer, to point to the stack. Two instructions are then used to manipulate the stack. They are PUSH and POP.

PUSH does what you might expect, it PUSHes a register pair onto the stack. That’s BC,DE,HL and AF. AF? Yes, in this case AF is treated as a register pair. Only pairs may be used with the stack (which in the case of AF is one way of manipulating the F register indirectly). Once pushed onto the stack the stack pointer is decremented twice to point to the next bit of memory where the next item will be pushed on. It decrements SP as the stack works backwards, starting high and getting lower with the more items on the stack. In essence the spike is stuck to the ceiling!

POP does the opposite, and POPs a register pair off the stack and increments SP twice. Note that it doesn’t have to be the same register pair at all, which can be useful for swapping register values around in pairs.

So our CALL instruction is effectively PUSH PC. So what’s POP PC?

RET is. RET is POP PC essentially. It can also use the same conditions as CALL and JP. So using CALL and RET you can CALL a subroutine and then use RET at the end of it to get back to where you were. Trust me, this is incredibly useful. I should also note at this point that you can LD SP,HL, ADD HL,SP, ADC HL,SP, SBC HL,SP, INC SP and DEC SP. There are other instructions with SP but I’ll wait another time to detail all the instructions and their various iterations.

Finally there is the JR instruction. JR is short for jump relative as doesn’t LD PC,xxxxh, no, it’s more of a ADD +/-xxh,PC. That is to say using two’s complement PC is moved up to 127 bytes forward or 128 bytes back. The advantage of JR is that it’s one byte shorter than JP. The disadvantage is that only Z,NZ,C,NC can be used as conditions.

Now on a different note is the CP instruction, compare. It works basically by doing a subtraction without subtracting anything. So CP B is A-B but it doesn’t affect A. So what’s the use of it? Well what it will do is set the flags according to the result of A-B. So if A-B is zero the zero flag is set, if B is greater than A then carry is set and so on. CP can be used with any single register or a number.

6 Memory Manipulation ^

The final part of our introduction to Z80 deals with the memory. We’ve already been manipulating the memory using POP and PUSH and CALL and RET, but this section covers the major tools.

Firstly our old friend LD. We use brackets to signify that instead of being a number or register we actually mean the contents of the memory at that address. For example LD A,(ABCD) loads A with the memory at ABCD, not with ABCD itself (it wouldn’t fit anyway would it?). LD A,(HL) will load A with the contents at memory address HL, not with HL itself. We can also load memory with registers, LD (HL),A and LD (ABCD),A for example. We can also load a memory address with HL, a two byte number, or HL with a memory address of two bytes, a bit like using a stack at HL.

There are also a set of instructions called the block shift instructions because they are designed to move chunks of memory around and block search because they are designed to search through chunks of memory.

The block shift instructions start with LD, like load. Then then take either I or D, standing for increment and decrement, and then a R for repeat. So we have LDI, LDD, LDIR and LDDR. How do they work?

Well each instruction uses BC, DE and HL. BC is a counter, DE is the target, HL is the source. Each command works in a similar way, they’ll load the memory at DE with the memory at HL (hence target and source).

LDD will then decrement BC,DE and HL. It will then set the PV flag to zero, or parity odd, if BC is zero. It leaves the other flags unaffected though. LDDR works in much the same way but will continue working until BC is zero. LDI and LDIR are basically the same. However DE and HL are incremented instead of decremented.

Here’s an example of block shift:

LD DE,0000h
LD HL,4000h
LD BC,0100h
LDIR

Can you see how this works? Basically 100h bytes of data starting at 4000h are copied to 0000h through to 0100h. Block shift is useful in a text editor for example. You might want to delete a character and then shift everything else down in memory. Block shift will do that quickly and easily. You can think of it as a copy instruction.

Now block search is basically a CP with, I or D and R tacked onto the end. It works by comparing A with the memory at HL, then setting PV if the result is zero, leaving the other flags alone. CPI increments HL, CPD decrements HL and the both decrement BC. CPIR and CPDR repeat until BC is zero (and the zero flag is set) or PV is set. So BC is again a counter, HL is the source and A is the testing number.

So say you want to find the first occurrence of 124 in the valid memory space:

LD HL,0000h
LD BC,0000h
LD A,124
CPIR

Note if you used CPDR it would find the last occurrence of 124 in the valid memory space. Can you figure out why?