INSIGHT: Atari
Bill Wilkinson
Optimized Systems Software
Cupertino, CA
Last month, we tackled some of the fundamentals of I/O under Atari's OS. This month we will look at the extended disk operations available and will try our hand at writing a useful program in assembly language.
There simply isn't space to repeat the charts given in last month's article, so you will have to open to those pages: we will be referring to them often.
Atari I/O, Part 2: Disk File Manager
Notice that the title of this section is not "ATARI DOS." There is a simple reason, which I expounded on before: Atari does not have a DOS. (But please don't tell them I said so; they think they have to call it "DOS," because that's what evenbody else calls it.) Atari has an "OS"; actually a much more powerful system than what is normally called "DOS" on microcomputers. And please recall from last month that the Atari OS understands named devices, such as "P:" and "E:". The Disk File Manager (DFM) is actually simply a device driver for the disk ("D:") device. It was written completely separately from Atari OS and interfaces to OS the same way any other driver does. In fact, there is nothing magic about the DFM. In theory, by the end of next month's article you should know enough about Atari OS and the DFM to implement your own File Manager and to replace the one that Atari supplies you. (In theory. In practice, you had better know the principles of disk space allocation, I/O blocking and deblocking, and much more, before tackling such a job.) Even if you aren't quite that ambitious, we hope that this series will give you some "insight" into how such things as BASIC'S I/O are implemented.
Extended Disk Operations
We should first note that most of the extended disk operations are documented in the Atari Basic Reference Manual in the section about the XIO command. There are two exceptions, NOTE and POINT, which were given special BASIC commands (and we will see why soon). Naturally, the Atari Disk Operating System II Reference Manual is pertinent, but it doesn't really give more information about the internal workings of Atari's OS than does the BASIC manual. Before delving into assembly language, let's examine each of the extended disk operations in a little detail:
ERASE, PROTECT, UNPROTECT — Also known as Delete, Lock, and Unlock, these three commands simply provide OS with a channel number (i.e., the X-register contains IOCB number times 16), a command number (ICCOM), and a filename (via ICBAL/ICBAH). When OS passes control to the DFM, an attempt is made to satisfy the request. Note that the filename may include "wild cards," as in "D:*.??S" (which will affect all files on disk drive one which have an ‘S’ as the last letter of their filename extension).
RENAME — Very similar to ERASE, et al, in usage. The only difference is in the form of the filename. Proper form is:
"Dn:oldname.ext,newname.ext"Note that the disk device specifier is not and cannot be given twice.
NOTE, POINT — Other than OPEN, these are the only commands we have encountered so far (including last month) which use any of the AUXilliary bytes of the IOCB. For these commands, one specifies the channel number and command number and then receives or passes file pointer information via three of the AUX bytes. ICAX3/ICAX4 are used as a conventional 6502 LSB/MSB 16-bit integer: they specify the current (NOTE) or the to-be-made-current (POINT) sector within an already OPENed disk file. ICAX5 is similarly the current (NOTE) or to-be-made-current (POINT) byte within that sector. These are complex commands to use, but their operation from BASIC is adequately covered in the Atari DOS II Manual so it will not be covered here.
OPEN—Open is not truly an extended operation, but for disk I/O we need to know that the DFM allows two additional "modes" beyond the fundamental OS modes (which are 4, 8, and 12 for read, write, and update). If ICAX1 contains a 6 when DFM is called for OPEN, then the disk DIRECTORY is opened (instead of a file) for read-only access. The filename now specifies the file (or files, if wild cards are used) to be listed as part of a directory listing. Note that DFM expects this type of OPEN to be followed by a succession of GETREC (get text line) OS calls (and we present an example of this below). If ICAX1 contains a 9, the specified file is opened as a write-only file, but the file pointer is set to the current end-of-file. Caution: DFM only appends on sector boundaries (normally this is transparent to the user, but caveat artificer).
Error Handling
This may not be the best place to introduce this topic, but the information is needed for examples which follow. Space doesn't permit a listing of all the I/O error codes, so we must refer you again to the BASIC and/or DOS II reference manuals. There are four fundamental kinds of errors that can occur with Atari OS:
HARDWARE ERRORS—Such as attempting to read a bad disk, write a read-only disk, etc.
SERIAL BUS ERRORS—Errors which occur when data is transferred between the computer and a peripheral device. Examples include Device Timeout, Device NAK, Framing Error, etc.
DEVICE DRIVER ERRORS—Found by the driver for the given device, as in (for the DFM) File Not Found, File Locked, Invalid Drive Number, etc.
OS ERRORS—Usually fundamental usage problems, such as Bad Channel Number, Bad Command, etc.
On return from any OS call, the Y-register contains the completion code of the requested operation. A code of one (1) indicates "normal status, everything is okay." (I know, why not zero, which is easier to check for? Remember, I said Atari was good, not perfect.) By convention, codes from $02 to $7F (2 through 127 decimal) are presumed to be "warnings." Those from $80 to $FF (128 through 255 decimal) are "hard" errors. These choices facilitate the following assembly language sequence:
JSR CIOV ;call the OS TYA ;check completion code BMI OOPS ;if $80-$FF,it must be an error
In theory, Atari's OS always returns to the user with condition codes set such that the TYA is unnecessary. In practice, that's probably true; but a little paranoia is often conducive to longer life of both humans and their programs.
A Real, Live Example
Believe it or not, you now have all the information you need to do from assembly language any and all I/O done by Atari BASIC and/or BASIC A+ (excepting graphics, but that's coming…hold your breath). In an attempt to make you believe that statement, we will write a program in both BASIC and assembly language.
The BASIC Program
100 DIM BUFFER$(40) 200 OPEN #l,6,0,"D:*.*" 300 TRAP 700 400 INPUT #1,BUFFER$ 500 PRINT BUFFER$ 600 GOTO 400 700 CLOSE #1
This program will list all files on disk drive one (Dl:) to the screen. This is exactly equivalent to using the "A" option of Atari's menu "DOS" (and then hitting RETURN for the filename) or to using "DIR" from OS/A + . Admittedly, this program is easily improved. For example, replace line 200 with:
200 INPUT BUFFER$ : OPEN #l,6,0,BUFFER$
and now you can choose to list only some files. You might also wish to send the listing to the printer (change PRINT to LPRINT). However, we will leave such changes as an exercise to the reader and discuss only our simplified version.
Please now refer to the listing in Program 1.Since it follows the scheme of the above BASIC listing, it is almost self-explanatory. A few words are in order, though. The equates at the beginning have been kept to a minimum; I refer you to the "SHOOT" listing in COMPUTE! #16 if you want a comprehensive list. (The mnemonics used are not all identical to those in the "SHOOT" listing; those shown are from our standard equates file.)
The program is intended to be called from BASIC via the USR function. However, no check is performed to see if the BASIC program were coded as (for example) PRINT USR(1600,0) instead of just PRINT USR( 1600). (Note that 1600 decimal = 640 hex, the starting address.) If you would like to test this program with the BUG debug monitor, you should replace the RTS at the end of the program with a BRK before saying ‘G641’ (641 to avoid the PLA).
All errors, including an error on the OPEN DIRECTORY call to OS, are treated as end-of-file. A better program would verify the error status and print a message or some such. As an example of a minor improvement, at LINE700 one could save the Y-register (status) value in FR0 and zero in FR0+1 ($D4 and $D5), thus returning the error code to the calling BASIC program.
Notice that values stored into the IOCB for FILE0 (the console screen output) were stored directly into ICCOM, etc., without an X-register offset. This is perfectly valid, so long as the X-register contains the proper value on calling CIO. In fact, we could have stored the values for FILE1 (the directory) by coding (for example) STA ICCOM + FILE1. Obviously, this technique only works when one uses a constant channel number; but most BASIC programs and many language programs can use predefined channel numbers.
There isn't really much more to say other than, "Try it!" It really does work. And, even if you don't understand the concepts on first reading, actually entering the program and following the program flow and remarks might give you a painless introduction to I/O from assembly language.
The Easiest Way Of Making Room?
With an ATARI 400 or 800, there are many ways and places to find "safe" hunks of memory, places to put assembly language routines, player/missile graphics, character sets, etc. Many of the programs that I have seen involved techniques that I consider risky. For example, moving BASIC'S top of memory down requires that one do so only after issuing a GRAPHICS, command for the most memory-consuming graphics mode used in the program.
Other programs use machine language subroutines: but such subroutines must themselves have a place to stay. The best of such routines, however, approach the "official" Atari method. The approved method is normally used (by Atari) to add device drivers to the OS; in fact, the drivers for both DOS and the RS-232 ports follow these rules:
- Inspect the system LOMEM pointers.
- Load your routine (or reserve your buffer) at the current LOMEM.
- Add the size of the memory you used to LOMEM and
- Store the resultant value back into LOMEM.
If each routine, driver, etc., followed these rules, one could reserve more and more of memory without disturbing any following routine. (In fact, Atari drivers presume that LOMEM will never grow beyond 16K, $4000, or even less; but the principle holds.) Actually, there's a hole in the above method; if the SYSTEM RESET button is pushed, OS goes through and resets all its tables, including the value in LOMEM. A "good" device driver can even take this into account, but we are going to make a few presumptions that are generally valid.
By now, you should realize that all of BASIC's fundamental I/O commands are simply implementations of OS calls. PRINT becomes PUT TEXT RECORD; INPUT becomes GET TEXT RECORD; OPEN and CLOSE are essentially unchanged. In fact, the only BASIC commands that are not obvious clones of their assembly language counterparts are GET and PUT. Suffice it to say that these are actually simply special case implementations of GET BINARY RECORD and PUT BINARY RECORD (commands 7 and 11) where the buffer length is set to one byte.
Next month, we tackle the task of understanding how device drivers work, and we actually write a new and useful one that talks to a device built into all Atari machines (but one that Atari didn't provide a driver for). And we haven't forgotten the promise to show how graphics routines (such as PLOT and DRAWTO) are actually I/O routines.
The trick: BASIC always, repeat always, LOADs new programs at what it perceives LOMEM to be! Unfortunately, BASIC keeps its own MEMLOW pointer, which is loaded from LOMEM only on execution of a NEW, not on execution of LOAD or RUN and (significant!!!) not even in the case of SYSTEM reset. However, when there's a will…
—ATARI BASIC— 10 LOMEM = 743:MEMLOW = 128 20 ADDR = PEEK(LOMEM)+256 * PEEK (LOMEM +1) 30 ADDR = ADDR+SIZE 40 HADDR = INT(ADDR/256):LADDR = ADDR -256 * HADDR 50 POKE LOMEM, LADDR:POKE LOMEM+1, HADDR 60 POKE MEMLOW, LADDR:POKE MEMLOW+1, HADDR:RUN "D:PROGRAM2" —BASIC A+ — 10 lomem = 743:memlow = 128 20 addr = dpeek (lomem):dpoke lomem, addr+size 30 dpoke memlow, addr+size:run "D:PROGRAM?"
The above listing is Program A, whose only purpose in life is to set up memory for the real program, Program B. "SIZE" is the amount of memory to be reserved. The program changes both the system and BASIC bottom-of-usable-memory Pointers so that either NEW or RUN "…" will recognize the reserved memory. The beginning lines of PROGRAMB follow:
—ATARI BASIC— 10 LOMEM = 743:MEMLOW = 128 20 POKE LOMEM,PEEK(MEMLOW):POKE LOMEM+1,PEEK(MEMLOW+1) —BASIC A+— 10 dpoke 743, dpeek(128)
The only reason for these lines in PROGRAMB is in case of SYSTEM RESET. If the user types RUN after the reset, BASIC will copy its MEMLOW (the value which includes the reserved space!) into the system's LOMEM, just so they agree with each other. A caution: I don't know what will happen if you hit SYSTEM RESET as BASIC is in the process of loading PROGRAMB.
As far as I can tell, the only real problem that could occur would be if SYSTEM RESET were followed by a "DOS" command from BASIC. The OS would then get control, thinking that LOMEM had not been changed. In a normal running program environment, though, this is, at worst, unlikely, so this method seems more than adequate.
Columnar Output
A problem inherent in Atari BASIC is that the default tabbing (when using ‘PRINT exp,exp’) is ten columns while the screen is 38 columns wide. This produces an output something like this:
PRINT 1,2,3,4,5,6,7,8,9,10 1 2 3 4 5 6 7 8 9 10
Not too pretty. POKE 82, 0 will change the left margin of the screen to zero (default is column 2), thus producing a 40 column screen and thus making 10 column tabbing an excellent choice. Unfortuntely, many TV sets have too much overscan to handle a true 40 column screen. Fortunately, Atari BASIC allows one to change the number of columns used in tabbing via a POKE 201, <tabwidth>. But the only factors of 38 are 19 and 2, meaning you can have 19 columns of 2 characters each or 2 columns of 19 characters each. Not much improvement so far.
Consider, though, the table of factors shown in Figure 1. As an example, if we have a screen 36 characters wide, we can have 2, 3, 4, 6, 9, 12, or 18 columns. And to get a screen 36 characters wide is easy: just POKE 83, 37 (presuming that location 82 still contains a 2). So look at the list of factors, choose a screen width of N, and you can use a tab width equal to any factor. NOTE: a tabwidth of two will not print numerics in only two columns.
Finally, consider the flexibility available by judiciously choosing your tabwidth setting:
20 POKE 201,4: PRINT 1, 2, 30 POKE 201,7: PRINT 3, 40 POKE 201,10: PRINT 4, 5
Printing various values in a loop with this method can actually produce some quite readable columnar listings.
N | Factors of N |
40 | 2,4,5,8,10,20 |
39 | 3,13 |
38 | 2,19 |
37 | none |
36 | 2,3,4,6,9,12,18 |
35 | 5,7 |
34 | 2,17 |
33 | 3,11 |
32 | 2,4,8,16 |
Figure 1.