Insight: Atari
Bill Wilkinson
Optimized Systems Software
Cupertino, CA
This month I will first respond to some of that unanswered mail; then part six of Inside Atari BASIC [a continuing series within this column] will delve further into string and array magic; and Finally we will do a preliminary exploration of the depths of Atari's FMS.
Graphics Revisited
Actually, the title of this section might better be "machine language revisited." Probably none of my columns has generated as much response as part four of my Atari I/O series, subtitled "Graphics," in the February, 1982, issue of COMPUTE! Unfortunately, most of the response has been of the "I can't make it work" variety. Of course, my first response is "but I know it works!" Yet still the letters ask, "How?"
I do not intend to turn this column into a tutorial on machine language. There are several good books available on 6502 machine language (including the Inmans’ book specifically for The Atari Assembler), and any struggling beginner who is trying to make do without at least one of them is simply a masochist. However, my ego says that it will be better fed if more readers understand my articles.
For the most part, it seemed that those who had trouble with my February article assumed that what was published was some neat program to be used as is. Not so! I had simply given you a set of subroutines to use with your own programs. For an example, let us take a simple BASIC routine and its machine language equivalent. First, the BASIC:
30000 POKE 20, 0 : POKE 19, 0 30010 IF PEEK(19) = 0 THEN 30010 30020 RETURN
Now, you would not mistake that for a complete BASIC program. But, if I told you that entering this routine and then executing a GOSUB 30000 from your program would produce a 4.2667-second pause, you would know when and how to use it. So let's do the same thing in machine language:
PAUSE LDA #0 STA 20 ; "poke 20, 0" STA 19 ; "poke 19, 0" LOOP LDA 19 BEQ LOOP ; "if peek(19) = 0 then loop" RTS ; "return"
Again, this is not a complete program! But if you enter it (say at the end of your own machine language program) and then execute a JSR PAUSE, it will produce a 4.2667 second pause. Note, then, that JSR in machine language is the equivalent of GOSUB in BASIC.
The graphics routines (Program 5) in my February article are just subroutines, to be placed in your own machine language program and then JSRed to perform their actions. Perhaps the biggest mistake I made was in presenting these as an assembled listing (complete with "* = $660"). I certainly never used them as such. In point of fact, I tested them by .INCLUDEing them in my test programs, which were written with the OSS EASMD Assembler/Editor. And one of the test programs I used was, indeed, the example given on page 77 of that same article.
So how do you get these subroutines in and working for you? First and foremost, you obviously must type in all that code. Perhaps the best thing to do would be to type it in exactly as shown, including even the "* = $660" and the ".END". Then assemble it and carefully compare the object code generated with that in the magazine. When all appears correct, remove the "* = " line and the ".END" line, renumber the whole thing (I would suggest REN 29000,5 or something similar), and LIST it to diskette or cassette. Now use NEW and write your mainline code. When you are reasonably satisfied with it, LIST it to disk or cassette also.
Now what? Obviously, if you have OS/A +, I suggest you use .INCLUDE (an assembler pseudo-op which allows you to include one file while assembling another). In fact, I tend to write assembly code structured as follows:
.INCLUDE #D : SYSEQU.ASM <my mainline code> .INCLUDE #D : library-routine-number-1 .INCLUDE #D : library-routine-number-2 ... .END
If my "mainline" code is big enough, I may even break it into two or three pieces and .INCLUDE each of them separately.
But what if you don't have .INCLUDE capability? Well, several assemblers have "FILE" or "CHAIN," which are not quite as flexible (since you don't return to where you left off after you have assembled a chained-to file...thus making the procedure next to useless for zero page equate files, etc.); but the principle is generally the same: put your mainline code first and then CHAIN to the subroutine files.
And what if you have the Assembler/Editor cartridge? (For all of its faults, it is still a remarkably flexible tool, especially considering that it is usable with cassette-based systems.) Again, the principle holds. The only real difference is that you must do the INCLUDEs yourself. How? Via the ENTER command. If you haven't noticed it up until now, get your manual and read up on the ",M" option of ENTER. You can merge two or more machine language program files (including cassette files) via the ",M" option! Just as you can with BASIC, except that BASIC always presumes you want to merge.
Are there things to watch out for? Of course. Would I ever give you a method without a handful of caveats? (1) If you ENTER/merge a file with line numbers which match some (or all) of those in memory, you will overwrite the in-memory lines. (2) If you EVER forget the ",M" option, you will wipe out everything in memory so far. (3) You won't find out about duplicate labels until you assemble the whole thing.
But even with all these cautions, I strongly recommend that you store each of your hard-earned routines on its own file/cassette. It then becomes almost easy to write the next program that needs some of those same routines.
By the way, caution number 1 in the previous paragraph is the reason I suggested RENumbering the graphics routines to 29000, or some such out of the way place. If you make notes of what each file (or cassette) does, as well as what line numbers it occupies, you can build a powerful library. And a P.S.: generally, .INCLUDE, FILE, and CHAIN commands do not require unique line numbers, so you need not worry about RENumbering subroutines for use in such environments.
Gozinta and Gozouta
As long as we are on the subject of machine language techniques, I would like to point out the absolute necessity of establishing entry and exit conventions for each and every subroutine. Again, if you will refer to Program 5 from the February issue, you will note that each routine (GRAPHICS, COLOR, POSITION, PLOT, LOCATE, DRAWTO, and SETCOLOR) specified ENTER and EXIT conditions. For example, GRAPHICS requires that the desired graphics mode number be placed in the A-register before the JSR GRAPHICS. Upon return (RTS), the Y-register is guaranteed to contain a completion status.
On machines with more registers, it is good practice to write subroutines in a way that any registers not specifically designated in the ENTER and EXIT conditions are returned to the caller unchanged. On the 6502 microprocessor, though, it is generally hard to write any significant routine that does not affect all three registers. Therefore, I have adopted the opposite convention for this CPU: If the ENTER/EXIT comments don't say otherwise, I presume that all registers are garbage when the routine returns. What convention you adopt doesn't really matter; just be sure to stick to one, and only one, method and you won't go wrong.
FILL From Machine Language
For those of you who are experienced machine language programmers and have not been kept entertained up to this point, take heart. The other question most asked about my February article was something like "so how do you call FILL from assembler?" I guess my comment that FILL from assembly language was exactly the same as from BASIC didn't make a very good impression. So, okay, I know when I'm licked. Herewith is a FILL subroutine, which I would hope you would include with the rest of the graphics routines and keep in your library for future use.
This time, I won't make the mistake of putting in line numbers and using "* = " and ".END" This is a straight subroutine; type it in and JSR to it only after you have satisfied its ENTER conditions.
FILL H, V | |
ENTER: | Must have previously drawn the right hand edge of the area to be FILLed via JSR's to PLOT and DRAWTO. Just prior to JSR FILL, it must have performed a JSR PLOT to establish the top (or bottom of the line which will define the left edge of the area to be FILLed. FILL presumes that the color to fill with is that which was most recently chosen via JSR COLOR. Finally, on entry, FILL expects the registers to specify the ending position of the line which will define the left edge of the filled area, as follows: |
h (horizontal) position in X,A registers (X has LSB of position, A has MSB) v (vertical) position in Y register | |
EXIT: | Y-register has completion status from OS fill routine |
FILDAT = 765 ;where XIO wants the fill color
CFILL = 18 ;fill is XIO 18
; rest of equates are from February article and program;
FILL
JSR | POSITION | ; subroutine from Feb. 1982 article |
LDA | SAVECOLOR | ; value established via JSR COLOR |
STA | FILDAT | ; see BASIC manual: color used for FILL |
LDX | #6*$10 | ; file 6...where S: normally is |
LDA | #CFILL | ; the fill command (XIO 18) |
STA | ICCOM,X | ;... is specified |
LDA | #0 | |
STA | ICAUX1,X | ; remember, XIO 18, #6,0 |
JSR | CIO | ; and let the OS do the work |
RTS | ; ...and give us status in the Y-reg |
By the way, did you notice that we didn't actually specify "S:" for the XIO, as specified in the BASIC manual? That's because the BASIC manual doesn't tell the whole truth. If you perform XIO on an already open file, the operating system ignores any filename you give it! Want to save a little space in your BASIC programs? Use ‘XIO 18,#6, 0,0junk$’ where ‘junk’ is any string variable you happen to be using for any other purpose in your program.
Inside Atari BASIC: Part 6
Last month, we delved into the hopefully-no-longer-mysterious details on how string and array space is allocated from Atari BASIC and BASIC A + . We showed how to fool BASIC into believing that a perfectly ordinary string was located smack in the middle of screen space. The advantage of such deceptions is that BASIC can move strings of bytes at extremely high speeds, faster than you could ever hope to accomplish with any BASIC subroutine.
We did not discuss one other significant use of such string moves: Player/Missile Graphics. Obviously, if you can move the screen bytes around, you can move the players around just as well, and just as fast. Again, several games and utilities now available on the market use just this technique.
I also promised in the last column to tell of possible uses for multiple variables in the same address space (that is, having a string and an array occupying the same hunk of memory). If the idea interests you, read on.
One thing which BASICs in general lack is a good means of handling record input/output. How many times have you seen programs doing disk I/O using PRINT# and INPUT#? Yuch. (I have several reasons for that "yuch," but the best one is simply that PRINT#ing an item means that the number of disk bytes occupied depends upon the contents of the item.) But what is the alternative? With many BASICs, there is none. With Atari BASIC there is at least GET# and PUT#, but they are slow. So let us examine a way to make PRINT# and INPUT# work for us, instead of against us.
First, we will examine a small program:
100 DIM RECORD$(1),NAME$(20), QUANTITY ORDERED(0) 110 OPEN #l, 8, 0, "D : JUNK" 120 VVTP = PEEK(134) + 256*PEEK (135) 130 POKE VVTP + 4, 27 : POKE VVTP + 6, 27 140 GOSUB 900 150 PRINT "GIVE NAME AND QUANTITY : " 160 INPUT NAME$ 170 INPUT TEMP : QUANTITYORDERED(0) = TEMP 180 PRINT #1; RECORD$ 190 CLOSE #1 200 REM --- READ FILE WE JUST CREATED --- 210 OPEN #l, 4, 0, "D : JUNK" 220 GOSUB 900 230 INPUT #1, RECORD$ 240 PRINT "WE READ BACK IN : " 250 PRINT ,,NAME$ 260 PRINT ,,QUANTITYORDERED(0) 270 CLOSE #1 290 END 900 REM --- CLEAR THE VARIABLES --- 910 NAME$ = " " : REM 20 BLANKS 920 QUANTITYORDERED(0) = 0 930 RETURN
Surprised? Even though we cleared the variables in line 220, the input of line 230 re-read them from the file. How? Because line 130 set the dimension and length of RECORD$ to 27, which includes the original single byte of RECORD$, the 20 bytes of NAME$, and the six bytes of the single element of the array QUANTITYORDERED. So PRINT# thought it had to print 27 bytes for RECORD$, and INPUT# allowed RECORD$ to accept up to 27 bytes.
Wow! With one fell swoop we have managed to allow fast disk I/O of any sized record, right? Wrong. Unfortunately, there are several limitations to this technique. (1) The record cannot be over 255 bytes long or INPUT# won't be able to retrieve it all. And any size over 127 bytes will wipe out routines/data in the lower half of page $600 memory. (2) The record cannot contain a RETURN (155 decimal, 9B hex) character. It will print fine, but the INPUT# will terminate on the first RETURN it sees. (3) The other strings in the record (NAME$ in our example) will not have their lengths set properly by the INPUT#, thus necessitating something like the routine at line 900. But if you insert "280 PRINT LEN(NAME$)", you will always get a result of 20.
Well, limitations one and three are easy enough to predict and understand, but how do you insure that your data does not contain a RETURN code? For strings which have been INPUT by a user, that's easy: the RETURN code will never appear in such a string. But what about numbers? Remember that we will be printing the internal form of Atari decimal floating point numbers. Can such numbers contain a byte with a value of 155 ($9B)? Yes, but such a number would be in the range of-1E-74 to -9.E-73, which is unlikely enough to ignore for most purposes.
So, in summary, is this make-a-record technique useful? I'm not sure. Certainly BGET/BPUT or RGET/RPUT from BASIC A + or their USR equivalents under Atari BASIC are much easier to code and use. And, yet, there is a certain elegance to record-oriented techniques which is not entirely lost to me. I probably will stick with the constructs we invented for BASIC A+ , but I would respect a program using the above techniques.
A few last commments: the pokes of line 130 depend on RECORD$ being the first variable defined. Recall my comments from last month about LISTing and reENTERing a program to insure a particular order of definition. Also, if you need to alter a variable other than variable number zero, remember that the formulas are:
VVTP + 8 * VNUM + 4 for the LSB of the length VVTP + 8 * VNUM + 6 for the LSB of the DIMension
(and, again, see last month's article for fuller explanations).
And, finally, I really would be interested in hearing from anyone who uses the techniques I have devised here to produce a unique, real-world program that does things that can't be done otherwise.
Fun With FMS, Canto The First
Remember that fix for burst I/O I gave you in the May, 1982, issue? Did you try it?. Did it prevent burst I/O errors? Yep. Did it slow down every kind of disk read? Yep. Oooooopsy daisy. Well, you can't be completely right all the time. This month, we will try again.
First, I would like to explain, in terms of the FMS listing and the commentary (Chapter 12 — BURST I/O, Inside Atari DOS, COMPUTE! Books) why the fix I gave you in the May, 1982, issue worked insofar as it fixed the burst I/O problems.
To begin with, examine the code at locations $09F8-$09FD and $0AD2-$0AD7. These are the locations in PUT-BYTE and GET-BYTE, respectively, where the burst I/O routine is called. But lo! In PUT-BYTE, the JSR to burst I/O is directly preceded by a BCS, meaning that burst I/O won't occur unless carry is clear. But, in GET-BYTE, the JSR to burst I/O is directly preceded by a BCC — burst I/O occurs in read mode only if carry is set!
Now, if you examine the label "WTBUR" at $OA1F, you will note that the first thing that occurs is a test of FCBFLG to find out if we are in update mode or not. If we are updating, we don't burst. But note that GET-BYTE called the label "RTBUR", AFTER the test, and so would always burst, whether in update mode or not. What I tried to do was change the "JSR RTBUR" (at $0AD4) to a "JSR WTBUR" and then use the carry flag to distinguish between the type of request (I changed the BMI at $0A24 to a BCC). Great! It worked! Except...it worked too well. Unfortunately, FCBFLG is zero (and therefore plus) when we have a file open for read only; so, therefore, the burst I/O was suppressed for all reads. Nuts.
We try again, using a slightly different approach. We will still count on the carry being set when called from PUT-BYTE and reset when called from GET-BYTE. This time, though, we will examine the actual I/O mode in use. FMS receives the I/O mode from CIO when the file is opened and places it in FCBOTC. Recall that the only legal values are 4, 6, 8, 9, and 12. Well, burst I/O is only illegal in modes 6 (read directory) and 12 (update). But mode 6 is handled separately (see $0AC5-$0ACB), so 12 is all we are really concerned with. Anyway, without further ado, here's the listing of the FMS patch:
* = $0A1F ; ; first, patch the code where WTBUR used to be WTBUR BURSTIO LDA FCBOTC,X ; Open Type Code byte EOR #$0C ; check for mode 12...update BEQ NOBURST ; it IS update...don't burst ROR A ; move carry to MSB of A register NOP ; filler only TBURST ; ... and the STA BURTYP remains ... but now BURTYP is; negative if BURSTIO was called from GET-BYTE and ; positive if it was called from PUT-BYTE. ; *= $0A41 ; so we must patch here to account for the sense of being ; inverted from the original. BPL WRBUR ; called from PUT-BYTE *= $0AD4 ; finally, we must patch the GET-BYTE call so that it no ; longer JSR's to RTBUR. JSR BURSTIO ; call the common burst routine ; .END
And for those of you who don't want to type all that in, you might simply use BUG to do the following changes:
C A20 < 82, 13, 49, 0C, F0, 24, 6A, EA C A41 < 10 C AD5 < IF
And, last but not least, from BASIC you may use the following:
POKE 2592, 130 POKE 2593, 19 POKE 2594, 73 POKE 2595, 12 POKE 2596, 240 POKE 2597, 36 POKE 2598, 106 POKE 2599, 234 POKE 2625, 16 POKE 2773, 13
Fun With FMS, Canto The Second
Not long ago, an OSS customer told me that he couldn't use Atari DOS to SAVE (option K on the menu) the contents of ROM. "How sneaky, " cried I, "Best to use the SAVE command under OS/A +. We wouldn't do anything that nasty to you!"
But we did. And we do. And it isn't because we or Atari are sneaky or nasty; it is yet another phenomenon of burst I/O. Recall that when the burst I/O test is passed, FMS calls SIO to transfer the sectors of data directly from the user's buffer space. In order to do so, though, it must write the sector link information (last three physical bytes in a sector) into the correct spot in the user's buffer before calling SIO. Then, when SIO returns, it restores those three bytes and tries to write the next sector the same way. Again, if you have Inside Atari DOS, you can follow this happening at addresses $0A52-$0A7A, in the "WRBUR" code.
Ah...but what happens when you try to do burst I/O writes from ROM? FMS blindly tries to put its goodies into those three bytes and call SIO. SIO does what it is told, and FMS thinks that all is OK. Except that all is not OK! Those three bytes did not get changed, so what was written to the disk is garbage. And even ERAsing the file won't work, because the sector links are badly messed up. Crunchy, crunchy goes the disk, under worst-case circumstances.
Now this restriction is fairly easy to get around: one simply writes a program (in BASIC or machine language) which writes the desired bytes to the disk one at a time, thus preventing burst I/O. So I don't feel that I am giving away deep, dark Atari secrets when I give you an easier method to prevent burst I/O. Simply do either of the following:
from BUG: C A2E < from BASIC: 0 POKE 2606, 0
Again, for those of you with the FMS listing, note that what we are doing is changing the AND #$02 which checks for text mode (the read and write text line commands are $05 and $09, neither of which have bit $02 turned on) into an AND #$00 instruction, thus fooling the BEQ that follows into thinking that FMS can't do burst I/O because it's doing text mode I/O. Not too terribly tricky, and it works well.
I cannot recommend that you make this patch a permanent part of most system disks, since it completely disables burst I/O and makes the system load and save files considerably slower. Change it, use it, and then forget it.