Assembly Course for Beginners, Part 1
What actually is assembly? An assembler is a translation program that translates mnemonic codes such as LD I,A or LDIR into machine-level code—so-called machine code. You can also do it by hand or program directly in MC, but that is very time-consuming, and almost impossible without the tables included in the last issues of SCENE+; that's why assemblers were written.
The mnemonic codes serve to help visualize, behind the bare numbers of a processor, what the CPU is supposed to do with them—or does everyone understand what 201 means? (Well alright, that one is easy!) A human, on the other hand, understands much more under RET: RET means return from a routine back to the program. What the CPU does when it encounters 201 is a bit more complicated, however, because it takes a value from the address pointed to by the SP register and jumps to that address defined by the value.
These processes and the fact that the Z80 has no hardwired instructions lead to quite a few clock cycles elapsing before the instruction is executed (unlike modern RISC-like CPUs). Some instructions require far more clock cycles than others, and for this reason democoders don't always use single instructions, because they may take longer to execute than some two-byte instructions. As an example, I can mention the following: LD HL,Number would take 10 T-states, being slower than LD H,n1 : LD L,n2 at only 8 T-states.
With modern production methods that were already used, for instance, in the 486, a Z80 would be feasible that could handle all instructions within one to two cycles. The DMA used in MB02 also handles its memory-moving instructions within 2 clock cycles per memory location because it was specially made for that (and a few other things). The Z80 itself, by contrast, needs a whopping 21 cycles per memory location with LDIR, because on every iteration it decodes the LDIR instruction and checks whether BC has already reached zero; if not, it increments DE, increments HL, subtracts one from BC, and repeats the whole thing.
For everyone who doesn't know it yet: LDIR means LoaD, Increment, Repeat (Redo). BC is always used as a loop counter (Byte Counter) and counted down accordingly. HL and DE are always incremented by 1. LDI (not to be confused with LD I) does the exact same thing, but without repeating the instruction as long as BC is greater than zero.
Ever noticed? Copying a screen's contents with LDIR consumes 21 times 6912 cycles! It follows: with this instruction, it is impossible to copy a screen's content within 1/50 of a second!
Nevertheless, MC is the fastest language and far superior even to compiled BASIC. So anyone who wants to write games, demos, or certain application programs should take a closer look at assembly. Even if only to write very small, time-critical routines. You can exchange variables between MC and BASIC and vice versa quite easily with the help of POKEs, or if you start an MC program with LET x=USR adr, the variable x will take the value of the BC register when the MC program terminates.
When returning to BASIC, you should make sure that the stack has not grown, that the program terminates with RET, and that interrupts are enabled again with EI (Enable Interrupts).
I will start with something very simple: loading a register.
What are registers?
You could describe them as variables, though only for whole numbers; on the other hand, registers are also intended for system control. Then there are the FLAGS, one-bit registers such as CARRY, ZERO, etc. All these flags are stored in the F register and can only take the value 0 or 1 (0 = not set, 1 = set). For this reason, you can never address the F register directly, except through the AF register pair. F, by the way, stands for Flag, just as HL stands for High/Low.
All Z80 registers with the exception of the flags have a width of 8 bits. Indexed registers like IX or IY are inherently 16 bits wide, but there are some undocumented Z80 instructions that also address IX and IY as 8-bit registers (HX, LX / HY, LY). It should also be noted that these indexed instructions require a bit more processing time.
Some 8-bit registers can be combined to form 16-bit registers. Such pairs are, for example, AF (registers A and F), BC (registers B and C), DE, etc. One could say a lot about this. In any case, you cannot use the registers arbitrarily as was the case in BASIC; each register is used in specific instructions for a specific purpose.
Since I mentioned LDIR earlier, I can add the following: it is used for copying or filling memory blocks. The DE register points to where the bytes should be copied (DEstination), HL points to the origin of the data, and BC (Byte Counter) specifies how many bytes are to be copied.
Anyone looking for registers like LH or CB or even HC will look in vain, because our Z80 will not accept that under any circumstances. You cannot create fantasy double registers simply by writing them into an assembler program.
There are also shadow/alternate registers, indicated by an apostrophe. You cannot use them directly; instead, you must always swap all registers using the EXX instruction, or EX AF,AF' for the AF register pair. While this is also suitable for temporarily buffering registers—especially when fast access is required—one should still consider storing variables in memory after all.
Fundamentally, there are the following load instructions for registers:
LD (address),register: The contents of the 8- or 16-bit register are poked into the address "address".LD (register1),register2: 8-bit register 2 is poked into the address held in register 1.LD register1,register2: Register 1 (8-bit) takes the value of register 2 (also 8-bit).LD register,number: Register (8/16-bit) takes the value of the number (same number of bits).LD register,(address): Register (8/16-bit) takes the value of the memory location(s) "address".LD I,A/LD A,I: Special instruction for the interrupt register.LD SP,(address)/number/reg.: Special instruction for the stack pointer.
And that brings us right to the stack pointer. Everything is stored here: return addresses, PUSHed registers. From the address indicated by SP (corresponds to CLEAR in BASIC), the 16-bit values are always placed "downwards". You should therefore take care that every CALL is ended with RET, and every PUSH with POP, otherwise you risk the most glorious system crashes.
Jumps
Here there are several types, which in turn are subdivided into smaller types: CALL (corresponds to GOSUB), JP (GOTO), and JR (relative jump!). All of them can occur conditionally, depending on whether the Carry flag, Zero flag, Minus/Sign flag, etc. are set or not set.
Using CALL as an example:
CALL C,nn: Call subroutine if Carry is set.CALL NC,nn: Call subroutine if Carry flag is zero.CALL Z,nn: Call subroutine if Zero flag is set.CALL NZ,nn: Call subroutine if Zero flag is zero.CALL M,nn: Call subroutine if Minus flag is set (the result of a comparison is negative).CALL P,nn: Call subroutine if the Minus flag is zero (positive).CALL PE,nn: Concerns the Parity flag when set (parity even).CALL PO,nn: Parity flag not set (parity odd).
Bit Operations
Arguably the most extensive chapter of Z80 opcodes: simply loading registers and jumping is only useful for calling ROM routines, but if we want to do something more, we need to know the following instructions:
-
ADC: Add including Carry, store inAorHL. -
ADD: Same as above, but without the Carry flag. -
AND: Logical AND,Ais combined with a byte:01010101 AND 11000000 ------------ 01000000Only where the bits of both registers (or of the register and an 8-bit number) show a 1, the result will also be a 1. The entire result is stored in
A. -
BIT:BIT b,rtests bitbof registerrand copies its inverse into the Zero flag. The bit order is: 76543210. -
CCF: Complement Carry Flag, invert the value of the Carry flag. -
CP: Compare, compare registerAwith a register, number, or memory content. The Zero flag is set if the comparison is positive, i.e., when both numbers are equal. Naturally, the Carry flag is set if the content of theAregister is smaller than the byte being compared. -
CPD: Compare, Decrement. So compare viaHLand decrementHL.BCis decremented anyway. Do not confuse withCP D. -
CPDR: LikeCPD, only the whole thing is repeated until the result matches orBCis zero. -
CPI: LikeCPD, onlyHLis incremented; don't forget,BCis always decremented! -
CPIR: LikeCPDRwithCPI, logically. -
CPL: Do not confuse withCP L. All bits of theAregister are inverted; only 4 clock cycles are needed, so just as many asNOP. -
DAA: BCD (Binary Coded Decimal) correction of theAregister. -
DEC: Decrease, decrement register or memory content. -
DJNZ: Decrement, Jump Not Zero. Decrement theBregister by 1, and jump (relatively) if the result of the comparison withBis not zero. -
IN: Like in BASIC:LET A=IN BC. In MC:IN A,(C). -
INC: Increase, increment register by 1. -
IND:INfollowed by decrementingHLandB. The procedure is as follows: the value retrieved withIN (C)is placed into the address pointed to byHL.HLandBare then decremented. -
INDR: LikeIND, only a loop is executed as long asBis greater than zero. This is among other things the reason why the hard disk interface uses an 8-bit address:Cas address andBas loop counter! That could never work with a 16-bit address becauseBkeeps getting smaller and the data is read fromIN (BC)! -
INI: LikeIND,HLis incremented. -
INIR: LikeINDR,HLis incremented. -
NEG: Invert the sign of the register content ofA(two's complement negation). -
OR: Logical OR betweenAand a byte, store the result inA. OR means if a bit inAor in the byte being combined is 1, it becomes 1 in the result as well:01010101 OR 11000000 ------------ 11010101 -
OUT:OUT,OUTD,OUTI,OTDR,OTIR: Just likeIN,IND,INI,INDR, andINIR, only withOUT (C)instead ofIN (C). -
RES: Reset, clear bit. -
RL: Rotate byte left; bit 0 is filled from the Carry flag, Carry flag becomes whatever bit 7 was before. Smooth scrolling is only possible using these instructions. Rotation is also used for division and multiplication, by the way. -
RLA: LikeRL A, but twice as fast and with a different effect on the flags. -
RLC: Rotate left, the overflow is stored into Carry and into bit 0. -
RLCA: LikeRLC, with the same differences asRLA. -
RLD: Rotate BCD digit left: the content of addressHLwith consideration of theAregister:
A Reg. (HL)
+----------+ +----------+
| 7654 3210| | 7654 3210|
+---^--+---+ +---^--+---+
| | | |
| +----------+ |
+----------------+
RR:RR,RRA,RRC,RRCA, like theRLopcodes, only the rotation happens to the right.RRD: LikeRLD, only rotating to the right: A
Reg. (HL)
+ - >> - + +->-+ 7654 3210 7654 3210
+ - << - +
- SBC: Subtract with carry taken into account, store the result in A or HL.
- SCF: Set Carry Flag, carry becomes 1.
- SET: Set a bit, i.e., make it 1.
- SLA: Shift Left Arithmetic; 0 is shifted into bit 0, and bit 7 goes into the Carry Flag.
- SRA: Shift Right Arithmetic. Bit 7 remains unchanged.
- SRL: Shift Right Logical; bit 7 becomes zero. (Note: given as RL in text)
- SUB: Subtract without using the Carry Flag, store in A.
- XOR: Exclusive OR, the result is written back into A.
Well, didn't I promise it would get complex? But without these opcodes, more complex programs with artificial intelligence couldn't be written at all, though even as it is, it's very complex. What remains are the special instructions:
- DI: Disable interrupts, used when the program needs to run undisturbed. Most routines, such as tape loaders, start with this instruction.
- EI: Re-enables interrupts; if you forget this instruction, the keyboard won't respond upon returning to Basic.
- EX: Exchange registers with stacked values, or DE with HL, or AF with AF'.
- EXX: Exchange register pairs BC, DE, and HL with BC', DE', and HL'.
- HALT: Wait until the vertical blank, i.e., until the television picture is redrawn. Excellent for synchronization to code flicker-free programs.
- IM: Interrupt mode; there are 3 of them. IM 0 and 1 are used by the Spectrum ROM, leaving only Mode 2 freely available.
- NOP: Do absolutely nothing for 4 clock cycles except waste time.
- POP: Retrieve a register from the stack.
- PUSH: Save a register onto the stack.
- RET: Return from a subroutine called with CALL, or return back to Basic. The return address is pulled from the stack, so you should strictly avoid modifying SP unless appropriate measures are taken to preserve the Stack Pointer.
- RETI / RETN: RETI = Return from an interrupt routine, RETN = Return from an NMI routine; in both cases, a standard RET also works.
Those would be all the instruction groups of the Z80 processor. What one can actually do with them right now is rather little. You also have to assemble sensible programs out of these opcodes, and that really isn't easy—otherwise there would be far more assembly programmers on the Spectrum. Just for a quick comparison: the C64 has significantly fewer opcodes on its processor, but it can execute them faster; only for this reason can it almost keep up with the Spectrum (ever seen Elite on the C64?).
Another thing to keep in mind in assembly programming is how many clock cycles (T-states) individual instructions consume. For some instructions, this isn't always the exact same amount. I already mentioned that LDIR requires 21 cycles per loop iteration, but that's not entirely true: LDIR only needs 21 cycles if BC is not zero. If BC does reach zero—which inevitably has to happen at the end—the last loop cycle takes only 16 cycles. Now it should be clear why many coders prefer executing LDI multiple times in a row: that also takes only 16 cycles because BC isn't checked.
It is sometimes used to clear the screen, but there is an even faster method: PUSH Reg takes exactly 10 cycles to poke a register pair into memory (except for IX and IY, which take 14 cycles because they are two-byte instructions). But that's not all: PUSH also decrements the Stack Pointer by 2 bytes. To clear the screen, you should therefore save the Stack Pointer, point it to the end of the display, and then PUSH for all you're worth. For example, you could use 16 PUSH instructions to clear one line and repeat the whole thing 192 times; theoretically, you could then clear the entire screen within a single frame (1/50 of a second), even if the screen memory is located in SLOW RAM!
So, LDIR takes 21 cycles to copy one byte, while PUSH takes only 10 for two bytes! Let's try writing such a fast CLS routine:
LD (23296),SP
LD B,192
LD SP,22527
LD HL,0
Loop:
PUSH HL ; (a total of 16 times PUSH HL from here!)
DJNZ Loop
LD SP,(23296)
RET
It works wonderfully, and if you load the HL register pair with anything other than 0, the screen will be filled with patterns. DJNZ decrements B and, if it is not zero, jumps relatively forwards or backwards in memory, making it wonderful as a loop—essentially FOR.. TO.. NEXT in Basic.
As you can see, it's not nearly as complicated as many believe. If you'd like to write something on this topic, feel free to contribute!
End
Share your thoughts about the article