The Circle Game

See all articles from SYNC v4 n2

Of all the problems with machine code programming, there is one that really stands out: it is too simple!

Oh, sometimes it can be pretty hard to figure out how to make your program do what you want. But that is because the Z80 microprocessor—the “brain” inside your ZX/TS computer—is really rather simple-minded. It can add and subtract well enough—and even multiply and divide by two—as long as the numbers are between 0 and 65,535.

But for anything more complicated, the Z80 just cannot handle it alone. You—the programmer—have to do most of the work. You are the one who has to write a routine that will multiply or divide, or print something on the screen. If you want the Z80 to do something really fancy like square roots or trigonometric functions or floating-point math, you have to figure out some way to simplify it all down to very simpleminded jobs like adding and subtracting.

Either that, or you have to get someone else to do it for you.

In this and the next installment of this series of articles on Z80 machine code, we will try it both ways as we tackle a relatively complicated challenge: getting the ZX81 or TS1000 to draw a circle on the screen. This time we will do it the hard way, translating a circle drawing routine into machine code ourselves. Next time, we will let someone else do most of the work—using a special section of the Basic ROM called the “calculator.”

Why do it yourself if you can use ROM routines to do it more easily? Because the ROM “calculator” is only about twice as fast as Basic would be. “Pure” machine code is much faster than that. There is another reason, too: sometimes you may want to do something that the “calculator” in ROM just cannot handle. With regular machine code, you can do almost anything—if you can only figure out how…

Round and Round

Figure 1 shows one of the most common ways of drawing a circle using Basic. Type in the program and RUN it. You will see it slowly draw a circle, PLOTting one point at a time, on your screen.

Figure 1. Common circle drawing routine in Basic.
10 LET R=20
20 LET S=32
30 LET T=22
40 FOR N=0 TO 2*PI STEP PI/60
50 LET X=R*COS N
60 LET Y=R*SIN N
70 PLOT X+S,Y+T
80 NEXT N

This circle drawing routine is pretty simple. It is based on a couple facts from trigonometry that show up in lines 50 and 60:

X=R*COS N
Y=R*SIN N

Or, as your trigonometry teacher might put it, “For any angle, the x-coordinate of a point on the circle equals the radius times the cosine of the angle, and the y-coordinate equals the radius times the sine of the angle.” If you have never had a trigonometry teacher, do not panic. SIN and COS are too complicated to explain here in detail, but, even if you do not understand exactly how they work, the program will run anyway.

In this routine, the radius of the circle is variable R. The LET command in line 10 sets it to 20. The center is at point (S,T); lines 20 and 30 make that (32,22), the center of the screen. You can change lines 10, 20, and 30 to make larger or smaller circles, with centers in different places on the screen. Watch out, though; if your routine tries to plot a point that is off the screen, you will get an error, and the program will stop.

There are really only two problems with this routine: SIN and COS. If the sine and cosine functions are complicated to explain, they are even more complicated for the computer to work out. The routine draws a circle so slowly because it spends most of its time figuring the sines and cosines.

Fortunately, we are not stuck with using these functions for drawing a circle. There are other ways to do it. Probably the easiest is the one worked out by Marvin Minsky, a computer scientist at the Massachusetts Institute of Technology. (You may have heard of Minsky; he is most famous for his work on an “artificial intelligence” computer language called LISP.)

A few years back, Minsky was looking for a better way to draw a circle with a computer—a way that would be simpler for the computer to do. He came up with a routine that, in Basic, looks something like Figure 2.

Figure 2. Alternate circle drawing routine.
10 LET R=20
20 LET S=32
30 LET T=22
40 LET X=R
50 LET Y=0
60 FOR N=1 TO 202
70 LET X=X-Y/32
80 LET Y=Y+X/32
90 PLOT X+S,Y+T
100 NEXT N

It certainly seems like a much simpler routine, that’s for sure. There is no more SIN or COS—just adding and subtracting, multiplying and dividing. Since simpler means faster on a computer, it can draw a circle in much less time than the SIN and COS version. Type it in and run it, and you will see just how much faster it is.

