THE ATARI® GAZETTE
Optimized Systems Software
This month marks the end of my series on Atari I/O. That certainly doesn't mean that we won't continue to discuss assembly language I/O of related topics; it simply means that I feel I have finished my formal presentation of the material. Again, I strongly urge you to purchase the Atari Technical User's Notes (available from Customer Service, 1340 Bordeaux Ave., Sunnyvale, CA 94086, for $30, including shipping). There is a lot of detail in those "notes," including much that I have glossed over. I hope that my presentation, though, has served as a usable introduction to the subject.
Also this month, I give you a method for creating relocatable assembly language programs (and a method to then load them). We use the loader to implement our "M:" driver from last month, completely via BASIC (thus making it usable for those of you not yet into assembly language...and it is usable).
Finally, we continue our discussion of how BASIC works. De Re Atari, and the serialized version thereof which appears in this month's BYTE, does a good job of discussing the how of BASIC's syntaxer; we will delve into the why.
Atari I/O, Part 4: GRAPHICS
Errata! Before we get started on this month's topic, I must report an error I made in COMPUTE! #18. On page 100, in Table 1, under the "Note" pertaining to ICBLL/ICBLH, I stated that the length is decremented by one for each byte transferred. Actually, Atari's OS is smarter than that: upon return from GET/PUT RECORD (text or binary) ICBLL/ICBLH contain a count of the number of bytes successfully transferred. This result is eminently usable (e.g., in copying records or even whole files), and perhaps we will have a program here soon that demonstrates its use.
On with the new : this whole series started as a result of a comment that I read which said something like "Atari graphics from assembly language are hard to do — you have to know about display lists, vertical blank interrupts, etc." Knowing how BASIC does graphics for its users I said, "Nonsense! It's easy! Someone should show how easy!" And Richard Mansfield, of COMPUTE!, said, "Gee, I wonder who we could get..." Ahem.
If what you are trying to do is write an improved version of Eastern Front or Pacman or some other such pioneering project, then you need to know everything ever published and then some. But, if what you want is simply a way to transfer what you have learned or written using BASIC into a reasonably simple set of assembly language routines, read on.
Remember, BASIC does all its graphics and I/O via Atari's OS. BASIC knows nothing of graphics modes, display lists, character sets, color registers, etc. (True, BASIC A + does its own thing with Player/Missile Graphics, but that's only because Atari's OS doesn't know about PMG.) So, anything done with standard BASIC statements can be duplicated easily in assembly language. To demonstrate the truth of this, Figure 1 contains a list of the seven BASIC graphics statements together with a note on how each is accomplished.
Accompanying this article is a listing of my proposal for a set of standard routines to be used by assembly language programmers when interfacing to OS graphics. These routines duplicate, as far as practicable, the statements used to do BASIC graphics. The listing clearly calls out ENTRY and EXIT parameters for each routine (i.e., register usage), so study it carefully.
As a very simple example of the routines' usage, I offer a program fragment that is written in both BASIC and assembly language:
|GRAPHICS 3||LDA #3|
|COLOR 3||LDA #3|
|PLOT 10, 10||LDX # 10|
|DRAW TO 25, 15||LDX #25|
|SETCOLOR 2, 0, 14||LDX #2|
Before leaving this topic, some notes on the routines might be helpful: since the A-register will be zero upon entry to PLOT, DRAWTO, LOCATE, and POSITION for all graphics modes except GRAPHICS 8 (or 24), placing a LDA #0 in the beginning of POSITION would save code for anyone not using mode 8. Remember, Atari's "S:" driver can accomodate GRAPHICS 0 through 11 and 17 through 24. Adding 32 ($20) to any graphics mode (at the time of the call to GRAPHICS) will suppress the erasure of the screen. (I haven't figured out a use for this yet, but it's nice to know it's there.)
Obviously, one could save time (and sometimes space) by performing COLOR and SETCOLOR and POSITION via simple stores (e.g., STA), but there is a certain structuring and elegance that goes with the use of the routines. The graphics routines listed herein were assembled in the $600 page of memory, a much overworked location. I would hope that you would take the time to type them in to your assembler/editor and include them directly in future programs (EASMD users may.INCLUDE them indirectly). I really would appreciate hearing of your successes (or failures, if any) using these routines.
So far, no assembler available for the Atari produces relocatable, linkable object files (and, from what I have heard, neither will Atari's Macro Assembler). When we produced BASIC A+ and EASMD, we wanted them to move themselves to the top of memory, so we re-invented a scheme I have seen in several incarnations before: Assemble the program twice, setting the origin for any portion(s) to be relocated one page (256 bytes) higher for the second assembly, producing two object files. Write a program that compares the two objects and notes all locations that differ by one (differing by any other amount is an error). Produce a table (or bit map, or ...) of all these differences. At relocatable load time, read in the first object file (to where it is to be relocated) and use the table to change all the bytes which need to be relocated.
The system is a kludge, but a very effective one. It has a few limitations: you still don't have linkable object files, you must relocate in full page increments (i.e., multiples of 256 bytes), and you have to have some place safe to put the relocating loader. Are you willing to live with those limits? Then try this.
I present here three BASIC programs together with instructions for their use. The first program, MAKEREL (Program 1), seems to be to be perfectly adequate as is, written in BASIC. It's a little slow, but one only uses it when ready to create a new relocatable object file. The other two programs, LOADREL.A and LOADREL.B (Programs 2 and 3), could be advantageously rewritten in assembly language. They are presented here in BASIC because (1) this method fulfills the requirement for a "safe place" for the loader and (2) by presenting them in BASIC they can be used by those not yet ready to tackle assembly language and (3) it was easier for me.
The instructions below presume the use of the Atari Assembler/Editor or the OSS EASMD, but they can be easily adapted to most systems that produce Atari DOS-compatible object files.
How To Use The Relocator Programs
- Write, assemble, and debug your code using some fixed address(es).
- Ensure that your code is all in one piece (i.e., there is only one * =, at the beginning of the code segment).
- Origin your code on an even page boundary (i.e., use * = $hh00, there ‘hh’ specifies any page from 02 through FE). Assemble the code into an object file on disk named "OBJECT1" (use ASM,,#D:OBJECT1).
- Change your origin to one page higher in memory (* = $nn00, where ‘nn’ = ‘hh’ + 1). Assemble the code to "OBJECT2" (ASM,, #D:OBJECT2).
- Run the MAKEREL program. It will produce the file "DATA.REL".
- Adjust the value of the variable NUMBEROF-PAGES in both LOADREL.A and LOADREL.B (Programs 2 and 3) to reflect the number of 256-byte pages needed by your routine. SAVE the adjusted versions.
- Anytime you want to load your routine, simply use RUN "D:LOADREL.A".
- Generally, it's a good idea to have your routine start execution at the origin (*=) point. Then you can invoke it from BASIC via USR(PEEK(128) + 256 *(PEEK(129) - NUMBEROFPAGES))
- If you RUN "D:LOADREL.A" again without hitting RESET, it will load another copy above the first. Not too neat, but the advantages of being able to thus load several different modules should be obvious!
- LOADREL.B performs an ENTER "D: DATA.REL". Rather than waiting for the ENTER each time, you may SAVE the resultant program (after taking out the ENTER line) for a slightly faster load of a specific module.
Finally, we offer Program 4 which may be added to LOADREL.B to produce a relocatable load of last month's "M:" driver. (Again, be sure to delete the ENTER line from LOADREL.B.)
For once, I haven't forgotten you cassette users. If you enter LOADREL.A (carefully, please!) and CSAVE it (or SAVE "C:") on a blank tape you need only change the last line to read RUN "C:". Then NEW and enter LOADREL.B, leaving out the ENTER line, but including the listing of Program 4. Use SAVE"C:" (do NOT use CSAVE...it won't work!) to place the resultant combination on the tape after LOADREL.A (and, of course, you could then follow on the same tape with a program of your own). You may now enjoy the "M:" driver via this tape by CLOADing and RUNning the first program (or use RUN"C:" if you used SAVE"C:", my own preference for all but the largest programs).
MAKEREL could also be adapted to cassette usage, though not without difficulty and/or a relatively large amount of memory. Obviously, these programs can be improved upon tremendously by simply adding, for example, flexibility of file name. But my intention was to present something as simple and straightforward as possible, in the hopes that everyone would find it readable and useful. Obviously, my techniques could be adapted to other machines (does the PET have a relocating assembler?), so adapt away (and be sure to send COMPUTE! the results to share with the rest of us). On to lighter subjects.
Inside Basic, Part 2: The Why Of Syntaxing
Last month I presented a program to print out the keywords of BASIC. If you took the time to enter and run that program, you saw some strange things in the printout of the operators. But there was a method to our madness, as you will see.
Let us examine the tokenized (internal) form of the following line:
1025 PRINT "HI THERE", THIS * (3 + IS(FUN)) : STOP
Assuming that we had just previously NEWed, the tokenized form of that line is as follows (all numbers in decimal):
01 04 36 33 32 15 08 72 73 32 84 72 69 18 128 36 43 14 64 03 00 00 00 00 37 129 56 130 44 44 20 36 38 22
Now that isn't too terribly useful or readable, so let's examine the tokens one at a time:
|01 04||This is the line number (4*256 + 1 = 1025) in standard 6502 form.|
|36||This is the line length, including the line number and this byte.|
|33||Statement length of the first statement. Actually, this is the displacement to the beginning of the next statement (from the beginning of the line).|
|32||The token for PRINT. Check the output of the keyword printing program from last month.|
|15||A special token that says a string constant follows.|
|08 72 73 32 74 72 69 82 69||The string constant consists of a byte that gives the length of the string followed by the characters of the string. Note that the quotes have disappeared.|
|18||The comma, tokenized.|
|128||Our first variable! Operator tokens over 127 are variables. The variable number (in the variable table) is 128 less than the token value. This variable is THIS.|
|36||The multiplication operator.|
|43||One variety of left parenthesis. This one is a normal or expression left parenthesis.|
|14||Another special token (actually, number 2 of 2), says a numeric constant follows.|
|64 03 00 00 00 00||The constant, in Atari BASIC internal floating point form. This is unique, as we shall see soon.|
|37||An addition operator.|
|129||The variable IS (already known to be an array, though it has not yet been DIMensioned).|
|56||Another left parenthesis. This one is called an "array left paren" in the BASIC source listing. We will later see why it is distinct.|
|130||Our last variable, FUN.|
|44 44||Two right parentheses. Strange, they are both the same.|
|20||Our End-Of-Statement token, otherwise known as a colon.|
|36||The statement end displacement for the second statement on this line.|
|38||The token for STOP. Again, refer to the keyword listing program.|
|22||An End-Of-Line token, otherwise known as a RETURN.|
|GRAPHICS g||If bit 4 ($10) of ‘g’ is on, this is the same as OPEN #6, 12, g-16 "S:" If the bit is off, this is the same as OPEN #6, 16 + 12,g, "S:"|
|(Note: the fifth bit, $20, of ‘g’ should be copied into AUX1, the OPEN mode.)|
|COLOR c||Simply saves ‘c’ in a safe place.|
|POSITION h,v||Places ‘h’ in locations $55 and $56 (LSB, MSB)|
|Places ‘v’ in location $54|
|PLOT h,v||Performs a POSITION h,v and then Performs a PUT #6,c (where ‘c’ is the color saved by COLOR)|
|LOCATE h,v,c||Performs a POSITION h,v and then Performs a GET #6,c|
|DRAWTO h,v||Performs a POSITION h,v and then Does a POKE 763,c (‘c’ is the COLOR saved, as above) and then Performs an XI0 17, #6,12,0, "S:"|
|SETCOLOR r,h,lu||Is equivalent to POKE 708 + r, h*h16 + lu|
Note: FILL may be performed from assembly language by following exactly the same sequence specified in the Basic Reference Manual, using XI0 18, etc.
Program 1: MAKEREL
100 REM *** OPEN ALL 3 FILES *** 110 OPEN #1, 4, 0, "D : OBJECT1" 120 OPEN #2, 4, 0, "D : OBJECT2" 130 OPEN #3, 8, 0, "D : DATA.REL" 150 REM *** INITIALIZE VARIABLES *** 160 LINE = 10000 170 DCNT = 0 200 REM *** STRIP HEADER ($FFFF) WORD *** 220 GET #1,FF:GET #1, FF 230 REM STRIP HEADER AND ADDRESSES FROM FILE2 240 GET #2,FF:GET #2,FF:REM HEADER 250 GET #2,FF:GET #2,FF:REM START ADDRESS 260 GET #2,FF:GET #2,FF:REM END ADDRESS 300 REM *** PROCESS ADDRESSES *** 310 GET #1,LOW:GET #1, FIRSTHIGH:FIRST=LOW+256*FIRSTHIGH 320 GET #1,LOW:GET #1, HIGH:LAST=LOW + 256*HIGH 400 REM *** READY TO PRODUCE OUTPUT *** 410 FOR ADDR=FIRST TO LAST 420 IF DCNT=0 THEN PRINT #3;LINE;" DATA ": LINE=LINE+10 430 GET #1,B1:GET #2,B2 440 IF B1=B2 THEN 480 450 IF B2<>B1+1 THEN PRINT "BAD RELOCATION":STOP 460 B1=B1-FIRSTHIGH:REM THE RELOCATION FACTOR 470 PRINT #3:"*":REM AND FLAG THIS BYTE 480 PRINT #3;B1; 490 DCNT=DCNT+1 500 IF DCNT <=9 THEN PRINT #3;","; 510 IF DCNT >9 THEN DCNT=0:PRINT #3 520 NEXT ADDR 530 REM *** CLEAN UP *** 540 IF DCNT=0 THEN PRINT #3;LINE;" DATA "; 550 PRINT #3;"=" 560 PRINT #3;"GOTO 500" 580 CLOSE #1:CLOSE #2:CLOSE #3 590 END
Wasn't that fun? For a masochist? Hopefully, you are asking questions that begin with "Why."
Why tokenize at all? For compactness: in our example we saved six bytes over a straight source line. For speed: it is much faster (at run-time) to discover that, for example, 32 means "PRINT" than it would be if we had to examine the letters "P", "R", "I", "N", "T" for a keyword match. Because tokenizing is almost an automatic by-product of syntaxing.
Why syntax-check at entry? Because it is embarrasing to give a program to someone, have them run it, and get a SYNTAX ERROR message at line 23776 (the line that handles disk full conditions, which we never got to when we were testing). Because it makes program entry so much easier for beginners, particularly kids. Because I like it.
Program 2: LOADREL.A
10 REM *** THIS IS LOADREL.A *** 20 REM (THIS SIMPLY SETS UP MEMORY FOR LOADREL.B) 30 NUMBEROFPAGES=1:REM CHANGE THIS AS NEEDED 40 SIZE=256*NUMBEROFPAGES 100 REM *** SEE COMPUTE! #19 *** 110 LET LOMEM=743:MEMLOW=128 120 LADDR=PEEK(LOMEM):HADDR=PEEK(LOMEM+1) 129 REM -- LINE 130 ENSURES THAT 1K BYTES STARTS ON PAGE B BOUNDARY -- 130 IF LADDR <> 0 THEN LADDR=0:HADDR=HADDR+1 140 ADDR=LADDR+256*HADDR 150 ADDR=ADDR+SIZE 160 HADDR=INT(ADDR/256):LADDR=ADDR-256*HADDR 170 POKE LOMEM,LADDR:POKE LOMEM+1, HADDR 180 POKE MEMLOW,LADDR:POKE MEMLOW+1,HADDR:RUN "D:LOADREL.B"
Program 3: LOADREL.B
100 REM *** THIS IS LOADREL.B *** 110 REM 120 REM THIS PROGRAM DOES THE ACTUAL RELOCATABLE LOAD 130 REM 140 DIM TEMP$(10) 150 NUMBEROFPAGES=1:REM ADJUST TO SAME AS LOADREL.A 200 REM AGAIN. SEE COMPUTE! #19 210 LET LOMEM=743:MEMLOW=128 220 POKE LOMEM, PEEK(MEMLOW):POKE LOMEM+1, PEEK(MEMLOW+1) 300 REM RPAGE IS THE MEMORY PAGE WHERE WE RELOCATE TO 310 RPAGE=PEEK(MEMLOW+1)-NUMBEROFPAGES 330 REM OBVIOUSLY, THIS VALUE SHOULD MATCH THE MEMORY 340 REM RESERVED IN ‘LOADREL1.SAV’ 350 ADDR=RPAGE*256:REM STARTING ADDR OF LOAD400 REM **************************** 410 REM * GET THE RELOCATION DATA * 420 REM **************************** 450 ENTER "D:DATA.REL" 500 REM *** THE ENTER BRINGS US HERE *** 510 READ TEMP$ 520 IF TEMP$ (1,1)="=" THEN END 530 IF TEMP$(1,1)<>"*" THEN POKE ADDR,VAL(TEMP$):GOTO 550 540 POKE ADDR,VAL(TEMP$(2))+RPAGE:REM RELOCATION 550 ADDR=ADDR+1:GOTO 510
Program 4: DATA.REL
520 IF TEMP$ (1,1)="=" THEN 1000 1000 REM LINE 1010 IS USED TO INITIALIZE THE M: DRIVER 1010 JUNK=USR(RPAGE*256+48) 1020 END 10000 DATA 162,0,189,26,3,240,10,201,77,240 10010 DATA 26,232,232,232,208,242,96,169,77,157 10020 DATA 26, 3, 169, 59, 157, 27, 3, 169, *0, 157 10030 DATA 28, 3, 169, 0, 157, 29, 3, 169, 0, 141 10040 DATA 231, 2, 169, *1, 141, 232, 2, 96, 104, 240 10050 DATA 205, 168, 104, 104, 136, 208, 251, 240, 197, 76 10060 DATA *0, 111, *0, 146, *0, 133, *0, 159, *0, 73 10070 DATA *0, 76, 74, *0, 160, 1, 96, 189, 74, 3 10080 DATA 41, 8, 240, 13, 173, 229, 2, 141, 210, *0 10090 DATA 172, 230, 2, 136, 140, 211, *0, 173, 210, *0 10100 DATA 141, 206, *0, 173, 211, *0, 141, 207, *0, 160 10110 DATA 1, 96, 189, 74, 3, 41, 8, 240, 12, 173 10120 DATA 206, *0, 141, 208, *0, 173, 207, *0, 141, 209 10130 DATA *0, 160, 1, 96, 72, 32, 181, *0, 104, 160 10140 DATA 0, 145, 224, 32, 192, *0, 96, 32, 160, *0 10150 DATA 176, 7, 160, 0, 177, 224, 32, 192, *0, 96 10160 DATA 32, 181, *0, 205, 208, *0, 208, 9, 204, 209 10170 DATA *0, 208, 4, 160, 136, 56, 96, 160, 1, 24 10180 DATA 96, 173, 206, *0, 133, 224, 172, 207, *0, 132 10190 DATA 225, 96, 172, 206, *0, 208, 3, 206, 207, *0 10200 DATA 206, 206, *0, 160, 1, 96, 0, 0, 0, 0 10210 DATA 0, 0, =
Program 5: Graphics Routines, Equates
0000 1010 .PAGE "Equates, etc." 1020 ; 1030 ; CIO EQUATES 1040 ; E456 1050 CIO = $E456 ; Call OS thru here 0342 1060 ICCOM = $342 ; COMmand to CIO in IoCb 0344 1070 ICBADR = $344 ; Buffer or filename ADdRess 0348 1080 ICBLEN = $348 ; Buffer LENgth 034A 1090 ICAUX1 = $34A ; AUXilliary byte # 1 034B 1100 ICAUX2 = $34B ; AUXilliary byte # 2 1110 ; 0003 1120 COPN = 3 ; Command OPeN 000C 1130 CCLOSE = 12 ; Command CLOSE 0007 1140 CGBINR = 7 ; Command Get BINary Record 000B 1150 CPBINR = 11 ; Command Put BINary Record 0011 1160 CDRAW = 17 ; Command DRAWto 0012 1170 CFILL = 18 ; Command FILL (note used in this demo) 1180 ; 0004 1190 OPIN = 4 ; Open for INput 0008 1200 OPOUT = 8 ; Open for OUTput 1210 ; 1220 ; 1230 ; EQUATES used by the S: driver and
Why one-byte variable numbers? Again, for speed and compactness. Use variable names as long as you like: only the first usage eats up any more memory than a single-character, undecipherable variable name. There are disadvantages: a maximum of 128 different variables, a mispelled variable name can't be purged from the variable table without LISTing and reENTERing. On the whole, a very wise choice (I can say that, it's one part of Atari BASIC I didn't design into the specs).
Why internalized numeric constants? For speed. Period. Well, maybe for simplicity at run-time, but that's only a maybe. Did you know that numeric constants in Atari BASIC actually execute faster than variables? Write a timing loop and prove it to yourself.
Why line length bytes? Do you need them if you have statement length bytes? We don't need them, but they make line skipping (as when we are executing a GOTO) faster than it would be if we had to skip individual statements.
Why statement length bytes? Given that you have line length bytes? This one is harder to answer, because it has to do with how we execute GOSUB/RETURN, etc. I will leave that for a later article, but I will note that these bytes were extremely helpful when it came to implementing the IF...ELSE...ENDIF structure in BASIC A +.
Why decimal floating point? Because it is easier for beginners to understand (try PRINT 123.123-123 using Applesoft) and is obviously preferable for money applications. Actually, our decimal add and subtract are faster than the corresponding binary routines. Admittedly, multiply suffers a little and divide suffers a lot.
Why different kinds of left parentheses? Why several kinds of equal sign? Because it's easy for the syntaxer to see the different kinds of equal signs in, for example, LET A = B = C + D$ = E$. Sure, we could tell the difference at run time from context, but why should we when it's so easy to distinguish between a 45 and a 34 and a 52?
Why doesn't Atari BASIC have string arrays? I really didn't want to put this question in, but I wanted to save myself the letters and threatening phone calls. The best reason is that it was a choice of string arrays or syntax checking. (Obviously, I like the choice.) Other rationales include the fact that Atari was aiming for the educational market, where the HP2000 (with 72-character, Atari-style strings) was the de facto standard.
My personal favorite reasons are twofold: (1) anything you can do with string arrays you can also do with long strings (admittedly, sometimes with a little more difficulty) though the reverse is definitely not true; and (2) string arrays are unique to DEC/ Microsoft/??? BASIC and do not appear in that form in any other of the more popular languages (e.g., FORTRAN, COBOL, PASCAL, C, FORTH, etc.). Techniques learned with long strings are portable to these other languages: techniques involving string arrays are, at best, difficult to transfer. Finally, long strings as implemented on the Atari have some unique advantages not immediately obvious. I hope to explore some of these advantages in future columns.