Marcus Jeffery introduces more marvels of machine code programming
One set of extremely useful instructions in Z80 machine code is known as 'logic' instructions. Those are similar to the logic instructions available in Basic, such as AND and OR.
We will look at those, and then develop a set of picture routines which will allow you to hold more than one screen in memory, copy from one to another, exchange them and merge them together.
When writing programs using Basic, most people have used instructions such as
IF key "Z" OR key = "X" THEN ...
in their programs. If you have read the section in the User Manual, then you'll also know that all those instructions return TRUE and FALSE values, where
TRUE = 1 and FALSE = 0
That means that if we write the following code:
LET a = 1 LET b = 0 PRINT a AND b PRINT a OR b PRINT NOT a
it will print '0', '1' and '0'. Also, typing
PRINT a>b PRINT b>a
will print '1' and '0', because 'a is greater than b' is true, but not vice versa. In fact, you'll find that any non-zero value is true. Try changing the value of 'a' to a different number in the above code. You may be surprised by some of the results, but if you try something like
IF 15 THEN PRINT "The number 15 can also represent TRUE"
you will see what we mean.
What is the point of all this? Well, try
PRINT 99 AND 77
on the Spectrum, and you will get the answer 99. On most computers such as the BBC micro and Commodore 64 - that would give the result 65! The computer came up with that unexpected result by testing the individual bits of the numbers, with each bit representing its own true/false value.
We can see that better by considering the logic in binary:
1100011 (99) AND 1001101 (77) ------- gives 1000001 (65)
Even on the Spectrum, when you're working in Z80 machine code, ANDs and ORs work in the same way. So if we had the assembly code
LD A,15 (00001111 in binary) OR 60 (00111100)
then the value remaining in the accumulator would be 63 (binary 00111111). If we then had
AND 170 (10101010)
we would end up with 42 (00101010) in the accumulator - I wonder if that is the way Deep Thought found the ultimate answer in the Hitchhiker's Guide to the Galaxy!
The AND and OR instructions always return a result in the accumulator, so there is no need to specify that in the assembly language instruction. All those instructions are shown in figure one. For each bit in the two bytes which are being logically ANDed or ORed, the following apply:
bit 1 | bit 2 | b1 AND b2 ----------|-----------|----------- 0 | 0 | 0 0 | 1 | 0 1 | 0 | 0 1 | 1 | 1
bit 1 | bit 2 | b1 OR b2 ----------|-----------|----------- 0 | 0 | 0 0 | 1 | 1 1 | 0 | 1 1 | 1 | 1
There is another instruction available in Z80. That is an exclusive- or instruction, and is represented by XOR. This can be thought of as 'if bit one is true or bit two is true, but not both'. The logic table is as follows:
bit 1 | bit 2 | b1 XOR b2 ----------|-----------|----------- 0 | 0 | 0 0 | 1 | 1 1 | 0 | 1 1 | 1 | 0
One special use of the XOR instruction which you may come across is
What's the use of exclusive- ORing the A register with itself? Let us say the accumulator contained the value 19 (00010011 in binary). If we execute the instruction 'XOR A', the following operation is executed:
00010011 XOR 00010011 -------- gives 00000000
In other words, it clears the accumulator, whatever it contains.
That might, for instance, be used before some addition or subtraction instructions. You could, of course, use the instruction 'LD A,0', but that requires an extra byte and takes seven clock cycles rather than four for the XOR instruction.
Finally, the NOT operation known in machine code as CPL which stands for 'complement'. That reverses all the bits in the accumulator so '1's become '0' and vice versa.
Now to the picture routines. There are four routines altogether, and you could easily add your own. They all operate on a principle which uses an area of memory as though it were screen memory. Though we cannot see that area of memory, as we do with the real screen, we can still put it to all sorts of uses.
The assembly code is given in figure two, with the Basic loader and application code in figure three. Before typing them in, let us look at one or two ways in which the logic instructions have been used.
The first routine, MERGE - use RANDOMIZE USR 60000 - will combine the present screen picture with another presently in memory. Each screen pixel is either lit or unlit, which is a '1' or '0' respectively in screen memory. So, to combine them, we just need to OR all the bits. Doing that for a couple of diagonal lines in one character is shown in figure four. The logic instruction that does this is
which ORs the accumulator - containing a byte from the additional screen in memory - with the corresponding byte from the real, visible screen.
You might like to try changing that instruction to 'XOR (HL)'; change "B6" to "AE" in line 2020 of the Basic loader to get more unusual effects, as shown in figure five. You could also change it to 'AND (HL)' "B6" to "A6", as in figure six.
The other way we have used logic in this routine is the OR C which is used to check for the end of the loop. We are effectively counting down the BC register pair until it reaches zero. Unfortunately, we cannot check a register pair. What we are really doing is jumping to MNEXT if either of B or C is not zero. That could be done by checking B, and jumping if not zero (JR NZ), then checking C and jumping if not zero. However, we can get the same effect by ORing the register values and jumping. That is a fairly standard and accepted method of completing loops where the loop counter exceeds one byte, that is, 255.
We have used the same looping technique in EXCH - use RANDOMIZE USR 60046 - which will exchange the picture in memory with the real screen. Do not worry about the meaning of the EX AF,AF' instructions. They are only used to save and restore the accumulator, and you could just as easily use 'PUSH AF', for the first and 'POP AF', for the second, but the instruction used works faster.
The other two routines are COPY1 (RANDOMIZE USR 60021) which will copy the visible screen to memory, and COPY2 (RANDOMIZE USR 60034 which does the opposite. The LDIR instruction effectively replaces the series of instructions:
LOOP LD A,(HL) LD (DE),A INC HL INC DE LD A,B OR C JR NZ,LOOP
- a very useful instruction which we will be considering at a future data.
To use the routines, you have to initially CLEAR room for the invisible screen. In the Basic example, that screen has been placed in the 6144 bytes from location 53850. That is the reason for the CLEAR 53849 instruction. To specify that this is the screen for the routines to use, the first screen location has to be POKEd into the two locations 23278 and 23279. Those are two unused locations in the area used for Spectrum system variables - see pages 127-130 of the Spectrum User Guide. Those locations are very useful, keeping the screen location safely out of the way of any machine code application programs.
The numbers which should be poked into these locations are
POKE 23728, screen - 256 * INT (screen / 256) and POKE 23729, screen / 256
The way the memory has been used in the example program is shown in figure seven. Of course, there's no reason why, by changing the values in locations 23728 and 23729, you should not hold a number of hidden screens, using the routines to carry out operations on all of them.
Previous article in series (issue 41)
Next article in series (issue 43)