You may wonder how it can do the work of SIN and COS when it is so simple. Actually, it is just looks simple. The ideas behind it are quite complex—even more complicated than the ones behind using the sine and cosine functions. But this is not a course in college-level math; if it were, this issue of SYNC would be 500 pages long. Just remember that, like SIN and COS, it does work—even if you do not understand exactly how.

A Circle in Any Language

Translating this circle program into Z80 machine code really is not too difficult. It is long, that is true—but, if you translate the Basic program one line at a time, it is not hard. The translation is in Figure 3.

Figure 3. Translation of Figure 2 into MC.
Basic program            MC instructions
10 LET R=20              (you can load these
20 LET S=32              three variables with
30 LET T=32              their values before
                         the routine begins)
40 LET X=R               LD A,(R)
                         LD H,A
                         LD L,0
                         LD (X),HL
50 LET Y=0               LD H,0
                         LD (Y),HL
60 FOR N=1 TO 202        LD A,1
                         LD (N),A
70 LET X=X-Y/32          LD HL,(X)
                         LD DE,(Y)
                         LD B,5
                   DIV1: SRA D
                         RR E
                         DJNZ DIV1
                         OR A
                         SBC HL,DE
                         LD (X),HL
80 LET Y=Y+X/32          LD DE,(Y)
                         LD B,5
                   DIV2: SRA H
                         RR L
                         DJNZ DIV2
                         ADD HL,DE
                         LD (Y),HL
90 PLOT X+S,Y+T          LD DE,(X)
                         LD A,(S)
                         RL E
                         ADC A,D
                         LD C,A
                         LD A,(T)
                         RL L
                         ADC A,H
                         LD B,A
                         CALL PLOT
100 NEXT N               LD A,(N)
                         INC A
                         LD (N),A
                         CP 202
                         JR C,line 70

Let’s look at how some of the lines are translated; they are not all exactly what you might expect. Line 70, e.g., seems to be a pretty complicated piece of translation:

70 LET X=X-Y/32

Subtracting is easy, but how do we divide by 32?

We already know how to divide a number by 2. We can use the Z80 is “shift” and “rotate” instructions. A shift instruction, by itself, will divide the number in a register by 2. Using shift and rotate instructions together, we can do the same thing to the number in a register pair, such as DE or HL. For example, the instructions

SRA D
RR E

divide the number in register pair DE by 2.

Now try this: divide a number by 2, then divide the result by 2 again. The answer you get is the same as if you had divided the original number by 4. (Remember, 4 = 2 * 2.) If you divide that answer by 2 again, it is as if you had divided the original number by 8 (2 * 2 * 2).

What we want to do is divide by 32 (2 * 2 * 2 * 2 * 2). To do this, we put the shift and rotate instructions that will divide by 2 in the middle of a loop, like this:

      LD B,5
DIV1: SRA D
RR E
DJNZ DIV1

Do not be thrown off by the DJNZ instruction. It is a special instruction that only works with register B; it is exactly like the two instructions

DEC B
JR NZ,DIV1

Every time the Z80 gets to the DJNZ instruction, it will subtract one from the number in register B. If the result is greater than zero, the program will jump; if it is zero, it just continues with the next instruction.

In other words, since register B starts out with the number 5 we will run through this loop 5 times. Each time the number in register pair DE will be divided by 2, and the number in register B will drop by 1, until finally B contains zero—and the program continues. In Basic, it might look something like this:

10 LET B=5
20 LET DE=DE/2
30 LET B=B-1
40 IF B>0 THEN GOTO 20

As you have probably noticed, the DJNZ instruction works a lot like the NEXT command in Basic. Here is another Basic version of the routine:

10 FOR B=5 TO 1 STEP-1
20 LET DE=DE/2
30 NEXT B

If you add a couple more lines such as

5 INPUT DE
50 PRINT DE

to either of these short programs, you can try running them in Basic. You will find that, as expected, each of them divides by 32.

The Fixer

There is something else that is a bit unusual in this translation. Though the variables R, S, and T are ordinary numbers, X and Y use a very special kind of arithmetic called fixed point arithmetic.

Here is why: when the plotting section of this routine starts its work, X equals 20 and Y equals 0. The first thing we do is divide Y by 32 and subtract the result from X:

