home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Oakland CPM Archive
/
oakcpm.iso
/
cpm
/
asmutl
/
meyertut.ark
/
MEYER09.TXT
< prev
next >
Wrap
Text File
|
1987-12-04
|
19KB
|
440 lines
CP/M Assembly Language
Part IX: The Z80 Instruction Set
by Eric Meyer
Up till now, we have been sticking to the basic Intel 8080
instruction set, for the excellent reason that all CP/M systems
come with 8080 assemblers and can run 8080 code.
Now, the fact is that the 8080 is 10 years old, and most
CP/M computers today use more advanced CPUs that support the 8080
codes as a subset.
The latest arrivals on this scene are the HD64180, a very
fast 8-bit CPU, and the V20, actually a 16-bit CPU that can
emulate both an 8088 and an 8-bit 8080.
But of course the most common chip for some years now has
been the Zilog Z80 .
CP/M was designed and written for the 8080, but now runs
primarily on Z80s.
The Z80 has a number of more powerful features -- but most
CP/M systems give you no way to take advantage of them, at least
not directly.
Worse, whoever wrote the CP/M BIOS for your computer may
have made it positively difficult for you! For example, the
original Osborne Exec BIOS code used, and didn't preserve, some
of the extra Z80 registers on interrupts. Thus some software
(like TURBO Pascal) that uses the Z80 fully will crash unless you
fix the BIOS. (There's a program called TPATCH.COM in the FOG
library for this -- FOG-CPM.009.)
Despite all these obstacles, it's worthwhile to learn about
the advantages of the Z80.
First, then, we'll jump from the Intel to the Zilog world,
and look at a sampling of the Z80 CPU instruction set; finally
we'll return for a look at some clever tricks to allow you to
slip Z80 opcodes past an 8080 assembler.
1. Zilog Mnemonics
"Mnemonics" refers to the conventional names (like "RET")
given to the various CPU instructions. You will have to use those
that your assembler is designed to recognize.
DRI's 8080 assemblers (ASM, MAC) use Intel mnemonics. Most
Z80 assemblers (like SLR's Z80ASM) use Zilog mnemonics. A few,
like Microsoft's M80, allow you to use either or both.
It is unfortunate that Zilog decided to invent their own set
of assembler mnemonics, but in many ways they are more logical
than Intel's.
For example, all "load" instructions (loading some value
into some register) have the mnemonic "LD"; an indirect value
(address) is indicated by parentheses.
Single registers are denoted by one letter ("H"), pairs by
two ("HL").
So we have:
INTEL ZILOG INTEL ZILOG
mvi a,## ld a,## lda #### ld a,(####)
mov a,d ld a,d sta #### ld (####),a
lxi h,#### ld hl,#### ldax b ld a,(bc)
lhld #### ld hl,(####) stax b ld (bc),a
shld #### ld (####),hl mov a,m ld a,(hl)
mov m,a ld (hl),a
(Remember, LD HL,0080h puts the VALUE 0080 into HL, while LD
HL,(0080h) puts the value AT ADDRESS 0080 into HL.)
Unfortunately they don't stop there, but go on to change the
names of just about everything else too:
INTEL ZILOG INTEL ZILOG
cpi ## cp ## push psw push af
cmp d cp d pop b pop bc
adi ## add a,## cc #### call c,####
add d add a,d cnz #### call nz,####
aci ## adc a,## jmp #### jp ####
adc d adc a,d rnc ret nc
inr d inc d stc scf
inx h inc hl cmc ccf
dcr e dec e xchg ex de,hl
dcx b dec bc xthl ex (sp),hl
add hl,bc pchl jp (hl)
and so on . . .
Actually it's pretty straightforward, and not too difficult
to get the hang of it.
If you're going to do serious Z80 work you should get hold
of a reference book.
I have the Z80 Instruction Handbook (by N. Wadsworth,
published by Hayden).
It describes all the instructions, lists them
alphabetically, and gives the actual hex codes (which you need to
include Z80 codes using an 8080 assembler -- more on this later).
2. The Z80 CPU
Zilog designed the Z80 to be an extension of the 8080.
Thus it contains all the 8080 registers as a subset, and
handles all the 8080 instructions (though they go by different
names).
In addition, it has an extra set of registers, and a number
of more powerful instructions.
First the extra registers: they are a complete set of
duplicate or "alternate" registers A'-L', plus a pair of "index"
registers IX, IY.
8080 SUBSET EXTRA Z80 REGISTERS
+--------+---------+ +------------------+
| A | F | | AF' |
+--------+---------+ +------------------+
| B | C | | BC' |
+--------+---------+ +------------------+
| D | E | | DE' |
+--------+---------+ +------------------+
| H | L | | HL' |
+--------+---------+ +------------------+
+------------------+ +------------------+
| SP | | IX |
+------------------+ +------------------+
| PC | | IY |
+------------------+ +------------------+
The "alternate" registers can be accessed by means of two
new instructions:
EX AF,AF' - exchange AF with AF'
EXX - exchange BC,DE,HL with BC',DE',HL'
Thus you have two separate "banks", each like an entire 8080
register set.
Unfortunately, the advantage of these extra registers is not
as great as it might be, because there are NO instructions to
access them other than these two! (There are no commands LD
HL',HL, etc.)
The only way to get a value from one bank to the other would
be to PUSH it onto the stack, exchange banks, and POP it back --
very slow and awkward.
You need to have two quite separate things going on at once
to find the alternate registers useful. Most programmers don't
use them at all. (This is why Osborne felt free to commandeer
them to preserve the other registers during interrupts in the
original Exec BIOS.)
The "index" registers, which are much more useful, are an
extra set of registers that can be used to point to things. They
work rather like the HL pair, but more powerfully.
Thus you can refer to the byte AT the address in IX just as
you can the byte AT the address in HL (the 8080 "M register"):
LD A,(HL) CP (HL) SUB (HL) . . .
LD A,(IX) CP (IX) SUB (IX) . . .
But with the index register, you can also use an OFFSET:.
(Note: some assemblers may require specifying the offset even if
it's zero, e.g., LD A,(IX+0) above.)
LD A,(IX+4) CP (IX+4) SUB (IX+4) . . .
This automatically refers to things that are a certain
number of bytes past the address in IX, without having to keep
altering the index register itself, as you would have to with HL:
to add together the bytes (LIST), (LIST+4), and (LIST+9),
LD IX,LIST is equivalent to LXI H,LIST
LD A,(IX) MOV A,M
ADD A,(IX+4) LXI D,4
ADD A,(IX+9) DAD D
ADD M
DAD D
LXI D,5
ADD M
Even without this extra power, the index registers would
be a welcome addition just as an extra set of pointers, once you
realize how quickly BC, DE, and HL can be exhausted.
3. More Power
Not only does the Z80 have more registers, it can do more
with the ones it has. Out of all possible bytes 00 . . FF, there
were a handful not defined in the 8080 instruction set, and Zilog
has used them to implement new features on the Z80.
For example, the Z80 extends to BC and DE some of the
features that only work with HL on the 8080: you can do:
LD BC,(####) LD DE,(####)
LD (####),BC LD (####),DE
without having to get the value into HL first.
On the Z80 you can do 16-bit subtraction, as well as
addition, more easily, because the ADC "add with carry" and SBC
"subtract with carry" instructions work with the HL register
pair. (Recall that you can subtract constants even on the 8080;
if you want to subtract 10 from HL, rather than use DCX H ten
times, you can write:
LXI D,-10
DAD Db
Your assembler will translate -10 into FFF6h, and adding
that will have the effect of subtracting 10. But the Carry flag
is not affected, and there is no direct way to subtract the
contents of DE from HL, in general.)
On the Z80 you can do not only ADD HL,DE but also
ADC HL,DE and SBC HL,DE.
Either of these is much nicer than the six separate ones
required to do the same task on the 8080. (Remember what these
were?)
There is a new set of bit operations that are handy for all
sorts of purposes. Recall that the 8 bits in a byte are commonly
numbered from 0 (least) to 7 (most significant).
You now have:
BIT #,reg SET #,reg RES #,reg
where # is 0-7, and "reg" is any 8-bit register, including A-L,
or an indirect (HL) or (IX).
BIT tests the specified bit, setting the Zero flag if it's
0. SET sets the bit to 1; RES clears it to 0. Note that some of
this could be done in 8080:
SET 3,A does the same as ORI 8 but these are easier to
understand, and they work with many registers other than the
Accumulator.
There are some nice new "shift" operations: e.g., SRL A
does the same as RLA (Intel RAL) except that a 0, rather than the
Carry flag, is rotated into the empty bit, making division by 2
far easier.
And all the rotate and shift instructions work on every
register, not just A.
There are even Z80 instructions that are whole programming
loops in themselves!
The easiest way to explain how "LDIR" works is to show you a
loop that does the same thing in 8080:
LOOP: MOV A,M
STAX D
INX H
LDIR INX D
DCX B
MOV A,B
ORA C
JNZ LOOP
This is our old "move a string of bytes from one place to
another" routine, all in one instruction!
Bytes are moved from the HL address to the DE address, one
at a time, each time incrementing the pointers, for a total count
in the BC register. There's also an instruction "LDDR", just the
same except that it decrements HL and DE each time instead of
incrementing them.
It's worth pointing out that these instructions are not just
a convenience for the programmer; they are also much faster.
Each instruction in a program has to be fetched into the CPU
from memory (at the PC pointer) before it is executed, a process
which takes several clock cycles.
There are 10 instruction bytes in the LOOP: above (the JNZ
takes three bytes), and they all have to be fetched again each
time through the loop.
If you are moving 1000 bytes, that's 10,000 code bytes
fetched, compared to 2 for LDIR.
Another loop instruction is "CPIR", an amazingly efficient
way to find a byte in a string. Here again is an 8080 routine
that does approximately the same thing:
PUSH PSW
LOOP: POP PSW
CMP M
INX H
DCX B
CPIR JZ EXIT
PUSH PSW
MOV A,B
ORA C
JNZ LOOP
POP PSW
EXIT:
This goes along a string, starting at the HL address, until
it finds a match with the byte in A, or runs out of the count in
BC. There is also an instruction "CPDR" that works downward, each
time decrementing the address in HL.
Note that all the loop instructions move the pointer even on
the last cycle; so they end up pointing one PAST the byte found,
or the last one moved. So if you want to point to the first
occurrence of BYTE in STRING, you would do something like
LD HL,STRING
LD BC,LENGTH
LD A,BYTE
CPIR
DEC HL
After a "CPIR" the Z flag will be set (just like a regular
"CP") if a match was actually found.
There are "relative jump" instructions on the Z80, denoted
"JR" instead of "JP".
With a Z80 assembler you can write either
JP LABEL JP Z,LABEL or
JR LABEL JR Z,LABEL
The difference is that if "LABEL" is nearby enough (within
128 bytes) a relative jump takes only two bytes, because instead
of storing a two-byte address it stores a one-byte offset.
Also, code consisting of relative jumps can be moved bodily
to any location in memory and will work just the same.
A particularly nice relative jump is "DJNZ", as it combines
the common operations of Decrementing a counter (in this case the
B register) and Jumping while NonZero:
DJNZ LABEL is equivalent to DEC B
JR NZ,LABEL
All that in two bytes! (Note that it uses B alone, not BC.)
There are still more extended Z80 instructions, but these
are about the most useful.
4. Faking Out ASM
All right, so you want the speed, power, and convenience of
Z80 instructions.
The only question is, how? Well, you could buy a Z80
assembler (like Z80ASM or M80), or experiment with one of several
in the public domain (one of these is also called Z80ASM [FOG-
CPM.111], but don't confuse it with SLR's product).
But for now, you don't have to buy anything, or even rewrite
all your existing code in Zilog mnemonics, because you can patch
Z80 opcodes right into your 8080 programs.
Remember it's just a matter of getting the right hex codes
into the COM file, right? If your assembler won't do that
automatically, you can do it by hand.
You will need a reference to tell you the hex equivalents of
the Z80 instructions you want, for example:
LDIR ED B0 SBC HL,BC ED 42
LDDR ED B8 SBC HL,DE ED 52
CPIR ED B1 LD BC,(xxyy) ED 4B yy xx
CPDR ED B9 LD (xxyy),BC ED 43 yy xx
EX AF,AF' 08 LD DE,(xxyy) ED 5B yy xx
EXX D9 LD (xxyy),DE ED 53 yy xx
So all you have to do to get an "LDIR" in the middle of your
program is type in:
DB 0EDH,0B0H
Useful, but ugly. You could also define it as an equate:
LDIR EQU 0B0EDH
so that you could have a nice, readable line like:
DW LDIR
instead. (Note that we had to reverse the byte order in the EQU
statement because two-byte values [DW] get stored low byte first.
Also, when you specify a hex value that starts with a digit
A-F, you must begin with a zero!
If you just typed in " B0EDH", ASM would think that was a
label name, and complain that IT never got defined.)
If you have a macro assembler, like MAC, you can do this
even more suavely and define LDIR as a macro:
LDIR MACRO
DB 0EDH,0B0H
ENDM
so that now you can be just like the Z80 folks and type in:
LDIR
In fact, the macro approach can easily be extended to
include instructions that require ARGUMENTS.
For example, the BIT opcode is CB xx, where "xx" is 40 plus
an amount that varies with the bit and the register. You will
find that MAC, at least, assigns numerical values to register
names as follows:
B=0, C=1, D=2, E=3, H=4, L=5, M=6, A=7.
So you could define a BIT macro as follows:
BIT MACRO B,R
DB 0CBH,40H+(8*B)+R
ENDM
Accordingly, BIT 0,A will produce CB 47; BIT 3,M gives CB
5E; and so on, much more easily than defining each of the
(roughly 80!) possible BIT instructions separately.
Some MAC users received a file Z80.LIB along with MAC and
HEXCOM. This is a library of macros just like BIT above, that
allow you to use most extended Z80 instructions conveniently.
If you have it, browse through it. If you don't, you can
create one yourself, following this example. (Beware of conflicts
between your macro names and existing 8080 opcodes, or MAC
reserved keywords. You'll have to call "SET" something like
"SETB" instead, and so on.)
5. Caveats
There is one price to pay for using Z80 instructions: if you
do, your code will run on most, but not all, CP/M systems.
It will run on a Z80 or compatible chip (like the HD64180,
itself an extension of the Z80), but will not work -- in deed,
will almost always crash -- on a computer with an 8080 or 8085
CPU, or an IBM clone with V20 8080 emulation. (Zenith, for one,
built a lot of 8085 machines that are still out there.)
You might want to build in a test for the presence of a Z80,
and abort with an error message if you don't have one, before
something less civil happens.
The best way to do this is to take advantage of a tiny
difference in the Z80's use of the Parity flag. We haven't met
this flag before, as its purpose is rather arcane: after an
arithmetic or logical operation in the Accumulator, the 8080 P
flag gets set or cleared according to whether the Parity of the
result (the number of 1 bits) is Even or Odd.
As it happens, for arithmetic the Z80 uses the P flag (which
it calls the Parity/Value flag) to indicate signed overflow
instead, with the result that the operation "SUB A" (which zeros
A) will set the P flag (for Even parity) on an 8080/85, but clear
it (for no overflow) on a Z80!
So the following code can determine which CPU is running:
(Intel) SUB A (Zilog) SUB A
JPO ISZ80 JP PO,ISZ80
<abort with error> <abort with error>
ISZ80: <ok, continue> ISZ80: <ok, continue>
In fact, this incompatibility is itself another reason why
the P flag doesn't get used as often as Z or C.
6. Conclusion
If you start looking at the source code to many public
domain programs, you will often find puzzling little things like
DW 0B0EDH scattered around. Now you know what they are, and why
they (usually) work.
In the future, we'll be working in some mixture of 8080 and
Z80-speak. This can be slightly confusing, but it's hard to
avoid; although CP/M systems don't come with Z80 support
software, most programmers do use Z80 instructions. With a little
effort, though, you can keep things straight.