Netronics TINY BASIC was designed to be a small
but powerful language for hobbyists. It allows the user to write and debug
quite a variety of programs in a language more "natural" than hexadecimal
absolute, and programs written in TINY are reasonably
compact. Because the language is small, it is not as convenient for some
applications as perhaps a larger BASIC might be, but
the enterprising programmer will find that there is very little that cannot
be done from TINY with only occasional recourse to
machine language. This is, in fact. as it should be: the high level language
provides the framework for the whole program, and the individual esoteric
functions done in machine language fill in the gaps.
First, how do subroutines work? In 1802 machine language a subroutine may be called with the SEP instruction. This changes the Program Counter to a different address register, while keeping the return address in the original Program Counter register. This is rather limiting in the number of subroutines which may be called, so there are tricks to save the return addresses on a stack so that the address registers may be re--used. These tricks are described in the RCA reference manual for the 1802 (MPM-201A) and are not important to the discussion here.
When the subroutine has finished its operation it executes another SEP instruction to return control to the program that called it. Depending on what function the subroutine is to perform, data may be passed to the subroutine by the calling program in one or more of the CPU registers, or results may be passed back from the subroutine to the main program in the same way. If the subroutine requires more data than will fit in the registers then memory is used, and the registers contain either addresses or more data. In some cases the subroutine has no need to pass data back and forth, so the contents of the registers may be ignored.
If the main program and the subroutine are both written in TINY BASIC you simply use the GOSUB and RETURN commands to call and return from the subroutine. This is no problem. But suppose the main program is written in TINY and the subroutine is written in machine language? The GOSUB command in TINY is not implemented internally with a SEP instruction, so it cannot be used. This is rather the purpose of the USR function.
The USR function call may be written with up to three arguments. The first of these is always the address of the subroutine to be called. If you refer to USR(l2345) it is the same as if you had written a sequence of instructions to load decimal 12345 into address register R3, followed by SEP R3; The computer saves its return address in a register (you may assume it is in R5) and jumps to the subroutine at (decimal) address 12345 with P=3.
So now we can get to the subroutine from a TINY BASIC program. Getting back is easy. The subroutine simply executes a SEP R5 instruction, and TINY BASIC resumes from where it left off. For those of you which worry about such things, TINY BASIC does use the Standard Call and Return Technique described by RCA, so the return address is actually in R6. R5 actually points to another subroutine whose sole function is to copy the address out of R6 into R3, and to pop a new address off the stack into R6. But this need not concern you unless your machine language routine also needs to call other subroutines.
If you want to pass data from TINY to the subroutine in the CPU registers, you may do that also. This is the purpose of the second and third arguments of the USR function call. If you write a second argument in the call, this is evaluated and placed in R8; if you write a third argument it goes into RA; the low byte of the last argument is also placed in the accumulator (the D register). If there are results from the subroutine's operation, they may be returned in RA.1 and the D register, and TINY will use them as the value of the function (D contains the low byte of the 16-bit result). Thus writing the TINY BASIC statement
LET P = USR ( 12345, 0, 13 )is approximately equivalent to writing in machine language
LDI #30Now actually there are some disc, repancies. As I said, the program does not go back immediately. Also, TINY only works with 16-bit numbers, though I did not show what happens to RA.1. If you have trouble understanding 1802 machine language, you will probably man t to work through A SHORT COURSE IN PROGAMMING, also available from Netronics.
PHI R3
LDI #39
PLO R3
LDI 00
PHI R8
PLO R8
LDI p
PLO RD
LDI 13
SEP R3
STR RD
It is important to realize that the three arguments in the USR function are expressions. That is, any valid combination of (decimal) numbers, variables, or function calls joined together by arithmetic operators can be used in any argument. The following is a perfectly valid statement In TINY BASIC:
13 P=P+0*USR(256+24,USR(256+20,47),13)It happens that memory address 0114 is the machine language routine for the PEEK function, and 0118 (decimal 280) is the POKE routine. When this line is executed, the inner USR call occurs first, jumping to the PEEK subroutine to look at the contents of memory location 002F; this byte is returned as its value, which is passed immediately as the second argument of the outer call, which stores a carriage return at the memory location addressed by that byte. We are not interested in any result data from that store operation, so the result is multiplied by 0 (giving zero) and added to some variable (in this case P), which leaves that variable unchanged. We could also have written (in this case)
13 POKE PEEK (47), 13What kinds of things can we use the USR function for? As we saw in the example above, we can use it to do the PEEK and POKE operations, though it is hardly worth the trouble. Until you get around to writing your own machine language subroutines, you can use it for certain types of input and output.
On the other hand, you can use the USR function to directly access the character input and output routines that TINY uses. but you need to be careful that the characters do not come faster than your TINY BASIC program can take them. The following program inputs characters, converts lower case letters to capitals, then outputs the results:
10 REM READ ONE CHARACTERBecause of the timing limitations of direct character input, it may be preferable to use the buffered line input controlled by the INPUT statement of TINY. Obviously for input of numbers and expressions there is no question, but for arbitrary text input it is also useful, with a little help from the PEEK function. The only requirement is that the first non-blank characters be a number or (capital) letter. Then the command
20 A=USR(256+6)
30 REMOVE PARITY FOR TESTING
40 A=A-A/128*128
50 REM IF L.C., MAKE CAPS
60 IF A>96 IF A<123 THEN A=A-32
70 REM OUTPUT IT
80 A=USR(256+19,A,A)
90 GO TO 10
300 INPUT Xwhere we do not care about the value in X, will read a line into the line buffer, affording the operator (that's you) the line editing facilities (backspace and cancel), and put what TINY thinks is the first number of the line into the variable X. Now, remembering that the line buffer is in 0030 to 0078 (approximately, the ending address varies with the length of the line), we can use the PEEK function to examine the characters at our leisure. To read the next line it is essential to convince the line scanner in TINY that it has reached the end of the input line. Location 002F normally contains the current pointer to the input line; if it points to a carriage return the next INPUT statement will read a new line, so all that is needed is to store a carriage return (decimal 13) in the buffer memory location pointed to by this address (see line 13 above).
In a similar fashion we can access the cassette routines to save some part of memory, or to reload it. The cassette save routine is at location 2557 (decimal); the following command saves the contents of the TV display buffer (locations 0DB0-0F08) on cassette:
A=A+0*USR(2557,3848,3504)Here the first argument is the address of the routine, the second argument is the ending memory address, and the third is the starting memory address. This will save the actual dots of the display, which can be reloaded into another program (like the sample routine in the Basic ELF II instruction manual) for display. You can also save any data areas you may have set up (more about these later). You should be aware, however, that the machine language routine does not put out the "TURN ON RECORD" message, and the cassette recorder must be turned on immediately when this command is executed (watch the Q light; it will come on when it starts to record the leader).
The cassette load routine was also not designed for this application, but it will work, if you understand what is going on. If the data loads correctly you will get error stop #556 (syntax error), but if there is a tape read error, no message results. I know it's backwards, but as I said, it was not designed for this; I only thought of it while writing this section. The address is 2554, and the following command will read a block from the cassette into a memory buffer whose address is in variable B:
IF USR(2554,1,B)*0=0 THEN PRINT "TAPE ERROR"Notice that no ending address is specified in this call. That is because the routine used in TINY reads until an error occurs, or two consecutive bytes of zero are read. Sorry about that! All those pretty pictures on the screen you can save, but you cannot reload into memory in TINY, because they are mostly zeros (all the black space is zero). You can, however, save and reload a data block from some other part of memory (if it has no consecutive zeros; machine language code usually meets this requirement).
If we are careful, we can fill up the beginning of the TINY BASIC program with long REM statements and use them to hold character strings (this allows them to be initialized when the program is typed in). For example:
2 REMTHIS 15 A 50-CHARACTER DATA STRING FOR USE IN TINYIf you insert one line in front to GOTO the first program line, then your program may run just a little bit faster and you do not need the letters REM at the beginning of each line (though you still need the line number and the carriage return). If you are careful, you can POKE the carriage returns out of all but the last line and the line numbers from all but the first line (replace them with data characters), and it will look like a single line to the interpreter. Under no circumstances should you use a carriage return (decimal 13) as a data character; if you do, none of the GOTOs, GOSUBs or RETURNs in your program will work.
3 REM0 1 2 3 4 5
4 REM12345678901234567890123456789012345678901234567890
5 REM...IT TAKES 56 BYTES IN MEMORY: 2 FOR THE LINE #,
6 REM.....3 FOR THE "REM", AND ONE FOR THE TERMINAL CR.
Gee, you say, if it weren't for that last caveat, I could use the same
technique for storing arrays of numbers.
You can let the variable A hold the starting address of an array and N the number of elements, and a bubble sort would look like this:
500 LET J=1Of course this is not the most efficient sort routine and it will be veerrry sloooow. But it is probably faster than writing one in machine language, even though the machine language version would execute much faster. There are better sorting algorithms which you can code into TINY; I did not use one of them because they are more complicated.
510 LET K=0
520 IF PEEK(A+J) >= PEEK(A+J-1) GOTO 540
525 K=PEEK(A+J)+256
530 POKE A+J,PEEK(A+J-1)
535 POKE A+J-1,K
540 J=J-1
550 IF J<N THEN GOTO 520
560 IF K<>0 GOTO 500
570 END
When you execute a GOSUB (or, more precisely, when TINY executes one), the line number of the GOSUB is saved on a stack which grows downward from the end of the user space. Each GOSUB makes the stack grow by two bytes, and each RETURN pops off the most recent saved address, to shrink the stack by two bytes. Incidentally, because the line number is saved and not the physical location in memory, you do not need to worry about making changes to your program in case of an error stop within a subroutine. Just don't remove the line that contains an unRETURNed subroutine, unless you are willing to put up with TINY's complaint.
The average program seldom needs to nest subroutines (ie. calling subroutines from within subroutines) more than five or ten levels deep, and many computer systems are designed with a built-in limitation on the number of subroutines that may be nested. The 8008 CPU was limited to eight levels; the 6502 is limited to about 120. Many BASIC interpreters specify some maximum. I tend to feel that stack space, like most other resources, obeys Parkinson's Law: the requirements will expand to exhaust the available resource. Accordingly, the TINY BASIC subroutine nest capacity is limited only by the amount of available memory. This is an important concept. If my program is small (the program and stack contend for the same space), I can execute hundreds or even thousands of GOSUBs before the stack fills up. If there are no corresponding RETURN statements, all that memory just sits there doing nothing.
If you read your User's Manual carefully, you will recall that memory locations 0026-0027 point to the top of the GOSUB stack. Actually they point to the next byte not yet used. The difference between that address and the end of memory (found in 0022-0023) is exactly the number of bytes in the stack. One greater than the value of the top-of-stack pointer is the address of the first byte in the stack.
If you know how many bytes of data space you need, the first thing you can do is execute half that many GOSUBs:
400 REM B IS THE NUMBER OF BYTES NEEDEDBe careful that you do not try to call this as a subroutine, because the return address will be buried under several hundred "420"s. If you ware to add the line,
410 LET B=B-2
420 IF B> -2 THEN GOSUB 410
430 REM SIMPLE, ISN'T IT?
440 RETURNthe entire stack would be emptled before you got back to the calling GOSUB. Remember also that if you execute an END command the stack is cleared, but an error stop or a break will not affect it. Before you start this program you should be sure the stack is clear by typing END; otherwise a few tines through the GOSUB loop and you will run out of memory.
If you are careful to limit it to the main program, you can grab bytes out of the stack as the need arises. Whether you allocate memory with one big grab, or a little at a time, you may use the PEEK and POKE operations to get at it.
The other way for using the stack for storing data is a little more prodigal of memory, but it runs faster. It also has the advantage of avoiding the POKE command, in case that still scares you. It works by effectively encoding the data in the return address line numbers themselves. The data is accessed, in true stack format: last in, first out. I used this technique successfully in implementing a recursive program in TINY BASIC.
This method works best with the computed GOTO techniques described later, but the following example will illustrate the principle: Assume that the variable Q may take on the values (-1, 0, +1), and it is desired to stack Q for later use. Where this requirement occurs, use a GOTO (not a GOSUB) to jump to the following subroutine:
3000 REM SAVE Q ON STACKWhen the main program wishes to save Q, it jumps to the entry (line 3000), which selects one of the three GOSUBs. These all converge on line 3200, which simply jumps back to the calling routine; the information in Q has been saved on the stack. To recover the saved value of Q it is necessary only to execute a RETURN. Depending on which GOSUB was previously selected, execution returns to the next line, which sets Q to the appropriate value, then jumps back to the calling routine (with a GOTO again!). Q may be resaved as many times as you like (and as you have memory for) without recovering the previous values. When you finally do execute a RETURN you get the most recently saved value of Q.
3010 IF Q<0 THEN GOTO 3100
3020 IF Q>0 THEN GOTO 3150
3050 REM Q=0. SAVE IT.
3060 GOSUB 3200
3070 REM RECOVER Q
3080 Q=0
3090 GOTO 3220
3100 REM Q<0. SAVE IT.
3110 GOSUB 3200
3120 REM RECOVER Q
3130 Q=-1
3140 GOTO 3220
3150 REM Q>0. SAVE IT.
3160 GOSUB 3200
3170 REM RECOVER Q
3180 Q=1
3190 GOTO 3200
3200 REM EXIT TO (SAVE) CALLER
3210 GOTO . . .
3220 REM EXIT TO (RECOVER) CALLER
3230 GOTO . . .
For larger numbers, the GOSUBs may be nested, each saving one bit (or digit) of the number. The following routine saves arbitrary numbers, but in the worst case requires 36 bytes of stack for each number (for numbers less than -16383):
1470 REM SAVE A VALUE FROM VNote that this subroutine is designed to be placed in the path between the calling routine and some subroutine which re-uses the variable V. When the subroutine returns, it returns through the restoral part of this routine, which eventually returns to the main program with V restored. The subroutine which starts at line 1550 is assumed to be recursive, that is, it may call on itself through this save routine, so that any number of instances of V may be saved on the stack. The only requirement is that to return, it must first set V to 0 so that the restoral routine will function correctly. Alternatively wee could change line 1550 to jump to the start of the subroutine with a GOSUB:
1480 IF V>=0 THEN GOTO 1490
1482 LET V=-1-V
1484 GOSUB 1490
1486 LET V=-1-V
1488 RETURN
1490 IF V>V/2*2 THEN GOTO 1510
1500 GOSUB 1520
1502 LET V=V+V
1504 RETURN
1510 GOSUB 1520
1512 LET V=V+V+1
1514 RETURN
1520 IF V=0 THEN GOTO 1550
1522 LET V=V/2
1524 GOTO 1490
1550 REM GO ON TO USE V FOR OTHER THINGS
1550 GOSUB . . .This requires another two bytes on the stack, but it removes the restriction on the exit conditions of the recursive subroutine.
1552 LET V=0
1554 RETURN
If you expect to put a hundred or more numbers on the stack in this
way you might consider packing them more tightly. If you use ten GOSUBs
and divide by 10 instead of 2 the numbers will take one third the stack
space. Divide by 41 and any number will fit in three GOSUBs, but the program
gets rather long.
One common way to handle dollars and cents is to treat it as an integer number of cents. That would be OK if your balance never vwent over $327.67, but that seems a little unreasonable. Instead you can break it up into separate numbers for dollars and cents as in Chapter 5 in the first part of this book. This allows you balance to go up to $32,767.99 which is good enough for most of us. I will not dwell on this example here, but the idea can be extended. You can handle numbers as large as you like, putting up to four digits in each piece.
A similar technique may be used to do floating point arithmetic. The exponent part is held in one variable, say E. and the fractional part is held in one or more additional variables. In the following example we will use a four-digit fractional part in M, adding to it a number in F and N:
1000 REM FLOATING POINT ADD FOR TINY BASICThis subroutine uses decimal normalization; by changing the divisors and multipliers appropriately, it can be made into a binary, hexadecimal, or even ternary floating point machine. By using the multiple precision techniques described in the checkbook balancing example, greater precision can be obtained in the fractional part.
1010 IF E-4>F THEN RETURN
1020 IF N=0 THEN RETURN
1030 IF E+4<F THEN LET M=0
1040 IF M=0 THEN LET E=F
1050 IF E=F GOTO 1130
1060 IF E>F GOTO 1100
1070 E=E+1
1080 M=M/10
1090 GOTO 1040
1100 F=F+1
1110 N=N/10
1120 GOTO 1020
1130 M=M+N
1140 IF M=0 THEN E=0
1150 IF M=0 RETURN
1160 IF M>9999 THEN GOTO 1230
1170 IF M>999 RETURN
1180 IF M<-9999 THEN GOTO 1230
1190 IF M<-999 RETURN
1200 M=M*10
1210 E=E-1
1220 GOTO 1170
1230 E=E+1
1240 M=M/10
1250 RETURN
110 IF A=1 GOTO 1000Now there-is nothing wrong with this form of program, but I'm too lazy to type all that, and besides, I could not get his whole program into my memory. Instead of lines 110 to 140 above, the single line
120 IF A=2 GOTO 2000
130 IF A=3 GOTO 3000
140 IF A=4 GOTO 4000
150 GOTO 100
125 IF A>0 IF A<5 GOTO A*1000does exactly the same thing in less memory, and probably faster.
Another part of this program simulated a card game, in which the internal numbers, 11-14 were recognized (using the same kind of sequence of IFs) in three different places, and for each different number, the name of the corresponding face card was printed. The thing that caught my attention was that the same sequence of IFs, PRINTs, and GOTOs was repeated three different places in the program.
Now I'm glad this person enjoys using TINY BASIC, and that he likes to type in large programs to fill his voluminous memory; but as I said, I'm lazy, and I would rather type in one set of subroutines:
10110 PRINT "JACK"then in each of the three places where this is to be printed, use the simple formula:
10115 RETURN
10120 PRINT "QUEEN"
10125 RETURN
10130 PRINT "KING"
10135 RETURN
10140 PRINT "ACE"
10145 RETURN
2510 GOSUB 10000+B*10Along the same line, when memory gets tight, you may be able to save a few bytes with a similar techneque. Suppose your program has thirteen "GO TO 1234" statements in it. If you had an unused varlable (say, U) you can, in the direct execution mode, assign it the value 1234 (i.e. the line number all those GOTOs go to), then replace each "GO TO 1234" with a "GOTOU", squeezing out the extra spaces (TINY ignores them anyway). This will save some thirty or forty bytes, and it will probably run faster also.
R=2+3But the second form will execute much faster because it is unnecessary for the interpreter first to ascertain that it is not a REM, RUN, or RETURN statement. In fact, the LET keyword is the first tested, so it becomes the fastest-executing statement, whereas the other form must be tested against all 17 keywords before it is assumed to be an assignment statement.
LET R=2+3
Another way to speed up a program depends on the fact that constant numbers are converted to binary each time they are used, while variables are fetched and used directly with no conversion. If you use the same constant over and over and you do not otherwise use all the variables, assigning that number to one of the spare variables will make the program both shorter and faster. You can even make the assignment in an unnumbered line; the variables keep their values until explicitly changed.
Finally it should be noted that GOTOs, GOSUBs and RETURNS always search the program from the beginning for their respective line numbers. Put the speed-sensitive part of the program near the front, and the infrequently used routines (setup, error messages, and the like) at the end. This way the GOTOs have fewer line numbers to wade through, so they will run faster.
As we have seen, there is not much that TINY BASIC cannot do (except maybe go fast). Sure, it is somewhat tedious to write all that extra code to get bigger numbers or strings or arrays, but you can always code up subroutines which can be used in several different programs (like the floating point add, lines 1000-1250), then save them off on cassette.
Remember, your computer (with TINY BASIC in it) is limited only by your imagination.