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 ISZ80: ISZ80: 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.