X=X-Y/32
X=20-0/32
X=20

So far, so good. Next we will divide X by 32 and add the result to Y:

Y=Y+X/32
Y=0+20/32

Here is where the problem shows up. The Z80 microprocessor only uses integers, i.e., whole numbers. There are not any fractions in this kind of arithmetic. When you divide a number, the result is always rounded off to the next whole number down. It is as if you have used the INT function in Basic.

This means that, for the Z80, 20/32 equals zero. And this means, after all that work, X still equals 20, and Y still equals 0. No matter how many times we run these numbers through the loop, they will never change. That is not going to draw much of a circle.

How can we solve this problem? We will cheat. We can trick the computer into keeping at least part of a fraction by putting each of these two variables into the top half of a register pair. In the translation of our Basic program, we do that in lines 40 and 50.

Now remember: putting a number in the top half of a register pair is like multiplying it by 256. So to the computer, X is not 20 any more; it is 20 times 256, or 5120. (Y, of course, is still zero since 0 times 256 is still 0.)

When we run these two numbers through our routine now, the fractions do not completely disappear. They are still there in the lower half of each register pair:

X=X-Y/32            Y=Y+X/32
X=5120-0/32         Y=0+5120/32
X=5120              Y=160

This time, we do not end up with exactly what we started with. The second time through, the numbers change even more

X=X-Y/32            Y=Y+X/32
X=5120-160/32       Y=160+5115/32
X=5115              Y=319

—and we are off and running.

To PLOT these points, of course, we have to use the actual values of X and Y. To find them, we must divide by 256, but that is easy to do. We just use the value that is in the top half of each register pair.

In Basic, what we are doing would look something like this:

LET X=X*256
LET Y=Y*256
FOR N=1 TO 202
LET X=X-Y/32
LET Y=Y+X/32
PLOT (X/256)+S,(Y/256)+T
NEXT N

Get the idea? The computer thinks it is working with whole numbers, but we know that they are really fractions. This kind of arithmetic is called “fixed point” because it is as if we had stuck a decimal point between the top and bottom halves of the register pair. The imaginary decimal point is always in the same place; this is why we say it is “fixed.”

This fixed-point arithmetic is not as accurate as the floating point arithmetic that Basic uses, and it can only work with numbers in a limited range; in this case, e.g., X and Y cannot be less than -127 or more than 127. But fixed point arithmetic is an easy way to use fractions in machine code, and it is much faster than floating point.

There is something else you should notice about using these fixed point versions of X and Y—something that shows up when we translate PLOT from Basic to machine code. When we use the PLOT function in Basic, it ordinarily rounds the horizontal and vertical coordinates to the closest whole number. (Remember, PLOT cannot use fractions, either.)

For example, the Basic command

PLOT 16.8,7.2

is actually treated as if it were

PLOT 17,7

Since 16.8 is closer to 17 than 16, it is rounded up. 7.2 is closer to 7 than to 8, so it is rounded down instead.

In the machine code version, we could use instructions like these:

LD DE,(X)
LD A,(S)
ADD A,D

But with these instructions, only the whole number part of X would be added to S. Instead of being rounded to the closest number, X would always be rounded down. The result would look like the circle we get when line 90 of the Basic version is

90 PLOT INT (X+S),INT (Y+T)

Try running the Basic program again with this new line. You will see that rounding down makes a circle that is, well, not as round.

To smooth things out, we have added another step. We replace

ADD A,D

with

RL E
ADC A,D

These instructions round the number X+S to the nearest integer, like PLOT does. It is only a little more work, and it makes a much better looking circle.

Circling the Wagons

A routine that draws a circle is nice, but it is not really a circle drawing function. As it stands, this machine code subroutine does not offer any way to make circles with a radius other than 20, or with any other center than (32,22).

Fortunately, as with the Basic version, it is easy to change the machine code variables R, S, and T by POKEing numbers into those variables from Basic before our program hits the USR function.

But that takes time, and it is complicated and inconvenient, too. A better way would be to let the machine code routine get its own information from the Basic program. If we could put the information in the same line of the Basic program as the USR function that starts the circle drawing routine, it would be easier and simpler to use—and faster, too.

