2K RAM
More than two years ago, Sinclair announced that the 4K ROM of the ZX80 would be expanded to an 8K ROM and that one of the new commands would be DRAW. DRAW was just what it sounded like—a command to draw a line on the TV screen.
But DRAW never came to be. Even an 8K ROM has only so much room, and DRAW would not quite fit. There is a routine to do the same thing in the ZX81 and TS1000 manuals—but it is in Basic, and it is slow.
If you have been following this series of articles on programming in machine code, you have already learned how to translate many Basic commands into machine code. This time, we will look at two new instructions, and then see how they can be used in the machine code translation of that line-drawing routine. We will also look at numbers in base 16—the mysterious “hex” numbers—and see why they are handy for machine code programming.
Negative Thinking
You already know that each regular register in the Z80 microprocessor, like each byte of memory, can hold any number between 0 and 255. Sometimes, though, we want to keep track of a negative number—a number that is less than zero.
Fortunately, there is an easy way to do it. Suppose register A contains the number 255—the highest number it can hold. Now suppose the next instruction in the Z80 program is
INC A
In Basic, this would be
LET A=A+1
What is the number in register A now? It is zero. When a register goes “over the top,” it starts all over again at the bottom. It is as if the register automatically subtracts 256 if a number is too high. In other words, when you are using a regular register, 255 + 1 = 0
As you might guess, subtraction works the same way. Once a register hits bottom, it starts over again at the top. In this case, it is as if the register automatically adds 256. Thus, 0 – 1 = 255. But that does not help much—or does it?
It turns out that you really can use 255 instead of -1. Surprising as it sounds, all the arithmetic works. Consider this example:
14+(-1)=13
Can we really replace -1 with 255? Here is how it works:
14+255=269
Since 269 is higher than 255, the register automatically subtracts 256:
14+255=269-256=13
The arithmetic does work—and that can be a big advantage, as long as we keep track of whether the value in a byte or register is a regular number (between 0 and 255) or a negative number.
More Negative Thinking
Now that we have negative numbers, we can use an instruction that makes a number negative. The instruction
NEG
will take whatever value is in register A and make it negative. The equivalent in Basic might be
LET A=-A
That probably seems pretty straightforward—but there is a catch. Remember, we have to keep track of which numbers may be negative. For example, if the value in register A is 1, NEG will change it to -1, or 255. But, if the number in A is 255, NEG will not make it -255. NEG assumes 255 really means -1, so it changes the value to 1.
If this sounds complicated, here is a simple rule: When you are using negative numbers, figure that only the values from 0 to 127 are positive. The other values are negative numbers—from -1 down to -128. That way, you are not likely to lose track of what is negative and what is not.
Great Divide
The Z80 processor can add and subtract pretty well, but it has a tougher time multiplying and dividing. It can do one kind of division, though: it can divide by 2. (Maybe that does not seem so great—but it is better than nothing at all.)
The instructions that divide by 2 are called “shift” instructions. Here is why: suppose you divide 142 by 10. The answer is 14.2—which looks a lot like 142. The difference is that the answer has been shifted over one decimal place. The Z80 shift instructions do the same sort of thing, but using base 2 instead of base 10. Dividing by 2 shifts a number over one “binary” place.
Fortunately, we do not have to worry much about what a “binary” place might be. When you divide by 2, the answer is the same in binary or in decimal.
There are two different divide-by-2 instructions—one for numbers you know are positive (0 to 255) and one for numbers that might be negative (-128 to 127).
The first is the “shift to the right logically” instruction, abbreviated SRL. For this instruction, the Z80 assumes that the value in the register is a positive number and divides it by 2. For example, suppose register B contains 15. After the instruction
SRL B
register B will have the value 7—which is the next whole number less than 15/2.
For numbers that might be negative, you can use the “shift to the right arithmetically” instruction. If B is -15, then after
SRA B
This will equal -8, which is the next whole number less than -15/2.
Notice that it is always the next number lower, not closer to zero. It is exactly the same as the INT function in 8K Basic, though not the same as integer arithmetic in 4K Basic.
Why is one kind of shift called “logically” and the other “arithmetically”? It is jargon, that is all—and it is only mentioned here because those initials are used in the abbreviations SRL and SRA.
You can use these “divide-by-2” instructions with any of the regular registers: A, B, C, D, E, H, or L. You can also use register pair HL as a pointer, with the instructions
SRL (HL)
and
SRA (HL)
Doubling Up
If you can “shift to the right” to divide, can you “shift to the left” to multiply? Certainly. If register D equals 45, then after
SLA D
D will equal 90. The SLA instruction works with all the regular registers, and with positive or negative numbers. But remember, you may get something you do not expect if the result is outside the right range.
Drawing the Line
Now let’s look at the routine that draws a line. Listing 1 is a slightly modified version of a program in the “Graphics” chapter of the ZX81 or TS1000 manual.
Listing 1.
1000 LET U=XEND-XSTART
1010 LET M=ABS(U)
1020 LET X1=SGN(U)
1030 LET X2=X1
1040 LET V=YEND-YSTART
1050 LET N=ABS(V)
1060 LET Y1=SGN(V)
1070 LET Y2=Y1
1080 IF M>N THEN GOTO 1140
1090 LET U=N
1100 LET N=M
1110 LET M=U
1120 LET X2=0
1130 GOTO 1150
1140 LET Y2=0
1150 LET S=INT(M/2)
1160 FOR I=0 TO M
1170 PRINT AT YSTART,XSTART;CHR$(128);
1180 LET S=S+N
1190 LET XQ=X2
1200 LET YQ=Y2
1210 IF S<M THEN GOTO 1250
1220 LET S=S-M
1230 LET XQ=X1
1240 LET YQ=Y1
1250 LET XSTART=XSTART+XQ
1260 LET YSTART=YSTART+YQ
1270 NEXT I
1280 LET XSTART=XEND
1290 LET YSTART=YEND
1300 RETURN
It is designed to work as a subroutine in Basic. To draw a line from one point on the screen to another, you use LET statements to make XSTART and YSTART equal the starting coordinates of the line, and XEND and YEND the ending coordinates; then you simply GOSUB 1000. When it is finished drawing, the old ending coordinates become the new starting coordinates.
For example, to draw a triangle whose three corners are at coordinates (1,2), (3,8), and (10,5), you could do this:
10 LET XSTART=1
20 LET YSTART=2
30 LET XEND=3
40 LET YEND=8
50 GOSUB 1000
60 LET XEND=10
70 LET YEND=5
80 GOSUB 1000
90 LET XEND=1
100 LET YEND=2
110 GOSUB 1000
If you have the 4K ROM, you will not be able to run the Basic program. There is no PRINT AT command in integer Basic. And, if you have only 1K RAM, you probably will not have enough memory. That is because the display file, where the picture sent to your TV screen is stored, can take up as much as 729 bytes—nearly 3/4K.
Parlez-vous Z80?
Translating the line-drawing routine into Z80 machine code is generally pretty straightforward. Figure 1 shows the translation for a computer with 2K RAM. For most program lines each Basic statement becomes a short series of machine code instructions. The most complicated of them is the machine code version of PRINT AT.
But at the very beginning, you will see ABS and SGN, a pair of Basic functions we have not translated into machine code before. Let’s take a quick look at how our machine code version of ABS and SGN works.
ABS is the absolute value function in Basic. Simply put, it means this:
If X is positive, then ABS(X)=X
If X is negative, then ABS(X)=-X
If X is zero, then ABS(X)=0
In other words, you could replace line 1010 in the Basic program with
1010 IF U>0 THEN LET M=U
1012 IF U<0 THEN LET M=-U
1014 IF U=0 THEN LET M=0
SGN is the sign function:
If X is positive, SGN(X)=1
If X is negative, SGN(X)=-1
If X is zero, SGN(X)=0
You could replace line 1020 in the Basic program with
1020 IF U>0 THEN LET X1=1
1022 IF U<0 THEN LET X1=-1
1024 IF U=0 THEN LET X1=0
You can see that ABS and SGN work in much the same way. That is why, to shorten our machine code a little bit, we have combined the two functions into one short routine. Here is how it would look in Basic:
1010 LET X1=1
1012 IF U>=0 THEN GOTO 1018
1014 LET U=-U
1016 LET X1=-1
1018 IF U<>0 THEN GOTO 1022
1020 LET X1=0
1022 LET M=U
In machine code, it works like this:
When we subtract XSTART from XEND, the zero flag and the carry flag are either sent up or down. Remember, the zero flag goes up if the result equals zero, and the carry flag goes up if the result of a subtraction is less than zero; otherwise, the flags come down. By checking the flags, we can tell whether the number is positive, negative, or equal to zero.
We start with a guess—maybe the number is positive. The sign will go in register D, so we say
LD D,1
Next, we test our guess. If U is negative, the carry flag is up. Thus, we say
JR NC,PLUS1
that is, jump ahead if it is not negative. PLUS1 is just a dummy name; we will have to figure out later how far the jump really is.
If U is negative, we say
NEG
LD D,-1
That changes the negative number in A to a positive value, and makes the sign -1.
Now we are OK if the number is positive or negative. But what if it is zero? The zero flag still tells us whether the result of our subtraction was zero or not. If it is up, we will need to change the sign (in register D) to zero:
PLUS1: JR NZ,SKIP1
LD D,0
Finally, we will put our absolute value in register H for safekeeping:
SKIP1: LD H,A
When we are finished, the sign is in register D and the absolute value is in register H.
As usual, translating from Basic to machine code is not really very complicated. Just break the routine down into simple steps—steps that can be performed by something as simple-minded as the Z80 processor—and you are off and running.
Figure 1. Basic and Machine Code Versions of Listing 1.
1000 LET U=XEND-XSTART
3A F4 47 LD A,(XSTART) ;start by subtracting
XEND-XSTART
47 LD B,A
3A F6 47 LD A,(XEND)
90 SUB A,B ;now A=U; the flags are set
.................................................................
1010 LET M=ABS(U)
1020 LET X1=SGN(U)
1030 LET X2=X1
16 01 LD D,1 ;D=X1=SGN(U);
suppose it is a plus..
30 04 JR NC,PLUS1 ;if A-B is positive, then M=A,
X1=1
ED 44 NEG ;if it is negative, M=-A...
16 FF LD D,-1 ;...and X1=-1
20 02 PLUS1: JR NZ,SKIP1 ;finally, if A-B=0...
16 00 LD D,0 ;...then X1=0
67 SKIP1: LD H,A ;now H=M; D=X1 (and X2)
7A LD A,D ;let's store X1 now
32 FA 47 LD (X1),A
.................................................................
1040 LET V=YEND-YSTART
3A F5 47 LD A,(YSTART) ;now do the same thing, except
47 LD B,A ;with L=N; E=Y1 (and Y2)
3A F7 47 LD A,(YEND)
90 SUB A,B
.................................................................
1050 LET N=ABS(V)
1060 LET Y1=SGN(V)
1070 LET Y2=Y1
1E 01 LD E,1
30 04 JR NC,PLUS2
ED 44 NEG
1E FF LD E,-1
20 02 PLUS2: JR NZ,SKIP2
1E 00 LD E,0
6F SKIP2: LD L,A ;now L=N and E=Y1 (and Y2)
7B LD A,E ;let's store Y1 now
32 FB 47 LD (Y1),A
1080 IF M>N THEN GOTO 1140
7D LD A,L ;compare M and N
BC CP H
38 06 JR C,MHIGH ;if M is higher, jump
.................................................................
1090 LET U=N
1100 LET N=M
1110 LET M=U
1120 LET X2=0
1130 GOTO 1150
6C NHIGH: LD L,H ;otherwise, swap M and N
67 LD H,A ;remember, N was already in A
16 00 LD D,0 ;D=X2
18 02 JR SKIP3
.................................................................
1140 LET Y2=0
1E 00 MHIGH: LD E,0 ;if M>N then do this; E=Y2
.................................................................
1150 LET S=INT(M/2)
7A SKIP3: LD A,D ;now store the variables
32 FC 47 LD (X2),A
7B LD A,E
32 FD 47 LD (Y2),A
7D LD A,L
32 F9 47 LD (N),A
7C LD A,H
32 F8 47 LD (M),A
CB 2F SRA A ;S=M/2
32 FE 47 LD (S),A
.................................................................
1160 FOR I=0 TO M
3E 00 LD A,0 ;start I with 0
32 FF 47 LD (I),A
.................................................................
1170 PRINT AT YSTART,XSTART;CHR$(128);
2A 0C 40 PLOT: LD HL,(16396) ;start of the display file
23 INC HL ;position is 0,0
3A F4 47 LD A,(XSTART) ;if XSTART>31, do not plot it
FE 20 CP 32
30 14 JR NC,BUMP
5F LD E,A ;DE=XSTART
16 00 LD D,0
19 ADD HL,DE ;position is XSTART,0
3A F5 47 LD A,(YSTART) ;if YSTART>21, do not plot it
FE 16 CP 22
30 09 JR NC,BUMP
5F LD E,A ;DE=YSTART
06 21 LD B,33 ;there are 33 bytes per line
19 LOOP1: ADD HL,DE ;add YSTART bytes
05 DEC B
20 FC JR NZ,LOOP1 ;do it 33 times
36 80 LD (HL),128 ;plot position XSTART,YSTART
.................................................................
1180 LET S=S+N
3A F9 47 BUMP: LD A,(N) ;add S+N
47 LD B,A
3A FE 47 LD A,(S)
80 ADD A,B
32 FE 47 LD (S),A ;store S
4F LD C,A ;C=S
.................................................................
1190 LET XQ=X2
1200 LET YQ=Y2
3A FC 47 LD A,(X2)
57 LD D,A ;D=XQ
3A FD 47 LD A,(Y2)
5F LD E,A ;E=YQ
3A F8 47 LD A,(M)
47 LD B,A ;B=M
.................................................................
1210 IF S<M THEN GOTO 1250
79 LD A,C ;compare M and S
(registers B and C)
B8 CP B
38 0C JR C,SKIP4 ;if S<M then jump
.................................................................
1220 LET S=S-M
90 SUB B ;otherwise, subtract...
32 FE 47 LD (S),A ;...store S...
.................................................................
1230 LET XQ=X1
1240 LET YQ=Y1
3A FA 47 LD A,(X1) ;...replace X2 with X1...
57 LD D,A
3A FB 47 LD A,(Y1) ;...and Y2 with Y1
5F LD E,A
.................................................................
1250 LET XSTART=XSTART+XQ
3A F4 47 SKIP4: LD A,(XSTART) ;add
82 ADD D
32 F4 47 LD (XSTART),A ;store XSTART
.................................................................
1260 LET YSTART=YSTART+YQ
3A F5 47 LD A,(YSTART) ;add
83 ADD E
32 F5 47 LD (YSTART),A ;store YSTART
.................................................................
1270 NEXT I
3A FF 47 LD A,(I) ;add 1 to I...
3C INC A
32 FF 47 LD (I),A ;store I
47 LD B,A
3A F8 47 LD A,(M) ;M is the limit
B8 CP B
30 9D JR NC,PLOT ;if I<M then plot again
.................................................................
1280 LET XSTART=XEND
3A F6 47 LD A,(XEND) ;otherwise, update XSTART..
32 F4 47 LD (XSTART),A
.................................................................
1290 LET YSTART=YEND
3A F7 47 LD A,(YEND) ;...update YSTART...
32 F5 47 LD (YSTART),A
.................................................................
1300 RETURN
C9 RET ;...and that's it!
.................................................................
In this 2K RAM version, the variables are stored in the following memory locations:
Decimal Hex Variable Decimal Hex Variable 18420 47F4 XSTART 18426 47FA X1 18421 47F5 YSTART 18427 47FB Y1 18422 47F6 XEND 18428 47FC X2 18423 47F7 YEND 18429 47FD Y2 18424 47F8 M 18430 47FE S 18425 47F9 N 18431 47FF I
Off and Running . . . Almost
When you first looked at Figure 1, you may have noticed a few other things. This is easily the longest machine code routine we have tried—200 bytes long, in fact. And in the far left-hand column, where we would normally put each instruction in numerical codes—the version of the program the computer understands-there is a mixture of numbers and letters. What goes on here?
Because this routine is 200 bytes, it is not practical to POKE in the whole program each time you run it. To get around that problem, we will get the computer to do the work. The program in Listing 3 expects its first line to be a REM statement containing the numerical version of a machine language program. It converts the characters in the REM line into values between 0 and 255, and then POKEs them into memory starting at location START.
But instead of decimal numbers, this time the numerical codes are in base 16, or hexadecimal numbers—”hex” numbers for short.
Putting on the Hex
There is a chapter in your ZX81 or TS1000 manual that tells something about hex numbers—it is titled either “Counting” or “Number Systems,” depending on which edition you have. It explains how hexadecimal numbers use all the digits from 0 through 9, and the letters A, B, C, D, E, and F as well. If you have not looked at it before, be sure to read it.
But even more important for machine code programming is the “Character Set” appendix. It has all the machine code instructions, along with both the decimal and hex numbers the Z80 processor understands. That makes it very easy to translate a number between decimal and hex—you can just look in the appendix.
Why use hex numbers this time? To save space. Notice that every hex number up to FF (in decimal, 255) can be written with exactly two digits. That means each pair of digits is one number. You can cram them together with no space between, and then easily break them up into pairs again. For example, it is easy to see that
210000C9
means the same thing as
21 00 00 C9
You cannot do that with decimal numbers. Some decimal numbers have one digit, some two, and some three. Suppose you left the spaces out of this sequence:
6 173 22 2 201
Listing 2.
8K ROM
10 PRINT "HOW MANY BYTES?"
20 INPUT A
30 LET RT=PEEK 16388+256*PEEK 16389
40 LET RT=RT-A
50 LET H=INT (RT/256)
60 LET L=RT-256*H
70 POKE 16388,L
80 POKE 16389,H
90 NEW
4K ROM
10 PRINT "HOW MANY BYTES?"
20 INPUT A
30 PRINT "HOW MANY K OF RAM DO YOU HAVE?"
40 INPUT RT
50 LET RT=1024*(RT+16)-A
60 LET H=RT/256
70 LET L=RT-256*H
80 POKE 16428,49
90 POKE 16429,L
100 POKE 16430,H
110 POKE 16431,195
120 POKE 16432,108
130 POKE 16433,2
140 PRINT "YOUR ROUTINE WILL START AT ";RT
150 PRINT "PRESS NEWLINE TO RESERVE SPACE"
160 INPUT A$
170 LET A=USR(16428)
[Figure: Cartoon “It says the odds of you making that hand are 2,385,000 to 1, and the odds are 3 to 2 that a nut like you will try for it.”]
If you did not already know what the original numbers were, you would never be able to re-create the original sequence from
6173222201
Of course, you could use zeroes to make each number three digits:
006173022002201
But then our 200-byte routine would fill up a REM line 600 bytes long. That is a lot of memory to take up if you only have 2K. With two-digit hex numbers, it only takes 400 bytes, or two-thirds the space.
There is another advantage to using hex numbers. Most machine code listings in magazines and in many of the books on machine code programming are in hex numbers rather than decimal. So once you have the hang of hex, it is much easier to understand those books and articles—and use the machine code programs they describe.
Listing 3.
1 REM 3AF447473AF6479016013004ED4416FF20021600677A32FA473AF547473AF747901E013004ED441EFF20021E006F7B32FB477DBC38066C67160018021E007A32FC477B32FD477D32F9477C32F847CB2F32FE473E0032FF472A0C40233AF447FE2030145F1600193AF547FE1630095F0621190520FC36803AF947473AFE478032FE474F3AFC47573AFD475F3AF8474779B8380C9032FE473AFA47573AFB475F3AF4478232F4473AF5478332F5473AFF473C32FF47473AF847B8309D3AF64732F4473AF74732F547C9
10 LET RSTART=16514
20 LET START=PEEK 16388+256*PEEK 16389
30 LET A=0
40 LET H=PEEK (RSTART+2*A)-28
50 IF H<0 OR H>15 THEN STOP
60 LET L=PEEK (RSTART+2*A+1)-28
70 IF L<0 OR L>15 THEN STOP
80 LET N=16*H+L
90 POKE START+A,N
100 LET A=A+1
110 GOTO 40
16K RAM Changes
1 REM 3AF47F473AF67F9016013004ED4416FF20021600677A32FA7F3AF57F473AF77F901E013004ED441EFF20021E006F7B32FB7F7DBC38066C67160018021E007A32FC7F7B32FD7F7D32F97F7C32F87FCB2F32FE7F3E0032FF7F2A0C40233AF47FFE2030145F1600193AF57FFE1630095F0621190520FC36803AF97F473AFE7F8032FE7F4F3AFC7F573AFD7F5F3AF87F4779B8380C9032FE7F3AFA7F573AFB7F5F3AF47F8232F47F3AF57F8332F57F3AFF7F3C32FF7F473AF87FB8309D3AF67F32F47F3AF77F32F57FC9
4K ROM Changes
10 LET RSTART=16427
20 PRINT "START LOCATION?"
25 INPUT START
Quick Draw
To use this machine code line-drawing routine, very carefully type in the program in Listing 3. (It is designed for an 8K ROM computer with 2K RAM; be sure to use the alternate REM line if your computer has 16K.) Count the number of characters in the REM line after you have typed it in, to make sure you have not missed any—or better still, check the whole line to make sure it is right.
Now—before you try the program—SAVE it on tape! If you have made a mistake, you will be able to edit just the REM line to correct it, instead of typing the entire program again.
Next, use the program in Listing 2 to save space at the top of memory. For this routine, you should save 212 bytes: 200 for the program, and 12 for the variables.
Listing 4.
10 LET Q=PEEK 16388+256*PEEK 16389
20 FOR A=1 TO 22*32
30 PRINT " ";
40 NEXT A
50 INPUT XSTART
60 POKE Q+200,XSTART
70 INPUT YSTART
80 POKE Q+201,YSTART
90 INPUT XEND
100 POKE Q+202,XEND
110 INPUT YEND
120 POKE Q+203,YEND
130 LET A=USR (Q)
140 GOTO 90
4K ROM Changes
10 PRINT "START LOCATION?"
14 INPUT Q
18 CLS
Then LOAD the program you have SAVEd on tape, RUN it, then edit out each of the program lines. Finally, type in and RUN the program in Listing 4. It demonstrates the line-drawing routine. If your computer has SLOW mode, it will leave no doubt how much faster than Basic machine code can be.
Coming Attractions
A machine code routine like this one is fast—but it is also long. Everything the routine does must be included in the program, and sometimes that means “reinventing the wheel.” After all, there are already machine code routines for printing on the screen, plotting points, and many other functions already in your computer—stored in the ROM.
Next time, we will see how to put those routines—the “ROM calls”—to work. We will also learn about writing our own subroutines, when there is nothing in ROM that does exactly what we want. And we will take a look at the “stack,” and some of the special features of the Sinclair computers.
If you have comments or questions about machine code programming, or something is not quite clear, let me hear from you. Be sure to send along a stamped, self-addressed envelope if you need a reply.
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.