Atari Safe RAM
E. H. Foerster
Are you tired of waiting while your BASIC program initializes? Would you like to save two-thirds of the memory required to store the DATA statements for your USR routines, character sets, or player/missile data? Would you like to save data generated by your program along with the program using just SAVE commands? This article will show you how to create protected and SAVEable RAM within the BASIC token program. Several USR routines are also included to allow you to add or delete safe RAM, move data in memory, and point a string to any place in memory. Also, the game "SKI!" illustrates these techniques in a practical, executable program. And each of these utilities does its job in a fraction of a second.
The Atari 400/800 computers provide page six as free RAM to be used by BASIC. However, to use this RAM, the information must be stored in DATA statements in the BASIC program and then transferred to RAM using READ and POKE loops. This often delays the start of a program while the initialization routine is processed. You are also limited to 256 bytes when using page six.
A technique commonly used to circumvent this problem is to put the desired file in a string at the beginning of the string array storage area (STARP). The pointer to STARP is then moved past the desired file. When the program is SAVEd, the file is SAVEd along with the program. This technique requires special programming not only to save the file, but also to move the pointer back after the program is LOADed into the computer. You must insure that the pointers are not moved a second time when a program is executed following an initial run.
But there is another location within the BASIC token program that can be used to save and store data. The procedure we'll discuss will provide all the RAM you need at this location and protect it from any additions, deletions, or changes in the program. This protection, of course, does not extend to direct POKEs to this area.
No special routine will be needed to protect this RAM area once it has been inserted in the program. The RAM area and the information stored in it will remain there even when the program is SAVEd. The information is available immediately after the program is loaded and can be moved rapidly from this location to anywhere in memory using the MOVE DATA routine provided in this article.
Using The Pointers
Before explaining the technique, let's briefly review the BASIC token program. There are several sections to every BASIC program, each with a two-byte pointer to a particular memory address. The address of the location is obtained by multiplying the second byte by 256 and adding the first byte. The location, name, and purpose of the pointers for the Atari BASIC token program are:
128,129 LOMEM: A 256-byte section used by BASIC for temporary storage.
130,131 VNTP: A table containing a list of all the variable names.
132,133 VNTD: Ending address of variable name table, plus one, one byte containing a zero when fewer than 128 variables have been used.
134,135 VVTP: Variable value table containing values for scalar variables and offset dimension, and length for arrays and strings.
136,137 STMTAB: Statement table containing the tokenized version of program statements.
138,139 STMCUR: Current statement being executed.
140,141 STARP: String array storage area.
When a program is SAVEd, LOMEM is subtracted from each of the pointers, and these values are SAVEd. Along with these pointers, all the information from VNTP up to STARP is SAVEd. When a program is LOADed, the reverse process occurs. This process is necessary because the addition of peripheral devices such as disk drives changes the value of LOMEM and thus the location of the BASIC program.
These pointers and the contents of the various sections are not static, but are moved around in memory as additions and deletions are made. The use of a new variable name–even in direct mode (no line number)–creates changes in the program even though no changes in the listed program occur. The new name is added to the variable name table. The pointers from VNTD to end are incremented by the length of the name and the contents of VNTD to end-of-program are moved up in memory by the length of the name. Then eight bytes are reserved at the end of the variable value table, and the contents and pointers for STMTAB to end-of-program are similarly moved.
RAM Utilities
If we want to create RAM space for our own use within the BASIC program, several requirements must be satisfied. The area must be located in a place where it will be SAVEd when the program is SAVEd. The area must be in a location where any changes in the program (either in the direct mode or program mode) will not only leave the contents of the area intact, but also move the contents when the adjoining area is moved. Since the RAM area is not static, we also must have a pointer to this area. An area created between VNTD and VVTP is the only area that meets these requirements. Because VNTD is always one byte long, the location of safe RAM can be calculated from the formula:
PEEK(132) +1 + 256*PEEK(133)
Enter and run Program 1, and the routine for adding RAM along with four other utilities will be installed in safe RAM at location VNTD +1. You can now delete all program lines and save these utilities in a blank program for future use. You may want to leave a REM statement just to remind you what is in the program. The utilities and the calls for these utilities are as follows:
Add RAM: A = USR(VNTD +1, bytes of RAM)
Delete RAM: A = USR(VNTD +12, bytes of RAM)
Move Data: A = USR(VNTD +23, destination, source, bytes)
String Assign: A = USR(VNTD + 90, ADR (string), dimension, length, desired location) where VNTD = PEEK(132) + 256×PEEK(133)
You need to be careful when using these and other machine language routines. A mistake in any of the USR statements will more than likely lock up your computer. So before executing one of these calls, double-check your typing. If it is part of a program, SAVE the program before running it. Then if the computer locks up, shut it off, LOAD your program back in, and correct the mistake. No test is included in these routines to check for the correct number of arguments in the USR statement. If only one or some of the routines are needed for your program, then only those portions can be retained. We'll talk more about this later.
If you look at Program 1, you will notice that the Add RAM and Delete RAM routines are each only 11 bytes long. How do we go about moving all those pointers and memory with just 11 bytes of code? Why not let BASIC do it for us? The memory manager routines are located in the Atari BASIC cartridge at location 43135-43358 ($A87F-$A95E). All our USR routine does is load the 6502 registers with the appropriate values and then jump to the memory manager routines.
Routine Features
The Add RAM routine cannot create RAM and is therefore limited by the amount of memory in your computer. If you attempt to add more bytes than are available, an "ERROR 2 AT LINE X" message is returned. Memory is always added at the current location of VVTP. Thus, any additional RAM is added at the end of current safe RAM.
The Delete RAM routine similarly deletes RAM at the end of current safe RAM. However, if you try to delete more bytes than were previously added, you might as well start all over.
The Move Data routine lets you move a block of data rapidly from one place to another in memory, including safe RAM. For example, moving 1K of data takes about 13 seconds using a PEEK and POKE routine. Using the Move Data routine, this same move takes a fraction of a second.
The move routine can even move a section of memory into an adjacent overlapping area without erasing any part of it. To demonstrate this point, execute the following line:
DIM A$(9):A$ = "ABCDEF":A$(4) = A$:?A$
The result: A$ = "ABCABCABC". What happens is that the origination string is changed by the destination string during the transfer of data. However, the Move Data routine moves bytes starting with either the first or last byte of the section of code to be moved, depending on whether the movement is forward or backward in memory. If you execute the following line:
DIM A$(9):A$"ABCDEF":A = USR(PEEK(132) + 23 + 256xPEEK(133),ADR(A$) + 3,ADR(A$),6):?A$
the resulting A$ equals "ABCABCDEF". So A$(4) actually is equal to the original A$.
It is possible to use this routine for many purposes including player/missile movement. I have not tested to see what would happen if the routine were called from a Vertical Blank Interrupt. Since the routine uses part of the memory manager routine and some of BASIC'S zero page, it might interfere with the BASIC program. If this occurs, then the Vertical Blank Interrupt must first save locations $99-9C and then restore them at the end of the routine.
Safe Strings
Safe RAM may also be used for storing strings using string manipulation techniques. The String Assign routine readily performs that task for us. Usually strings are manipulated by changing values in the variable value table. Bytes two and three must be changed to contain the low and high bytes of the offset of the location of the address of the string from STARP. Safe RAM, however, is located before STARP, and the offset from STARP is negative. To get a positive offset for a string located before STARP, we must add 65536 to the location before subtracting STARP and converting to a low and high byte offset. We are simply assuming that memory wraps around to zero when top-of-memory is reached.
Before we get too involved in how to calculate offset and where to POKE the values, let's look at the String Assign routine. This routine will calculate the offset for you and POKE it, the current length, and dimension values into the corresponding bytes in the variable value table for the variable identified by ADR(string). All you need to do is provide the address where the routine is located, the dimension, the length, and where you want the string to be located.
Before going to the String Assign routine, you must DIM the string to avoid getting an ERROR 9 message. But to save memory in the string array area, DIMension the string to length one. Then call the routine and change to the desired dimension. The term ADR(string) in the USR call is not used directly, but is used by the routine to identify the string to be changed. If you have ever tried string manipulation by calculating and POKEing string offsets, then you will certainly appreciate this routine.
String Assign is best used in a program. If used in direct mode, it is best done with all statements in a single, direct mode line. If, for example, you locate A$ at location 1536, the address for A$ will remain at 1536 while the direct line is being executed or while a program is running. But as soon as the program returns with a READY, the address for A$ will change.
There is a good reason for this. Any line that is typed in direct mode is entered as the last line in the statement table. STARP is moved backwards or forwards in memory, depending on the length of the line. Along with STARP, all pointers (offsets) for strings and arrays are changed accordingly. A string residing in the normal string array area can still be printed. However, a string moved outside that area will have only its offset changed, but not its location.
This routine can be used to clear memory for player/missile graphics. Point string A$ to the location of the player/missile area and set length and dimension to the number of bytes to be cleared with the String Assign routine. Then follow with the statement:
A$(l,l) = CHR$(0):A$(2) = A$
Deleting The Utilities
If, after finishing a program, you want to delete part or all of the utility routines, you must perform several steps:
- Move the utilities to location 1536 +1 (page six). Then use the USR routines in page six to perform the remaining steps. Substitute 1536 for VNTD in the USR calls.
- Move the routines you want to keep to the beginning of safe RAM.
- Delete the remaining safe RAM.
- Change all lines in the BASIC program that contain references to safe RAM to the new location within safe RAM (e.g., the first argument of the USR routines).
- SAVE the program.
Safe RAM can be used for USR routines, player/missile data, custom character sets, and strings. But certain rules must be followed. For example, only relocatable USR routines can be left in safe RAM. Others must be moved to their designated location. If you want to change a program containing a USR routine, try the program by leaving it in safe RAM. If it does not work there, transfer it to the prescribed location using the Move Data routine. Character sets must be located on a 1K or 1/2K memory boundary and must be moved from safe RAM to their correct location.
Even though data must be moved from safe RAM to another location, you will still save more memory than when using DATA statements. For example, the 133 bytes of data in Program 1 occupy a total of 555 bytes when stored in the DATA statements. This is because each digit of each number and each comma occupies one byte of memory in the BASIC token program.
A Practical Application For Safe RAM
As an example of the improvements which can be made using this technique, I have converted "SKI!" (Atari version of "Slalom") from February COMPUTE! to initialize from safe RAM (see Program 2). The 15-second initialization has been cut to just a fraction of a second. And preloading the data into safe RAM saves 930 bytes more than the original initialization routine.
Other additions include a USR routine to generate the course, with the option of viewing the course before running it. Control of the skier's horizontal motion has been added to the Vertical Blank routine. This makes horizontal motion proportional to the scrolling rate. These changes do not affect the nearly instantaneous start of the program.
To enter the modified version, first type in the BASIC loader for SKI!. The DATA statements contain the safe RAM utilities and initialization data for the game. Each DATA line includes a checksum, which should greatly reduce the chance of errors. When you RUN the loader, it will POKE the data into safe RAM, then erase most of itself. Delete the remaining lines and, for safety, SAVE a copy of the safe RAM portion. Then type in modified SKI!. You must SAVE, not LIST, the combined program to tape or disk, since a LISTed version will not include the safe RAM portion. If you typed in the original SKI!, you'll be amazed at the increase in speed when you RUN the new version.
Using safe RAM and the utilities given in this article, you should be able to write programs that do not start with the message "Just a Moment" or "Initializing." The uses for safe RAM are not limited, of course, to the examples we've discussed. There's a lot of room for you to develop your own applications for safe RAM.
Explanation Of USR Routines
Add RAM (11 bytes):
The routine uses a JMP $A881 to the memory manager routine for moving pointers and contents of token program to a higher location in memory. Before jumping, the following registers are loaded: X = token file pointer, which in our case is $86(VVTP); A = MSB(length); Y = LSB(length). Jumping to $A871 automatically loads A = 0.
Delete RAM (11 bytes):
This routine is identical to the above, except for JMP $A8FD.
Move DATA (67 bytes):
The routine stores destination in $9B,9C and source in $99,9A. It then determines if the move is positive or negative. For positive direction, a routine at $A8E3 is used which requires that X = MSB (length) + 1, Y = LSB (length), $9A = $9A + MSB (length), and $9C = $9C + MSB (length). For a negative move, the routine at $A94C is used which requires X = MSB (length) + 1, Y = complement of LSB (length) and ($99), Y and ($9B), Y point to first byte of source and destination, respectively.
String Assign (44 bytes):
This routine first obtains the variable number for the desired string from the statement table and loads it into the accumulator. A JSR $AC28 returns with the address of the desired string in the variable value table in $9D,9E. The dimension and length are then pulled off the stack and stored in ($9D),7 through ($9D),4. The desired location is then pulled off, and the offset calculated and stored in ($9D),2 and ($9D),3.
Program 1: safe RAM10 FOR A = 1 TO 133 : READ B : POKE 1536 + A, B : C = C + B : NEXT A : IF C < > 18631 THEN PRINT "CHECK DATA STATEMENTS" : END 20 REM ADD RAM 30 DATA 104, 104, 170, 104, 168, 138, 162, 134, 76, 129, 168 40 REM DELETE RAM 50 DATA 104, 104, 170, 168, 138, 162, 134, 76, 253, 168 60 REM MOVE DATA 70 DATA 104, 162, 3, 104, 149, 153, 202, 16, 250, 56, 165, 155, 229, 153, 165, 156, 229, 154, 104, 170, 144, 16, 24, 101, 154, 133, 154, 138 80 DATA 101, 156, 133, 156, 232, 104, 168, 76, 227, 168, 232, 104, 168, 101, 153, 133, 153, 176, 2, 198, 154, 152, 24, 101, 155, 133, 155 90 DATA 176, 2, 198, 156, 152, 73, 255, 168, 200, 76, 76, 169 100 REM STRING ASSIGN 110 DATA 104, 104, 104, 160, 4, 200, 177, 138, 201, 60, 208, 249, 200, 200, 200, 177, 138, 32, 40, 172, 160, 7, 104, 145, 157, 136, 192, 2 120 DATA 208, 248, 56, 170, 104, 229, 140, 145, 157, 200, 138, 229, 141, 145, 157, 96 130 A = USR (1536 + 1, 133) 140 A = USR (1536 + 23, PEEK (132) + 1 + 256 * PEEK (133), 1537, 133)
Program 2: Safe RAM Application
BASIC Loader For SKI!
0 REM 10 ? "JUST A MOMENT" : DIM A $ (746) : A = 1 : B = 0 : C = 20 : FOR D = 0 TO 36 : G0SUB 70 : NEXT D : C = 6 : GOSUB 70 20 IF B < > 73882 THEN ? "CHECK ALL DATA LINES" : END 30 VNTD = PEEK (132) + 256 * PEEK (133) 40 A = USR (ADR (A $ ), 746) 50 A = USR (ADR (A $) > + 22, VNTD + 1, ADR (A $), 746) 60 GOTO 1000 70 E = 0 : FOR F = 1 TO C : READ G : E = E + G : B = B + G : A$ (A, A) = CHR$ (G) : A = A + 1 : NEXT F 80 READ F : IF F < > E THEN ? "CHECK DATA STATEMENTS AT LINE "; 100 + D * 10 : END 90 RETURN