QL Hardware World Issue 44 Contents Multi-User Dungeon

Machine Code



Mission for area control

Create a Scramble type display in machine code
with Marcus Jeffrey to interpret the scroll

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.

Figures

Figure 1.

                     ORG   60000
                     LOAD  60000

EA60 3E01     SCROLL LD    A,1        ;Scroll top third of screen
EA62 CD70EA          CALL  LEFT       ;    one character left
EA65 3E02            LD    A,2        ;Scroll middle of screen
EA67 CD9DEA          CALL  RIGHT      ;    one character right
EA6A 3E03            LD    A,3        ;Scroll bottom of screen
EA6C CD70EA          CALL  LEFT       ;    one character left
EA6F C9              RET

EA70 FE01     LEFT   CP    1          ;If A = 1, then set HL
EA72 2005            JR    NZ,LNOTOP  ;    for top of screen
EA74 21E13F          LD    HL,3FE1H
EA77 180F            JR    LTSCR
EA79 FE02     LNOTOP CP    2          ;If A = 2, then set HL
EA7B 2005            JR    NZ,LNOMID  ;    for middle of screen
EA7D 21E147          LD    HL,47E1H
EA80 1806            JR    LTSCR
EA82 FE03     LNOMID CP    3          ;If A = 3, then set HL
EA84 C0              RET   NZ         ;    for bottom of screen
EA85 21E14F          LD    HL,4FE1H
EA88 0640     LTSCR  LD    B,64       ;B = 64 pixel lines
EA8A C5       LLOOP  PUSH  BC
EA8B 012000          LD    BC,32      ;BC = 32 bytes per line
EA8E 09              ADD   HL,BC
EA8F 0D              DEC   C          ;BC = 31 LDIR loops
EA90 54              LD    D,H
EA91 5D              LD    E,L
EA92 1D              DEC   E          ;DE = HL - 1
EA93 E5              PUSH  HL
EA94 1A              LD    A,(DE)     ;A = leftmost byte
EA95 EDB0            LDIR             ;Shift pixel line
EA97 12              LD    (DE),A     ;Rightmost byte = A
EA98 E1              POP   HL
EA99 C1              POP   BC
EA9A 10EE            DJNZ  LLOOP      ;Repeat for 1/3 screen
EA9C C9              RET

EA9D FE01     RIGHT  CP    1          ;If A = 1, then set HL
EA9F 2005            JR    NZ,RNOTOP  ;    for top of screen
EAA1 21FE3F          LD    HL,3FFEH
EAA4 180F            JR    RTSCR
EAA6 FE02     RNOTOP CP    2          ;If A = 2, then set HL
EAA8 2005            JR    NZ,RNOMID  ;    for middle of screen
EAAA 21FE47          LD    HL,47FEH
EAAD 1806            JR    RTSCR
EAAF FE03     RNOMID CP    3          ;If A = 3, then set HL
EAB1 C0              RET   NZ         ;    for bottom of screen
EAB2 21FE4F          LD    HL,4FFEH
EAB5 0640     RTSCR  LD    B,64       ;B = 64 pixel lines
EAB7 C5       RLOOP  PUSH  BC
EAB8 012000          LD    BC,32      ;BC = 32 bytes per line
EABB 09              ADD   HL,BC
EABC 0D              DEC   C          ;BC = 31 LDDR loops
EABD 54              LD    D,H
EABE 5D              LD    E,L
EABF 1C              INC   E          ;DE = HL + 1
EAC0 E5              PUSH  HL
EAC1 1A              LD    A,(DE)     ;A = rightmost byte
EAC2 EDB8            LDDR             ;Shift pixel line
EAC4 12              LD    (DE),A     ;Leftmost byte = A
EAC5 E1              POP   HL
EAC6 C1              POP   BC
EAC7 10EE            DJNZ  RLOOP      ;Repeat for 1/3 screen
EAC9 C9              RET

                     END

Workarea - A717 to A8B9
ORG  end - EACA
LOAD end - EACA

Figure 2.

  10 CLEAR 59999
  20 GO SUB 1000
  30 RESTORE
  40 FOR i=USR "a" TO USR "b"+7
  50 READ j
  60 POKE i,j
  70 NEXT i
  80 CLS
  90 PRINT AT 0,5;"Scramble-like display"
 100 LET yy=0: PLOT 0,147
 110 FOR x=1 TO 15: LET y=INT (40*RND)-20-yy: DRAW 16,y: LET yy=yy+y: NEXT x
 120 DRAW 15,-yy: LET yy=0: PLOT 0,23
 130 FOR x=1 TO 15: LET y=INT (47*RND)-23-yy: DRAW 16,y: LET yy=yy+y: NEXT x
 140 DRAW 15,-yy
 150 PRINT AT 12,3;"AB": REM UDGs A & B
 160 RANDOMIZE USR 60000
 170 GO TO 160
 180 STOP
 190 DATA 96,248,255,127,127,63
 200 DATA 31,15,0,0,0,248,196
 210 DATA 255,252,240
 999 STOP
