Plotting with 4K Basic

See all articles from SYNC v2 n2

Have you tried plotting with the 4K Basic? Or experimented with user-input functions? The 4K Basic does not seem well suited to those purposes, does it? For many plotting applications, resolution appears limited to the size of the “inverse space.” Functions being used repeatedly need to be in the body of the program; changing them with each run is a source of bugs.

Of course, if you have the 8K Basic, your problems are largely solved by using PLOT, PRINT AT and VAL. But how would you have them with 4K Basic?

If you have only 1K of RAm, this program provides you with a field of 10 lines by 7 columns for plotting. (More RAM? See the notes at the end of this article.) Pixel graphics provide 280 plotting positions: the field is 20 pixels high by 14 pixels wide.

After loading the program, key GOTO 30, NEWLINE. The plotting field will appear, with the prompt XM=. Supply the starting value for X (the independent variable); key NEWLINE. The prompt XS= will appear. Supply numerical value which you want to represent by one pixel in the horizontal direction. (Remember that there are 14 pixels to the line.) Key NEWLINE. The prompt YS= will appear. Enter the numerical value which you want to represent by one pixel in the vertical direction (with 20 pixels to the column); key NEWLINE.

At last the prompt Y= will appear, with the cursor between quotation marks. Enter the expression you wish to plot. For example, if you want to plot a parabola, your entry might be X**2-96. (Your expression is limited to ten characters and can include both arithmetic and logical operators. Of course, integer math will govern the outcome of your calculations.) End the expression with NEWLINE. After a pause of about two seconds, your plot will appear.

After one expression has been plotted, you will see the cursor inside the quotation marks. You will not see the expression printed on the screen. (This is to avoid out-of-screen and out-of-memory errors.) Your response to the cursor depends on your choice from among three options:

a) To overlay the plot of another expression on the existing plot, using the current values of XM, XS and YS: Key M (More), NEWLINE, then the new expression and NEWLINE.

b) To produce a clean field for a new expression: Key N (New), NEWLINE, then proceed as before.

c) To exit the program: Key NEWLINE without M or N.

The line at the middle of the field represents the line Y=0 (the “X axis”). That does not actually limit the range of Y values you can examine. For example, suppose you want to plot Y=X**2. All of the Y values will be positive. However, you do not have to waste the lower half of the field. You can use its full height by subtracting a number from Y. Suppose XM=-6 and XS=1. Then the maximum X will be +7. The greatest value of Y will be +49, the least, 0. A value of YS=6 would make the plot fit the field, but use only the upper half. Instead, plot Y=X**2-25. Now you can use YS=3 to spread your plot some, using the entire field. Just remember that your plot has been arbitrarily shifted down by 25.

If you try to enter an expression longer than ten characters, the program will return to the prompt without attempting to plot.

Values of Y outside the range of the field will not blow the program—they will just be skipped. Operations which cause overflow and illegal operations will stop the program.

The three major challenges to constructing the program were:

a) to create the field, reserving space in the display file;

b) to process the user-input expression without returning to the list;

c) to determine and plot the correct ZX80 character.

Creating the Field

The field is essentially a rectangle formed of spaces, with a line of shifted W’s for the X axis. In order to calculate plot positions within the field, I wanted the lines to be of known and constant length. The solution was to print a series of spaces: four lines of seven spaces, one line of seven shifted T’s, and five lines of seven spaces.

After the field routine ran well in Basic, I rewrote it in Z80 machine language, using a program by Dr. I. S. Logan (SYNC 1:2) as my model (see Listing 2). The advantages were conservation of program memory (leaving more memory for the display file) and improved speed of execution; about two seconds in Basic became “instantaneous” in machine language.

(Finding that I could program the ZX80 in machine language led me to investigate the subject further. For the interested reader, I would strongly urge the purchase of How to Program the Z80 by Rodney Zaks.)

Processing the Expression

When input, the expression is stored as Y$. I reserved space for a numerical copy of the expression in line 10, which is initialized as LET Y=000000000000. Line 20 is a RETURN, allowing the expression to be evaluated repeatedly by the command GOSUB 10.