One way to do that is to make both the USR function and the information part of an IF-THEN statement. It might look something like this:

60 IF USR (Q) THEN REM R5 S13 T7

As you might guess, “R5” would set the radius variable R to 5; “S13” and “T7” would set S to 13 and T to 7, putting the center of the circle at (13,7) on the screen.

Here is how we can use a line like this, first from point of view in Basic, then from the machine code side.

When Basic gets to IF, it expects the next thing in the line to be an expression like “A=5”, a statement that will either be true or false. (If a number follows IF instead of an expression, it is still either true or false to the computer, i.e., it is “false” if the number is zero, and “true” otherwise.) If it is true—or “true”—then Basic performs the command that follows THEN; otherwise, that part of the line is skipped. In this case, if does not matter whether Basic thinks USR (Q) is true or false. The THEN part of the line is just a REM statement. Either way, Basic will skip over it.

But the USR function is in the IF part of the line, so Basic evaluates it anyway, i.e., the computer performs the machine code routine. Then it goes on to the next line of the program.

In other words, we can safely put our circle information in that REM statement, and it is even on the same program line as USR (Q). But how will the machine code routine know where to look for that information?

We are in luck for that: the system variable “CH-ADD” always keeps track of where the computer is in a Basic program. This variable is stored at locations 16406 and 16407 on the TS1000, ZX81, or ZX80 with 8K ROM and at 16422 and 16423 on the ZX80 with 4K ROM.

This is a two-byte pointer, so it can hold any value from 0 to 65535. This means it can point to any location in memory. In fact, it is always pointing to the section of your Basic program that is about to be performed, e.g., whenever Basic gets to a USR function, CH-ADD points at whatever immediately follows it. So, if a line starts out “IF USR (Q)”, CH-ADD points to what is just after the USR function, i.e., at the THEN statement. Now remember: THEN, like all the other keywords in Timex and Sinclair Basic, takes up only one byte of memory. So does REM.

Suppose we write a machine code routine that copies the number that is in CH-ADD; that number points at THEN. If we add 1 to this number, it will point at REM. Increment it again, and it will point at the first character in the REM statement which is where we have put the information we want the machine code routine to find.

In other words, your machine code routine can easily find where the information is in memory; all it takes is three Z80 instructions:

LD BC,(CH-ADD)
INC BC
INC BC

Reading Lesson

Now that you know where the information is, it is relatively easy to get it out of the REM statement. Figure 4 is a Basic routine that demonstrates how it is done.

The routine is a bit long, but it is straightforward. Lines 30-90 figure out what variable the information belongs to; then lines 100-180 convert the characters in the REM line to a number, which is PRINTed by line 190. Run this program, then try it with different values for R, S, and T.

Figure 5 is the machine code translation of the routine. This time, instead of being PRINTed, the numbers are loaded into the appropriate machine code variables. You should be able to use it as a model, so your own routines can get information from the REM statement in an IF-THEN line.

This routine will work with either the 8K ROM or the 4K ROM (as long as you remember that each ROM keeps the CH-ADD system variable in a different location).

The 4K ROM does not have the PLOT function, though, so it cannot use the circle drawing routine. There is something else the 8K ROM allows you to do that the 4K ROM will not: with the TS1000 or ZX81, you do not have to put your machine code routine at the top of memory. Instead, you can POKE a machine code routine into a REM statement at the beginning of your program. That means you do not have to go to the trouble of reserving space as we have done in the past; your machine code routine will be protected as part of the Basic program.

Furthermore, we do not need a different version of the routine for different amounts of RAM; as long as it is in the first line of your program, the machine code will always start in the same place.

