In this, the final part of our series on machine language programming for beginners, the remaining Z80 machine code instructions will be described. These deal with the index registers, with input and output commands, and with interrupts. All of these items have been left until now quite deliberately. The ideas involved in these commands are quite complex, and the beginners to machine language programming will probably manage quite happily without them. However, for completeness, these command sets will be covered. At the end of this page there will be a list of recommended books where these complex commands, and all the Z80 machine code instructions dealt with in the series can be found. And, in keeping with the general style of this series, there are some machine code programs which you can try for yourself. First of all though, let's examine the index registers.
The Z80 CPU has two 16-bit registers, denoted IX and IY which perform as index registers. Unlike all the other so-called 16-bit registers, IX and IY only work in 16 bits, and with them you do not have the option of working with one half of the 'pair', i.e. 8-bits, at one time. IX and IY can be likened to the HL register pair, and many of the operations of IX and IY are identical to HL.
Those operations which work on IX and IY in the same way as HL result from direct addressing. In other words, operations carried out directly on IX and IY. For example, LD IX,nn and ADD IY,BC work exactly the same way as their counterparts with HL (LD HL,nn and ADD HL,BC).
What is special about IX and IY arises from their use in indirect addressing. Remember in a previous part of this series, (HL) denoted 'the byte in memory whose address is the value contained in the HL register pair'. This is indirect addressing. IX and IY can also be used for indirect addressing, but their use differs from that of HL. Instead of simply having (IX) or (IY), the assembly language mnemonics for indirect addressing with these registers are (IX + d) and (IY + d). 'd' stands for displacement. It is a one byte value in the operand (so having a value between 0 and 255) which is added to the value in IX or IY to obtain the effective address for indirect addressing. The value of this extra facility may not be immediately obvious, but it is often used by machine code programmers for sequentially accessing a block of bytes.
This extra facility offered by the index registers can be quite powerful, but is only likely to be used by the more experienced machine code programmer, so we'll deal with it in theory only. The similarity of IX and IY to HL continues in the same way the machine code bytes are formed. Take an example like ADD HL,BC. The machine code opcode for this is a single byte instruction, 09 hex. To get the corresponding machine code for IX (ADD IX,BC) you precede the byte 09 with DD hex, and for IY, the opcode is preceded with FD. This pattern governs the formation of all IX and IY opcodes; precede the opcode for the corresponding HL instruction with DD for IX and FD for IY. ADD IX,BC is DD09; ADD IY,DE is FD19; PUSH IX is DDE5 (PUSH HL is E5) and POP IY is FDE1 (POP HL is E1). Simple!
The formation of the indirect addressing instructions with IX and IY is also simple once you know the rules; again, DD and FD precede the corresponding HL opcode, but you must remember that there is at least one operand, the displacement 'd'. For example, DEC (HL) has the opcode 35. DEC (IX + d) has the opcode DD35, to which the value of 'd' must be added, to give a three byte instruction. If d is to be 8, then the three bytes for DEC (IX + 8) are DD3508 hex.
These rules get a little more complicated when there is a further operand. For example. LD (HL),20 (hex) has the opcode 36 and an operand 20, so the instruction is 3620. For LD (IY + 08),20 the opcode is FD36. Next comes the value of 'd' (08), and finally the operand 20, giving a four byte instruction FD360820. And, just to confuse you a little more, the rules are yet more complicated for the formation of indirect addressed instructions of the BIT, SET, and RESET of IX and IY. Take BIT 6 (HL); the opcode for this is CB76. BIT 6, (IX + 08) is DDCB0876; in other words the corresponding HL opcode is now split when translated to the IX or IY opcode by the value of 'd'.
These fiddly rules emphasise the usefulness of assemblers, which convert assembly language mnemonics to machine code. It is so easy to forget how the instruction is made up, that mistakes in manual coding of IX and IY instructions are common. Indexed addressing with the Z80 CPU is not as powerful as with other processors, such as the 6502, so if you don't want to use them, it is usually relatively easy to just use the other registers instead. One good reason for avoiding the use of IX and IY is that they are used by the BASIC interpreter and operating system of the Spectrum, so altering their values could cause a crash on return to BASIC.
The IN and OUT instructions enable the Z80 to communicate with the 'outside world'. So far, the machine code instructions have only dealt with operations within the CPU itself, or communication between the CPU and memory. For effective operation, the Z80 must be able to communicate with other devices; notably the keyboard, the cassette interface, and (if you have one) the printer. In this list of external devices, the TV or monitor isn't always included. As we have seen before, the screen display occupies a certain area of RAM, and this is translated to a screen image through specialised hardware and software connections.
The hardware of the computer assigns to these external devices an identifying number, and connects them to the CPU through links which are termed 'ports'. The IN and OUT instructions allow values (bytes) to be transferred between the CPU and these ports. IN transfers a value from the port to the Z80, while OUT enables the opposite transfer to occur.
There are two types of IN and OUT instruction, the simplest are IN A,(C) and OUT A,(C). The brackets around 'C', if you recall, suggest indirect addressing. In this case it means that the register 'C' holds the identifying value of the port. So, IN A,(C), with 'C' holding a value of 254 would instruct the transfer of a value from port number 254, and place it in register 'A'. Similarly, OUT (C),A sends the value in A to port 254. Similar instructions are available for all the registers, and their opcodes are listed in the table below. They are all two byte opcodes, preceded by ED.
Also two bytes in length are the other IN and OUT instructions, but in these cases the opcode is only one byte. These take the form IN A,(n) and OUT (n),A. Here, transfer of data is only possible to and from the 'A' register. The port is identified by the operand which follows the single byte opcode; DB for IN and D3 for out. Hence the instruction DBFE collects a value from port 254, and places it in register 'A'. Port 254 is the cassette interface on the Spectrum. You could write your own LOAD and SAVE routines in machine code knowing that fact; but, if you did want to use these routines in machine code, it is far easier to use the routines already available in ROM for performing these functions.
There are block IN and block OUT instructions, which are equivalent to LDIR and LDDR with load, but these are rarely used.
Finally, let's take a look at interrupts.
Interrupts are a special form of communication for the CPU. Whenever you are working with the computer, it seems that the CPU seems to progress through machine code (either yours, or that in ROM when you are working in BASIC) continually. However, the CPU is often being interrupted. An interrupt is caused by a device connected to the CPU which demands priority over the current work of the CPU. Let's first take a look at the various types of interrupt, then at some examples to see the purpose of interrupts.
Although there are other types of interrupt available to the Z80 CPU, the important ones are the non-maskable interrupt (NMI) and maskable interrupt. As their names suggest, you cannot prevent an NMI, but you can stop a maskable interrupt.
In all forms of interrupt, a signal is sent to the CPU via an interrupt line, by a device requesting service. The NMI routine on the Spectrum does little, apart from resetting the machine (RST 00).
NMIs are of little use to the programmer, as they cannot be controlled. This is not the case with maskable interrupts. These interrupts can be masked by the programmer with the machine code instruction DI (Disable Interrupt) and permitted with EI (Enable Interrupt). There are three types of maskable interrupt denoted by mode 0, 1, or 2. These are set by the machine code instructions IM 0, IM 1, and IM 2. So, whenever a maskable interrupt signal is received by the CPU, it is only accepted if the interrupt is not masked, and then the appropriate interrupt mode enacted, depending on which mode is set by the programmer (or by ROM, when in BASIC).
Mode 0 has a quite specialist use. It requires that the device requesting the interrupt places, on the data bus, the machine code bytes to be interrupted by the CPU. For example, a ROM or EPROM overlaying the main ROM could be brought into use by this interrupt.
Mode 1 saves the PC contents on the stack, and places the value 38 hex in the PC. The address is the start of a ROM routine, which on the Spectrum causes the keyboard to be read. In normal operation, the computer has interrupt enabled, and mode 1 set, which allows the keyboard to be scanned for a keypress every 1/50th of a second. Sometimes, however, it is not convenient for this continual scanning to take place. The LOAD and SAVE routines are one example. At certain times in the LOAD and SAVE sequence, it is vitally important that the listening or output routines are not interrupted. During this period, interrupts are disabled. During non-critical periods in the LOAD or SAVE routine, interrupts are enabled, to allow the keyboard to be scanned (to check for the pressing of BREAK).
Mode 1 is a fixed routine. While it is useful for the programmer to know what it does, he can do little with it apart from turning the keyboard scan routine on or off.
Mode 2 provides a lot more flexibility. It makes use of another register, the I (or interrupt) register. When a mode 2 interrupt is encountered, the PC contents are stored as with the other interrupts, and PC is filled with two values. One is supplied by the device calling the interrupt, the other is in the I register. Together they form a 16-bit address, which points to another two byte value held in memory. It is this second address which is placed in PC. This 'vectored' system allows the user to define where the interrupt handling routine is, so allowing the user to write his/her own interrupt routine, and have, if desired, a table of address vectors. The programmer can set the value of the interrupt register with the command LD I,A. This Interrupt is something for the experienced programmer, but is still rather useful.
OK That's all the theory covered! If the series has given you an appetite for machine code, then you may well want to take your studies further. An excellent 'bible' for Z8O machine code programmer is the book by Rodnay Zaks, 'Programming the Z80'. This covers all the theory of Z80 programming in great detail, and is an excellent reference text. If you want something a little bit more machine specific, then '40 Best Machine Code Routines for the ZX Spectrum' by John Hardman and Andrew Hewson can be recommended.
The final example in this series is one for special effects. It allows full screen scrolling, left or right, moving attributes as well, and incorporates wrap-around. This effect means that characters which fall off the edge of the screen re-appear on the other side. If you don't want the wrap around effect, you can replace the bytes marked with * in the assembly language listing below with zeros (00).
So, download the program and then RUN it to install the bytes. The code is fully re-locatable (i.e. it doesn't matter where in RAM you store it, it should always work), so on a 48K machine, you could load it higher in RAM, so that not so much memory is wasted.
Once you run the program, the machine code is placed above RAMTOP, and you can then use the 5 and 8 keys to move the screen:
5 does a RANDOMISE USR 30000 which will cause the whole screen display to move one byte to the left, while:
8 does a RANDOMISE USR 30060 which causes the display to move to the right.
If you want to incorporate these routines in a BASIC program, then one suggestion is to have the display move when the appropriate 'arrow' key is pressed; as in the example:
200 IF INKEY$="5" THEN RANDOMISE USR 30000
210 IF INKEY$="8" THEN RANDOMISE USR 30060
Well - that's it! I hope you have enjoyed reading the series as much as I've enjoyed writing it. You should find machine code much simpler than you ever expected. Try writing your own routines, and carry on working out how routines written by others work.