Z80 Assembly - The Basics

Assembly in general

A program is a series of instructions given to a computer to carry out a given task. There are lots of languages existing in which you can define such a task, and certain programs – compilers – will translate your instructions into the native language of the computer: machine code. Machine code consists of series of numbers that the computer can directly understand without further interpretation. Assembly is a simple language that is very close to machine code, but short, easy-to-memorise words are used in it instead of the numbers. Besides this, assembly offers some features machine code does not, e. g. labels (explained later). Since there is almost a one-to-one correspondence between the elements of assembly and the internal instructions of the computer, you can completely exploit the capabilities of the hardware. This is actually what makes assembly hard to learn: you have to think like the computer instead of normal human thinking.

Compiling programs

As assembly is a text-based language, the computer cannot understand it directly. Compilation is a process that converts the text source into a binary program that can be executed. I don’t intend to elaborate on this process too much, because this guide is about Z80 in general, while compilation depends on what kind of calculator you have (I might be using the word “calculator” frequently, but the guide is really useful for everything driven by a Z80). You will most probably find everything you need in the Programming section of ticalc.org. To facilitate the search, here is the list of things you will need:

  1. Graph-link cable - assembly programs are written and compiled first on a PC, then they have to be sent to the calculator. It is possible in some cases to get over without a PC, but I don’t recommend these solutions, because they are rather cumbersome.
  2. Assembler and linker - the programs that convert the source into a binary that can be sent to the calculator without further modification. Along with these programs you are going to find more information about the syntax of the assembly sources (which might depend on which assembler you choose).
  3. Emulator - a program that simulates a virtual calculator on your PC. This will enable you to test your programs before physically sending them to your calculator. I strongly advise you to do so, because it’s better to crash the emulator than the real calc. The former can be reset without any problem, while in the case of a calculator crash, you have to take out all batteries and wait for a day until you can use it again... I recommend Rusty Wagner’s Virtual TI.
  4. Calculator specific documentation - instructions that let you use the built-in functions of your calculator saving you from having to do everything on your own.

Data and numbers

The memory of the computer contains 0’s and 1’s. Each of these digits is called bit (from binary digit). However, the computer cannot access these bits directly, only in groups of eight. These groups are called bytes. The bits of a byte are numbered from 7 to 0, where the number is the exponent of 2 that the digit belongs to. As one byte consists of eight bits, it can be easily calculated that it can contain 28=256 different values. The computer interprets these values as the integers from 0 to 255. For example, %01110101=117, since:

0*27=0
1*26=64
1*25=32
1*24=16
0*23=0
1*22=4
0*21=0
1*20=1

Total:117

(% stands for the fact that the number is represented in base 2). As you can see, the calculation is the same as in base 10, but instead of the powers of 10, each digit is worth a power of 2. This is the smallest unit of data that the computer can directly handle. The next one is called word, which is simply two bytes put together, i. e. a unit of 16 bits. Therefore, a word can take values between 0 and 216-1=65535. On other CPU’s there are other units like 32-bit doublewords, but the Z80 can only cope with bytes and words, so it is enough to know these two. The larger data blocks of kilobytes (1024 bytes), megabytes (1024 kilobytes) and so are impossible to directly interpret for the computer (in the very end of the process, of course), these units are only used by humans in reference to the size of the blocks.

You should be aware of the hexadecimal (base 16) representation of numbers as well, because it is very useful and widely used. Hexadecimal numbers start with a $, and as the value of each digit can range from 0 to 15, the ones above 9 are represented with the first 6 letters of the alphabet: A stands for 10, B for 11 etc. Note that hexadecimal and binary numbers can be easily converted into each other, as one hexadecimal digit is equivalent to four binary digits. Using the example of 117 from above: %01110101=$75=7*16+5=112+5=117.

There is a vital concept to mention here: endianness. There are two ways to represent large numbers in the memory, hence two processor families: little endian (e. g. Intel) and bigendian (e. g. Motorola). If a number needs more bytes to represent, you have to decide whether the LSB (least significant byte) or the MSB (most significant byte) should be written to the lowest address. The Z80 is little endian, i. e. it stores the LSB first. Therefore, if you have a 16-bit number, the lower byte is at the lower address. If you ever have to manipulate numbers on byte level—which is very likely if you dive deep into assembly—you should always keep to this rule.

Registers

In most of the cases (which means “always” in the case of the Z80) memory is accessed through the CPU. As the CPU processes and transfers data, it must have its own memory for temporary data storage. This memory area is referred to as the registers of the CPU, and it can be accessed very fast compared to any other kind of memory in the computer. The Z80 has altogether 26 bytes of this kind of memory, whose bytes (sometimes words) all have their own names. It is actually senseless to think about them as a whole. These registers are the following (with their number of bits in parentheses):

