Insight: Atari
Bill Wilkinson
Optimized Systems Software
Cupertino, CA
This month has been a most hectic one. We just finished exhibiting both our new and old products at the seventh West Coast Computer Faire. (The seventh? Is that possible? I remember attending the first!) And, of course, we saw many, many, many new products for Atari Computers there. (Oh, all right, there were some for those other brands, also.) As I have said before, I won't review other companies' software products in this column, but I hope my dear editor won't object if I mention some of the more prominent new hardware products. Presumably, we will be seeing full blown reviews of these products in these pages in the future. And, since COMPUTE! was also there, I won't do more than just the mentions.
New Atari Peripherals
There were two companies there with add-on disk drives for the Atari: Percom Data Corporation and MPC Peripherals. It is hoped that both will be delivering double density drives by the time you read this, and the word is that we can expect doublesided, double-density very soon.
32K Byte memory cards were in abundance. And, of course, there was already Axlon's RAM-DISK. And how about a 64K card for the Atari 400? It's availble now in Germany. I'm not sure when and/or how it will appear here.
The long-awaited 24-by-80 display (24 lines of 80 characters, instead of Atari's 40 characters) was shown by BIT3 Corporation (who make a similar board for the Apple II).
And Stargate Enterprises (an Atari dealer near Pittsburgh, PA) brought and demonstrated the most innovative prototype: a small, radio-controlled robot. This might not sound exciting until you realize that the controlling end of the radio link was being driven by an Atari.
And wouldst that I could go into the software. Some of the latest arcade games have been, or are being, converted to Atari. And many of the best Apple II games will shortly appear for us, also. The best is yet to come, I believe. My aching pocketbook.
Anyway... as a consequence of all this, I simply didn't have time this month to do a fancy, full-blown program like last month's. Instead, I will just note a couple of the things I've been carrying around on spare scraps of paper before they get lost. But this won't be a short column; part five of my series on the internals of Atari BASIC is a fairly long and complex article on how variables are used and accessed and more. But first, the tidbits.
Control One Atari Screen
I am constantly amazed at the number of Atari owners (and not necessarily new owners) who are not aware that you can temporarily halt text screen output. They are forever typing LIST (for example) and then trying to hit the BREAK key at exactly the right time. For shame! You didn't read your manuals.
To temporarily pause, simply hit CONTROL-1 (hold down the CTRL key and hit the numeral 1 key). To continue, hit CONTROL-1 again. That's all there is to it.
Now, don't you feel silly? Would it help if I told you that somebody had to tell me, too?
Y Not Do It Later?
There is a minor, but terribly frustrating, bug in the Atari Assembler/Editor cartridge. There is no fix, but it is relatively easy to avoid if one is aware of it. So, if you haven't already been bitten, here is some bug repellant.
The problem has to do with using the ComPare-Y immediate instruction (CPY #xxx) when using the cartridge's debugger. One cannot always Step or Trace through such an instruction. Usually, an attempt to do so will cause the instruction to be treated as a BReaK (though I have heard tales of systems crashing).
The sort-of-a-solution is simply to avoid the instruction altogether. If possible, use CPX instead. Or try the following:
WAS: NOW: CPY #7 CPY VALUE7 ... VALUE7 .BYTE 7
This new method eats up two more bytes of memory, but the CPY # should be a fairly rare instruction so this technique won't make a lot of difference.
Using Print Without Using
Every now and then, I see a routine listed and/or used that is supposed to simulate PRINT USING on a BASIC that doesn't have such a capability. (For those of you who don't know what PRINT USING is, suffice to say that it is a very nice tool which allows beautifully formatted numeric output.) Well, I couldn't let these routines go unchallenged, since I had also designed such a routine many years ago. So here is that routine spruced up for Atari BASIC:
32000 REM formatted money 32010 TRAP 32020 : DIM QNUM$(15): TRAP 4000032020 IF ABS(QNUM) > = 1E8 THEN QNUM$ = STR$(QNUM) : RETURN 32030 QNUM$ = "^$^^^^^^^^.^^^^" : IF QNUM < 0 THEN QNUM = -QNUM : QNUM$ = "($^^^^^^^.^^)^" 32040 QNUM$(11 - LEN(STR$(INT(QNUM))), 10) = STR$(INT(QNUM)) 32050 QNUM$(11, 13) = STR$(100 + INT((QNUM - INT (QNUM)) * 100 + 0.5)) : QNUM$(11, 11) = "." : RETURN
Alternatively, you might replace the last statement of line 32030 with
QNUM$(14, 15) = "CR"
NOTE: to facilitate your counting, I have used an up arrow ("^") where you should type a space.
To use the routine, simply place the number you want formatted into QNUM and GOSUB 32000. The routine returns with the formatted string in QNUM$. Some things to observe about the routine: it uses no temporary variables, it dimensions its own string (but only once; notice the TRAP), it could be easily translated to any Microsoft BASIC that allowed MID$ on the left side of the equal sign.
Inside Atari BASIC: Part V
Last month we discussed the seven main memory pointers used by Atari BASIC and BASIC A+, and I promised to make the variable table the main topic for this month. In addition, I said that we would learn how to fool BASIC in useful ways. Many of the techniques I will present this month are not my original ideas: I must credit many sources, including De Re Atari and COMPUTE!'s First Book of Atari. However, the material bears repeating; and perhaps I can give some deeper insight into why and how some of the tricks work.
The Structure Of The Variable Value Table
Please recall from previous articles that the variable value table (VVT) of Atari BASIC is kept distinct from the variable name table. The reason for this is to speed run-time execution. Recall that the tokenized version of a variable is simply the variable's number plus 128 (80 hex), resulting in variable tokens with values from 128 to 255 ($80 to $FF). Since each entry in the VVT is eight bytes long, the conversion from token to address within VVT is fairly simple. For those of you who are interested, the following code segment is a simplified version of the actual code as it appears in BASIC:
; we enter with the token value ; ($80 through $FF) in A register ; LDY #0 STY ZTEMP + 1 ;a zero page temporary ASL A ;token value *2 ; but ignore the high bit ASL A ; token value *4 ROL ZTEMP + 1 ; carried into MSB also ASL A ; token value *8 ROL ZTEMP + 1 ; again, into MSByte CLC ;(not needed ... included for clarity) ADC VVTP ; add in LSB of VVT Pointer STA ZTEMP ; gets LSB of pointer to var LDA ZTEMP + 1 ADC VVTP + 1 ; add the two MSBs STA ZTEMP + 1 ; to obtain complete pointer LDA (ZTEMP), Y ; see text
When we exit this routine, ZTEMP has become a zero-page pointer that points to the appropriate eight-byte entry within the variable value table. But just what does it point to? The Aregister contains the first byte of that entry. What is that first byte? Read on...
Since each entry in the VVT is eight bytes long (yet may be a simple numeric variable, a string, or an array) obviously the entries must vary in contents. However, the first two bytes always have the same meanings. In particular, the first byte is the "flags" byte, and the second byte is a repeat of the variable number (without the MSBit on). We could probably have dispensed with the repeat of the variable number; but including that byte made the entry size come out to eight bytes (more convenient), and we found several uses for it in the actual implementation.
The "flags" byte is the heart of the whole VVT scheme: until BASIC examines a variable's flag byte, it doesn't even know whether it is working with a string, array, or scalar. But note how neatly we managed to arrive at the end of the routine above with the appropriate flag byte safely in the Aregister, where it can easily be checked, compared, or whatever. This, then, is the meaning of the individual bits within the flags byte:
Bit | Hex | Meaning |
Number | Value | (if bit is on) |
0 | $01 | Array or String is DIMensioned |
6 | $40 | this is an Array |
7 | $80 | this is a String |
Note that there is no special flag that says "this variable is a simple scalar numeric." Instead, the absence of all flags (i.e., a $00 byte) is used to indicate such variables. Since we have now used the first two bytes of each VVT entry, we now have to figure out what to do with the remaining six bytes. It is no coincidence that Atari floating point numbers consist of six bytes (a one byte exponent and a five byte mantissa): that numeric size was purposely chosen as one that gave a reasonable degree of accuracy as well as reasonable efficiency on the VVT layout. (Yes, I know, seven bytes would have worked well also, especially if we hadn't used the redundant variable number. Oh well.)
So scalar numeric variables obviously have their value contained directly in the VVT (hence the name, variable value table). But what about strings and arrays, which might be any size? The answer is yet another set of pointers, etc. Before proceeding, let us examine the layout of the three kinds of VVT entries, including the already-discussed scalar type:
BYTE NUMBER | 0 | 1 | 2 3 | 4 5 | 6 7 |
SCALARS | 00 | vnum | (floating point #, 6 bytes) | ||
STRINGS | 80/81 | vnum | address | LENgth | DIM |
ARRAYS | 40/41 | vnum | address | DIM1 + 1 | DIM2+1 |
For strings and arrays, byte zero (the flag byte), varies depending upon whether or not the variable has yet been DIMensioned. (Incidentally, BASIC always resets bit zero of the flag byte and zeros bytes two through seven for all variables whenever you tell it to RUN a program.)
The "address" in bytes two and three of string and array variables is not the actual address where the string or array is located. Instead, it is actually an offset (or, if you prefer, relative address) within the string/array space allocated to the program. Recall from last month that location $8C (140 decimal), names STARP (STring and ARray Pointer), points to the base location of such allocated space. Thus, for example, when BASIC receives a request for "ADR(XX$)", it simply uses the variable number (for XX$, which was generated when the program was typed in) to index into VVT (as above), and then retrieves the "address" from the VVT entry and adds it to the current contents of STARP.
For strings, the length and dimension values seem obvious: the DIM value is what you specify with the BASIC DIM statement, and the length is the same as that returned by the LEN function.
For arrays, we need note that DIM1 and DIM2 are as specified by the programmer in the DIM statement [e.g., DIM ARRAY(3,4) ]. The reasons they are incremented by one in VVT are twofold: a zero value is used to indicate "dimension not in use" (obviously only effective for DIM2, since flag bit 0 will not be set if neither is in use) also, since the zeroeth element of an array is accessible (whereas the zeroeth character of a string is not), using DIM + 1 makes out-of-range comparisons easier.
And that's it. There really are no other magic tricks or secrets. Once DIMensioned, strings and arrays don't change their offsets (relative addresses) or dimensions. There are no secret flag bits that mean funny things. Turning on the MSBit of the variable number only spells disaster. I really have told all.
Making Use Of What We Know
BASIC is not smart enough to check entries in these tables for validity. It assumes that once you have declared and/or DIMensioned a variable the VVT entry is correct (it must be...BASIC made it so). Thus the implication is that one can go change various values in VVT and BASIC will believe the changes. So let's examine what we can change and what effects (good and bad) such changes will have.
First, as usual, some cautions: BASIC DIMensions variables in the order the programmer specifies. Thus "DIM A$(100),B(10)" will ensure that the address of array B will be 100 higher than that of string A$. Neat, sweet, petite. However, the order in which variables appear in the VVT (and Variable Name Table) depends entirely upon the order in which the user ENTERED his program. An example:
NEW 20 A = 0 40 DIM B$(10) 10 DIM C$(10) 30 DIM D(10 LIST [and BASIC responds with: 10 DIM C$(10) 20 A = 0 30 DIM D(10) 40 DIM B$(10)
Assuming that you typed in the lines above in the order indicated, the variables shown would appear in VVT in alphabetical order (A,B$,C$,D). But, if you RUN the program, the DIMensioned variables would use string/array space as follows:
C$, 10 bytes, offset 0 from STARP D(), 66 bytes, offset 10 from STARP B$, 10 bytes, offset 76 from STARP
Though you can figure out this correspondence (especially if you list the variable name table, with a short program in Atari BASIC or with LVAR in BASIC A +), it is probably not what you would most desire. It would be handy if the VVT order and the string/array space order were the same. Solution: (1) Place all your DIMensions first in the program, ahead of all scalar assignments. (2) LIST your program to disk or cassette, NEW, and reENTER — thus insuring that the order you see the variables in your program listing is the same order that they appear in the VVT. From here on in this article I will assume that you have taken these measures, so that variable number zero is also the first variable DIMensioned, etc.
So let's try making our first change. The simplest thing to change is STARP, the master STring/ARray Pointer. A simple program is probably the easiest way to demonstrate what we can do:
100 DIM A$(24 * 40) : A$(24 * 40) = CHR$(0) 110 WAIT = 900 120 A$(1, 24) = "THIS IS ORIGINAL A$ !!! " 130 A$(25) = A$ 140 PRINT A$ : GOSUB WAIT 150 SAV140 = PEEK(140) : SAV141 = PEEK(141) 160 TEMP = PEEK(560) + 256 * PEEK(561) + 4 170 POKE 140, PEEK(TEMP): POKE 141, PEEK (TEMP + 1) 180 PRINT CHR$(125); 190 A$(1, 11) = "HI there..." : GOSUB WAIT 200 A$(12) = A$ : GOSUB WAIT 210 POKE 140, SAV140 : POKE 141, SAV141 220 PRINT A$ 230 END 900 REM WAIT SUBROUTINE 910 POKE 20, 0 : POKE 19, 0 920 IF NOT PEEK(19) THEN 920 930 RETURN
BASIC A+ users might prefer to delete line 160 and change the following lines:
150 sav140 = dpeek(140) 170 dpoke 140, dpeek(dpeek(560) + 4) 210 dpoke 140, sav140 910 dpoke 19, 0
"‘Simple’, he said. Who's he kidding!" Honest, it's simpler than it looks. Lines 100 through 140 simply initialize A$ to an identifiable, printable string and print it. The WAIT routine is simply to give you time to see what's happening. Note that A$ is DIMensioned to exactly the same size (in bytes) as screen memory. We then save BASIC'S STARP value and replace it with the address of the screen (lines 150 through 170). Since A$ is the first item in string/array space, its offset is zero. Thus pointing STARP to the screen points A$ to the screen.
We then clear the screen and initialize A$ again – to a short string. Notice the effect on the screen: capital letters and symbols are jumbled because of the translation done on characters to be displayed. (Recall that Atari has three different internal codes: keyboard code, ATASCII code, and screen code. Normally we are only aware of ATASCII, since the OS ROMs do all the conversions for us.)
At line 200, we proliferate our short string throughout all of A$ – look at the effect on the screen. Finally, lines 210 through 230, we restore STARP to its original value and print what BASIC believes to be the value of A$. Surprised?
As interesting as all the above is, it is of at best limited use: moving all of string/array space at once is dangerous. In our example above, if there had been a second string DIMensioned, it would have been reaching above screen memory, into never-never land. Let me know if you can find a real use for the technique.
A better technique would be one which would allow us to adjust the addresses of individual strings (or arrays). While a little more complex, the task is certainly doable. Our first task is to find a variable's location in the VVT. If the variable number is "n", then its VVT address is [VVTP] + 8*n (where "[...]" means "the contents of...").
In BASIC:
PEEK(134) + 256 * PEEK(135) + 8 * n
or BASIC A+:
dpeek(134) + 8 * n
We can then add on the byte offset to the particular element we want and play our fun and games. Again, a sample program might be the best place to start:
100 DIM A$(1025), B$(1025) : A$(1025) = CHR$(0) : B$ = A$ 110 STARP = PEEK(140) + 256 * PEEK(141) 120 VVTP = PEEK(134) + 256 * PEEK(135) 130 CHARSET = 14 * 4096 : REM HEX E000 140 VNUM = 1 : REM the variable number of B$ 150 LET NEWOFFSETB = CHARSET – STARP 160 TEMP1 = INT(NEWOFFSETB/256) 170 TEMP2 = NEWOFFSETB - 256 * TEMP1 180 POKE VVTP + VNUM * 8 + 2, TEMP2 : POKE V VTP + VNUM * 8 + 3, TEMP 1 190 A$ = B$ 200 PRINT ADR(B$), CHARSET
optionally, in BASIC A + :
100 dim a$(1024), b$(1024) : a$(1024) = chr$(0): b$ = a$ 110 starp = dpeek(140) 120 vvtp = dpeek(134) 130 charset = 14 * 4096 140 vnum = 1 180 dpoke vvtp + vnum * 8 + 2, charset-starp 190 a$ = b$ 200 print adr(b$), charset
equivalently:
100 DIM A$(1024) 110 CHARSET = 14 * 4096 120 FOR I = 1 TO 1024 130 A$(I) = CHR$(PEEK(CHARSET + I - 1)) 140 NEXT I
or again, optionally, in BASIC A + :
100 dim a$(1024) : a$(1024) = chr$(0) 110 move 14 * 4096, adr(a$), 1024
The intent of all four of the above program fragments is the same: to move the Atari character set font from ROM (at $E000) into the string A$. The third method will probably be the most familiar to most of you. Unfortunately, it is also the slowest. The fourth method, admittedly is clearest in BASIC A + , though: its line 110 summarizes what we are trying to do in each of the other three.
The first method is of course the one which deserves our attention since it relates to this article.
Line 100 simply allocates and initializes our two strings. We must DIMension these strings one greater than we need because of the bug in Atari BASIC which moves too few bytes when string movements involve moving exact multiples of 256 bytes. Lines 110 and 120 simply get the current values of the two pointers that we need, VVTP and STARP.
Lines 130 and 140 actually simply set up some constants. The Atari character set is always located at $E000, of course. The VNUM is set to one, in accordance to what we noted above. Be careful! The VNUM will not necessarily be one if you did not type this program in the order shown! When all else fails, use LIST and reENTER.
We use line 150 to figure out how much B$ must move (and it will always move "up," since the ROM is always above the RAM) and then calculate its new "offset" within STARP. Of course, it is now actually outside of string/array space, but BASIC doesn't know that. Why should it care?
Unfortunately, lines 160 and 170 are needed in Atari BASIC (and most other BASICs) to manipulate 16-bit numbers into digestible, byte-sized pieces.
Finally, with line 180 we establish B$ as pointing to the character set memory. Line 190 moves the entire 1025 bytes, with one simple operation, from there to the waiting arms of A$, in RAM, where it can be manipulated.
With Atari BASIC (and, indeed, with most BASICs), the only other way to get the speed demonstrated here is to write an assembly language subroutine to do the move. Obviously, if you were simply moving the character set once, this is not the way to do it. But if you are interested in manipulating a lot of different memory areas with great speed (player missile graphics? multiple screens?), this works.
A couple of comments: We did not really need to DIMension and set up B$ in our example. After all, as long as we are faking the address, why not fake the DIMension, LENgth, and flags as well? We could accomplish all that this way:
POKE VVTP + 8*VNUM, 65 : REM say B$ is dimensioned ($41), see above) POKE VVTP + 8*VNUM + 4, 1 : REM 1sb of 1025 ($0401), the length POKE VVTP + 8*VNUM + 5, 4 : REM msb of ditto POKE VVTP + 8*VNUM + 6, 1 : REM and the DIM is the same as the len POKE VVTP + 8*VNUM + 7, 4 : REM msb of the DIMM
Now we have fooled BASIC into thinking B$ is set up properly but we haven't actually used any memory for it. P.S.: can you think of any reasons to have two variables pointing to the same memory space? A string and an array pointing the same space? We'll discuss all that next month.