by Tom Hudson
Well, Boot Camp fans, here I am again, after a month's absence. I hope all Boot Camp readers typed in the BOFFO program from last issue- it'll be a big help to you in the future when building BASIC USR call structures.
Speaking of BASIC USR calls, that just happens to be the subject of this issue's column. Were going to take a couple of problems and solve them using assembly language, via the USR function.
As we all know, interpreted BASIC is a very slow way to get things done in a computer. For an explanation of what happens in interpreted BASIC, take a look at issue 13's Boot Camp. We're going to look at a way to overcome this inherent sluggishness, using the USR (call user subroutine) function. The format of this function is:
USR( aexp1 [,aexp2] [,aexp3...] )
... where aexp1 is the address of the assembly code to be executed, and aexp2, aexp3, etc. are optional numeric arguments which are passed to the assembly routine..
The USR function simply tells BASIC to perform a JSR to the assembly code located at the address indicated by aexp1. Take a look at the following USR call:
A=USR(1536)
This USR call will execute the code at location 1536 ($0600) and return to BASIC, where the program will continue execution with the next statement. If the subroutine is to return a value to BASIC, it will be returned in the variable A. This USR call doesn't use any arguments.
Many times, you'll want your USR call to process some type of data or accept parameters of some sort. This is done by using the optional arguments. Look at this example:
A=USR(30926,102,Q*3)
This USR function calls the assembly code located at address 30926 ($78CE) and passes two values, 102 and the value of Q*3, to the subroutine. The arguments must be integers in the range 0-65535, and the assembly code must be equipped to handle the two parameters properly.
What happens to the parameters we send? BASIC places them on the 6502 stack for easy retrieval by the assembly code. For example, look at the following USR call:
A=USR(1536,509,200)
This code calls the routine at 1536 ($0600) and passes two arguments, 509 and 200, to the subroutine. After the USR function is processed, and control is passed to the assembly code, the stack looks like Figure 1.
Figure 1.
Remember that the stack grows downward in memory, so the first number on the stack is the number of twobyte arguments placed on the stack by BASIC, or 2. This value is important for assembly code which may require a variable number of parameters, since the subroutine can tell how many parameters are available by this number.
The next item on the stack is the first parameter, 509. As you can see, the high byte of this value will be pulled off the stack first, followed by the low byte. All arguments are placed on the stack in high-byte, low-byte order.
Next in the stack is the second parameter, 200. Like the first argument, it is placed in high-byte (0), lowbyte (200) order.
Finally, the stack contains the return address for the assembly subroutine. In this stack illustration, the address is shown as nnnn, since we don't know where BASIC will go when the assembly routine returns. This is simply an RTS return address, and-after processing the parameters, if any-the subroutine merely executes an RTS instruction to return to BASIC.
Remember how I said the assembly code could return a value to BASIC? If this is necessary, the assembly code should place the low byte of the value in location 212 ($D4) and the high byte in location 213 ($D5). BASIC automatically puts the value into the proper variable. The programmer must take care, however, that the result is in the range 0-65535.
I've always said that there's no better way to learn something than hands-on experience, so let's write a simple program to add two arguments passed by a BASIC program.
We'll place the subroutine on page 6 (you could put it anywhere in free RAM) and call it with the following code:
A=USR(1536,VAL1,VAL2)
Now let's write the assembly code necessary for the routine. First, we must set up the program for binary arithmetic with the CLD instruction. I can't overemphasize how important it is to know the status of the decimal mode flag, especially in programs which perform mathematical operations. Our program looks like this:
Next, we use the PLA instruction to pull the first byte from the stack. As explained earlier, the first byte tells the routine how many arguments were passed to the routine. For simplicity, we'll assume the USR call was properly set up with two arguments, and ignore this value. Now our program looks like this:
The next step is to pull the first argument from the stack, placing it in a temporary storage location. We have several bytes available on page zero, from $CB$Dl, so we'll use $CB (for the low byte) and $CC (for the high byte) to hold parameter 1. Remember, parameters are stored in high-byte, low-byte order. We first pull the high byte from the stack and store it, then the low byte. Our program now looks like this:
Now we must pull the second argument from the stack and place it in temporary storage. Well use locations $CD (for the low byte) and $CE (for the high byte) to store parameter 2. Once again, we must pull the parameter from the stack, high byte first, then low byte, storing each in the proper location. Our program so far:
Okay, we've pulled all the arguments from the stack, and we're now ready to add them together and put the result in locations $D4 and $D5, which we'll label RESLO and RESHL The addition is a simple, two byte add, like many we've covered before. After the addition, we place an RTS instruction to take us back to BASIC, and the assembly code is complete. The final program looks like this:
Now we must assemble the program and place the object file on disk. Use BOFFO (see issue 24 of ANA, LOG Computing) to convert the object file to BASIC DATA statements. If you don't have BOFFO, you can do this by hand. Using BASIC, you must POKE this data into memory, starting at location 1536, then call the subroutine with the USR call shown earlier. Figure 2 shows a BASIC program which does this.
Figure 2.
Line 10 READS the DATA statements and POKEs each byte into page 6. This sets up the assembly subroutine so we can use it through BASIC.
Line 20 accepts the two values to be added and places them in the variables VAL1 and VAL2. Be careful that the values you enter will not add up to more than 65535, or you'll get an incorrect result.
Line 30 calls the assembly subroutine with the USR function and places the result (VAL1 + VAL2) in the variable A.
Line 40 prints the result of the addition.
Line 50 loops back to accept another set of values to add.
Lines 60-70 are the DATA statements which contain the numeric values for the assembly code.
The first value, 216, is the decimal value for the
CLD instruction, the first instruction in the sub
routine. The next number, 104, is the value of
the PLA instruction, and so on.
When the program is executed, enter two numbers and press RETURN. BASIC will send the values to the assembly routine, which will add them and return. BASIC will then print the result. Nifty, huh?
Our first example of using the USR function showed how to call in a fixed memory location, such as page 6. Sometimes we can't use page 6 for some reason, so we must find another place to store our routines. Luckily for us, BASIC has a built-in way to reserve RAM: Strings!
Strings are usually used in BASIC to hold alphanumeric information, such as names, messages or other text. As you will soon see, strings are not limited to these uses. Each position in a string can hold a single byte, with a value from 0-255, just like any other memory location. What we're going to do is load the bytes of an assembly subroutine into a string and call it with a USR function.
There's one small snag with this technique, though: strings can move around in memory! Yes, that's right. They aren't always in the same place. When you execute a BASIC program, the BASIC interpreter puts the strings in the first available space it can find. If you add or delete code in your program, BASIC must move the string to the appropriate location. We can always find the string's location with the ADR function, but the code placed in the string must be made relocatable, or address-independent. This simply means that the code will execute no matter where BASIC places it in memory. We covered this subject in ANALOG Computing issue 22's Boot Camp, so, if you haven't read that, do so now.
Our second USR call example will return a random number from 0-65535. The assembly subroutine will be placed in the string RAND$. To call the subroutine, we'll use the code:
A=USR(ADR(RAND$))
As you can see, we are using the ADR function to find the address of RAND$, so that the USR call will know where the routine is located in memory. You should also note that there are no arguments being passed to the subroutine, so we won't have to worry about storing any parameters. We will, however, have to pull the number of parameters, which will be zero.
This time we don't care how the decimal mode is set, because we aren't going to perform any arithmetic. Therefore, our first action in the program is to pull the number of arguments from the stack. Remember to PLA this value; even if there are no arguments, BASIC puts the number of arguments on the stack. After pulling this value, we will forget about it. So far, our code looks like this:
Note that I have set the program counter to $0600, even though we will be placing this code in a string. Most assemblers require a starting address, so we're providing a dummy address. If you like, you can always place this code at $0600, since it'll execute anywhere in memory.
Next, we need to get a random number. Fortunately for us, the designers of the Atari computer systems gave us a handy memory location, RANDOM. RANDOM is located at $D20A (53770) and gives a random number from 0-255 when it's read. In order to get a random number from 0-65535, we just have to read RANDOM twice, each time placing the byte read into the BASIC return value locations $D4 and $D5. For example, assume the first RANDOM byte is 194, and that it's placed in the low byte of the result. Further, assume the second RANDOM byte is 49, and that it's placed in the high byte of the result. When we return to BASIC, the random number will be 12738 ((49 * 256) + 194). When the random number is stored, we can return to BASIC with the RTS instruction. After the addition of this code and the necessary equates, our program looks like this:
Now you can assemble the code and convert it into BASIC DATA with the BOFFO program. Figure 3 shows the BASIC code needed to install and use the random number routine.
Figure 3.
Line 10 dimensions the RAND$ string to 12 bytes. That's how many bytes the routine will occupy in memory.
Line 20 READs the DATA statements in Line 60 and places them into the RAND$ string, using the CHR$ function. This function simply places the numeric values of the assembly code into the string. If you print the string, you'll see the ATASCII character equivalents of your code.
Line 30 calls the assembly subroutine and puts the random number into the variable A.
Lines 40-50 print the random number and loop back to Line 30 to get another.
Line 60 contains the decimal DATA for the random number routine.
Now that you've seen a couple of simple examples of BASIC USR calls, I've got a challenge for you to try at home. Write a USR call which will accept two arguments. Add them and divide the total by two, returning this value to BASIC. This is a simple, average routine, and you should be able to handle it easily, since we've covered all the techniques you need to solve the problem. The answer will be an integer, so don't worry about fractional results.
When you get a solution, let me know. Send your solutions to:
Boot Camp
c/o ANALOG Computing
P.O. Box 23
Worcester, MA 01603
Next issue, we'll go deeper into USR calls, including variable-argument calls. Stay tuned! El