1000 REM HEX LOAD ROUTINE
1010 DEF FN p(x)=CODE h$(x)-48-7*(CODE h$(x)>=65)
1020 LET byte=0
1030 RESTORE 2000
1040 READ start
1050 READ h$
1060 IF h$="*" THEN GO TO 1160
1070 IF LEN h$<>2*INT (LEN h$/2) THEN PRINT "Odd number of hex digits in: ";h$: STOP
1080 FOR i=1 TO LEN h$
1090 IF NOT((h$(i)>="0" AND h$(i)<="9") OR (h$(i)>="A" AND h$(i)<="F")) THEN PRINT "Illegal hex digit: ";h$(i): STOP
1100 NEXT i
1110 FOR i=1 TO LEN h$ STEP 2
1120 POKE start+byte,16*FN p(i)+FN p(i+1)
1130 LET byte=byte+1
1140 NEXT i
1150 GO TO 1050
1160 PRINT "Code entered"
1170 PAUSE 150
1180 RETURN
2000 DATA 60000,"3E01","CD70EA"
2010 DATA "3E02","CD9DEA","3E03"
2020 DATA "CD70EA","C9"
2030 DATA "FE01","2005","21E13F"
2040 DATA "180F","FE02","2005"
2050 DATA "21E147","1806","FE03"
2060 DATA "C0","21E14F","0640"
2070 DATA "C5","012000","09"
2080 DATA "0D","54","5D","1D"
2090 DATA "E5","1A","EDB0","12"
2100 DATA "E1","C1","10EE","C9"
2110 DATA "FE01","2005","21FE3F"
2120 DATA "180F","FE02","2005"
2130 DATA "21FE47","1806","FE03"
2140 DATA "C0","21FE4F","0640"
2150 DATA "C5","012000","09"
2160 DATA "0D","54","5D","1C"
2170 DATA "E5","1A","EDB8","12"
2180 DATA "E1","C1","10EE","C9"
2190 DATA "*"

Figure 3.


Figure 4. Spectrum screen layout


Figure 5. New Z80 instruction codes

CPD      - Compare the accumulator with the contents of the location addressed
           by the HL register pair. Set the zero flag accordingly, and decrement
           the HL and BC register pairs.
CPDR     - Repeat CPD instruction until either the comparison is true
           (ie. accumulator equals memory) or the BC register pair is zero.
CPI      - Compare the accumulator with the contents of the location addressed
           by the HL register pair. Set the zero flag accordingly, decrement
           the BC register pair and increment the HL register pair.
CPIR     - Repeat CPI instruction until either the comparison is true
           (ie. accumulator equals memory) or the BC register pair is zero.
LDD      - Copy the contents of the location addressed by the HL register pair
           to the location addressed by the DE register pair. Decrement the BC,
           DE and HL register pairs.
LDDR     - Repeat LDD instruction until the BC register pair is zero.
LDI      - Copy the contents of the location addressed by the HL register pair
           to the location addressed by the DE register pair. Decrement the BC
           and increment the DE and HL register pairs.
LDIR     - Repeat LDI instruction until the BC register pair is zero.

Figure 6.

       LD    B,8        ;Eight pixel lines
       LD    DE,4000H   ;First byte of screen
       LD    HL,4001H   ;Second byte of screen
LOOP   PUSH  BC
       LD    BC,31      ;Number of bytes to scroll
       PUSH  DE
       PUSH  HL
       LD    A,(DE)     ;Store leftmost byte
       LDIR             ;Shift pixel line
       LD    (DE),A     ;Restore as rightmost byte
       POP   HL
       INC   H          ;HL = HL + 256
       POP   DE
       INC   D          ;DE = DE + 256
       POP   BC
       DJNZ  LOOP       ;Repeat for eight lines
       RET

Previous article in series (issue 43)
Next article in series (issue 45)



QL Hardware World Issue 44 Contents Multi-User Dungeon

Sinclair User
November 1985
Another Fine Product transcribed by Jim Grimwood at YRUA?