A (8)F (8)A’ (8)F’ (8)
B (8)C (8)B’ (8)C’ (8)
D (8)E (8)D’ (8)E’ (8)
H (8)L (8)H’ (8)L’ (8)
IX (16)IY (16)
SP (16)I (8)R (8)
PC (16) 

Each group has its special property:

A, B, C, D, E, H, and L can be directly accessed by the instructions of the CPU, and the pairs BC, DE and HL can be treated together as if they were 16-bit registers. In this case B, D and H are the more significant bytes (holding the greater powers) of these words, respectively. A and F are actually independent of each other, but they are sometimes paired.

IX and IY are 16 bits long, and they are very similar to HL in functionality. These three are often used to address the memory, i. e. to determine which byte of the memory we are referring to. IX and IY are only divisible into 8-bit parts using undocumented instructions. These instructions are actually officially defined in later versions of the CPU, and they work on every Z80, even on the older ones. The term “undocumented” is rather traditional. IX is divided into IXH and IXL (or HX and LX in another notation), while the two halves of IY can be referred to as IYH and IYL (or HY and LY).

SP is the stack pointer. The stack is a place where the values of 16-bit register pairs can be saved for later retrieval. It will be explained later.

I stands for the Interrupt Vector; its usage will be explained later.

R is the Refresh register, whose value is increased by the length of every instruction, when they are executed. Only seven of its bits are updated, its MSB (most significant bit) is always zero. This register has normally no practical use for the programmer.

PC is the abbreviation for Program Counter, and it always contains the address of the currently executed instruction. It cannot be accessed directly by any of the instructions.

F is the Flags register, its bits give information about the results of the operation carried out by the preceding instruction(s). It cannot be accessed directly either. The information appears in the individual bits of the register:

The registers denoted by apostrophes are the shadow registers. They always come in pairs. There are special instructions that exchange the values of the shadow registers with their corresponding pairs, and neither of the other instructions can access them. They are normally used for saving data, but in a different manner than that of the stack.

All the registers have some kind of special role. These roles are connected to certain instructions, and I will explain them step by step in the following sections. Besides these special roles, most of the registers can be used for general purpose data storage. The exceptions are F, PC, R and I. SP is also possible to use for storing data temporarily, but I would certainly discourage you from doing this unless you are an advanced programmer.

Variables in assembly

The variable is probably the most important concept of programming. A variable is something that holds data for further reference. It is always located somewhere in the memory. You must understand something very important: from the point of view of the computer, the variable is just a nameless piece of memory, which can be referred to by its address. A memory address is a 16-bit number that tells the computer exactly at “how manieth” byte of the memory the variable is located. Since it would be cumbersome to memorise the exact position of your variables, you can substitute the numbers by names. This is actually a chief difference between machine code and assembly: in the latter, memory addresses can be given names, and these names are called labels. From then on, this label is used to access the contents of the variable. Here is an example of defining a label:

VarName:                         ; the name of the label followed by a colon
  .byte 29                       ; the initial value of the variable

This piece of code is already assembler specific. Most assemblers will require you to start label names at the very beginning of a line, while the instructions must be tabbed in. I will also keep to this rule from now on; the syntax used is that of Telemark Assembler (TASM). When the assembler sees VarName, it associates this name with the address where it is currently during compilation. The directive .byte tells the assembler to put a byte of 29 at the current address, and advance the address by one. (Note: it might be something else than .byte with your assembler. E. g. .db is also quite common.) This way we have defined a label and allocated one byte of memory to it, which will initially contain 29 when you run the program.

Registers can also be used for temporary data storage, but as their number is very limited, you end up putting most of the data into the memory. There are two special properties of the registers: they can be accessed really fast compared to normal memory, and they have to be used in basically every calculation – there is only a small number of operations where registers are not used. A simple example:

  ld a,5                         ; load 5 into A
  ld b,7                         ; load 7 into B
  add a,b                        ; add B to A, store result to A
  ld (Result),a                  ; store the value of A into the variable Result

Here you could see just two important things. First of all, there is apparently no reason why the result of the addition is stored into A. This is because addition is not an operator, but an individual instruction called add. In the Z80, this instruction is defined in a way that it automatically stores its result into the first argument, which can be only either A (8 bits) or HL/IX/IY (16 bits). The other thing is that the references to memory variables are between parentheses. Do not forget that any variable names that you define will be simply substituted with numbers during compilation. Parentheses always indicate memory accesses, but they do not necessarily contain well defined variable names. The references can be registers as well. For instance, you could change the last instruction of the previous example to the following:

  ld hl,Result                   ; load the address of the Result variable in HL
  ld (hl),a                      ; store the value of A into the byte pointed by HL

What happens in this case? Result, i. e. the address of the variable we call “Result”, a constant 16-bit value is loaded into HL, and A is stored at the address pointed by this value. Using registers to address memory is inevitable in many cases, e. g. when working with arrays.