Figure 4. Basic program to get information out of REM statement.
10 REM R5 S13 T7
20 LET BC=16514
30 LET BC=BC+1
40 LET A=PEEK (BC)
50 IF A=CODE("R") THEN LET HL=CODE("R")
60 IF A=CODE("S") THEN LET HL=CODE("S")
70 IF A=CODE("T") THEN LET HL=CODE("T")
80 IF A=0 THEN GO TO 30
90 IF A=118 THEN GO TO 210
100 LET D=0
110 LET BC=BC+1
120 LET A=PEEK (BC)
130 IF A<CODE("0") THEN GO TO 190
140 IF A>CODE("9") THEN GO TO 190
150 LET A=A-28
160 LET A=A+D*10
170 LET D=A
180 GO TO 110
190 PRINT CHR$(HL),D
200 GO TO 40
210 STOP
Figure 5. MC translation of Figure 4.
Basic program                     MC instructions
 10 LET BC=PEEK (CH-ADD)          LD BC,(CH-ADD)
 20 LET BC=BC+1                   INC BC
 30 LET BC=BC+1                   INC BC
 40 LET A=PEEK (BC)               LD A,(BC)
 50 IF A=CODE("R")                CP 55
    THEN LET HL=location          JR NZ,line 60
    of variable R                 LD HL,R
 60 IF A=CODE("S")                CP 56
    THEN LET HL=location          JR NZ,line 70
    of variable S                 LD HL,S
 70 IF A=CODE("T")                CP 57
    THEN LET HL=location          JR NZ,line 80
    of variable T                 LD HL,T
 80 IF A=0                        CP 0
    THEN GO TO 30                 JR Z,line 30
 90 IF A=118                      CP 118
    THEN GO TO 210                JR Z,line 210
100 LET D=0                       LD D,0
110 LET BC=BC+1                   INC BC
120 LET A=PEEK (BC)               LD A,(BC)
130 IF A<CODE("0")                CP 28
    THEN GO TO 190                JR C,line 190
140 IF A>=CODE("A")               CP 38
    THEN GO TO 190                JR NC,line 190
150 LET A=A-28                    SUB A,28
160 LET A=A+D*10                  LD E,A
                                  LD A,D
                                  ADD A,A
                                  ADD A,A
                                  ADD A,D
                                  ADD A,A
                                  ADD A,E
170 LET D=A                       LD D,A
180 GO TO 110                     JR line 110
190 POKE HL,D                     LD (HL),D
200 GO TO 40                      JR line 40
210 (next routine)

Figure 6 is the full circle drawing program, including the routine that checks for information in an IF/THEN statement. It is designed to start at location 16514—the first character in a REM statement that is the first line of a Basic program.

Use the program in Figure 7 to load the machine code routine into memory. Test it by typing

IF USR 16514 THEN REM R5 S58 T38

It should draw a small circle in the upper right corner of your screen. Once you are sure it works, you can delete the loader program except for line 1. Now you can SAVE the routine on tape or make it part of another program.

It still takes about 5 seconds to draw the circle, even using machine code; the computer spends quite a while in the ROM subroutine, PLOTing each of the circle’s 202 points. But it is quite a bit faster than Basic, and that is what machine code is all about.

Figure 6. Full circle drawing program.
Machine code               MC instructions
ED 4B 16 40                LD BC,(CH-ADD)
03                         INC BC
03                  LOOP:  INC BC
0A                LOOP+1:  LD A,(BC)
FE 37                      CP 55
20 03                      JR NZ,3
21 1A 41                   LD HL,R
FE 38                      CP 56
20 03                      JR NZ,3
21 1B 41                   LD HL,S
FE 39                      CP 57
20 03                      JR NZ,3
21 1C 41                   LD HL,T
FE 00                      CP 0
28 E5                      JR Z,LOOP
FE 76                      CP 118
28 1B                      JR Z,CIRCLE
16 00                      LD D,0
03                 DIGIT:  INC BC
0A                         LD A,(BC)
FE 1C                      CP 28
38 10                      JR C,STORE
FE 26                      CP 38
30 0C                      JR NC,STORE
D6 1C                      SUB A,28
5F                         LD E,A
7A                         LD A,D
87                         ADD A,A
87                         ADD A,A
82                         ADD A,D
87                         ADD A,A
83                         ADD A,E
57                         LD D,A
18 EA                      JR DIGIT
72                         STORE: LD (HL),D
18 C7                      JR LOOP+1
3A 1A 41          CIRCLE:  LD A,(R)
67                         LD H,A
2E 00                      LD L,0
22 1D 41                   LD (X),HL
26 00                      LD H,0
22 1F 41                   LD (Y),HL
3E 01                      LD A,1
32 21 41                   LD (N),A
2A 1D 41             EXT:  LD HL,(X)
ED 5B 1F 41                LD DE,(Y)
06 05                      LD B,5
CB 2A               DIV1:  SRA D
CB 1B                      RR E
10 FA                      DJNZ DIV1
B7                         O A
ED 52                      SBC HL,DE
22 1D 41                   LD (X),HL
ED 5B 1F 41                LD DE,(Y)
06 05                      LD B,5
CB 2C               DIV2:  SRA H
CB 1D                      RR L
10 FA                      DJNZ DIV2
19                         ADD HL,DE
22 1F 41                   LD (Y),HL
ED 5B 1D 41                LD DE,(X)
3A 1B 41                   LD A,(S)
CB 13                      RL E
8A                         ADC A,D
4F                         LD C,A
3A 1C 41                   LD A,(T)
CB 15                      RL L
8C                         ADC A,H
47                         LD B,A
CD B2 0B                   CALL PLOT
3A 21 41                   LD A,(N)
3C                         INC A
32 21 41                   LD (N),A
FE CA                      CP 202
38 BB                      JR C,NEXT
C9                         RET