Lines 200-270 accomplish the conversion of the expression from a string variable to a program line. CODE(Y$) determines the first character in the expression. That character is then POKEd into the location originally occupied by the first 0 in line 10. TL$(Y$) then removes the first character of Y$. The operation is repeated until the entire expression has been POKEd into line 10, or until an eleventh character is detected. If the final character in the expression is the tenth, or lower, then a “+” is POKEd into the next location in line 10, to provide a mathematically correct expression.

Would your believe that this program modifies itself? For example, after processing Y=X**2-25, line 10 will read

10 LET Y=X**2-25+00000.

While this technique may be valuable in other applications, you should observe the following cautions: Be sure that you modify only the portion which you intend to. Be very sure that your documentation (REMARKs, user notes, etc.) states clearly what you have done, and what the purpose is. Otherwise, someone else using your program may become very confused by such changes in the program listing, especially if unexplained errors have occurred during a run. Even you might forget just what you had intended.

The program is modified again, to the initialized state, with another machine language routine which restores the 0’s to line 10. See Listing 3. This is done to allow the More and New options, while still keeping the user out of the list. I considered using a Basic FOR-TO-NEXT loop, or setting lines 200-270 apart as a subroutine which could then have processed Y$=”000000000000″.

In each case, the memory “overhead” seemed too high for 1K: The subroutine approach would have used an additional 38 bytes; the machine language routine uses only 11 bytes. (An additional four bytes are used by the Basic REMARK line in which the routine is stored. This was done for the sake of clarity; the 0-POKEing routine could have been stored in the same line as the field routine.)

Determining the Character

Given the field coordinates (row and column) of the pixel to be plotted, the problem was to determine which character (if any) occupied those coordinates, and replace it with a character containing the required pixel and any existing pixel.

The ZX80 character set provides characters which represent all possible combination of pixels grouped two by two—from the “space” (nothing plotted) to the “inverse space” (all four locations plotted). Since each pixel in a group can be only dark or light, it is easy to view the group as a four digit binary number: a 0 represents a light pixel and a 1 represents a dark one. I did that and coded the possible group configurations that way. See Figure 1.

Figure 1. A character cell divided into four pixels: digit 3 (P=3) and digit 2 (P=2) on top, where Y/2≠Y-(Y/2); digit 1 (P=1) and digit 0 (P=0) below, where Y/2=Y-(Y/2); the left column is I/2=I-(I/2), the right column I/2≠I-(I/2). Example: binary 1000 (decimal 8) + binary 0110 (decimal 6) = binary 1110 (decimal 14)
Figure 1. A character cell divided into four pixels: digit 3 (P=3) and digit 2 (P=2) on top, where Y/2≠Y-(Y/2); digit 1 (P=1) and digit 0 (P=0) below, where Y/2=Y-(Y/2); the left column is I/2=I-(I/2), the right column I/2≠I-(I/2). Example: binary 1000 (decimal 8) + binary 0110 (decimal 6) = binary 1110 (decimal 14)

Figure 2.

             J =
           DECIMAL OF     C(J)=
D(3) D(2) D(1) D(0)   BINARY CODE   ZX-80 CODE
 0    0    0    0          0            0
 0    0    0    1          1            7
 0    0    1    0          2            6
 0    0    1    1          3            3
 0    1    0    0          4            5
 0    1    0    1          5          130
 0    1    1    0          6            8
 0    1    1    1          7          132
 1    0    0    0          8            4
 1    0    0    1          9          136
 1    0    1    0         10            2
 1    0    1    1         11          133
 1    1    0    0         12          131
 1    1    0    1         13          134
 1    1    1    0         14          135
 1    1    1    1         15          128
Figure 2 also shows, in a first column headed CHARACTER, the ZX80 graphic character for each code
Figure 2 also shows, in a first column headed CHARACTER, the ZX80 graphic character for each code

Unfortunately, the ZX80 character set does not use such a transparent system of values. In order to translate the ZX80 codes into my system of binary codes, I took the following approach:

I created arrays C (with 16 elements) and D (with 4 elements). D is used to hold the binary code for a particular group: Each element of D is either a 1 or a 0 depending on the pixel it represents. C is loaded with ZX80 character codes in such a fashion that C(J) contains the ZX80 code of the character which has a binary code equal to (decimal) J. For example, C(0)=0, the space; C(15)=128, the inverse space. See Figure 2.

The values of I (equivalent to X/XS) and Y are first converted into field rows and columns. Then a PEEK is done at the corresponding location in the display file to determine which character is currently at that location. The ZX80 code for that character is matched against the elements of C until the correct match is found, or until all elements have been compared. If a match is found, the value of J at that point is taken as the decimal value of the binary code for the character. If no match can be made, the character is treated as a space, and J is set equal to zero.

Lines 540-570 convert the decimal value of J into binary, loading the elements of D with either 1’s or 0’s as appropriate. This is done by a series of (integer) divisions, which have the additional use of leaving J set equal to zero. Then the element of D which corresponds to the pixel to be plotted, D(P), is set equal to one, effectively making that pixel dark, regardless of its previous state.

Lines 600-620 convert the elements of D back into a decimal value, again called J. Element C(J) is now the ZX80 code for the desired character to be plotted, and it is POKEd into the same location where the PEEK was done.

When you study the program listing, you will not see any lines which DIMENSION C and D, or any which give the values to the elements of C. These operations take place during the original entering and saving of the program. Listing 4 shows a program which will create C and D and provide the proper values of C. This program should be entered and run before the plotting program is entered. C and D will then exist in memory and will be saved along with the plotting program. After you have run Listing 4, be sure not to key NEW to enter the plotting program—just enter the new lines of code. The line numbers shown in Listing 4 have been chosen to match those of the final program, so you do not have to delete anything.

The purpose of this manner of handling C and D is primarily to conserve memory. The 4K Basic always saves variables along with the program. This is true even if the “variable” is actually an array of constants, or one in which all the elements equal zero. Consequently, it is not always necessary to use up memory with lines of code for creating constants or dimensioning arrays. Nor is it necessary to use cumbersome, slow-running routines to simulate DATA statements. (DATA statements are provided in many version of Basic for just this purpose.) There are disadvantages and dangers involved in the technique used here! Again, the intent is not clear unless the documentation spells out what is going on. Also, if the program is kicked off by RUN rather than a GOTO command, the variable storage area of memory will be cleared—including C and D! The ZX80 will not recognize C and D when it encounters them in the program; this will produce an error situation and will stop the run.

Listing 1

 10 LET Y=000000000000
20 RETURN
30 LET I=USR(16427)
100 PRINT "XM=";
110 INPUT XM
120 PRINT XM,"XS=";
130 INPUT XS
140 PRINT XS,"YS=";
150 INPUT YS
160 PRINT YS,"Y="
170 INPUT Y$
180 LET I=USR(16495)
200 LET I=16512
210 IF Y$="" THEN GO TO 270
220 IF I=16522 THEN GO TO 170
230 POKE I,CODE(Y$)
240 LET I=I+1
250 LET Y$=TL$(Y$)
260 GO TO 210
270 POKE I,19
300 FOR I=0 to 13
310 LET X=XM+I*XS
320 GOSUB 10
330 LET Y=Y/YS
340 IF Y<-10 OR Y>9 THEN GO TO 640
400 LET P=0
410 IF I/2=I-I/2 THEN LET P=1
420 IF NOT Y/2=Y-Y/2 THEN LET P=P+2
430 LET X=I/2+1
440 LET Y=(9-Y)/2
450 LET C=PEEK(256*PEEK(16397)+PEEK(16396)+8*Y+X)
500 FOR J=0 TO 15
510 IF C(J)=C THEN GO TO 540
520 NEXT J
530 LET J=0
540 FOR K=0 TO 3
550 LET D(K)=J-2*(J/2)
560 LET J=J/2
570 NEXT K
580 LET D(P)=1
600 FOR K=0 TO 3
610 LET J=J+D(K)*2**K
620 NEXT K
630 POKE 256*PEEK(16397)+PEEK(16396)+8*Y+X,C(J)
640 NEXT I
700 INPUT Y$
710 IF Y$="N" THEN GO TO 30
720 IF Y$="M" THEN GO TO 170