Let’s put this together to an example that actually makes sense. We have two 8-bit variables called Width and Height, which contain the width and the height of a rectangle. We want to calculate the perimeter of the rectangle, and store it into a third 8-bit variable called Perim. For now, we assume that all these three values remain between 0 and 255. Here is the code:

  ld a,(Height)                  ; loading Height into A
  ld b,a                         ; loading this value into B from A (it cannot be done
                                 ; directly from the variable; see the possibilities below)
  ld a,(Width)                   ; loading Width into A
  add a,a                        ; adding A to itself, making it multiplied by two
  add a,b                        ; adding the height once
  add a,b                        ; adding the height again to complete the sum
  ld (Perim),a                   ; storing the result into Perim

Hopefully the example needs no more explanation. Perim will take the value Width + Width + Height + Height, following the order of the code. This could have been done of course more efficiently as well, but I did not want to introduce new instructions this time. However, here is a good opportunity to show one source of error. Consider the code above, and look at this one:

  ld a,(Height)                  ; loading Height into A
  ld b,a                         ; loading this value into B from A (it cannot be done
                                 ; directly from the variable; see the possibilities below)
  ld a,(Width)                   ; loading Width into A
  add a,b                        ; adding the height once
  add a,b                        ; adding the height again to complete the sum
  add a,a                        ; adding A to itself, making it multiplied by two
  ld (Perim),a                   ; storing the result into Perim

The result won’t be the one we expect, if we expect it to return the correct perimeter... At first glance one could think that we only mixed the order of the additions, so it should return the same result. However, as I mentioned above, addition is not an operator but a complete instruction that writes back its result into the first operand. The final value of A will be therefore (Width+Height+Height)*2, i. e. Height*2 more than the result we wanted to see. The reason for this is that when you add A to the sum, it is not the original Width but already the sum calculated so far. Be prepared that such simple rearrangements can so easily mess up things. In this case, it was quite obvious, but it might turn out to be much harder to find such errors when writing a real program.

The LD instruction in detail

This instruction is the most fundamental one. It is the equivalent of the “->” of TI-Basic, the = of C or the := of Pascal. It is used to load values into registers or memory areas, and it has both 8-bit and 16-bit versions. What it exactly does is simply copying the contents of the second operand into the first operand. The two operands must fit in size; it’s impossible to load an 8-bit value into a 16-bit register, even if it would sound logical. There is no automatic type conversion in assembly, except for constants. One important thing to note: not all the combinations are possible. I have included a table summary of these combinations in the instruction reference appendix.

8-bit combinations

16-bit combinations

If you ever retrieve or write words to the memory using 16-bit instructions, they will handled in the little endian way, as I mentioned above. There might be times when this can be inconvenient, but there’s no way to change it. It is still important to keep in mind, since normally 16-bit numbers are also accessed using 8-bit instructions as well on other occasions. Also, if you need to use larger numbers (e. g. 32-bit doublewords), you should store them in a similar way, because the little endian representation fits the Z80 instruction set better.

Stack operations

Understanding the concept of the stack is very important, because while being a basic element of assembly language, it does not exist in high-level languages. I wrote above that the stack is a place for backing up the values of 16-bit registers for later retrieval. This is close to the truth, but it’s a bit confusing. More exactly, the stack is a part of the memory pointed by SP. There are two things you can do: push or pop values. push means putting a value to the stack and pop means retrieving a value, to be short. Both instructions have one operand, which can be AF, BC, DE, HL, IX or IY. The exact operation looks like this:

If you use push hl, first the value of SP is decremented by two, then virtually an ld (sp),hl happens. I say virtually, because such an ld combination cannot be used normally.

The case of pop hl is just the inverse: first ld hl,(sp) occurs, then the value of SP is incremented by 2.

Let’s see a neat example:

  push af                        ; decrement SP by 2; store the value of AF into (SP)
  push bc                        ; decrement SP by 2; store the value of BC into (SP)
  pop de                         ; store the value of (SP) into DE; increment SP by 2
  pop hl                         ; store the value of (SP) into HL; increment SP by 2

Just think it over. First we put two words on the stack, then we retrieve them, naturally in the reversed order. This way DE will take the value of BC and HL will be equal to AF. These four operations are equivalent to ld de,bc and ld hl,af (both impossible). The first could be actually done with two loads: ld d,b and ld e,c. However, ld l,f does not exist. Using push and pop we could also overcome the problem that the flags register cannot be used in ld instructions, because we could move it into L through the stack.

As you can see, the stack does not know whose value it contains. All the registers are saved to the same area, namely at the location pointed by SP. I wanted to make this clear because there are many people who misunderstand its operation. The stack is the very same memory as the one where your program and variables reside, there is nothing “magical” about it.

Back to the index