14                  411A:  R=20
20                  411B:  S=32
16                  411C:  T=22
00 00               411D:  X=0,0
00 00               411F:  Y=0,0
00                  4121:  N=0
Figure 7. MC loader routine.
1 REM ED4B164003030AFE372003211A41FE382003211B41FE392003211C41FE0028E5FE76281B1600030AFE1C3810FE26300CD61C5F7A87878287835718EA7218C73A1A41672E00221D412600221F413E013221412A1D41ED5B1F410605CB2ACB1B10FAB7ED52221D41ED5B1F410605CB2CCB1D10FA19221F41ED5B1D413A1B41CB138A4F3A1C41CB158C47CDB20B3A21413C322141FECA38BBC91420160000000000
10 LET S=16514
20 LET A=0
30 LET H=PEEK (S+2*A)-28
40 IF H<0 OR H>15 THEN STOP
50 LET L=PEEK (S+2*A+1)-28
60 IF L<0 OR L>15 THEN STOP
70 LET N=16*H+L
80 POKE S+A,N
90 LET A=A+1
100 GOTO 40

Letters, We Get Letters . . .

Several readers have asked, “Is there any difference between PRINT USR B and LET A = USR B and RUN USR B and RAND USR B? When should each one be used?”

To Basic, USR is just another function, like INT or SQR or SIN. It takes one number (the “argument”), does something to it, and results in another number. For example, SQR 4 takes the number 4, finds its square root, and results in the value 2. You can use SQR 4 anywhere you can use the number 2, e.g.,

LET A=SQR 4

will give A the value of 2.

USR works the same way. It is always looking for an argument, and it always produces a number. But, when the computer gets to a USR function in a Basic program, it goes flying off to perform your Z80 machine code routine. When it finally RETurns to Basic, the number that was in register pair BC is sent back to Basic as the result.

In other words, you can use USR B anywhere that you can use a number or a variable in Basic. PRINT USR B will print whatever was in BC on the screen. RUN USR B will treat what was in BC as a line number and try to run the Basic program starting there. RAND USR B will take BC and make it the new “seed” for the random number generator—a pretty harmless thing to do with it.

LET A=USR B

will make A equal to whatever was in BC. In fact, you can use USR B with any command or function that can accept a number; there is a complete list of them in chapter 21 of the Timex manual.

The rule of thumb is this: do not tell the computer to do anything you do not want it to do. If you do not want something on the screen, do not PRINT USR B. If you do not want to lose the number in A, do not LET A = USR B.

Get the idea? Treat USR B like a number, and apply a large helping of common sense. It should not give you any trouble.

Coming Attractions

Next time we will look at the “calculator” in your TS1000 or ZX81 ROM. It is surprisingly easy to use, and with a little practice you can make floating point numbers and complicated math functions part of your machine code programs. We will also take a quick look at machine code programming on the new Timex Sinclair 2068 Color Computer.

If you have comments or questions about machine code programming, or if 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.