SYNCSUM= 59 (As the listing is shown—without REM lines 1 and 2 or any machine language routines.)

Listing 2

USR(16427)

    LD HL, (DFILE)       42, 12, 64    Locate start of Display File.
INC HL 35 Point to first column, first line.
LD B, #4 (#9) 6, 4 (9) Set up for top 4 (9) lines.
(1) PUSH BC 197 Save line tally (B).
CALL SUB I 205, 94, 64 Create a line.
POP BC 193 Recover line tally.
DJNZ (1) 16, 249 Repeat for 4 (9) lines.
LD B, #7 (#32) 6, 7 (32) Prepare to draw X axis.
(2) LD (HL), #3 54, 3 Poke code of X axis character (CHR$(3)).
INC HL 35 Point to next column.
DJNZ (2) 16, 251 Repeat for 7 (32) columns.
LD A, #117 62, 117 Prepare for "NEWLINE".
INC A 60 Increase A to 118 (="NEWLINE").
LD (HL),A 119 POKE "Newline" at end of line.
INC HL 35 Point to first column, next line.
LD B, #5, (#10) 6, 5 (10) Set up for lower 5 (10) lines.
(3) PUSH BC 197 Save line tally.
CALL SUB I 205, 94, 64 Create a line.
POP BC 193 Recover line tally.
DJNZ (3) 16, 249 Repeat for 5 (10) lines.
LD DF-EA, HL 34, 14, 64 Tell System where field ends.
LD DF-END, HL 34, 16, 64
LD A, #14 (#4) 62, 14 (4) Set up System Line Counter.
LD LINE COUNTER, A 50, 37, 64
LD A, #0 62, 0 Set up System Space Counter.
LD SPACE COUNTER, A 50, 36, 64
RETURN 201
(I) LD B, #7 (#32) 6, 7 (32) Set up for 7 (32) columns.
(4) LD (HL), #0 54,0 POKE a "space".
INC HL 35 Point to next column.
DJNZ (4) 16, 251 Repeat 7 (32) times.
LD A, #117 62, 117 Prepare for "NEWLINE".
INC A 60 Put 118 in Accumulator.
LD (HL), A 119 POKE "NEWLINE" at end of line.
INC HL 35 Point to the first column, next line.
RETURN 201

Listing 3

USR(16495)

    LD HL, ADDRESS       33, 128, 64   Point to first character
LD B, #12 6, 12 of 12 characters.
(1) LD (HL), #28 54, 28 POKE the first zero.
INC HL 35 Point to next position.
DJNZ (1) 16, 251 Repeat 12 times.
RETURN 201

Listing 4

Loading C and D

 10 DIMENSION C(15)
20 CLS
30 FOR J=0 TO 15
100 INPUT C(J)
110 PRINT J, C(J), CHR$(C(J))
120 NEXT J
130 DIMENSION D(3)
140 FOR I=0 TO 3
150 PRINT "D(";I;")=";D(I)
160 NEXT I

Enter and run this program. Enter, in order; 0, 7, 6, 3, 5, 130, 8, 132, 4, 136, 2, 133, 131, 134, 135, 128. The screen will display the relationship between the binary code (giving the decimal value, J) which the program uses, the ZX80 character code, and the character (group of pixels) being represented. Below that, D(0) through D(3) should show as zero.

From this point on, do not use RUN or CLEAR unless you have saved arrays C and D—or you are willing to do this again.

Loading Machine Language Routines

Listing 5 is a program to load the routines shown in Listings 2 and 3. Space is reserved in the REMARK lines 1 and 2 for storing the routines. Enter the program as shown in Listing 5. You may do this either before or after you run the program of Listing 4; if you do it after, remember not to use RUN, but rather GOTO 10. Again, the line numbers have been chosen to avoid the need to delete lines when entering Listing 1.

