Machine Code |
There are a few instructions which place the Z80 in a class of its own amongst the 8-bit chips. Those are the block data transfer and search instructions.
A single assembly language instruction can do an enormous amount of work, as shown by this month's program, which moves large areas of the screen to produce a Scramble-like display.
We have already used one or two of those in past programs, but never explained how they work. If we look back to the programs which used the alternative screen, we will find they have the following sort of structure:
LD HL, from_screen_start_address LD DE, to_screen_start_address LD BC, number_of_screen_bytes LDIR
Simple logic tells us that, for the routine to work, the LDIR instruction must cleverly copy all the bytes of the 'from_screen' to the 'to_screen', but how does it do that? The slightly simpler LDI instruction copies the data from the location addressed by the HL register pair to the location addressed by the DE register pair. It then increments both the DE and HL registers, and decrements the BC register pair. So, if we were to execute the following code,
LD BC,5432l LD DE,12345 LD HL,23456 LD (HL),99 LDI
the registers and locations would have the values
BC = 54320 DE = 12346 HL = 23457 (23456) = 99 (12345) = 99
That is an interesting instruction but its use tends to be limited.
The LDIR instruction, on the other hand, is very useful. It performs the same operation as LDI, but will continue to transfer data items - incrementing DE and HL each time - until the BC register pair reaches zero. You should now be able to see how the screen copier works.
There are many other uses for LDIR. Suppose that you had a machine code program in memory at location 60000, and found that you had run out of room at the top of memory. An easy solution would be to CLEAR 49999, then execute the following routine
LD HL,60000 LD DE,50000 LD BC,number_bytes LDIR
The only remaining job is to modify any absolute locations in the code. Alternatively, you may want to set a number of bytes in memory to the same value. That could be used to set a number of screen bytes to a particular pattern, or to initialise a table of bytes prior to processing. The easy answer is to use the following piece of code:
LD HL,start_location LD D,H ; LD E,L ;DE=HL+1 INC DE ; LD BC,no_bytes-1 LD (HL),pattern_byte LDIR
That works by copying the initial pattern_byte value into the next location, then updating the HL register so that it equals the previous DE register pair, which has also been incremented, ready to copy the same value again.
There are two similar instructions to LDI and LDIR, known as LDD and LDDR. Those perform a similar operation, but instead of incrementing the DE and HL register pairs, they are decremented - BC is always decremented.
Those can be very useful in order to avoid overwriting relevant locations. For example, if we wanted to copy 2000 memory locations from location 50000 to location 51000, we would have a problem. Using the LDIR instruction, we would probably write something like
LD HL,50000 LD DE,51000 LD BC,2000 LDIR
However, the first 1000 iterations of the loop will overwrite locations 51000 to 51999 before they are copied. We can avoid the problem by using LDDR:
LD HL,51999 LD DE,52999 LD BC,2000 LDDR
That will still overwrite the same locations, but only after they have been copied. If you look at the assembly code in figure one, you will notice that the same method has been used to avoid overwriting when scrolling the screen to the left or right.
In addition to those transfer instructions, there is a corresponding set of search operations. Those have the mnemonics CPD, CPDR, CPI and CPIR. The CPD instruction will compare the value in the accumulator with the value held in the location addressed by the HL register pair, just like the CP (HL) instruction. However, the CPD instruction will also decrement both the BC and HL register pairs.
That may not seem of much use, but the repeated version is far more powerful. The CPDR operation will repeat the CPD instruction, stopping when either the accumulator equals the current memory location addressed by the HL register pair, or if the BC register pair reaches zero.
That form of the instruction can have hundreds of uses, especially when operating with tables which may have a variable length. When handling databases, you can set HL to the start of the data, and BC to the maximum number of items. You can then easily search the table for a specific item, without running over the end. With variable length records, just use a dummy value - an impossible data value - to distinguish the end of the table. You can then search for that value in the accumulator, using the BC register pair to count the number of items.
The CPI and CPIR instructions are very similar to CPD and CPDR, but instead of decrementing the HL register pair after each comparison, HL is incremented. All of those instructions are summarised in figure five.
This month's example program implements two of the most useful of those instructions, LDIR and LDDR, to scroll parts of the display screen. The assembly code for the routines is shown in figure one, and the usual Basic loader and application programs are given in figure two. Just type that in and run it, taking care with the graphics characters in line 150.
There are two main routines, shown as LEFT and RIGHT in figure one. Those scroll the screen to the left and right respectively. Figure three shows how that is done for any particular line of pixels. When moving screen information to the left, it is important not to overwrite a byte before copying it, so the LDIR instruction is used. Conversely, the right scroll routine uses the LDDR instruction.
That still leaves the problems of overwriting the leftmost or rightmost byte. To avoid that the contents of the location addressed by the DE register pair are placed in the accumulator - which is unaffected by LDDR and LDIR - before shifting each pixel line. When the shift is complete, that value is placed back at the opposite end of the screen, giving a wraparound effect.
The DJNZ loop at the end of each routine uses the B register to loop around for all the pixel lines. If B were set to the total number of lines on the screen, then the whole screen would scroll. However, to make things a little more interesting, the routines have been modified to scroll only one third of the screen.
Figure four shows how the Spectrum screen locations are naturally divided into three areas. When calling the routines, the accumulator should be set to
1 - scroll top of screen. 2 - scroll middle of screen. 3 - scroll bottom of screen.
You can modify the routines easily to scroll as many or as few lines as you choose. When doing that bear in mind the Spectrum screen layout. The routines, at present, add 32 to the HL register pair to move to the next line. That means the top pixel line of eight character lines will scroll first, followed by the second pixel line of the same eight character lines, and so on. To scroll a single character line, it is only necessary to increment the most significant byte of the register pairs. So, to scroll the top line of the display to the left, you would use a routine like that in figure six.
Using a generalised version of that sort of routine, you could have alternate lines easily scrolling in opposite directions. That would be handy for such games as Frogger.
Next month we will look at a number of hidden registers, and a new type of addressing mode which can be used with common instructions.
|
|
|
|
|
|
Previous article in series (issue 43)
Next article in series (issue 45)