The Z80 processor? Never heard of that one!
The Z80 is indeed a very old processor, I think it was first released in 1977 (correct me if I'm wrong). But it is still used in various hand-held calculators, and gaming consoles, as it is quite robust, fast (a 6MHz Z80 CPU is quite a bit faster than a 6MHz Intel CPU) and furthermore it doesn't need too much power.
Where is the Z80 used?
I can only state a few examples: TI-8x calculators, Nintendo Gameboy, the Sega Master System and the Sinclair Spectrum, to name a few. I believe that is also used for various simple hardware as controller.
Is the Z80 still being produced?
The Z80 is still in production, but you can't get the chip from Zilog. You have to get it from other distributers, like this one. They are really cheap, you can get the 5 MHz version for about $2, the 20 MHz version for $5.
Do you know any good books on programming the Z80?
"Programming the Z80" by Rodnay Zaks is BY FAR the best book on assembly language programming. There also exist some other useful ones, like "Z80 assembly language subroutines" by Lance A. Leventhal and Winthrop Saville. What's more, I suggest at least getting an opcode table, so you can look up all possible combinations while programming. You might also find some information on the net.
An opcode - what's that?
The Z80 is able to perform various instructions. An instruction specifies the operation to be performed by the microprocessor. From a simplified standpoint, every instruction may be represented as an opcode followed by an optional literl or address field, comprising one or two words. The opcode specifies the operation to be carried out.
What is a register?
For faster access, the CPU itself contains a few "variables", so-called registers. All main operations, like math, etc. are performed using these, as they can be accessed more quickly than the main memory. On the Z80, these registers are called: A, B, C, D, E, H, L (and IX, IY, F, I, R, SP: see below), A = accumulator being the most important one. As the Z80 is an 8-bit processor, these registers are all 8-bit wide, this means they can hold any value from 0-255 (= 8 bit or one byte). However, to store larger numbers, you can combine a few of them: BC, DE, HL. The result is a 16 bit register, having a range from 0-65535. Note that if you modify for instance B, the upper byte from BC will be modified, too:
Given that BC is $467C. Now you execute the instruction:
ld B, $58 ;load $58 (hexadecimal) into B
|
Then B will be equal to $58, and BC will be $587C. Same's true for DE and HL.
You didn't mention the IX, IY, F, I, R and SP registers.
These are special purpose registers. They can be only used (or directly accessed) for few operations:
- The index registers IX and IY are mostly used as auxiliary HL. They are both 16 bit registers where you can't modify the upper/lower byte independently (without using special code). The reason for their name is that they can be used to index fields in a structure, like
ld IX, $8000 ;point IX to memory location at $8000
ld A, (IX+$10);load A with value of memory location
... ;at IX+$10 = $8010
|
This is of course comfortable in some cases, however, it is suggested to avoid using the index registers, as the execution is slower and one opcode needs more memory.
- The F (flag) register is perhaps the most important one of those listed here. It contains the so-called flags, which store certain conditions after executing opcodes. The flag register is basically a normal 8 bit register, but each of the eight bits represent a flag:
| S | Z | | H |P/V| N | C |
S ... sign flag (P = plus / M = minus)
Z ... zero flag (Z = zero flag set / NZ = not set)
H ... used internally
P ... parity (PE = even / PO = odd)
N ... used internally
C ... carry flag (C = carry flag set / NC = not set)
The S,Z,P and C flags can be checked using various instructions, like the conditional calls (call nc, ...) or jumps. The most important flags are the zero flag and the carry flag. The zero flag indicates whether the result of the last operation was zero (Z) or not (NZ). The carry flag indicates an overflow (i.e. if you add 20 to 240, even though the register only can handle numbers from 0-255, the carry flag will be set (C)).
- I is the interrupt register. It's an 8 bit register that contains the upperword of the interrupt table. (this is in most cases uninteresting, since the OS would handle it, like Usgard). This register is interrupt specific, you can only load it with A (the accumulator) or load A from it.
- The R register is the memory refresh register. After each instruction, it'll be incremented. It can be used as a poor random number generator. Like the I register, it can only be loaded with A or the other way round.
- The Stackpointer (SP) is a very important register, though it's changed most of time indirectly by PUSHing/POPing values to/from the stack.
How would you use S and P flags?
These two flags can only be checked using the CALL or the JP instruction. The Sign flag is used to indicate whether the result of the last operation has a positive or negative result (that is, depending on the operation, the 7th bit or the 15th bit set to 1).
The partiy/overflow flag performs two different functions. Specific instructions will set or reset this flag depening on the parity of the result, which is determined by counting the total number of ones in the result. If this number is odd, partiby bit will be set to 0, otherwise to 1. Parity is most frequently used on blocks of characters. The parity bit is an additional bit which is added to the seven-bit code representing the character, in order to verify the integrity of data which has been stored in a memory device. FOr example, if one bit in the code representing the character has been changed by accident, due to a malfunction in the memory device, then the total number of ones in the seven-bit code will have changed. This error can be detected using the parity flag.
Furthermore, the parity flag is also used as an overflow flag. It detects the fact that, during an addition or subtraction, the sign of the result is "accidentally" changed due to the overflow of the result into the sign bit.
Finally, this bit is also used for two unrelated functions: during the block transfer and search instructions, this flag is used to detect whether the counter register BC has attained the value 0.
How can you represent negative numbers?
Basically, the Z80 doesn't know real negative numbers. However, you can regard the numbers from 0-255 also as numbers from -128 to 127. Take a look at the following example:
We want to add a number to A, which will result in an overflow. We'll see what happens.
ld A, 10
add A, 254 ;overflow: carry set, A = 8
|
As you see, the result is 8, which would be practically the same as if we would do a SUB 2. Generally speaking, this means that X+(256+Y) = X-Y. Example: 14+(256+(-1)) = 14+255 = 13, which is the same as 14-1. Of course, the compiler will accept code like this:
This is of course more comfortable. As you can see above, negative 8 bit numbers reach from -128 to -1 to 127. This would be, converted with the formula above, 128 (10000000b) to 255 (11111111b) to 127 (01111111b). If you read this carefully, you should have noticed that negative numbers have bit 7 set. So bit 7 can also be referred to as "sign" bit.
The same is true for 16 bit numbers, where the 15th bit can be treated as sign bit.
And what about decimal numbers?
You can handle decimal numbers on the Z80 in two ways:
- Fixed-point decimal numbers: divide a 16 bit (or more bits) number into two sections: integer and fractional part. Typically, you use the upper 8 bits as integer part and the lower 8 bits as fractional part, resulting in an accuracy of 1/256 = 0.0039. Addition and subtraction can be performed just as normal, but after multiplication and division, you have to correct the values again. (example: 8*8 would be $0800 * $0800 = $400000, you have to perform a shift right by 8 bits to get the correct number $4000).
Fixed point numbers are typically faster, use them in critical passages where you don't need very accurate numbers.
- BCD numbers: this is an approach which can serve you with as accurate numbers as you want, however, processing time is slower and you need special routines. A BCD byte is parted in two 4 bit parts, each representing a number from 0-9. A BCD number can consist of as many BCD bytes as you want. Therefore, if you want to have a 20 digit number, you need 10 BCD bytes and an additional byte specifying the location of the decimal point. Mathematical operations are more difficult to perform, however, the Z80 serves with the DAA instructions, which will correct BCD numbers after addition, subtraction, etc.
Use BCD if you need very huge or accurate numbers, but not in time-consuming passages.
Can you explain the basic arithmetic operations in BCD?
Of course. The following functions for addition and subtraction assume that both BCD numbersare stored with their least significant digits at the lowest address. Both numbers must have the same length. Multiplication and division are pretty difficult to program in BCD, plus, they need about 520 bytes of free memory space. I might cover them later, but I don't think so.
- Addition:
Input: HL = base address of addend, DE = base address of adder, B = length of numbers
Output: Addend replaced by addend plus adder
ld A, B
or A
ret z ;test whether length = 0
Loop:
ld A, (DE) ;get byte of adder
adc A, (HL) ;add it to addend, care for carry
daa ;convert to BCD-decimal
ld (HL), A ;save number back in addend
inc HL ;next number
inc DE
djnz Loop ;continue until all bytes summed
ret
|
- Subtraction:
Works nearly the same, but instead of the ADC, a SBC is used.
Input: HL = base address of minued, DE = base address of subtrahend, B = length of numbers
Output: Minuend replaced by minuend minus subtrahend
ld A, B
or A
ret z ;test whether length = 0
ex DE, HL ;exchange subtrahend and minuend
Loop:
ld A, (DE) ;get byte of minuend
sbc A, (HL) ;subtract byte of subtrahend
daa ;convert to BCD-decimal
ld (DE), A ;save number back in minuend
inc HL ;next number
inc DE
djnz Loop ;continue until all bytes summed
ret
|
What's the purpose of the stack?
The stack is used for two different things: first, for storing temporary values(PUSHing/POPing) and to store the return address of a calling program. Here's a little example:
... ;DE contains some important number
push DE ;save DE onto the stack, as we'll
... ;need it later on
... ;use DE for other stuff meanwhile
pop DE ;now we need it, load it from stack.
...
|
Notes: only 16 bit registers can be PUSHed/POPed: AF (A and flags), BC, DE, HL, IX, IY. Also, note that PUSHing isn't equal to saving. Here's a more precise example:
...
push DE ;write DE to memory pointed to by SP-2,
... ;decrement stack pointer by 2 bytes
...
push HL ;write HL to memory pointed to by SP-2,
... ;decrement SP by 2
;NOTE: if you would now do a pop DE, DE would actually contain
;the value of HL!
pop BC ;load BC with memory pointed to by SP,
... ;this is, in fact, the previous HL.
... ;After that, increment BC by 2
...
pop DE ;load DE with memory pointed to by SP,
... ;which is the same as the DE above.
... ;Increment SP by 2 after that.
|
This could be looked at like this:
DE -> (SP-2), SP = SP-2
HL -> (SP-2), SP = SP-2
BC <- (SP), SP = SP+2
DE <- (SP), SP = SP+2
|
How can I use the OR, AND & XOR instructions?
These instructions are used for bit manipulation. Before reading the following example, look at the following tables. The first number (in the tables, referred to as X) is on the Z80 always the A register, the second number (table: Y) can be any register or number. The tables only show what happens with each bit.
AND | OR | XOR |
X | Y | X and Y | X | Y | X or Y | X | Y | X xor Y |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 1 |
0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
;Examples for AND, OR, XOR.
;Using binary numbers to demonstrate effect.
ld A, 10010101b
and 00001111b ;after that, A = 00000101b
ld B, 11000000b
or B ;A = A or B = 11000101b
xor 11111111b ;A = A xor 11111111b = 00111010b
|
What about the shift instructions? (SLA, SRL, RRC, etc.)
Shifting is the process of moving the bits inside a byte to the left or the right. Shifting left (SLA) will move bit 0 to bit 1, bit 1 to 2, ... and bit 7 to carry. Bit 0 will be set to 0. When shifting right, there are two modes: arithmetical (SRA) and logical (SRL, being the commonly used one). Logical shifting right is the opposite of shifting left, i.e. bit 7 -> bit 6, etc. bit 0 -> carry, bit 7 will be set 0. Arithmetical shifting nearly works like this, but bit 7 is preserved. This is useful when working with negative numbers(see above), as the sign is kept.
Often, shifting is used as a faster alternative to multiplication by 2 or division by 2.
Rotation is practically the same as shifting, but the last bit (i.e. 7 or 0) won't get "lost". The Z80 differs between 9-bit rotation (RR - instructions) and 8-bit rotation (RRC - instructions). While 9-bit rotation left will move the 7th bit into the carry AND the carry into bit 0 (i.e. carry is the 9th bit), 8-bit rotation will directly move the 7th bit into the 0th bit and set the carry to this value. Rotation right works the other way round, this means that 0th bit is stored in 7th bit / carry.
;Examples for shift instructions
ld A, 00001000b
sla A ;now, A = 000100000b, Carry = 0
ld B, 10000000b
sla B ;B = 00000000b, Carry = 1
ld C, 00110010b
srl C ;C = 00011001b, Carry = 0
rrc C ;C = 10001100b, Carry = 1
|
How can I have a list of numbers?
This is a pretty simple task. First, you have to reserve as many bytes as you need. For instance, if you need 10 numbers, each 1 byte, you reserve 10 bytes. Let's pretend that you got a label called LIST, which contains 10 1-byte numbers.
LIST:
.db 10,9,8,7,6,5,4,3,2,1
|
Then, you need some function to access a certain number from this list. The following function, GetElement, needs two inputs: HL is a pointer to the list, and A is the index of the number you want to retrieve. Upon return, A will contain the correct number.
GetElement:
ld E, A ;load DE with the index
ld D, 0
add HL, DE ;add index to list pointer
ld A, (HL) ;get number out of list into A
ret
|
Pretty simple, isn't it? Now, you could add code to preserve that A is too big, but this isn't too important usually. If you wanted 2 byte numbers, you'd call the ADD HL, DE two times, and load the number into a 16 bit register pair.
And what about matrices?
Matrices can be treated nearly equally to lists. However, it is suggested to use formats where the width is 2^n (2, 4, 8, 16, 32, ...). This makes it more easy to write the matrix-GetElement code.
In memory, the matrix is stored like a list of wdt*hgt elements: there are hgt lines, each having wdt elements. You can see below how a matrix can be stored.
Basically, you can access an element from a matrix using a formula like this A = DE+(C*wdt+B), C being row, B column to read, and DE pointer to the matrix.
The code is for matrices with a width of 16, it can be easily modified though.
GetElement:
ld L, C ;get row
ld H, 0
add HL, HL ;*2
add HL, HL ;*4
add HL, HL ;*8
add HL, HL ;*16 -> HL = C * 16
add HL, DE ;add to matrix pointer
ld E, B ;get column
ld D, 0
add HL, DE
ld A, (HL) ;get element
ret
;Matrix stored like this:
.db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 ;row 1
.db 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 ;row 2
.db ...
|
If you want to add a new question mail to Andreas Ess.
|
|
|