The PRINT commands which have no obvious purpose are there in order to move lines 1 and 2 off the top of the screen. This is because after the original numbers in those lines have been replaced with Z80 machine language, attempts by the Basic interpreter to produce characters from that language can be fatal. Consequently, after the program has been started, no attempt should be made to LIST the whole program, or lines 1 and 2. LIST 10 is fine. On the other hand, after you have saved the routines, go ahead and LIST, just to see the results.

When you run Listing 5, it will stop for the input of the machine language. This is done by providing, in sequential order, the numbers shown in the middle columns of Listings 2 and 3. These numbers are the decimal equivalents of the “op codes”—the numbers which the Z80 will interpret as commands. Each number is stored as one byte in the memory of the ZX80, waiting to be read and interpreted for direct execution by the Z80, bypassing the ZX80 4K Basic interpreter.

As the program proceeds, it accumulates the sum of the op codes which you enter. After you have entered all the op codes for a particular routine, the screen will display all the codes which you have entered, along with the value of that sum—the “checksum.” The checksum should be compared with the one shown, providing a fair check for the accuracy of your entries. You should also compare the screen against the copy shown. Corrections can be made by rerunning the entire program, or by POKEing the individual codes. For example, if the fifth code of Listing 1 has been entered as 7, it can be corrected with POKE 16427+4,6.

Listing 5

Loading USR(16427)and USR 16495)

  1 REM 1234567890223456789032345678904234567890523456789062345678907234
2 REM 12345678901
10 LET C=0
20 FOR I=0 TO 63
30 INPUT J
100 LET C=C+J
110 PRINT J,
120 POKE 16427+1,J
130 NEXT I
140 PRINT
150 PRINT "CHECKSUM=";C
160 STOP
170 LET C=0
180 FOR I=0 TO 10
200 INPUT J
210 LET C=C+J
220 PRINT J,
230 POKE 16495+I,J
240 NEXT I
250 PRINT
260 PRINT "CHECKSUM=";C
270 PRINT
300 PRINT
310 PRINT

Line 1: 64 characters after REM—placeholders for Z80 op codes. If the OR routine is being used, line 2 must have 13 more characters.

Enter and run the program. At the prompts for input, enter the op codes shown in Listing 2 in the sequence shown. Use the numbers in parentheses only if you are creating a 20 by 32 field. After 64 op codes have been entered, execution will stop. The screen should look like this:

42      12      64      35
6       4       197     205
94      64      193     16
249     6       7       54
3       35      16      251
62      117     60      119
35      6       5       197
205     94      64      193
16      249     34      14
64      34      16      64
62      14      50      37
64      62      0       50
36      64      201     6
7       54      0       35
16      251     62      117
60      119     35      201

CHECKSUM=4804

Compare this to the screen and note any corrections which will be required. Command CONTINUE. Enter the op codes from Listing 3. After 11 op codes have been entered, execution will stop. The screen should look like:

33      128     64      6
12      54      28      35
16      251     201

CHECKSUM=828

SAVE as soon as you have gotten this far. Remember not to use LIST. LIST 10 (or higher) is fine.

Notes and Exercises

If you have more than 1K of RAM, you can expand the plotting field. A good size is 20 lines by 32 columns, giving a 40 by 64 pixel field. The changes required in the machine language routines are shown in parentheses in Listing 2. Enter those numbers in place of the ones shown. The checksum will, of course, change accordingly. The corresponding changes which need to be made in the Basic program are:

300 FOR I=0 TO 63
340 IF Y<-20 OR Y>18 THEN NEXT I
440 LET Y=(19-Y)/2
450 LET C=PEEK(256*PEEK(16397)+PEEK(16396)+33*Y+X)
630 POKE 256*PEEK(16397)+PEEK(16396)+33*Y+X,C(J)

Another useful change allowed by more RAM would be the usage of more lengthy expressions. Determine the number of characters which you wish to permit in the expression; add two to that, and place that many zeroes in line 10. Change the 12 in Listing 2 to that number as well.

You do not have more RAM? There may be some things you can do to stream line the program, even in Basic. Real size advantages can be had in machine language, though. For example, you can make use of the Z80 OR operation. (See SYNC 1:6 pp. 9-11). OR compares the contents of the accumulator register, A, with a chosen internal register, and performs a logical OR on the contents, bit by bit. The results are placed in the accumulator. Suppose, for instance, that A contains 6 and register L contains 3:

Register    Decimal    Binary
   A           6        0110
   L           3        0011
OR(A,L)        7        0111

Actually, each register contains eight bits rather than four. Only four are shown here to help clarify the point: The operations carried out in lines 540-620, using array D, could be replaced with an OR of J and P**2. Of course, that requires another machine language routine, and some Basic to set things up. Both are shown in Listing 6. The machine language routine makes use of a characteristic of the USR function: USR returns a value—whatever number is in the HL pair of internal registers when the return to Basic is made. Consequently, L is chosen as the register to be ORed with A, H is set equal to zero, and the results are copied from A into L before return. Since L is the least significant byte of the HL pair, and H is zero, the value returned to the Basic program is the value of L alone:

OR(P**2,J).

The savings are significant: Lines 540-630 take up 123 bytes of memory; array D takes up another 10 bytes. Replacement lines 540-560 require 75 bytes and the machine language routine only 13 bytes. (Since we are trying now for better efficiency in the use of storage, I am not including bytes for a third REM line.) If I counted everything properly, the savings are 123+10-75-13 = 45 bytes. That is enough for another four columns or six rows!

Listing 6

USR(16508) and Revised lines 540-560

NOP                 0             Used to store J.
NOP 0 Used to store 2**P.
LD HL, (16506) 42, 122, 64 Put J in register L.
LD A, (16507) 58, 123, 64 Put 2**P in Accumulator.
LD H, #0 38, 0 Clear H (but not L).
OR L 181 OR A with L.
LD L, A 111 Put OR(A,L) in HL.
RETURN 201

(Note that 16508 is the address of the third op code in this routine, rather than the first.) The checksum is 1004.)

Revised Basic lines:

540 POKE 16506, J
550 POKE 16507,2**J
560 POKE 256*PEEK(16397)+PEEK(16396)+8*Y+X,C(USR(16508))

(Delete lines 570 through 630.)

Not only that, but surely the Basic program can be improved on. If you get too tricky, remember your documentation!

With more RAM and/or ingenuity, you could certainly add features to the screen. How about:

Drawing a reference line for X? (Poke a series of characters in a vertical line at some value of X or I. The value could be preselected or user defined. This line could serve as the Y axis.)

Labeling the axes? (Poke the characters X+ at or by the right end of the X axis and Y+ at or by the top of the Y axis.

Finding and labeling the extreme values? (Create two variables—perhaps YMAX and YMIN—and compare each new value of Y to them. Keep the highest or lowest each time. Record their addresses in ADMAX and ADMIN. If you are using a 32 column screen, the addresses would be 33*Y+X, after you have made the conversion from the Y returned by subroutine 10 to the Y used in the field coordinates. Label by POKEing a special character at those addresses (+256*PEEK(16397)+PEEK(16396)) and printing the values elsewhere.)

Printing the expression below the field? (Remember that you have a limited number of rows available for printing.)

Clearing only the lower portion of the screen in order to take in different values of XM, XS and YS after the More response? (Perhaps you could have two More options: with the old values and with new values. Then you could easily compare the effects of scale changes. You should be able to adapt portions of Listing 2 to POKE a series of spaces into the lower lines.)

Whatever you do, however much RAM you have, above all, you should enjoy yourseelf while using this program. For me, that is the major point of entire thing.

Two last pieces of advice:

If the screen goes bonkers or the program crashes, suspect the machine language first. Take a PEEK at all such routines and compare them carefully to the listings. Make sure that the addresses in your USR calls are correct. Confirm that the byte located at each of those addresses is the one that should be there. However, if the program runs, but plots the wrong pixels, check the lines that PEEK at and POKE to screen addresses.

Finally, when the program runs smoothly in its original format, save it and keep it handy to show to friends who wonder what your ZX80 can do besides games. Make another copy to experiment with—leave the original alone until you have another version that works as well.

Have fun!

Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.