Index

COMPUTE! ISSUE 20 / JANUARY 1982 / PAGE 120

INSIGHT: Atari

Bill Wilkinson
Cupertino, CA

I have recently seen a copy of the complete De Re Atari (by Atari’s own Chris Crawford, author of SCRAM and EASTERN FRONT, et al). Since two out of three people I talk to say “Huh?” when I mention the name, I have personally subtitled it Everything You Ever Wanted to Know About the Atari Computers But Didn’t Know Enough to Ask. The book concerns itself with foibles, tricks, innards, hardware, software, and everything in between: there are even tricks using Atari BASIC (that are “obvious” upon discovery) which we never thought about when we designed the thing! I must heartily recommend that every serious Atari programmer trade in his or her left thumb, if necessary, for a copy of this book.

“De Re” (the insiders’ appellation) is currently being serialized in BYTE magazine (I guess Atari’s trying to impress the non-Atari world), but seeing the book in one piece is somehow more instructive. “De Re” is generally a fantastic resource, but it does often assume that the reader has intimate knowledge and understanding of the Atari Hardware Reference Manuals, etc. This is not a fault (the authors forewarn the reader); and, besides, it does leave room for columns like this. I don’t intend to duplicate material in either Atari’s manuals or “De Re”, but there is bound to be some overlap. I intend to present the “hows” and “whys” to supplement Atari’s “whats.”

I try to write this column for the programmer: the person who knows software, but is unfamiliar with Atari hardware and/or Atari’s system level software. If this column stretches your understanding of the Atari and/or its software, that’s probably good. And I am constantly amazed at the questions which beginners on the Atari come up with; they often show “insights” to solution methods that I wouldn’t dream of. The first questions are arriving in my mailbox. Send more!

This month’s column is part three of the series on the Atari Operating System. Next month we will cover screen output, including graphics, to formally end the series. I have a few ideas on what should come next for you non-BASIC Atari users, but I would welcome some input. Also, this month, we begin a series which will explore the inner workings of Atari BASIC.

Atari I/O, Part 3: Device Handlers

As we noted before, Atari’s OS is actually a very small program (approximately 700 bytes). Even so, it is able to handle the wide variety of I/O requests detailed in the first two parts of this series with a surprisingly simple and consistent assembly language interface. Perhaps even more amazing is the purity and simplicity of the OS interface to its device handlers.

Admittedly, because of this very simplicity, Atari’s OS is sometimes slower than one would wish (probably only noticeably so with PUT BINARY RECORD and GET BINARY RECORD) and the handlers must be relatively sophisticated. But not overly so, as we will show.

The Device Handler Table

Atari OS has, in ROM, a list of the standard devices (P:, C:, E:, S:, and K:) and the addresses thereof. So far, so good. But notice that, for example, the disk handler (D:) is not listed there; how does OS know about other devices? Simple. On SYSTEM RESET, the list is moved from ROM to RAM, and OS then utilizes only the RAM version. To add a device, simply tack it on to the end of the list: you need only specify the devices name (one character) and the address of its handler table (more on that in a moment). To reassure you that it is this simple, let me point out that this is exactly how the “D:” (Disk) handler is attached when the disk is booted.


      *=      $031A
HTABS                   ; the Printer device
     .WORD    PDEVICE   ; and the address of its driver
     .BYTE    'C'       ; the Cassette device
     .WORD    CDEVICE
     .BYTE    'E'       ; the screen Editor device
     .WORD    EDEVICE
     .BYTE    'S'       ; the graphics Screen device
     .WORD    SDEVICE
     .BYTE    'K'       ; the Keyboard device
     .WORD    KDEVICE
     .BYTE    0         ; zero marks the end of the table
     .WORD    0         ; ...but there's room for up to
     .BYTE    0         ; ...9 more devices et cetera

Figure 1.

In theory, all named device handlers under Atari OS may handle more than one physical device. Just as the disk handler understands “D1:” and “D2:”, so could a printer handler understand “P1:” and “P2:”. In practice, of all the standard Atari handlers only the Disk and Serial Port handlers can utilize the sub-device numbers. Incidentally, Atari OS supplies a default sub-device number of “1” if no number is given (thus “D:” becomes “D1:”). A project for those of you with two printers (there must be one or two of you): presumably one of them is connected via the MacroTronics interface; if so, try modifying the MacroTronics handler so that “P1:” refers to the Atari 850 interface while “P2:” refers to the MacroTronics. It’s really a fairly easy project, presuming you have the listings of Atari’s OS (which are available from Atari).

Rules For Writing Device Handlers

Each device which has its handler address placed into the handler address table (above) is expected to conform to certain rules. In particular, the driver is expected to provide six action subroutines and an initialization routine. (In practice, I believe the current Atari OS only calls the initialization routines for its own pre-defined devices. Since this may change in future OS’s and since one can force the call to one’s own initialization routine, I must recommend that each driver include one, even if it does nothing.) The address placed in the handler address table must point to, again, another table, the form of which is shown in Figure 2.

Notice the six addresses which must be specified; and note that, in the table, one must subtract one from each address (the “-1” simply makes CIO’s job easier…honest). A brief word about each routine is in order.

The OPEN routine must perform any initialization needed by the device. For many devices, such as a printer, this may consist of simply checking the device status to insure that it is actually present. Since the X-register, on entry to each of these routines, contains the IOCB number being used for this call, the driver may examine ICAX1 (via LDA ICAX1,X) and/or ICAX2 to determine the kind of OPEN being requested. (Caution: Atari OS preempts bits 2 and 3, $04 and $08, of ICAX1 for read/write access control. These bits may be examined, but should normally not be changed.)

The CLOSE routine is often even simpler. It should “turn off” the device if necessary and if possible.

The PUTBYTE and GETBYTE routines are just what are implied by their names: the device handler must supply a routine to output one byte to the device and a routine to input one byte from the device. However, for many devices, one or the other of these routines doesn’t make sense (ever tried to input from a printer?). In this case the routine may simply RTS and Atari OS will supply an error code.

The STATUS routine is intended to implement a dynamic status check. Generally, if dynamic checking is not desirable or feasible, the routine may simply return the status value it finds in the user’s IOCB. However, it is not an error under Atari OS to call the status routine for an unopened device, so be careful.

The XIO routine does just what its name implies: it allows the user to call any and all special and wonderful routines that a given device handler may choose to implement. OS does nothing to process an XIO call except pass it to the appropriate driver.

Note: In general, the AUXiliary bytes of each IOCB are available to each driver. In practice, it is best to avoid ICAX1 and ICAX2, as several BASIC and OS commands will alter them at their will. Note that ICAX3 through ICAX5 may be used to pass and receive information to and from BASIC via the NOTE and POINT commands (which are actually special XIO commands). Finally, drivers should not touch any other bytes in the IOCBs, especially the first two bytes.

Notice that handlers need not be concerned with PUT BINARY RECORD, GET TEXT RECORD, etc.: OS performs all the needed housekeeping for these user-level commands.


HANDLER
        .WORD   <address of OPEN routine >-1
        .WORD   <address of CLOSE routine >-1
        .WORD   <address of GETBYTE routine >-1
        .WORD   <address of PUTBYTE routine >-1
        .WORD   <address of STATUS routine>-1
        .WORD   <address of XIO routine>-1
         JMP    <address of initialization routine>
Figure 2.

Rules For Adding Things To OS

We touched on this subject last month, in the section titled “The Easiest Way of Making Room?”, but a review and an addition are in order. Both Atari FMS (File Manager System, otherwise known as DOS and/or the Disk Device Driver) and the serial port handlers follow the same scheme when they add themselves to OS, so it is safe to assume that this method may be considered the de facto Atari standard. We enumerate:

  1. Inspect the system MEMLO pointer (at $2E7, I called it LOMEM last month, which is BASIC’s name for it).
  2. Load your routine (including needed buffers) at the current value of MEMLO.
  3. Add the size of your routine to MEMLO.
  4. Store the resultant value back in MEMLO.
  5. Connect your driver to OS by adding its name and address into the handler address table.
  6. Fool OS so that if SYSTEM RESET is hit steps 3 through 5 will be re-executed (because SYSTEM RESET indeed resets the handler address table and the value of MEMLO).

In point of fact, step 2 is the hardest of these to accomplish. In order to load your routine at wherever MEMLO may be pointing, you need a relocatable (or self-relocatable) routine. Since there is currently no assembler for the Atari which produces relocatable code, this is not an easy task. (However, I just happen to have a method which works. But it will have to wait for a later article.)

Step 6 is accomplished by making Atari OS think that your driver is the Disk driver for initialization purposes (by “stealing” the DOSINI vector) and then calling the Disk’s initializer yourself when steps 3 through 5 are performed. This is a fairly simple process, but again, details must await a future article.

Yet Another Real Live Example

I promised last month that we would present a driver for a “peripheral” device found in every Atari, yet not supported by any Atari device handlers. I could have been cagey and presented a driver for a “Null” device. (A handy thing to have, actually: One can throw away one’s output very fast when trying to debug a program. See De Re Atari for a simple implementation of one. Better yet, try to write one from the information presented herein.) Being a glutton for punishment, I undertook to write a truly useful handler for Atari’s overlooked device: RAM memory!!

After the snickers and sarcastic comments die down, let me point out how truly useful such a device is to BASIC programs: program one can “write” data to RAM and then chain to program two, which then “reads” the same data back. Voila! Chaining with COMMON in Atari BASIC. So herewith the “M:” (Memory) driver, presented in its entirety in Figure 3.

Does It Work?

Some words of caution are in order. This driver does not perform step 6 as noted in the last section (but it may be reinitialized via a BASIC USR call). It does not perform self-relocation: instead it simply locates itself above all normal low memory usage (except the serial port drivers, which would have to be loaded after this driver). If you assemble it yourself, you could do so at the MEMLO you find in your normal system configuration (or you could improve it to be self-modifying, of course).

Other caveats pertain to the handler’s usage: it uses RAM from the contents of MEMTOP ($2E5) downward. It does not check to see if it has bumped into BASIC’s MEMTOP ($90) and hence could conceivably wipe out programs and/or data. To be safe, don’t write more data to the RAM than a FRE(0) shows (and preferrably even less).

In operation, the M: driver reinitializes upon an OPEN for write access (mode 8). A CLOSE followed by a subsequent READ access will allow the data to be read in the order it was written. More cautions: don’t change graphics modes between writing and reading if the change would use more memory (to be safe, simply don’t change at all). The M: will perform almost exactly as if it were a cassette file, so the user program should be data sensitive if necessary: the M: driver will not itself give an error based on data contents. Note that the data may be re-READ if desired (via CLOSE and re-OPEN).

Installing The M: Driver

The most obvious way to install this driver (Program 1) is to type in the source and assemble it directly to the disk. Then simply loading the object file from DOS 2 (or OS/A+) will activate the driver and move LOMEM as needed. You could even name the resulting file “AUTORUN.SYS” so that it would be automatically booted on power up.

If you don’t have an assembler and/or disk, the problem is a little more difficult. If you are comfortable writing BASIC programs that load assembly language data to memory, you might use the techniques described in last month’s “Make Room?” to reserve the required memory. Then a simple POKEr program which uses DATA statements would suffice.

But the assembly listing given here is designed for a disk system and would waste 5K bytes or so in a cassette system. So, if you can’t reassemble it and/ or write that POKEr program, you will just have to be patient: I will try to give you a simplified BASIC POKEr program next month.

A suggested set of BASIC programs is presented:

Ending of Program 1:
9900 OPEN #2,8,0,"M:"
9910 PRINT #2;LEN(A$)
9920 PRINT #2;A$
9930 CLOSE #2
9940 RUN "D:PROGRAM2"
Beginning of Program 2:
100 JUNK=USR(7984)
    [ to insure the M: driver is linked,
      in case of RESET ]
110 OPEN #4,4,0,"M:"
120 INPUT #4,SIZE
130 DIM STRING$(SIZE)
140 INPUT #4,STRING$
150 CLOSE #4

BASIC A+ users might find RPUT/RGET and BPUT/BGET to be useful tools here instead of PRINT and INPUT. And, of course, users of any other language(s) might find this a handy inter-program communications device.

BASIC, Part 1: Why?

The first “Why?” I usually hear is “Why not Microsoft BASIC?” After a little probing, I find that the question really boils down to “Why not string arrays?” There is no simple answer to that question, so I hope to save myself time in the future by pointing toward these articles. Because I intend to give the true and not-so-simple answer, along with some (hopefully) very interesting information.

Believe it or not, Atari BASIC pretty much works the way it was designed and specified. And yours truly must take a large part of the brickbats or roses you might throw because of those specifications. We (that is, at the time, Shepardson Microsystems) were just finishing the highly successful and very powerful Cromemco 32K Structured BASIC. And, while a few Cromemco users had carped about the lack of string arrays, on the whole the real power of the language is extraordinarily impressive. All this “power” probably went to our head(s), so of course we had to duplicate the feat for Atari.

Oops. A small problem: Cromemco gave us 32K bytes for Structured BASIC; Atari gave us 10K bytes. What comes out? Wrong question! What can stay in?! Of course, Atari had some ideas, too, and the important features that we ended up with include (in my opinion):

That last item won’t be appreciated by those of you who haven’t used a BASIC that doesn’t do it, so I will try to describe the horrors to you: You type in a long program which includes a line such as:

  3034 IF SYSTEMERROR THEN PINT "Bad Disk Drive":GOTO 4090

Did you catch it? It says ‘PINT’ where it should say ‘PRINT’. Most microcomputer BASICs will happily gulp that line in with nary a burp. Now, 13 months later, when that dreaded ‘systemerror’ actually occurs, your user (who lives in Hong Kong, naturally) sees the helpful message

  ***SYNTAX ERROR at LINE 3037

When you have fathomed the implications of that, calm your nerves so we can continue.

Needless to say, we were more than happy to include the Syntax Check feature. However, this inclusion had implications that rippled throughout the rest of the design of BASIC. First, you don’t get something for nothing: such syntax checking uses memory, perhaps one to two kilobytes. Second, pre-syntaxing implies that the user program will be “tokenized”: that is, the user’s source will be converted into internal tokens for ease of execution and efficiency. Even Microsoft BASICs tokenize the keywords of the language; Atari BASIC tokenizes everything: keywords, variables, constants, operators, etc. Thirdly, the decision to have strings longer than 255 characters (coupled with the tight memory requirements) simply precluded any implementation of string arrays. (In fact, I do not know of any small-machine BASIC that supports string arrays with elements longer than 255 characters.)

Before perusing some quickie programs to show the effects of tokenizing, I should like to give some credit where it is due. Though I participated in the specifications for Atari BASIC, I had little to do with the actual implementation. More history: Atari asked us (in September, 1978) to bid on producing a custom “consumer-oriented” BASIC for them. Sometime in October, the specifications were finalized and Paul Laughton and Kathleen O’Brien (with a very little help from three more of us) began to work in earnest. The contract called for delivery by April 6, 1979, and included delivery of a File Manager System (DOS I). Atari planned to take an early, 8K Microsoft BASIC to the Consumer Electronics Show (in Las Vegas) in January, 1979, and then switch later. The actual purchase order took a while to get through Atari’s red tape, and the final version thereof is dated 12/28/78—about one week after both BASIC and DOS were delivered to Atari! Atari took Atari BASIC to CES.

Investigating BASIC’s Tokens

There are three fundamental types of tokens in Atari BASIC, each of which occupies exactly one byte of RAM memory, with only two special cases. The token types are statement name tokens, operator name tokens (which include function names and some other miscellany), and variable name tokens. The special cases are numeric and string constants, which begin with an operator name token, but are followed by the actual value of the constant.

Statement name tokens can only occur as the first item of a statement and, thus, have their own keyword and tokenizing table. In theory, Atari BASIC’s structure could support up to 256 types of statements. Variable name tokens and operator name tokens are intermixed throughout the rest of a statement and are distinguished by the state of their upper bit: variable name tokens have their upper bit on, operators don’t.

A few of the statement types are also special cased in that they are not followed by operator and variable tokens. These special cases include the obvious REM and DATA and the not-so-obvious ERROR (the statement name given to lines containing a syntax error).

Since each variable is reduced to a single byte (with its upper bit set), there are a maximum of 128 different variable names per program. There is the further implication that BASIC must remember the association of name to token in order to LIST your program back to you. The actual ATASCII names are stored in the “Variable Name Table,” and we investigated its structure in COMPUTE! #17 under the heading of “VARIABLE, VARIBLE, VARABLE.” (Briefly, the names are simply stored one after the other, with the upper bit of the last character of each name turned on.)

The statement and operator names are obviously predefined in the BASIC ROM cartridge, and we offer herewith a program (Program 2) which prints out the token numbers and corresponding keywords. When you run the program, you will notice that some operators (especially the left parenthesis) appear to be repeated. They are. We will find out why next month.

Program 1.
A sample device driver for Atari's OS
--- general remarks ---
0000        1000        .PAGE "--- general remarks ---"
            1010 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1020 ;
            1030 ; The "M:" driver --
            1040 ;   Using memory as a device
            1050 ;
            1060 ; Includes installation program
            1070 ;
            1080 ; Written by Bill Wilkinson
            1090 ; for January, 1982 COMPUTE!
            1100 ;
            1110 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1120 ;
            1130 ; EQUATES INTO ATARI'S OS, ETC.
            1140 ;
034A        1150 ICAUX1 =    $34A      ; The AUX1 byte of IOCB
            1160 ;
0008        1170 OPOUT  =    8         ; Mode 8 Is OPEN for OUTPUT
            1180
02E7        1190 MEMLO  =    $2E7      ; pointer to bottom of free RAM
02E5        1200 MEMTOP =    $2E5      ; pointer to top of free RAM
            1210 ;
00E0        1220 FR1    =    $E0       ; Fltg Pt register 1, scratch
            1230 ;
0001        1240 STATUSOK =  1         ; I/O was good
0088        1250 STATUSEOF = $88       ; reached an end-of-file
            1260 ;
031A        1270 HATABS = $31A
            1280 ;
0100        1290 HIGH = $100           ; divisor for high byte
00FF        1300 LOW  = $FF            ; mask for low byte
            1310 ;
0000        1320        .PAGE "The installation routine"
            1330 ;
0000        1340        *=   $1F00
            1350 ; This first routine is simply
            1360 ; used to connect the driver
            1370 ; to Atari's handler address
            1380 ; table.
            1390 ;
            1400 LOADANDGO
1F00 A2     1410        LDX  #0        ; We begin at start of table
            1420 SEARCHING
1F02 BD1A03 1430        LDA  HATABS,X  ; Check device name
1F05 F00A   1440        BEQ  EMPTYFOUND ; Found last one
1F07 C94D   1450        CMP  #'M'        ; Already have M:?
1F09 F01A   1460        BEQ  MINSTALLED ; Yes, don't reinstall
1F0B E8     1470        INX
1F0C E8     1480        INX
1F0D E8     1490        INX            ; Point to next entry
1F0E D0F2   1500        BNE  SEARCHING ; and keep looking
1F10 60     1510        RTS            ; Huh? Impossible!!!
            1520 ;
            1530 ; We found the current end of the
            1540 ; table...so extend it.
            1550 ;
            1560 EMPTYFOUND
1F11 A94D   1570        LDA  #'M'       ; Our device name, "M:"
1F13 9D1A03 1580        STA  HATABS,X   ; is first byte of entry
1F16 A93B   1590        LDA  #MDRIVER&LOW
1F18 9D1B03 1600        STA  HATABS+1,X ; LSB of driver addr
1F1B A91F   1610        LDA  #MDRIVER/HIGH
1F1D 9D1C03 1620        STA  HATABS+2,X ; and MSB of addr
1F20 A9     1630        LDA  #0
1F22 9D1D03 1640        STA  HATABS+3,X ; A new end for the table
            1650
            1660 ; now change LOMEM so BASIC won't
            1670 ; overwrite us.
            1680 ;
            1690 MINSTALLED
1F25 A900   1700        LDA #DRIVERTOP&LOW
1F27 8DE702 1710        STA MEMLO     ; LSB of top addr
1F2A A920   1720        LDA #DRIVERTOP/HIGH
1F2C 8DE802 1730        STA MEMLO+1   ; and MSB therof
            1740 ;
            1750 ; and that's all we have to do!
            1760 ;
1F2F 60     1770        RTS
            1780 ;
            1790 ;
            1800 ;;;;;;;;;;;;;;;;;;;;;;
            1810 ;
            1820 ; This entry point is provided
            1830 ; so that BASIC can reconnect
            1840 ; the driver via a USR(RECONNECT)
            1850 ;
            1860 RECONNECT
1F30 6B     1870        PLA
1F31 F0CD   1880        BEQ LOADANDGO ; No parameters, I hope
1F33 A8     1890        TAY
            1900 PULLTHEM
1F34 68     1910        PLA
1F35 68     1920        PLA           ; get rid of a parameter
1F36 88     1930        DEY
1F37 D0FB   1940        BNE PULLTHEM  ; and pull another
1F39 F0C5   1950        BEQ LOADANDGO ; go reconnect
            1960 ;
1F3B        1970        .PAGE "The driver itself"
            1980 ;
            1990 ; Recall that all drivers must
            2000 ; be connected to OS through
            2010 ; a driver routines address table.
            2020 ;
            2030 MDRIVER
1F3B 4C1F   2040        .WORD MOPEN-1  ; The addresses must
1F3D 6F1F   2050        .WORD MCLOSE-1 ; ...be given in this
1F3F 921F   2060        .WORD MGETB-1  ; ...order and must,
1F41 851F   2070        .WORD MPUTB-1  ; ...be one (1) less
1F43 9F1F   2080        .WORD MSTATUS-1 ; ...than the actual
1F45 491F   2090        .WORD MXIO-1   ; ... address
1F47 4C4A1F 2100        JMP  MINIT     ; This is for safety only
            2110 ;
            2120 ; For many drivers, some of these
            2130 ; routines are not needed, and
            2140 ; can effectively be null routines
            2150 ;
            2160 ; A null routine should return
            2170 ; a one (1) in the Y-register
            2180 ; to indicate success.
            2190 ;
            2200 MXIO
            2210 MINIT
            2220
1F4A A001   2220        LDY #1        ; success
1F4C 60     2230        RTS
            2240 ;
            2250 ; If a routine is omitted because
            2260 ; it is illegal (reading from a
            2270 ; Printer, etc.), simply pointing
            2280 ; to an RTS is adequate, since
            2290 ; Atari OS preloads Y with a
            2300 ; 'Function Not Implemented' error
            2310 ; return code.
            2320 ;
1F4D        2330 .PAGE "The driver function routines"
            2340 ;;;;;;;;;;;;;;;;;;;;;;;;;;
            2350 ;
            2360 ; Now we begin the code for the
            2370 ; routines that do the actual
            2380 ; work
            2390 ;
            2400 MOPEN
1F4D BD4A03 2410        LDA  ICAUX1,X  ; Check type of open
1F50 2909   2420        AND  #OPOUT    ; Open for output?
1F52 F00D   2430        BEQ  OPENFORREAD ; No...assume for input
1F54 ADE502 2440        LDA  MEMTOP
1F57 8DD21F 2450        STA  MSTART     ; We start storing
1F5A ACE602 2460        LDY  MEMTOP+1   ; ...the bytes
1F5D 88     2470        DEY             ; ...one page below
1F5E 8CD31F 2480        STY  MSTART+1   ; the supposed top or mem
            2490 ;
            2500 ; now we join up with mode 4 open
            2510 ;
            2520 OPENFORREAD
1F61 ADD21F 2530        LDA  MSTART     ; simply move the
1F64 8DCE1F 2540        STA  MCURRENT   ; ...start pointer
1F67 A0D31F 2550        LDA  MSTART+1   ; ...to the current
1FAA 8DCF1F 2560        STA  MCURRENT   ; ...pointer, both bytes
            2570
1F6D A001   2580        LDY  #STATUSOK
1F6F 60     2590        RTS             ; we don't acknowledge failure
            2600 ;
            2610 ;
            2620 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2630 ;
            2640 ; the routine for CLOSE of M:
            2650 ;
            2660 MCLOSE


1F70 BD4A03 2670        LDA  ICAUX1,X  ; check node of open
1F73 2908   2680        AND  #OPOUT    ; was for output?
1F75 F00C   2690        BEQ  MCLREAD   ; no, close input 'file'
            2700 ;
1F77 ADCE1F 2710        LDA  MCURRENT  ; we estisblish our
1F7A BDD01F 2720        STA  MSTOP     ; ...limit so that
1F7D ADCF1F 2730        LDA  MCURRENT+1 ; ...next use can't
1F80 8DD11F 2740        STA  MSTOP+1   ; ...go too far
            2750 ;
            2760 MCLREAD
1F83 A001   2770        LDY  #STATUSOK
1F85 60     2780        RTS            ;and guaranteed to be ok
            2790 ;
            2800 ;
            2810 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2820 ;
            2830 ; This routine puts one byte
            2840 ; to the memory for later
            2850 ; retrieval
            2860 ;
            2870 MPUTB
1F86 48     2380        PHA            ; save the byte to be PUT
1F87 20B51F 2890        JSR  MOVECURRENT ; get ptr to zero page
1F8A 68     2900        PLA            ; the byte again
1F8B A000   2910        LDY  #0
1F8D 91E0   2920        STA  (FR1),Y   ; put the byte, indirectly
1F8F 20C01F 2930        JSR  DECCURRENT ; point to nxt byte
1F92 60     2940        RTS            ; that's all
            2950 ;
            2960 ;;;;;;;;;;;;;;;;;;;;;;;;;;
            2970 ;
            2980 ; routine to get a byte put
            2990 ; in memory before.
            3000 ;
            3010 MGETB
1F93 20A01F 3020        JSR  MSTATUS   ; any More bytes?
1F96 B007   3030        BCS  MGETRTS   ; no...error
1F98 A000   3040        LDY  #0
1F9A B1E0   3050        LDA  (FR1),Y   ; yes...get a byte
1F9C 20C01F 3060        JSR  DECCURRENT ; and point to next byte
            3070 MGETRTS
1F9F 60     3080        RTS
            3090 ;
            3100 ;;;;;;;;;;;;;;;;;;;;;;
            3110 ;
            3120 ; check the status of the driver
            3130 ;
            3140 ; this routine is only valid
            3150 ; when READing the
            3160 ; "M:" never gets errors when
            3170 ; writing.
            3180 ;
            3190 MSTATUS
1FA0 20B51F 3200        JSR  MOVECURRENT ; current ptr to zero page
1FA3 CDD01F 3210        CMP  MSTOP     ; any more bytes to get?
1FA6 D009   3220        BNE  MSTOK     ; yes
1FA8 CCD11F 3230        CPY  MSTOP+1   ; double chk
1FAE D004   3240        BNE  MSTOK     ; yes, again
1FAD A088   3250        LDY  #STATUSEOF ; oops...
1FAF 38     3260        SEC            ; no more bytes
IFB0 60     3270        RTS
            3280 ;
            3290 MSTOK
1FB1 A001   3300        LDY  #STATUSOK ;all is ok
1FB3 18     3310        CLC            ;flag for MGETB
1FB4 60     3320        RTS
            3330        .PAGE "Miscellaneous subroutines"
            3340 ;
            3350 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            3360 ;
            3370 ; finally, we have a couple of
            3380 ; short and simple routines to
            3390 ; manipulate MCURRENT, the ptr
            3400 ; to the currently accessed byte
            3410 ;
            3420 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            3430 ;
            3440 ; MOVECURRENT simply moves
            3450 ; MCURRENT to the floating
            3460 ; point register, FR1, in
            3470 ; zero page, FR1 is always
            3480 ; safe to use except in the
            3490 ; middle of an expression.
            3500 ;
            3510 MOVECURRENT
1FB5 ADCE1F 3520        LDA  MCURRENT
1FB8 85E0   3530        STA  FR1       ; notice that we use
IFBA ACCF1F 3540        LDY  MCURRENT+1 ; both the A and
IFBD 84E1   3550        STY  FR1+1     ; Y registers...this
IFBF 60     3560        RTS            ; is for MSTATUS use
            3570 ;
            3580 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            3590 ;
            3600 ; DECCURRENT simply does a two
            3610 ; byte decrement of the MCURRENT
            3620 ; pointer and returns with the
            3630 ;  Y register indicating OK status.
            3640 ; NOTE that the A register is
            3650 ;  left undisturbed.
            3660 ;
            3670 DECCURRENT
1FC0 ACCE1F 3680        LDY  MCURRENT  ; check LSB's value
1FC3 D003   3690        BNF  DEC LOW   ; if non-zero, MSB is ok
1FC5 CECF1F 3700        DEC  MCURRENT+1 ; if zero, need to bump MSB
            3710 DECLOW
1FC8 CECE1F 3720        DEC  MCURRENT   ; now bump the LSB
1FCB A001   3730        LDY  #STATUSOK  ; as promised
1FCD 60     3740        RTS
1FCE        3750        .PAGE "RAM usage and clean up"
            3760 ;
            3770 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            3780 ;
            3790 ; END OF CODE
            3800 ;
            3810 ;
            3820 ; Now we define our storage
            3830 ; locations.
            3840 ;
            3850 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            3860 ;
            3870 ;
            3880 ; MCURRENT holds the pointer to
            3890 ; the next byte to be PUT or GET
1FCE 0000   3900 MCURRENT .WORD 0
            3910 ;
            3920 ; MSTOP is set by CLOSE to point
            3930 ; to the last byte PUT, so GET
            3940 ; won't try to go past the end
            3950 ; of data.
1FD0 0000   3960 MSTOP  .WORD 0
            3970 ;
            3980 ; MSTART is derived from MEMTOP
            3990 ; and points to the first byte
            4000 ; stored. The bytes are stored
            4010 ; in descending addresses until
            4020 ; MSTOP is set by CLOSE.
1FD2 0000   4030 MSTART .WORD 0
            4040 ;
            4050 ; DRIVERTOP becomes the new
            4060 ; contents of MEMLO
2000        4070 DRIVERTOP = *+$FF&$FF00
            4080 ; (sets to next page boundary)
            4090 ;
            4110 ;
            4110 ; The following is how you make
            4120 ; a LOAD-AND-GO file under
            4130 ; Atari's DOS 2
            4140 ;
1FD4        4150        *=   $2E0
02E0 001F   4160        .WORD LOADANDGO
            4170 ;
            4180 ;
02E2        4190        .END
Program 2.
100 REM listing of a program to print token values
101 REM and their ATASCII equivalents
200 ? "The STATEMENT Token List":?
210 ADDR=42161:SKIP=2:TOKEN=0
220 GOSUB 1000:REM call the token printer
300 ? "The OPERATOR Token List":?
310 ADDR=42979:SKIP=0:TOKEN=16
320 GOSUB 1000:REM again call to print tokens
400 END

1000 REM Subroutine to print a keyword table
1001 REM On entry:
1002 REM    ADDR = the address of the keyword table
1003 REM    SKIP = number of bytes to skip
1004 REM           between keyword strings
1005 REM   TOKEN = the starting token number for
1006 REM           this table
1007 REM
1050 IF NOT PEEK(ADDR) THEN ? :? :RETURN

        [note! both tables end with a zero byte]

1060 PRINT TOKEN,:REM the token number
1100 REM Print the ATASCII string for this token
1110 BYTE=PEEK(ADDR):ADDR=ADDR+1
1120 IF BYTE<128 THEN ? CHR$(BYTE);:GOTO 1100
1130 PRINT CHR$(BYTE-128):REM last character in keyword has upper bit on
1140 ADDR=ADDR+SKIP:REM an address for stmts
1150 TOKEN=TOKEN+1:REM to next keyword
1160 GOTO 1000
COMPUTE! ISSUE 21 / FEBRUARY 1982 / PAGE 77

INSIGHT: Atari

Bill Wilkinson
Optimized Systems Software
Cupertino, CA

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
                    JSR GRAPHICS
COLOR 3             LDA #3
                    JSR COLOR
PLOT 10,10          LDX #10
                    LDA #0
                    LDY #10
                    JSR PLOT
DRAWTO 25,15        LDX #25
                    LDA #0
                    LDY #15
                    JSR DRAWTO
SETCOLOR 2,0,14     LDX #2
                    LDA #0
                    LDY #14
                    JSR SETCOLOR

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

  1. Write, assemble, and debug your code using some fixed address(es).
  2. Ensure that your code is all in one piece (i.e., there is only one *=, at the beginning of the code segment).
  3. 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).
  4. Change your origin to one page higher in memory (*= $nn00, where ‘nn’=‘hh’+1). Assemble the code to “OBJECT2” (ASM ,,#D:OBJECT2).
  5. Run the MAKEREL program. It will produce the file “DATA.REL”.
  6. 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.
  7. Anytime you want to load your routine, simply use RUN “D:LOADREL.A”.

Notes

  1. 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))
  2. 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!
  3. 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.
Figure 1.
BASIC Statement Action performed
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 XIO 17, #6, 12, 0, "S:"
SETCOLOR r,h,lu Is equivalent to POKE 708 + r, h*16 + lu

Note: FILL may be performed from assembly language by following exactly the same sequence specified in the Basic Reference Manual, using XIO 18, etc.

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.

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 misspelled 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.

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
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 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      ; AUXiliary byte # 1
034B        1100 ICAUX2 =    $34B      ; AUXiliary 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
            1240 ;   the VBLANK routines
            1250 ;
0055        1260 HORIZONTAL = $55
0054        1270 VERTICAL =   $54
02FB        1280 DRAWCOLOR = $2FB
02C4        1290 COLOR0 =    $2C4
            1300 ;
            1310 ; miscellany
            1320 ;
00FF        1330 LOW    = $FF
0100        1340 HIGH   = $100
            1350 ;
0000        1360        .PAGE "The actual routines"
            1370 ;
            1380 ; First, set the location and sone Miscellaneous
            1390 ;    RAM usage
            1400 ;
0000        1410 *= $660
            1420 ;
0660 00     1430 SAVECOLOR .BYTE 0     ; where COLOR is saved
            1440 ;
0661 53     1450 SNAME: .BYTE "S:",0   ; the filename for open
0662 3A
0663 00
            1460 ;
            1470 ;
            1480 ; GRAPHICS g
            1490 ;
            1500 ; ENTRY: A-reg contains graphics mode 'g'
            1510 ;  EXIT: Y-reg has completion status
            1520 ;
            1530 GRAPHICS
0664 48     1540        PHA            ; save 'g'
0665 A260   1550        LDX  #6*$10    ; file 6
0667 A90C   1560        LDA  #CCLOSE
0669 9D4203 1570        STA  ICCOM,X
066C 2056E4 1580        JSR  CIO ; First, we must close file #6
            1590 ; (we ignore any error's from the close)
            1600 ;
066F A260   1610        LDX #6*$10     ; again, file 6
0671 A903   1620        LDA #COPN      ; we will open this 'file'
0673 9D4203 1630        STA ICCOM,X
0676 A961   1640        LDA #SNAME&LOW
067B 9D4403 1650        STA ICBADR,X   ; we use the file name "S:"
067B A906   1660        LDA #SNAME/HIGH
067D 9D4503 1670        STA ICBADR+1,X ; by pointing to it
            1680 ;
            1690 ; all is set up for OPEN, now
            1700 ; we tell CIO (and S:) what kind of open
            1710 ;
0680 68     1720        PLA            ; our saved 'g' graphics Mode
0681 9D4B03 1730        STA  ICAUX2,X  ; is given to S:
            1740 ; (note that S: ignores the upper bits of AUX2)
0684 29F0   1750        AND  #$F0      ; now we get Just the upper bits
0686 4910   1760        EOR  #$10      ; and flip bit 4
            1770 ; (Read the text. S: expects this bit inverted
            1780 ;    from what normal BASIC usage is.)
0680 090C   1790        ORA  #$0C      ; allow read end write access (for CIO)
068A 9D4A03 1800        STA  ICAUX1,X  ; make CIO and S: happy
0680 2056E4 1810        JSR  CIO       ; and dc the OPEN of S:
0690 60     1820        RTS
            1830 ;
            1840 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1850 ;
            1860 ; COLOR c
            1870 ;
            1880 ;  ENTER: Color 'c' in A-register
            1890 ;   EXIT: Unchanged
            1900 ;
            1910 COLOR
0691 8D6006 1920        STA SAVECOLOR
0694 60     1930        RTS            ; exciting, wasn't it?
            1940 ;
            1950 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1960 ;
            1970 ; POSITION h,v
            1980 ;
            1990 ;  ENTER: h (horizontal) position in X,A
            2000 ;              registers (LSB,MSB)
            2010 ;         v (vertical) position in Y-register
            2020 ;
            2030 ;   EXIT: unchanged
            2040 ;
            2050 POSITION
0695 8655   2060        STX  HORIZONTAL
0697 8556   2070        STA  HORIZONTAL+1 ; read the text
0699 8454   2080        STY  VERTICAL  ; too simple, right?
069B 60     2090        RTS
            2100 ;
            2110 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2120 ;
            2130 ; PLOT h,v
            2140 ;
            2150 ;  ENTER: must have done a previous COLOR call
            2160 ;         X,A,and Y registers set as in POSITION
            2170 ;
            2180 ;   EXIT: Y-register has completion status
            2190 ;
            2200 PLOT
069C 209506 2210        JSR  POSITION
069F A260   2220        LDX  #6*$10    ; file 6, again
06A1 A90B   2230        LDA  #CPBINR   ; Command Put BINary Record
06A3 9D4203 2240        STA  ICCOM,X
06A6 A900   2250        LDA  #0
06A8 9D4803 2260        STA  ICBLEN,X
06AB 9D4903 2270        STA  ICBLEN+1,X ; if buffer length is zero...
06AE AD6006 2280        LDA  SAVECOLOR ; then CPBINR puts one char from A-reg
06B1 2056E4 2290        JSR  CIO       ; and this is how we PLOT
06B4 60     2300        RTS
            2310 ;
            2320 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2330 ;
            2340 ; LOCATE h,v,c
            2350 ;
            2360 ;  ENTER: X,A,and Y registers set up as in POSITION
            2370 ;   EXIT: A-register has the LOCATEd color
            2380 ;         Y-register has the completion code
            2390 ;
            2400 LOCATE
06B5 209506 2410        JSR  POSITION
06B8 A260   2420        LDX  #6*$10    ; file 6
06BA A907   2430        LDA  #CGBINR   ; Command Get BINary Record
06BC 9D4203 2440        STA  ICCOM,X
06BF A900   2450        LDA  #0
06C1 904803 2460        STA  ICBLEN,X
06C4 904903 2470        STA  ICBLEN+1,X ; if Buffer LENgth is zero,
06C7 2056E4 2480        JSR  CIO       ; then the character is returned in A
06CA 60     2490        RTS
            2500 ;
            2510 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2520 ;
            2530 ; DRAWTO h,v
            2540 ;
            2550 ;  ENTER: Must have done a previous PLOT
            2560 ;         X,A,and Y registers as in POSITION
            2570 ;
            2580 ;   EXIT: Y-register has completion code
            2590 ;
            2600 DRAWTO
06CB 209506 2610        JSR  POSITION
06CE AD6006 2620        LDA  SAVECOLOR
06D1 8DFE02 2630        STA  DRAWCOLOR ; where DRAWTO expects its color
06D4 A260   2640        LDX  #6*$10    ; file 6...once more
06D6 A911   2650        LDA  #CDRAW    ; just a command to "S:"
06D8 9D4203 2660        STA  ICCOM,X
06DB A90C   2670        LDA  #$0C
06DD 9D4A03 2680        STA  ICAUX1,X  ; insurance
06E0 A900   2690        LDA  #0
06E2 9D4B03 2700        STA  ICAUX2,X  ; ...guaranteed to work
06E5 2056E4 2710        JSR  CIO       ; do the actual DRAWTO
06E8 60     2720        RTS
            2730 ;
            2740 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2750 ;
            2760 ; SETCOLOR r,hue,lum
            2770 ;
            2780 ;  ENTER: X-register has color reyister 'r'
            2790 ;         A-register has hue
            2800 ;         Y-register has luminance
            2810 ;   EXIT: (undefined)
            2820 ;
            2830 SETCOLOR
06E9 0A     2840        ASL  A
06EA 0A     2850        ASL  A
06EB 0A     2860        ASL  A
06EC 0A     2870        ASL  A         ; we need hue * 16
06ED 9DC402 2880        STA  COLOR0,X  ; save it here for a nonce
06F0 98     2890        TYA
06F1 29 0E  2900        AND  #$0E      ; only luminance bits that matter
06F3 18     2910        CLC
06F4 7DC402 2920        ADC  COLOR0,X  ; end of the nonce
06F7 9DC402 2930        STA  COLOR0,X  ; and VBLANK will move this to hardware
06FA 60     2940        RTS
            2950 ;
06FB        2960        .END
COMPUTE! ISSUE 22 / MARCH 1982 / PAGE 130

INSIGHT: Atari

Bill Wilkinson
Optimized Systems Software

Good news! I have finally found out how and where you will be able to obtain copies of De Re Atari… and it won’t even cost you your left thumb. The Atari Program Exchange now has it available for $19.95 plus shipping. The part number for it is APX-90008, and you can order it through 800-538-1862 (800-672-1850 in California). There are several changes and improvements from earlier versions, including a section on the GTIA. One disappointment is that an appendix on random access files has been deleted. Oh well, leaves room for me to do a future article.

The How and Why articles on Atari BASIC that appeared in the last two issues were the result of requests for ways of “hooking into” BASIC, in order to add conmands, etc. I am trying to gently break the news that you can’t add commands to a RUNning program (though direct, keyboard commands can be done by intercepting keyboard input, as I presume the Eastern House “Monkey Wrench” does.). But I have been trying to lead up to why you can’t add commands, so that people won’t waste time on false leads in trying to prove me wrong.

However, I am suspending the How and Why series this month in order to take a look at the USR function. It is my belief that the USR function will give most of you access to all the added comands you could write, which lessens somewhat the impact of not being able to integrate your own commands. In addition to some suggestions on usage, this month we implement a really powerful USR function: one which will play a song (or most any kind of sound) in the background while your BASIC program continues to chug away (zapping Klingons, etc.). Naturally, there will also be the usual mix of tricks, etc.

In order to deliver on my promise to the BASIC users regarding the song-playing USR function, I must first lead the assembly language fanatics through a short intro to the Atari’s interrupt system. As far as I know, the Atari is the only low-end personal computer that gives you such complete access to a fully-integrated, usable interrupt system. The Atari OS is structured to take advantage of several of these interrupts; and, more importantly, the user is invited to gain full or partial control of most interrupt routines. This despite the fact that Atari’s interrupt service routines are in ROM.

The 6502 microprocessor supports two types of interrupts: NMI (Non-Maskable Interrupt) and IRQ (Interrupt ReQuest). A bit in the CPU status byte controls whether IRQ’s will generate interrupts, but if an NMI signal is presented to it the 6502 will always call in interrupt service routine. Atari, however, allows the user to prevent NMI’s from reaching the CPU (except for the RESET button), thus giving even greater control. Once again, I must refer you to the Atari Technical Manual for full details, but herewith is a sunnnary of the available interrupts.

Table 1. Available Interrupts
TypeDescription
NMIReset Button (the only uncontrollable interrupt)
NMIDisplay List Interrupt
NMIVertical Blank Interrupt (60 times per second)
IRQBREAK key
IRQany other key
IRQSerial Input (for SIO communication with disk, etc.)
IRQSerial Output (ditto)
IRQSerial Transmission Completed (ditto)
IRQTimer #4
IRQTimer #2
IRQTimer #1
IRQ6520 parallel port “A”
IRQ6520 parallel port “B”
IRQBRK instruction encountered (internal to 6502)

Each of the available interrupts, except the Reset Button and the BREAK key (and Timer #4 on all except newest machines), has a vector (two byte pointer) through RAM. To take control of an interrupt, simply put the address of your routine in the vector, and OS will call you instead of the default routine. The only exception is the Vertical Blank Interrupt, which is handled slightly differently and is the real subject of this article.

The Vertical Blank Interrupt (VBI) is really the key to many of Atari’s unique features. It occurs 60 times per second, at the bottom of each scan of the TV screen, and is used by the OS ROMs to do all sorts of things. First, and perhaps most obvious, it drives the three-byte clock at locations $12,$13,$14 (18,19,20 decimal) as well as several other usable event timers (e.g., serial bus timeout), most of which are accessible to the user. Second, and most useful, it allows changes to the graphics-related hardware at a time when nothing is being displayed on the screen: it moves all the “shadow” locations (see the technical manual) to their corresponding hardware ports.

Of necessity, then, the user would not normally want to interfere with the operations of the VBI routines. But, once again, the Atari software design team thought ahead: they provided not one, but two, VBI vectors. Thus, upon receipt of a VBI request, the ROM code first calls the routine pointed to by vector VVBLKI (at $0222) and then calls via the vector VVBLKD (at $0224). The ‘I’ and ‘D’ stand for “Immediate” and “Deferred,” respectively.

Normally, the user routine would not replace the vector at VVBLKI. Thus the Atari ROM code can update its clocks and move its “shadow” registers in confidence that it will finish its job before the screen starts displaying the next TV frame. The user may replace VVBLKD to cause his routine to execute directly after the Atari system code.

Some cautions are in order: (1) Disaster will strike if your VBI routine is not done before the next VBI occurs. If you simply need to synchronize your routine to a vertical blank, just wait for the system clock to tick before starting (see the label WAITVB in this month’s example program). (2) As with most Atari vectors, the safest way to use these is to move them somewhere in your own data area, replace them with your pointer, and have your code finish up by jumping back via the original Atari routine. This is particularly important to do with interrupt handlers, else the interrupt system may not be properly reset.

Finally, let me note that you may, if you really have to, steal the entire VBI processing for yourself. This is not necessarily bad (especially if you are writing a dedicated game, etc.), but be forewarned that you will have to worry about shadow registers, etc., yourself. There is a lot more to this subject, including what Atari refers to as time-critical I/O, but for most purposes you should be able to work within the rules I have outlined.

A Real, Live Example

The example program this month is designed to be used via USR from BASIC, but there is a simplified entry point from assembly language. You could lift this program as is and plunk it into any assembled game, etc. The idea behind the program is simple: a routine is passed a sequence of bytes which are interpreted to be commands to the sound generators of the Atari hardware. The routine examines the bytes and performs the requests. One of the available requests is to “play” sound(s) for a specified length of time; upon encountering this request, the routine waits the appropriate time before processing the next byte. Simple.

Except that this routine will operate (invisible to a running BASIC program) merrily playing along while BASIC continues what it is doing. To accomplish this, we have hooked into VVBLKD (as described above). The user specifies the note duration as a number of “jiffies” (60ths of a second), and we let the VBI count down the duration for us.

The commands are imbedded in a string of bytes passed to the routine. Playit recognizes six command types, as shown in Table 2. Playit is not pardcularly sophisticated. For example, all voices must play sounds for the same duration and, when changing volume or tone quality, all voices must be respecified. A more sophisticated sound interpreter would presumably mean smaller command strings but a bigger interpreter. If you go to the trouble to type in both Playit and Playit From BASIC, you will see that some more than acceptable sounds can be accomodated, so I am reasonably happy with the results.

Table 2. Playit Command Codes
Byte valueNameDescription
255 ($FF)CMDRRepeat the entire sound command string
254 ($FE)CMDSStop all sounds (do not end command string)
253 ($FD)CMDNNumber of voices is specified in next byte (0 4)
252 ($FC)CMDTVSpecify Tone and Volume (as in SOUND 0,freq, TONE,VOLUME). Must be followed by 0 4 bytes (one per each voice as specified by CMDN), each of which specifies a Tone/Volume for one channel.
0 ($00)CMDEEnd command, unhook from VVBLKD. Does not turn off sound, so is usually preceded by CMDS.
any otherAny other value is assumed to be a duration, given in 'jiffies' (60ths of a second). Must be followed by 0 4 bytes (one per voice as specified by CMDN), each of which specifies the frequency of the sound for one channel (as in SOUND 0,FREQ,tone,volume).

Some interesting projects remain: Why not convert Atari’s Music Composer disk files to Playit-compatible strings? Or how about a real Music Compiler written in BASIC? How about making Playit relocatable, a la last month’s article? Please write and tell of your successes (or failures?).

Last but not least, another caution: since I/O to anything but the screen or keyboard uses the SIO serial bus driver, and since the serial bus uses the sound generators to get its baud rates, etc., you MUST turn off sound generation (commands CMDS, CMDE) before doing such I/O.

Atari BASIC: On Sounds, Hex Numbers, And The USR Function

The featured idea and program in this issue is the Playit From BASIC listing which follows. The program itself is not very sophisticated: it simply allows the one-character command codes (R,S,N,T,E) and hex data bytes to be translated into characters in a string. It then passes the address of the string to Playit (the assembly language program) and comes back to the user, ready to compile the next string of commands. If you intend to emulate this scheme, rather than use the program as is, you might be advised to but the sound command string into memory you have reserved (e.g., via the “Simplest Method” given in previous articles in this series). Putting the command in a string is inviting trouble: if your program stops, if you ENTER new lines, if you DIMension more variables, etc., the string may move and Playit would start playing random sounds.

The commands have simply been entered into the program via DATA statements starting at line 9000. Those of you who go to the trouble to enter all this will, I hope, be pleasantly surprised by the sounds generated by lines 9400–9418. You will probably be dismayed, however, at the idea of putting in such a complex sound yourself. That is why I encourage someone to come up with a better “Music Compiler” along these same lines.

In any case, I invite you to compose your own music or sounds to be put into this system. Generally, I wrote a sound in BASIC to test it before committing it to DATA statements. For example, the “CHOO-CHOO” sound evolved from this BASIC line:

FOR V=15 TO STEP -1:SOUND 0,V,0,V:NEXT V

The above sounds like an explosion, but if you slow it down a little and repeat it regularly you can train it as you wish. On to the short subjects.

HexDec

If you have already peeked at the listing of Playit From BASIC, you may have noted an unustial looking hexadecimal to decimal conversion routine. In fact, I herewith present you with a “one-liner” HexDec program:

1 DIMH$(23),N$(9):H$=",ABCDEF
GHII!!!!!!JKLMNO":IN.N$:F.I=
1TOLEN(N$):N=N*16+ASC(H$(ASC
(N$(I))-47))
:N.I:?N:RUN

The underlined characters are control characters (control-comma is the heart, etc.). The abbreviations are necessary to get it to fit on one line. To see how it works, figure out what happens when you input “9A”. Recall that ASC("9") is 57 and ASC("A") is 65. 57-47 is 10 and 65-47 is 18. Look at the 10th and 18th characters in H$. What is ASC("control-I")? ASC("control-J")?

You can avoid the control characters by adding the -64 shown in Playit From BASIC. Simple.

DecHex

This isn’t really pertinent, but while we are on the subject of one-liners:

1DIMH$(16):H$="0123456789AB
CDEF":IN.N:M=4096:F.I=1TO4:J=
INT(N/M):?H$(J+1);:N=N-M*J:M=
M/16:N.N:?:RUN

The USR And ADR Functions

Even though the methods of using the USR function are fairly thoroughly covered in the Atari BASIC Reference Manual. I find that many users are not fully aware of the real power of this function. Recall that the general syntax of this function is:

USR(addr [,expr [,expr … ]])

In other words, in addition to giving BASIC an address to call, you may pass any number of expressions to the assembly language routine. BASIC converts each expression to a 16-bit integer, pushes the result on the CPU stack, and cleans up by pushing on a single byte which tells the number of such expressions it pushed, (The address, which may itself be an expression, is not pushed and is not counted by that single byte.)

So what can we pass to assembly language? Obviously, numbers in the range of 0 to 65535. But what about characters? Conceive of

USR(addr, ASC("T"), expr)

where the “T” might be used as a mnemonic command to tell the routine which of several functions is desired. How about strings of characters? Recall that the three essential ingredients defining a string in Atari BASIC are its DIMension, LENgth, and address. Since your program presumably DIMensioned the string, you know that value and may pass it as an expression. And the address and length are available from the ADR and LEN functions!

Would you like your assembly language routine to modify your string, affecting its length? Try something like this:

DIM XX$(XXDIM)
XX$(USR(addr,ADR(XX$),XXDIM)+1)=""

Recall that the USR function may return any 16-bit value to the BASIC program, which is automatically converted to floating point as needed. Assume that this USR routine puts something in the XX$ string and returns the number of characters it put in. The above will then set the LENgth of XX$ properly for use by other BASIC statements and functions.

Finally, there is floadng point. How about writing a matrix inversion program? If we are limited to passing 16-bit integers, how do we pass a floating point number via USR? Simple: we pass the address of the number, just as we do with a string. And how do we get the address of a number, when the ADR function only works with strings? Like this:

DIM FF$(1),FF(dim1,dim2)
JUNK=USR(addr,ADR(FF$)+1,dim1,dim2)

A little published fact about Atari BASIC is that DIMensioning of both strings and arrays proceeds in an orderly fashion according to the DIM statements encountered. And you are guaranteed that the order you DIM strings and arrays is the order they will occur in memory! So, by DIMensioning that one-byte string, FF$, directly before the DIMension of the array, FF(), we know that the address of the array is one greater than the address of the string. Thus we can pass all the pertinent information about the array (its address and dimensions) to our assembly language routine. Incidentally, if you don’t want to waste a one-byte string for this purpose, there is no reason FF$ can’t be any DIMension you need: just adjust the ‘+1’ to reflect the actual DIM you use.

One last note on this subject: the fact that you can predict the memory order of strings and arrays has fascinating possibilities in regards to record structures, etc. But (and how many times have you read this from me) that’s a topic for another article.

Program 1.
10 AUDCTL=53768:DBL=120
20 AUDF1=53760:AUDC1=53761
30 SOUND 1,10,10,15:SOUND 3,10,10,15
40 POKE AUDC1,0:POKE AUDC1+4,0
50 POKE AUDCTL,DBL
60 FOR J=10 TO 15:POKE AUDF1+2,J:POKE AUDF1+6,20-J
70 FOR I=0 TO 255:POKE AUDF1,I:POKE AUDF1+4,255-I:NEXT I
80 NEXT J
        ...VERY SMOOTH GLIDES...
Program 2.
10 AUDCTL=53768:DBL=120
12 DSC=1789790/2
20 AUDF1=53760:AUDC1=53761
30 SOUND 1,10^,0,0
40 POKE AUDC1,0:POKE AUDC1+4,0
50 POKE AUDCTL,DBL
60 P2=2^(1/12)
70 NTE=16:REM C IN THE REAL BASS
80 FOR I=1 TO 109
90 FREQ=INT(OSC/NTE-7+0.5):F0=INT(FREQ/256)
92 F1=FREQ-256*F0
100 POKE AUDF1,F1:POKE AUDF1+2,F0
102 POKE AUDC1+2,175
103 PRINT "NOW PLAYING ";INT(NTE+0.5);" HZ"
105 FOR J=1 TO 100:NEXT J
110 NTE=NTE*P2
120 NEXT I
130 GOTO 70
      ...9 OCTAVE CHROMATIC SCALE...

Playit From BASIC
1000 REM *****************************
1020 REM *
1040 REM * PLAYIT FROM BASIC, SAM
1060 REM *
1080 REM * This routine is a simple
1100 REM * sound "compiler", which
1120 REM * takes DATA statements, and
1140 REM * converts then into command
1160 REM * strings suitable for use by
1180 REM * the interrupt-driver PLAYIT
1120 REM * routine.
1220 REM *
1240 REM *
1260 REM * Written by Bill Wilkinson
1280 REM *
1300 REM *   for March, 1982, COMPUTE!
1320 REM *
1340 REM ******************************
1360 REM
1380 REM First, constants, routine addresses, etc.
1400 REM
1420 DIM HX$(2),CMD$(11),PLAY$(1000),HEX$(23),TYPE$(1),PLAYIT$(1000)
1440 HEX$=="@ABCDEFGHI!!!!!!JKLMNG"
1460 DOCMD=2300:LOOP=1800:HEXDEC=2600
1480 AGAIN=1700:EXITLOOP=2100
1500 PLAYIT=6*256:REM or wherever you put the routine
1520 REM
1530 SOUND 0,0,0,0:REM needed to initialize properly
1540 REM The command equates...
1560 REM notice that these match the
1580 REM assembly language routine
1600 CMDR=255:CMDS=254:CMDN=253:CMDTV=252:CMDE==0
1620 REM
1640 REM ******************************
1660 REM
16B0 REM This is the AGAIN of
1700 REM PLAY IT AGAIN, ATARI
1720 REM
1730 PRINT " <processing...please wait>"
1740 PLAY$="":PLAY=0
1760 REM
1780 REM This is LOOP
1800 PLAY=PLAY+1:REM to next end byte
1820 READ CMD$:REM a bunch of commands
1840 REM
1860 TYPE$=CMD$:REM use the command character
1880 IF TYPE$="R" THEN PLAY$(PLAY)=CHR$(CMDR):GOTO EXITLOOP
1900 IF TYPE$="S" THEN PLAY$(PLAY)=CHR$(CMDS):GOTO LOOP
1920 IF TYPE$="N" THEN NUMVCS=1:CMD=CMDN:GOSUB DOCMD:NUMVCS=DEC;GOTO LOOP
1940 IF TYPE$="T" THEN CMD=CMDTV:GOSUB DOCMD:GOTO LOOP
1960 IF TYPE$="E" THEN PLAY$(PLAY)=CHR$(CMDE):GOTO EXITLOOP
1980 REM *** IF TO HERE, ASSUME DURATION & FREQ ***
2000 HX$=CMD$:GOSUB HEXDEC:CMD=DEC:REM command is duration
2020 CMD$=CMD$(2):REM to fool DOCMD
2040 GOSUB DOCMD:GOTO LOOP
2060 REM
2080 REM exitloop
2100 REM
2120 REM do the sound playing
2140 REM
2150 PLAYIT$=PLAY$:REM else we alter what we are playing
2160 JUNK=USR(PLAYIT,ADR(PLAYIT$))
2180 REM
2200 PRINT "HIT RETURN FOR NEXT SOUND"
2220 GOTO AGAIN
2240 REM
2260 REM
2280 REM ******************************
2300 REM THE SUBROUTINES
2320 REM
2340 REM first, DOCMD
2360 REM
2380 PLAY$(PLAY)=CHR$(CMD):REM The command byte
2400 IF NUMVCS=0 THEN RETURN
2420 REM we process NUMVCS bytes
2440 FOR I=2 TO NUMVCS+NUMVCS STEP 2
2460 HX$=CMD$(I):GOSUB HEXDEC:REM convert the byte
2480 PLAY=PLAY+1:PLAY$(PLAY)=CHR$(DEC):REM and stuff it away
2500 NEXT I
2520 RETURN
2540 REM
2560 REM ............
2580 REM ******************************
2600 REM and now HEXDEC
2620 REM
2640 DEC=0:REM our accumulator
2660 FOR L=1 TO LEN(HX$)
2680 DEC=DEC*16+ASC(HEX$(ASC(HX$(L))-47))-64
2700 NEXT L
2720 RETURN
8999 REM siren-like
9000 DATA N01,TCF,1408,1412,R
9099 REM ...a fanfare of sorts...
9100 DATA S,N01,TA2,30F3
9102 DATA N02,TA3A3,30F3C1
9104 DATA N03,TA4A4A4,30F3C1A1
9106 DATA N04,TA5A5A5A5,60F3C1A17A
9108 DATA T00000000
9110 DATA N00,C0,R
9199 REM ...beeping off the seconds...
9200 DATA S,N01
9202 DATA TAE,0130
9204 DATA TAC,0130
9206 DATA TAA,0130
9208 DATA TA8,0130
9210 DATA TA6,0130
9212 DATA TA4,0130
9214 DATA TA2,0130
9216 DATA T00,3500
9218 DATA R
9299 REM ...choo-choo ??? ...
9300 DATA S,N01
9302 DATA T0E,010E
9304 DATA T0C,010C
9306 DATA T0A,010A
9308 DATA T08,010B
9310 DATA T06,0106
9312 DATA T04,0104
9314 DATA T02,0102
9316 DATA T00,0300
9318 DATA R
9400 DATA S,N01,TAC
9402 DATA 3051,305B,3044,183C,182D,3035
9404 DATA 303C,1820,3035,3044,303C,3051,3058
9406 DATA N04,TACA4A4A8
9408 DATA 30516C89A2,305B7990B6,30446C89A2
9410 DATA 183C4879B6,182D4879B6,3035485BD7
9412 DATA 183C4879B6,182D58B4B6,3035445B89
9414 DATA 3044516CA2,38325179F3
9416 DATA 423C4B5BB6,50445B6C89
9418 DATA S,N00,F0,R
9898 REM ...stop and end... to quit...
9999 DATA S,E
Main Assembly Listing
0000        1000        .PAGE "     equates, origins, etc."
            1010 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1020 ;
            1030 ; PLAYIT  -- a demonstration of performing
            1040 ;            clocked, interrupt-driven
            1050 ;            tasks under Atari OS.
            1060 ;
            1070 ; Written by Bill Wilkinson
            1080 ;   for March, 1982, COMPUTE!
            1090 ;
            1100 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1110 ;
0600        1120 ORIGIN =    $0600
0000        1130        *=   ORIGIN
            1140 ;
00FF        1150 LOW    =    $FF
0100        1160 HIGH   =    $100
            1170 ;
D200        1180 AUDF1  =    $D200     ; Frequency, audio channel 1 (sound 0)
D201        1190 AUDC1  =    $D201     ; Channel 1 control & volume
            1200 ;
0224        1210 VVBLKD =    $0224     ; Delayed Vertical Blank routine
            1220 ;
0014        1230 CLOCKLSB =  $14       ; the system clock, LSB of 3
            1240 ;
00CE        1250 PLAYADDR = $00CE      ; 2 byte pointer in safe place
            1260 ;
            1270 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1280 ;
            1290 ; Equates for our private sound commands
            1300 ;
00FF        1310 CMDR   =    255       ; Repeat
00FE        1320 CMDS   =    254       ; Stop sound (keep routine going)
00FD        1330 CMDN   =    253       ; Number of voices
00FC        1340 CMDTV  =    252       ; set Tone and Volume
0000        1350 CMDE   =    0         ; End (but sound not turned off)
            1360 ;
0600        1370        .PAGE "     install our PLAYIT routine "
            1380 ;
            1390 ; INSTALL is the entry point called from BASIC
            1400 ;
            1410 ; The BASIC program calls us via
            1420 ;    USR( INSTALL, ADR(playit-command-string) )
            1430 ;
            1440 ; The routine may be called from
            1450 ;    assembly language at INSTALL1
            1460 ;    by placing the address of the
            1470 ;    command string in A,Y (LSB,MSB)
            1480 ;
            1490 INSTALL
0600 68     1500        PLA            ; BASIC tells us how many parameters
0601 C901   1510        CMP  #1        ; better just have one!
0603 D0FE   1520 GOOF   BNE  GOOF      ; else only RESET will get him out!
0604 68     1530        PLA
0606 A8     1540        TAY            ; MSB to Y register
0607 68     1550        PLA            ; LSB to A register
            1560 ;
0608        1570 INSTALL1 =  *         ; assembly language entry point
            1580 ;
            1590 ; first, we wait for a vertical blank
            1600 ; ...to ensure we don't get a VBLANK
            1610 ; interrupt while we are working?
            1620 ;
0608 A614   1630        LDX  CLOCKLSB
            1640 WAITUB
060A E414   1650        CPX  CLOCKLSB  ; has clock ticked?
06DC F0FC   1660        BEQ  WAITVB    ; no...keep waiting
            1670 ;
            1600 ; OKAY TO PROCEED
            1690 ;
060E 85CE   1700        STA  PLAYADDR  ; we preempted a zero page spot
0610 8DC206 1710        STA  REPEAT    ; just in case of a repeat cmd
0613 84CF   1720        STY  PLAYADDR+1
0615 8CC306 1730        GTY  REPEAT+1  ; similarly for MSB
            1740 ;
0618 AD2402 1750        LDA  VVBLKD    ; prepare to save the ptr
061E AC2502 1760        LDY  VVBLKD+1
061E C93C   1770        CMP  #PLAYIT&LOW ; already saved?
0620 D004   1780        BNE  NOHINSTALL ; no
0622 D006   1790        CPY  #PLAYIT/HIGH
0624 F018   1800        BEQ  INSTALLED ; yes
            1810 ;
            1820 NOWINSTALL
0626 8DC406 1830        STA  SAVEVBLK
0629 BCC506 1840        STY  SAVEVBLK+1 ; save system vector
            1850 ;
062C A93C   1860        LDA  #PLAYIT&LOW
062E 8D2402 1870        STA  VVBLKD    ; and install our own
0631 A906   1880        LDA  #PLAYIT/HIGH
0633 8D2502 1890        STA  VVBLKD+1
            1900 INSTALLED
0636 A901   1910        LDA  #1        ; A single clock tick
0638 8DC706 1920        STA  DURATION  ; until we start playing
063E 60     1930        RTS            ; done with install!
            1940 ;
063C        1950        .PAGE "     The actual PLAYIT routine"
            1960 ;
            1970 ; PLAYIT is the entry point for our Delayed
            1980 ;   Vertical Blank routine
            1990 ;
            2000 ; PLAYIT 'reads' the sound command string
            2010 ;    and plays our 'song'
            2020 ;
            2030 ; SAM is simply the looping point for cmds
            2040 ;
            2050 PLAYIT
063C CEC706 2060        DEC  DURATION  ; keep on playing?
063F D029   2070        BNE  EXIT      ; yep...no changes
            2080 ;
            2090 SAM
0641 206706 2100        JSR  GETCMD    ; get a byte from command string
0644 C900   2110        CMP  #CMDE     ; End it now?
0646 F053   2120        BEQ  DOEND     ; yes
0648 C9FF   2130        CMP  #CMDR     ; D.C. al Fine?
064A F05E   2140        BEQ  DORPT     ; yes
064C C9FE   2150        CMP  #CMDS     ; Stop all sound ?
064E F03F   2160        BEQ  DOSTOP    ; yep
0650 C9FC   2170        CMP  #CMDTV    ; Tone and Volume on TV?
0652 F02A   2180        BEQ  DGTM      ; yeah
0654 C9FD   2190        CMP  #CMDN     ; Number of voices change?
0656 F015   2200        BEQ  DONUM     ; uh-huh
            2210 ;
            2220 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2230 ;
            2240 ; if none of the above, must be duration
            2250 ;
            2260 DODURATION
0658 8DC706 2270        STA  DURATION  ; we assume so
065B AEC606 2280        LDX  NUMVCS
065E 300A   2290        BMI  EXIT      ; no voices, just duration
            2300 FREQLP
0660 20B706 2310        JSR  GETCMD    ; yes...get next byte
0663 900002 2320        STA  AUDF1,X   ; and set the frequency
0666 CA     2330        DEX
0667 CA     2340        DEX            ; see if more voices
0668 10F6   2350        BPL  FREQLP    ; yes...keep trying
            2360 ; no...fall through to EXIT
            2370 ;
            2380 EXIT
066A 6CC406 2390        JMP  (SAVEVBLK) ; let OS clean things up
            2400 ;
            2410 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2420 ;
            2430 ; set number of voices
            2440 ;
            2450 DONUM
066D 20B706 2460        JSR  GETCMD    ; next byte...
0670 AA     2470        TAX
0671 CA     2480        DEX
0672 8A     2490        TXA            ; less one
0673 3003   2500        BMI  NUMOK     ; if < zero, leave it alone
0675 2903   2510        AND  #$03      ; Ensure 1-4 voices
0677 0A     2520        ASL  A         ; doubled, for ease of use
            2530 NUMOK
0678 8DC606 2540        STA  NUMVCS    ; as number of voices
067B 4C4106 2550        JMP  SAM
            2560 ;
            2570 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2580 ;
            2590 ; set tone and volume
            2600 ;
            2610 DOTV
067E AEC606 2620        LDX  NUMVCS
0681 30BE   2630        BMI  SAM       ; no voices to set
            2640 TVLP
0683 20B706 2650        JSR  GETCMD    ; get next byte
0686 9D01D2 2660        STA  AUDC1,X   ; treat as t&v command
0689 CA     2670        DEX
068A CA     2680        DEX            ; more voices?
06BB 10F6   2690        BPL  TVLP      ; yes
068D 30B2   2700        BMI  SAM       ; no
            2710 ;
            2720 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2730 ;
            2740 ;  STOP the sound (by clring all sound regs)
            2750 ;
            2760 DOSTOP
068F A207   2770        LDX  #7
0691 A900   2780        LDA  #0
            2790 STOPLP
0693 9D00D2 2800        STA  AUDF1,X   ;freq and vol to zero
0696 CA     2810        DEX
0697 10FA   2820        BPL  STOPLP
0699 30A6   2830        BMI  SAM       ; sound stops, pgm keeps going
            2840 ;
            2850 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2860 ;
            2870 ; END the processing (but doesn't stop sound)
            2880 ;
            2890 DOEND
069B ADC406 2900        LDA  SAVEVBLK
069E 8D2402 2910        STA  VVBLKD    ; restore system ptr
06A1 ADC506 2920        LDA  SAVEVBLK+1
06A4 8D25D2 2930        STA  VVBLKD+1  ; and, to OS, we aren't here
06A7 6CC406 2940        JMP  (SAVEVBLK) ; one last time
            2950 ;
            2960 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2970 ;
            2980 ; repeat the same stuff again
            2990 ;
            3000 DORPT
06AA ADC206 3010        LDA  REPEAT
06AD 85CE   3020        STA  PLAYADDR
06AF ADC3D6 3030        LDA  REPEAT+1
06B2 85CF   3040        STA  PLAYADDR+1 ; just reset the address
06B4 4C4106 3050        JMP  SAM       ; and try it again
06B7        3060        .PAGE "     the GETCMD subroutine"
            3070 ;
            3080 ; simply gets next byte from
            3090 ; command string
            3100 ;
            3110 GETCMD
06B7 A000   3120        LDY  #0
06B9 B1CE   3130        LDA  (PLAYADDR),Y ; get the byte
06BB E6CE   3140        INC  PLAYADDR  ; bump LSB of pointer
06BD D002   3150        BNE  GCEXIT    ; done
06BF E6CF   3160        INC  PLAYADDR+1 ; and the MSB
            3170 GCEXIT
06C1 60     3180        RTS
            3190 ;
06C2        3200        .PAGE "     ram usage"
            3210 ;
06C2 0000   3220 REPEAT .WORD 0        ; in case we hear it again
06C4 0000   3230 SAVEVBLK .WORD 0      ; so we can jmp indirect
06C6 00     3240 NUMVCS .BYTE 0        ; controls TVLP and FREQLP
06C7 00     3250 DURATION .BYTE 0      ; how long we hold a sound
            3260 ;
            3270 ;
06C8        3280        .END
COMPUTE! ISSUE 23 / APRIL 1982 / PAGE 162

INSIGHT: Atari

Bill Wilkinson
Optimized Systems Software
Cupertino. CA

This month, I present a session on how to steal a system. Before all you kleptomaniacs rejoice, though, I should explain that I mean to show you how to take control of your Atari’s software system when a user pushes the SYSTEM RESET button. This will, I hope, be useful to BASIC and assembly language programmers alike.

There will be more on the inner workings vs. outer appearances of Atari BASIC; and, as space permits, I will have my usual assortment of cute tricks and Did You Knows.

In a departure from the norm, I will review a product here. Since my company, Optimized Systems Software, both solicits and sells software for Atari (and Apple) computers, I do not think it would be fair for me to do software reviews. But, unless you and/or my dear editor object, I may, from time to time, discuss new and wondrous happenings in the world of Atari.

A Short Review

It generally strikes me as unfair for a magazine to carry a review of something it sells; but every other magazine does it, so I prevailed on COMPUTE! to let me review COMPUTE!’s First Book of Atari. I am doing so on the condition that the review must be run as I submit it. (Okay, okay, Richard…you can fix my punctuation.) I have to do a good review or they won’t let me do it again (just kidding… I think).

First, let me say that I did not start reading COMPUTE! regularly until about December, 1980, so most of the material in the book was new to me. Boy was it new to me! Quite frankly, I had lulled myself into thinking that, until I started writing my column, the poor Atari user had no insight (an insight gag) into the workings of Atari BASIC, DOS, etc. Not so!! There was a lot of good stuff published in COMPUTE! during 1980.

I don’t want to make this review sound like a whitewash, so let’s get the bad stuff over with first. The first warning that needs to be given is that, in general, this is not a book for software hackers: there is little of interest to the assembly language programmer (but see below for some notable exceptions), and the person who has read and understood (!!) the technical manuals and, perhaps, De Re Atari won’t find much he or she didn’t know. However, for most people there is much useful material here. There are some bloopers in the material presented, things which probably wouldn’t get past the current, more Atari-sophisticated, editorial staff. There’s a little duplication of material. And there’s a lot of stuff that has been updated by better articles which have appeared (usually in COMPUTE!) in 1981 and 1982. Actually, my biggest complaint is in a reprinted article titled “The OUCH in Atari BASIC”: the article states, and the editorial lead-in agrees (and the lead-in was written recently—Oh, for shame!), that keywords can’t be used in variable names. Yet, the very next article in the book says that all keywords can be used as variable names! (Still not quite true—“NOT” is poison as the first three characters of a name, and a few keywords, such as “TO” and “STEP” can’t be used as-is. Oh well, this was 1979 and 1980. And, come to think of it, even Atari’s BASIC Reference Manual still says not to use keywords as names. Of course, it also says that substrings are limited to 99 characters, so maybe it’s not a good reference point.)

OK. So much for the bad stuff. “Not possible!” you scream? Sorry, but it’s true. I really don’t have any dirt to sling. Oh, some of the little example programs might now be found in the Atari manuals, etc., but they aren’t bad, just not of as much value as the rest of the book. And I wish I had the time and space to correct every little goof I found. (But I gotta tell you one: the order and size of variables and their names has no impact at all on the speed of an Atari BASIC program. Honest.) With those caveats in mind, we examine the value of the book.

And the book is of value. If you had to choose between losing your left pinkie (not quite up to the left thumb, anymore), the First Book, and De Re Atari, you should really think about how useful a little pinkie is. If you must choose between De Re and First Book, let your experience level be your guide: if you almost understand the Atari technical manuals, you are probably ready for De Re. If you are just learning to program, stick with First Book. If you’re in the middle, better let the little pinkie go.

My own favorite pair of articles from the book are “Inside Atari BASIC” and “Atari Tape Data Files,” both by Larry Isaacs. I am just now getting to the point where I am discussing things in “Insight: Atari” that Larry explored over a year ago (there will be overlap, hopefully to your benefit). Other articles worth mentioning include the following (an asterisk indicates something of interest to assembly language buffs):

“Printing to the Screen from Machine Language” *
(not because of what the presented program does as much as for some of the techniques it introduces)
“The Fluid Brush”
(Ditto. And its ideas have been much copied.)
“Player/Missile Graphics” *
(by Chris Crawford. What more need I say?)
“Adding a Voice Track to Atari Programs” *
(This one was even swiped! There are more sophisticated methods shown in De Re, but this is adequate for many purposes.)
“Atari Memory Locations” *
(Just a table. You need to read the Technical Manual and/or De Re first, but this will serve as a handy reminder.)
“Input/Output on the Atari”
(I hesitated on this one: you should ignore what it says about XIO! It’s misleading. Read my Atari I/O series.)

You’ll note that most of that stuff is kind of heavyweight. Well, that’s what appeals to me and, I think, to a large portion of COMPUTE!’s Atari readership. However, there are several little goodies, usable by virtually anyone, which deserve honorable mention. No commentary on these: their names tell it all and you just have to try them to appreciate them:

You may have your own favorites, but my criterion for a good article (or good magazine-published program) is that it teaches you something. Thus I rate type-it-in-and-run-it games relatively low. (There are remarkably few of them that appear in the book.)

In final summary, I have to say that, for $12.95, you are unlikely to find this much (184 pages, including—can you believe it—a usable index) useful Atari material presented again (well…until the Second Book?). Real software hackers will find some of the material too elementary, but they are probably the only ones that will be disappointed.

Stealing A System

During my series on Atari I/O (COMPUTE! November, 1981, through March, 1982, issues 18 through 21), I mentioned (more than once) the “proper” way to add device drivers to OS. I summarize it here again:

  1. Inspect the system MEMLO pointer (at $2E7).
  2. Load or relocate your routine at/to the current value of MEMLO.
  3. Add the size of your routine to MEMLO.
  4. Store the resultant value back in MEMLO.
  5. If your routine is a device driver, connect it to OS by adding its name and address to HATABS.
  6. Fool OS so that steps three through five will be re-executed if SYSTEM RESET is hit.

In COMPUTE! (January, 1982, #20) we added the driver for the “M:” device by following steps one through five as above. We discussed step six briefly, but did not show how to implement it. This month, we will show how to fool OS. And, rather than repeating the lesson about adding device drivers, we will take this opportunity to show how to give Atari BASIC some measure of control over what happens on RESET.

In particular, we “steal” the system in a way that the user who hits RESET will cause a TRAPpable error in the running BASIC program. In other words, if you write your BASIC program in a way that TRAP (to a line number) is always active, you will be able to detect when your user hits the RESET key, but your program will not stop running, will not lose its variable values, and will be impacted in the minimum possible way.

Some cautions are in order (it seems like I always have to say that): before vectoring through RAM (and thus allowing our little trick) Atari’s OS ROMs perform several actions when SYSTEM RESET is hit. If you need to know exactly what happens, try to get hold of the CIO listings (they are moderately readable); generally, the following lists all that matters except to those who would make their own cartridges:

  1. The system resets any memory size pointers (MEMLO, MEMTOP, etc.).
  2. Most hardware registers are reset to zero ($D000–$D0FF, $D200–$D4FF).
  3. OS clears its own RAM ($200–$3FF, $10–$7F). Note that this zaps all IOCB’s for all files.
  4. All the ROM-based device drivers are initialized (via their own initialize routines).
  5. CIO’s initialization is called, which effectively marks all files as properly closed.
  6. Screen margins, etc., are reset and the E: device is opened on file channel #0 (which is equivalent to GRAPHICS 0 from BASIC).
  7. The file manager’s initialization routine is called via an indirect call through location DOSINH ($0C).
  8. If there are no cartridges installed, then DOS is invoked by an indirect jump through location DOSVEC ($0A). If a cartridge is installed and wants control, though, OS goes to the cartridge instead of DOSVEC.
(NOTE: OS/A+ uses a variation on 7., above, so don’t bang your head against the wall trying what is written here with OS/A+. I will be glad to tell you of the differences if the manual is not clear enough.)

Program 1 takes into account not only all of the above, but also the requirements of Atari BASIC related to executing a pseudo-warmstart. I will not try to explain why the various JSRs and tests shown are needed; just take my word for it that they are indeed necessary (I found out the hard way). Actually, the part pertaining to stealing the DOSINIT vector is straightforward, as you may note, and changing MEMLO is trivial.

Once again, for space and time reasons, I have cheated with this program: I have assumed that my routine can load and execute at $1F00 and can move MEMLO to $2000. Those of you who want to do the whole thing right can follow the techniques I showed in COMPUTE! February, 1982, #21, for generating relocatable programs. Also, please note that the listing, as is, is designed to produce an AUTORUN.SYS file. You may need to do a little surgery to use it in other ways (e.g., remove the load-and-go vector at the end, JMP directly to the start of the BASIC cartridge, etc.—experiment).

The most important thing to note about this routine’s implementation is how the address found in DOSINIT is moved into the JSR instruction (at the label RESET). Obviously, you could go look at the contents of DOSINIT and code the JSR directly, omitting the move of the address. And this will work as long as you use the same version of OS and DOS. But … all too many Atari software developers fell into the trap of thinking that OS and DOS were immutable, only to have Atari announce DOS 2S and OS version B.

To Atari’s credit, they have carefully documented which locations, vectors, etc., are guaranteed to remain unchanged. It you write your code properly, you should never have to change it. Incidentally, another example of this same concept appealed in my last article: the vector from VVBLKD was preserved, rather than simply JuMPing to the routines in ROM.

Enough preaching: investigate the listing. But I leave you with one last freebee. If you change three lines of code in the listing (lines 1950 to 1970) to the following two lines, you will cause BASIC to reRUN the program currently in memory, rather than causing an error TRAP.

1950 JSR $B755
1960 JMP $A962

The following short BASIC program illustrates the use of our stolen pointer:

10 TRAP 100
20 PRINT "LOOPING AT LINE 20"
30 GOTO 20
40 STOP :REM can't get here from there ... or anywhere
100 IF PEEK(195)<>255 THEN PRINT "HOW? WRONG ERROR CODE!":STOP
110 PRINT "RESET KEY WAS HIT...I WILL START AGAIN"
120 RUN

Note that you can’t get out of this program with the RESET key! Now, if you also trap the BREAK key, the user is truly locked in your program. If you have BASIC A+, of course, you can trap the BREAK key via SET 0. If not, then refer to the listing titled “Idiot-Proofing the Keyboard” in De Re Atari. (Summary of that listing: since the BREAK key is one of the two IRQ’s not vectored through RAM, you must change the system master IRQ vector to point to your own routine. In your routine, you check for and ignore BREAK key IRQ’s and pass other IRQ’s on unchanged. Not trivial, perhaps, but certainly less complicated than what we have done above.)

Inside Atari BASIC, Part 3: Enhancements

After skipping last month, we return to our discussion of the hows and whys of Atari BASIC. Recall that in COMPUTE! February, 1981, #21, we discussed how BASIC checks your entered line for correct syntax and produces a tokenized result. Let us begin this month with a discussion of how BASIC executes (RUNs) a program.

First, note that if you enter a direct line (one without a line number), BASIC arbitrarily assigns it to line number 32768 and then pretends that it is like any ordinary line. That means that even direct lines must go through the tokenizing and execution process. It also means that BASIC makes little or no distinction between statements (within a program) and commands (given directly); thus you can LIST or RUN or even CONTinue from inside a BASIC program.

Whenever a line is finished executing, BASIC checks to see if the next line exists (it doesn’t if a direct line was just executed) or if the next line has a line number greater than 32767 (i.e., if the line executed is the last one prior to the direct line). If either condition prevails, BASIC pretends to itself that it got an “END” statement and, presto, you are back staring at the “READY” prompt.

But let us assume that the direct command given was “RUN.” The execution of a RUN statement simply causes all BASIC’s pointers and flags to be reinitialized, including setting BASIC’s “next line” pointer to the beginning of the program. Then RUN returns to what we call “Execution Control” which decides that it needs to start executing the next line…which conveniently is the first line.

So far, so good. But how does BASIC know what to do with the tokens? The answer is that it doesn’t, really. Recall that there are two separate kinds of execution (as opposed to variable) tokens: statements and operators. Each of these has a table of two-byte pointers residing in BASIC’s ROM. Execute Control simply picks up the next byte of the program, assumes that it is a statement token (incidentally, in the range of $00–$7F), and uses double its value as an index into the table of statement pointers. It uses the address thus found as an indirect jump and goes to the appropriate statement execution routine.

In a non-syntaxed BASIC (i.e., Microsoft), much of the preceding applies virtually unchanged. But, when the statement execution routine gets control in such a BASIC, it has no idea what the next character or token in the program might be, so it must needs go through a set of checks to determine what is legal and what is not.

In Atari BASIC, though, the statement execution routine knows that the byte(s) that follow constitute legal syntax! So it need not waste time checking for legality. Since the bytes following the statement token may range from the non-existant (as in CONT, which has no following bytes) to the extremely complex (as in PRINT, in all its variations), each statement generally has responsibility for choosing what to do with these bytes.

With the exception of assignment-type operations (LET, READ, INPUT, etc.), file designators (PRINT #), and complex statements (FOR…TO…STEP), what follows the statement byte is generally a series of one or more expressions, separated by commas, equal signs, semicolons, etc. Thus it comes as no surprise that there is a major subroutine in Atari BASIC entitled “Execute Expression,” which can evaluate virtually any numerical or string expression.

As a simple example, let us examine the mechanism of POKE. The syntax is properly “POKE <aexp>,<aexp>” (where <aexp> means Arithmetic EXPression). So POKE’s statement execution routine simply calls Execute Expression for the first value, saves it away someplace safe, skips the comma (it knows the comma is there…the syntaxer said so!), calls Execute Expression for the second value, and stores the second into the memory location designated by the first. Now, in truth, POKE calls a variation on Execute Expression which is guaranteed to return a 16-bit (or 8-bit, as required) value; but the concept holds for most statements.

It is really beyond the scope of this article to try to explain the intricacies of Execute Expression. It will suffice to point out that it must worry about operator precedence (“*” before “+ ”, etc.) and parentheses and subscripted variables and functions (SIN, RND), etc.) and more.

And that’s about it. Except to note that when a statement is finished it usually simply returns to Execute Control, which checks for another statement on the same line and/or moves its pointer to the first statement of the next line.

Much of the point of this discussion has been to show why it is hard to fool BASIC into believing that it has a new statement to use. Even with the source, it is no easy task to make sure that the correct syntax for a new statement is entered into the syntax tables (which are actually a miniature language in their own right), the name tables, and the execution tables (to say nothing of writing the code to execute the statement). With Atari BASIC locked in ROM, the task is really impossible since BASIC makes use of no RAM-based pointers or indirect jumps throughout this process.

So how can we add features to Atari BASIC? Several ways:

  1. Try the USR function as suggested last month. This really is the simplest, most straightforward, most guaranteed-to-work.
  2. Make your own special device handler (a la “M:” in COMPUTE!, January, 1981, issue #20). Open a channel to it (OPEN #1,…). PRINT something to it. When your driver gets control, it can actually go in and look at the BASIC tokens and decide what to do from there. Cryptic, but it works.
  3. If you are interested in commands, as opposed to statements, you can intercept BASIC’s call to “E:” (for the next input line) and examine the line yourself (presumably as does Monkey Wrench). This implies that you must check syntax, find variables, convert ASCII to floating point, etc., in your routine. Tedious, but obviously feasible and usable.

As you can, no doubt, tell, I am much in favor of method 1. It is by far the easiest to do and requires the least knowledge of BASIC’s internals.

Is there yet another way? A month ago I would have said “no!” But, now, I have discovered a crack in the door. It is complicated, prone to programmer error, fairly inflexible, and of doubtful value for anyone but professional software developers. To explain it would take a couple of more columns, and I’m simply not willing to write that much on a topic of dubious value. If you feel you absolutely must know, write me (care of OSS). If enough people write, I may make up a pamphlet and sell it at an outrageous price. Are you sure you can’t live with method 1?

Easy Horizontal Joysticks

If you have an application (a polite way of saying game) that needs a joystick that moves only horizontally (or only vertically, if you are willing to hold your joystick turned 90 degrees from “normal”), then have I got a trick for you! Try this program, with joystick number 0 plugged in:

10 PRINT PTRIG(0)-PTRIG(1),
20 FOR J=1 TO 50:NEXT J
30 GOTO 10

Now push the joystick in all directions. Neat? Pushing it left gives you a value of -1 and right gives you + 1. And, of course, you can use A=PTRIG(2)-PTRIG(3) to read joystick number 1, etc.

Why does it work? Because the paddle triggers happen to use the same pins on the connector that the horizontal switches in the joystick use. I discovered that by reading the technical manual; so, you see, there is probably still buried gold in those books.

Unfortunately, no such happy coincidence exists for reading the vertical joystick switches. Incidentally, use of this trick does not affect STRIG in any way.

Dissonances

The algorithm Atari gives for figuring out what actual frequency will result from the divider FREQ in (for example) SOUND 0,FREQ,tone,volume is as follows: actual frequency = 63921 / (FREQ + FREQ + 2) This means that, at values for FREQ around 85 (the middle of the Atari’s frequency range), the minimum actual frequency step is about 4 Hz. While adequate for solo parts, this kind of frequency resolution can really grate on your ears when there are three or four voices active. To illustrate the real meaning of this, try the following one-liner:

FOR F=255 TO 0 STEP -1:SOUND 0,F,10,15:NEXT F

The resultant sound is a smooth glide until you get near the top end, when you begin to really hear the steps.

For those of you with a keen ear and/or a strong sense of music, cheer up. Atari, once again, gave us a solution. The entire Atari audio system is controlled by hardware register AUDCTL ($D208). Normally, the audio channels are clocked by an oscillator running at 63921 Hz. But, the user may specify that channels zero and two (which Atari calls one and three in the Technical Manual… oh well) are to be clocked by a 1,789,790 Hz oscillator. If you change 63921 to 1789790 in the formula above and plug in 255 (the highest value) for FREQ, you will see that the lowest note thus playable is around 3000 Hz!

But we have yet another solution available via AUDCTL: instead of an 8-bit counter for a single audio channel, we use a pair of channels to produce a 16 bit counter. (Unfortunately, we then are limited to two sound channels.) The modified formula then becomes: actual frequency = 1789790 / (FREQ + FREQ + 14) Since FREQ now has values from 0 to 65535, it’s obvious we have many more actual frequencies available to us. I present herewith two sample programs using this technique. Program 2 shows two voices doing very smooth glides. Program 3 shows a 9-octave chromatic scale (C, C#, D, D#, etc.). This compares to the 3-octave scale available via the standard SOUND commands.

Finally, if you simply need lower notes than you can get with SOUND, TRY POKEing AUDCTL to one. This has the effect of lowering all notes by approximately two octaves. Unfortunately, you do not get to have some channels high and others low. Example:

SOUND 0,60,10,12:SOUND 1,45,10,12:POKE 53768,1:FOR I=1 TO 500:NEXT I

Again, investigate De Re for even more details on some of the more complex aspects of the sound system. (Want to hear your Atari “MOO” like a cow?)

A Final Caution

I have had a couple of people write or call me complaining that, when they tried my assembly language routines, they didn’t work. Honest, they do work. The listings published in the magazine are the ones I actually used. Sometimes the problem simply resolves to a typo on the part of the user. But sometimes it turns out to be an address conflict.

I do most of my work with OS/A+ and BASIC A+ (naturally. But I use Atari BASIC to check out programs for these pages), and I usually use memory addresses which are convenient to me. Since I get tired of putting everything in page six, I sometimes use $1F00 or some such as an origin. You can use these addresses in your system with the Atari Assembler/Editor if you change MEMLO to, say, $3000 (my usual choice, and achievable with the LOMEM command of EASMD). However, it may be more convenient to use SIZE to look at where your source, etc. is and then change the origin to reflect your memory configuration.

Of course, you can always assemble into the dreaded page six or assemble directly to disk (or cassette). But, in any case, don’t use an origin (“*= xxxx”) which conflicts with SIZE in your system. I purposely give you source listings so that you can see how things work; take the time to type them in and it will probably prove easier in the long run.

Program 1.
equates and commentary
0000        1000        .PAGE "equates and commentary"
            1010 ;
            1020 ; STEAL A RESET
            1030 ;
            1040 ; listing for COMPUTE! magazine, April, 1982
            1050 ;
            1060 ;
            1070 ; there are two parts to this listing:
            1080 ;   1. what happens when this is first loaded
            1090 ;      (which initializes everything)
            1100 ;   2. what happens when the User pushes
            1110 ;      SYSTEM RESET.
            1120 ;
            1130 ; Most of 1 is understandable.  Most of 2
            1140 ; is magic.  If it works, don't knock it.
            1150 ;
02E7        1160 MEMLO  =    $2E7      ; BOTTOM OF USABLE MEM
            1170 ;
00FF        1180 LOW    =    $FF
0100        1190 HIGH   =    $100
            1200 ;
            1210 ; EQUATES INTO BASIC ROM
            1220 ;
BD72        1230 SETDZ  =    $BD72     ; ENSURE OUTPUT TO CONSOLE
0092        1240 MEOL   =    $92       ; FLAG: LINE MODIFIED
BD99        1250 FIXEOL =    $BD99     ; UNMODIFY
00B9        1260 ERRNUM =    $B9       ; AT LEAST BASIC THINKS SO
B940        1270 ERROR  =    $B940     ; HANDLE ERRORS
00BD        1280 TRAPFLAG =  $BD
DA51        1290 INITBUF =   $DA51     ; SAFETY
0011        1300 BRKFLAG =   $11
BD41        1310 CLOSEALL =  $BD41     ; close IOCBs 1-7
000C        1320 DOSINIT =   $0C       ; see article
            1330 ;
SETUP THE RESET VECTOR
0000        1340        .PAGE "SETUP THE RESET VECTOR"
            1350 ;
            1360 ; We move the OS DOSINIT vector to point to outselves
            1370 ;
            1380 ; ***** NOTE: change next line to suit!!! *****
0000        1390        *=     $1F00
            1400 CHANGEDOSINIT
1F00 A50C   1410        LDA  DOSINIT
1F02 8D231F 1420        STA  RESET+1
1F05 A50D   1430        LDA  DOSINIT+1 ; Self modifying code...nasty
1F07 8D241F 1440        STA  RESET+2
1F0A A922   1450        LDA  #RESET&LOW
1F0C 850C   1460        STA  DOSINIT
1F0E A91F   1470        LDA  #RESET/HIGH
1F10 850D   1480        STA  DOSINIT+1 ; We have changed the pointer
            1490 ;
            1500 ; Here we Move MEMLO...
            1510 ;   we arbitrarily use 256 bytes of space
            1520 ;
            1530 MOVEMEMLO
1F12 A900   1540        LDA  #RESETTOP&LOW
1F14 8DE702 1550        STA  MEMLO
1F17 A920   1560        LDA  #RESETTOP/HIGH
1F19 8DE802 1570        STA  MEMLO+1
1F1C 60     1580        RTS
            1590 ;
            1600 ; FROMBASIC  is just a second entry
            1610 ;   entry point into the initialization...
            1620 ;   for initializing from BASIC
            1630 ;
            1640 FROMBASIC
1F1D 68     1650        PLA            ; get cnt of parms off stack
1F1E F0E0   1660        BEQ  CHANGEDOSINIT ; good...no parms
1F20 D0FE   1670 OOPS   BNE  OOPS      ; otherwise, tough!
1F22        1680        .PAGE "THE ACTUAL RESET TRAP"
            1690 ;
            1700 ; On reset, DOS normally gets
            1710 ; called to reinitialize itself ...
            1720 ; we use this to our advantage by
            1730 ; reinit'ing both DOS and BASIC
            1740 ;
            1750 RESET
1F22 200000 1760        JSR 0          ; second two bytes get replaced
            1770 ;                   by the address of real DOSINIT
1F25 A2FF   1780        LDX #$FF
1F27 9A     1790        TXS            ; BASIC likes it this way
1F28 8611   1800        STX BRKFLAG    ; ensure no BREAK key pending
1F2A 2041BD 1810        JSR CLOSEALL   ; so everybody agrees
1F2D 2072DB 1820        JSR SETDZ
1F30 A592   1830        LDA MEOL
1F32 F003   1840        BEQ RST2
1F34 2099BD 1850        JSR FIXEOL
            1860 RST2
1F37 2051DA 1870        JSR INITBUF
            1880 ;
1F3A 20121F 1890        JSR MOVEMEMLO  ; to protect this code
            1900;
            1910 ; NOW we fool BASIC into thinking
            1920 ; an error occured
            1930 ;
            1940 ;
1F3D A9FF   1950        LDA  #255      ; (or your choice of errors)
1F3F 85B9   1960        STA  ERRNUM
1F41 4C40B9 1970        JMP  ERROR     ; do it
            1980 ;
            1990 ; THE FOLLOWING EQUATE IS USED TO SET
            2000 ; RESETTOP on a page boundary
            2010 ;
2000        2020 RESETTOP =  *+$FF&$FF00
            2030 ; SET UP LOAD AND GO
            2040 ;
1F44        2050        *=   $2E0
02E0 001F   2060        .WORD CHANGEDOSINIT
02E2        2070        .END
Program 2.
10 AUDCTL=53768:DBL=120
20 AUDF1=53760:AUDC1=53761
30 SOUND 1,10,10,15:SOUND 3,10,10,15
40 POKE AUDC1,0:POKE AUDC1+4,0
50 POKE AUDCTL,DBL
60 FOR J=10 TO 15:POKE AUDF1+2,J:POKE AUDF1+6,20-J
70 FOR I=0 TO 255:POKE AUDF1,I:POKE AUDF1+4,255-I:NEXT I
80 NEXT J
     ...VERY SMOOTH GLIDES...
Program 3.
10 AUDCTL=53768:DBL=120
12 DSC=1789790/2
20 AUDF1=53760:AUDC1=53761
30 SOUND 1,10,10,0
40 POKE AUDC1,0:POKE AUDC1+4,0
50 POKE AUDCTL,DBL
60 P2=2^(1/12)
70 NTE=16:REM C IN THE REAL BASS
80 FOR I=1 TO 109
90 FREQ=INT(DSC/NIE-7+0.5):F0=INT(FREQ/256)
92 F1=FREQ-256*F0
100 POKE AUDF1,F1:POKE AUDF1+2,F0
102 POKE AUDC1+2,175
103 PRINT "NOW PLAYING ";INT(NTE+0.5);" HZ"
105 FOR J=1 TO 100:NEXT J
110 NTE=NTE*P2
120 NEXT I
130 GOTO 70
 ...9 OCTAVE CHROMATIC SCALE...
COMPUTE! ISSUE 24 / MAY 1982 / PAGE 162

INSIGHT: Atari

Bill Wilkinson
Optimized Systems Software
Cupertino, CA

The major program for this month is perhaps the most exciting one to appear in this column to date. We will take advantage of Atari’s modular software construction to define a set of soft keys, a concept that is marketed for real $$$ on some machines. The most obvious use of soft keys is in writing BASIC programs. Even with the abbreviations allowed by BASIC, wouldn’t it be convenient to be able to use a single keystroke to get a disk directory listing? And, of course, when programming in assembly language there are certain character combinations chat are repeated often enough to justify the use of soft keys (e.g., “),V” or “.BYTE”).

The techniques presented in the soft key program include how to “steal” the system’s default I/O devices and adapt them for your own purposes. It might be worth your while to re-read my column on adding the “M:” driver, (COMPUTE!, January, 1982, #20, pg. 120) since I will be assuming your knowledge of some of the points made there.

For the BASIC user, the soft keys can he made truly “soft”—even to the point of allowing a running BASIC program to change the definition of what a soft key means. And, of course, there will be the usual set of tidbits for those who don’t feel up to tackling the soft keys project.

An Announcement

As most of you probably know, magazine articles and columns are written months before they actually appear. As I write this in mid-February, my company (Optimized Systems Software, Inc.) and COMPUTE! are frantically engaged in getting a new book ready for publication. Inside Atari DOS will presumably have made its appearance by the time you read this. Now, for the first time, Atari users will have access to the listing of the File Manager System of Atari DOS 2.0S, the current version of Atari DOS. Besides the listings, there is a complete description of each major subroutine, complete with entry and exit parameters and error conditions.

The book is not complete in and of itself: you would still need Atari’s listings of the OS ROMs and DUP to have access to all of the DOS secrets. But this book will tie together many loose ends.

Let me leave you with one caveat (don’t I always?): the book assumes that the reader has at least a working knowledge of 6502 assembly language. The book is of most value if you would like to see how such a complex organism as a DOS is built.

The terminology “soft” keys refers to keyboard keys that may change “meaning” as desired by the user. The Atari keyboard keys which we will make “soft” include the characters “control-A” through “control-Z” (that is, what are normally graphic characters produced by holding down the control key while hitting one of the letter keys).

The keys will be made soft in a very flexible fashion: each of the 26 keystrokes may be defined in such a way that entering one of them will “fool” the Atari OS (and hence BASIC, etc.) into thinking that a sequence of one or more ordinary keys have been depressed. The phrase “one or more” is literal: there is no effective limit on the number of characters a soft key may represent.

As this program is written, there are some limitations. Only characters with an ATASCII value of 1 to 127 decimal ($01 to S7F hex) may be placed in the string. The CR (RETURN or End-Of-Line) character is thus not permitted (since its value is $9B hex, 155 decimal). Since lack of CR seems to me to be a major flaw, each zero ($00) byte in the string is converted so that OS sees a CR instead.

The reason that only the values from 0 to 127 are acceptable is that a byte with its most significant bit (MSB, $80 or 128) turned on is our signal that this byte is to be the last character in the soft key string. Obviously, you can rewrite this part, if you desire, so that some other means is used to designate the end of string (a preceding length byte or trailing zero byte are obvious alternatives). However, the method chosen is simple and seems adequate for most purposes.

One more note before we get into implementation details: since there are times when you might really want the graphics character “hidden” by a soft key, I have designed an “escape” sequence. Pressing “control-comma” (normally the graphics heart character) signals to the soft key routine that the next character is not to be translated. Thus, even the heart may be generated by pressing control-comma twice.

The Nitty-Gritty

Program 1 shows the complete source of Easykey, the program which implements all the features mentioned above. The program is composed of five primary parts.

The first part, with line numbers in the 2000–2999 range, is used to hook the routine into Atari’s OS. First, we search the Handler Table looking for the E: device. When we find it, we hold onto its address and put the address of our replacement driver in the table instead. Recall that the address in HATABS must be the address of the Handler Routine Table. We copy the current table (presumably the Atari default table, from ROM) to our NEWETBL (new E. table) and replace the entry point for the get-a-single-character routine with the address of our new routine (NEWEGETCH) less one (always required, see commentary on the M: driver in my January column). We then change LOMEM so BASIC won’t wipe us out and exit.

The second part of this process is the new get-a-single-character routine for the screen editor (E:) device. Most of this code is copied directly from the OS ROMs, the only exceptions being the branches to locations in ROM and the call to the keyboard get-a-character routine (KGETCH).

The third part, NEWKGETCH (NEW Keyboard GET single CHaracter routine), is the heart of this whole process. Here is where individual keystrokes are actually interpreted from their hardware codes and characteristics to a more palatable ATASCII code. But here, also, is where the system is vulnerable to our machinations. Since nothing “downstream” of the keyboard handler (e.g., the rest of the E: driver, CIO, BASIC, etc.) knows what happened at the physical keyboard level, the calling routines will believe the keyboard handler no matter what it tells them.

Actually, NEWKGETCH is fairly simple. First it checks to see if it is already processing a soft key. If so, it simply hands the caller the next key of the soft key’s string. If not, it goes and gets a real key from the keyboard. If that key is a heart, it simply gets the next real keyboard key and passes that back to the caller (our “escape” clause). Otherwise, if the keyboard key was not control-A through Control-Z. then the key is returned to the caller unchanged.

If the keyboard key was one of the definable soft keys, its value is used to index into a table of soft key string pointers. Here one last validity check is made: if the string pointer is zero, the keyboard key is returned to the caller and no soft key string handling occurs. If a string pointer is encountered, its address is placed in KPTR and used to access all the characters in the string.

Note that zero bytes are translated to $9B (CR) characters and that any character with its most signiticant bit on terminates the string (by zeroing the high order byte of KPTR).

The fourth part of this routine is simply the above-mentioned table of soft key string pointers. Note that we take advantage of the fact that the assembler places zeroes into .WORDs (or .BYTEs) which are undefined.

The last part, of course, consists of the actual strings. Note the flexibility here. Control-D (label SD), for example, includes the modified CR character ($00) as also its last character by simply turning on the MSBit, producing a code of $80.

Soft Keys: Using Them From BASIC

To conserve space and to show the fiexibility of the soft key system. I included strings for only three keys: control-D, which causes a disk directory listing, control-S for “SETCOLOR,” and control-? for “PRINT#.” The simplest way to add more soft key strings is to put them into the source given (with labels “SA” through “SZ,” as appropriate) and assemble the whole thing at an address appropriate for your system.

But you can add or change soft key strings dynamically from BASIC via POKEs, etc. Note that Easykey, as given here, reserves over 450 bytes for soft strings. Note also the addresses of the labels “STABLE” and “STRINGS.” If you assemble your own copy of Easykey, be sure and note the addresses of these two labels and convert them to decimal if you intend to use dynamic soft keys.

Program 2, the BASIC program, is a sample which will allow you to redefine all 26 soft keys to any string you like and then save the resulting definitions in a disk file for later use. Study the technique, and you should be able to produce any kind of soft keys you might want. The program fragment (Program 3) will allow the reloading of predefined sets produced by the previous program.

Inside Atari BASIC: Part 4

This month we will feature a short discussion of the various “tables” used by Atari BASIC and how to find them. Some of this material is well covered by some of the articles in COMPUTE!’s First Book of Atari, so if you are too impatient to wait tor next month you can run out and buy the book. Next month we will begin to use the information we discover this month to “fool” BASIC into letting us do things it was never designed for. We begin:

When an Atari BASIC program is SAVEd to disk or cassette, there are only 14 bytes of zero page written out along with the main tables and program. These 14 bytes consist of seven two-byte pointers (in the traditional low-byte, high-byte form) which tell BASIC where everything is in the particular program being SAVEd (or later being LOADed), All the other important zero page locations (and there are over 50 of them) are regenerated and/or recalculated by BASIC anytime you type NEW or SAVE (or, for some locations, RUN, GOTO, etc.). The implication is that, if you control these seven pointers, you control BASIC so let’s examine their names and functions. Table 1 gives a summary thereof.

The first thing you may note about this table is that some of the locations (indicated by asterisks) have duplicate labels. If you examine the mnemonic meanings, you will probably see why: the pointer can mean different things in different contexts. For example, the space pointed to by location $80 (decimal 128) is used for different purposes, depending on whether BASIC is currently working on entering a new line (it uses OUTBUFF) or executing an expression within a program (when it uses ARGOPS).

You might also note that I provided a list of more than seven pointers. The locations $8E and $90 are not SAVEd and reLOADed because they are always dependent on the current state of the program (i.e., whether it is RUNning, whether it has executed a DIM statement, etc.). They are included here for completeness: aside from the zero page locations (and the $600 page locations with BASIC A+), these pointers completely define BASIC’s usage of the Atari computer’s memory space. So now let’s go into detail about what each of these pointers is used for.

Table 1: BASIC’s Critical Zero-Page Pointers

Location    Mnemonic   Which means:
Hex Decimal Label:

80  128     LOMEM    pointer to LOw MEMory limit
80  128*    ARGOPS   ARGument/OPerator Stack
80  128*    OUTBUFF  syntax OUTput BUFFer
82  130     VNTP     Variable Name Table Pointer
84  132     VNTD     Variable Name Table Dummy end
86  134     VVTP     Variable Value Table Pointer
88  136*    STMTAB   STateMent TABle
8A  138     STMCUR   CURrent STateMent pointer
8C  140     STARP    STring/ARray Pointer
--  ---
8E  142     ENDSTAR  END STring/ARray space
8E  142*    RUNSTK   RUNtimeSTacK pointer
90  144     TOPRSTK  TOP of Runtime STacK space
90  144*    MEMTOP   pointer MEMory TOP limit

We already noted that ARGOPS is used in expression evaluation. That is, whenever BASIC sees any kind of expression to be evaluated [e.g., 3*A+B or SIN(30) or 2*(LOG(4/EXP(Y*Z^3))-1/(Z*2.5+ATN(Z)) or even 1.25], it must init intermediate results and/or operators on a “stack.” ARGOPS points to a 256 byte area reserved for both the argument stack and the operator stack. (What actually happens in Execute Expression is extremely complex and far beyond the scope of this article.) Since expression evaluation and program entry cannot occur at the same time, OUTBUFF shares this same 256 byte space. When a program line is entered, BASIC checks it for syntax and converts it to internal tokens, placing these tokens temporarily into this 256 byte buffer (before moving them into the appropriate place in the program, depending on the line number). Again, this process is complex, but the results have been documented here in prior columns and in such places as De Re Atari and COMPUTE!’s First Book of Atari.

VNTP and VNTD point to the Variable Name Table. In Atari BASIC and BASIC A+, only the first occurrence of a name causes an entry to be added to this table. Within the tokenized program, the name(s) are replaced by a “variable number” which refers to the name’s position within the name table. The names are simply placed in this table one after the other, with no intervening bytes, and the end of a name is signaled by turning on the significant bit ($80, 128 decimal) of its last character. Note that the dollar sign on the end of a string name and the left parenthesis on the end of an array name are included in this table.

VVTP and ENDVVT define the limits of the Variable Value table. Aside from the actual tokenized BASIC piogram, this is probably the most interesting of the tables. Each variable occupies eight bytes in this table, so the variable number token need only be shifted left three times to index to the proper location herein. In Part 5 of this series, we will delve into this table in depth, finding many ways to fool BASIC, but there is no room in this issue for more on the subject.

STMTAB defines the beginning of the tokenized program; and, since there is no proper label to refer to, we may consider that STARP defines the end of same. Again, I refer to previous parts of this series for details on the structure of tokenized lines. STMCUR is interesting because it normally points to the actual line currently being executed. This would be one way of implementing special “statements” in Atari BASIC; a USR call would cause the subroutine to use STMCUR to examine the rest of the line for variables, etc. But my comments on ease of use, etc., from last month still apply: I don’t think this is really practical.

STARP is the last of the seven pointers that are SAVEd and LOADed. Actually, it is included only to point to the end of file program area. The string/array space is not SAVEd or LOADed (but see Al Baker’s article on “Atari Tape Data Files” in COMPUTE!’s First Book of Atari for some tricky techniques which I may expand on in future columns). STARP and ENDSTAR define the limits of the string/array space. Atari BASIC is different from Microsoft-style BASICs in that arrays and strings are allocated from this space in the order they are DIMensioned and are not moved around relative to each other after that. Thus, if you code “DIM A$(100),B(3,3)” then you can use ADR(A$) + 100 as the address of B(0,0).

Finally, there is the run-time stack, defined by RUNSTK and TOPRSTK. When a GOSUB or FOR (or WHILE in BASIC A+) is encountered, the current “address” (consisting of the line number and statement offset within the line) must be “pushed” onto a stack to wait for the corresponding RETURN or NEXT (or ENDWHILE) to “pop” it off, so that the loop or mainline routine may continue where it left off. This stack thus expands and contracts as necessary while a program is running. Again, full details of how the stack is accessed can’t be discussed here. In any case, the mechanism is relatively simple for GOSUB and WHILE; only FOR…NEXT presents some interesting problems.

Before we leave this topic for this month, we should note that when a program is SAVEd all seven pointers are “relativized” to zero. That is, each pointer has the value of LOMEM (which is also the first pointer) subtracted from it. Then when the program is LOADed. the current value of LOMEM is added to each pointer, thus allowing self-relocating BASIC programs. A side effect of this process is that the first pointer is thus always zero (actually two bytes t]f zero), and BASIC uses this fact as a self-check when LOADing: il assumes that any file which does not start with two zero bytes cannot be a BASIC SAVEd program.

Tidbit #1: Structured Programs

An often desirable construct within properly structured programs is this one:

1. IF ⟨expression⟩ THEN ⟨procedure-1⟩ ELSE ⟨procedure-2⟩

Since BASIC doesn’t support procedures, we will modify this to the more familiar-looking form:

2. IF ⟨expression⟩ THEN GOSUB ⟨line-1⟩ ELSE GOSUB ⟨line-2⟩ or, using BASIC A+,
3. IF ⟨expression⟩:GOSUB ⟨line-1⟩ ELSE : GOSUB ⟨line-2⟩:ENDIF

But still, the Atari BASIC programmer cannot use either of these forms. Take heart! There is a solution which is a logical replacement for 2., above:

4. ON ⟨logical-expression⟩+1 GOSUB ⟨line-1⟩,⟨line-2⟩

Note that there is a subtle difference: where the IF allowed “expression,” we now require “logical-expression.” The reason is fairly obvious if you recall that a logical expression in Atari BASIC (e.g., A<B or B>=0 or A$<>B$) always evaluates to a one (true) or zero (false). By adding one (the “+1” in 4.) to a logical expression’s value, we have a value of either one or two, something which ON…GOSUB is quite happy with since it GOSUBs to line-1 if the value is one and line-2 if the value is two.

If you really do have an “expression” to replace (e.g., IF A THEN…), simply change it into a logical expression by comparing it to zero, thus:

IF A THEN …
becomes
IF (A<>0) THEN …
which becomes
ON (A<>0) + 1 THEN

P.S.: If you want some structuring, but not too much, notice that the GOSUBs in 2. and 4. may be changed to GOTOs with similar effects.

Tidbit #2: A Bug in DOS 2.0S

DOS 2.0S and OS/A+ have an improvement which allows much faster disk leads and writes. When DOS detects that a large data transfer is about to take place, it drops into what is called Burst I/O Mode. However, when a file is opened for update (OPEN #1,12,…). burst I/O should not take place. DOS handles update writes correctly, but will often blow it on update reads. The following two, one-byte patches may be made and then DOS should be re-written to the disk (with INIT under OS/A+, with menu option “H” under Atari DOS 2.0S). Caution: do not apply these patches to any other versions of DOS!

from BASIC:         from DEBUG:
POKE 2596,144       C A24<90
                    C AD5<1F
POKE 2773,31        ('C is the Change command
                    in BUG.)

Tidbit #3: Clearing Memory (Revisited)

My thanks to Jerry White for permission to share his ideas on this with you. This concept is actually the result of a series of coincidences. Coincidence #1: a zero byte in screen memory is displayed as a space on the screen (not true on most machines, where $20—decimal 32—is the space character). Coincidence #2: the Atari CLEAR-SCREEN character (SHIFT-CLEAR or CTRL-CLEAR) is not subjected to most of the cursor range checks that other characters must go through. Coincidence #3: the code to clear the screen doesn’t just clear one line 24 times (as does, for example, the Apple II’s code); instead it simply starts at what it thinks is the lowest address being displayed and continues to the top of memory.

By now, it should be obvious that we are going to let the Atari’s CLEAR-SCREEN character do all the work for us. The only thing we must do is fool it into believing that the “screen” is where we want it and is the size we want it.

CLEAR-SCREEN starts clearing at the location pointed to by $58 (88 decimal) and continues until one-byte short of the page pointed to by $6A (106 decimal). That is, it always stops clearing at location $xxFF, where xx is one less than the contents of $6A. So our memory clear program fragment looks something like this:

  LOWADDR=????:REM the lowest address to clear
  HIADDR=????:REM the highest address to clear
                !! must end on xxFF boundary !!
* SVLOW1=PEEK(88):SVLOW2=PEEK(89)
  SVHI=PEEK(106)
  POKE 106,INT((HIADDR+1)/256)
  TEMP=INT(LOWADDR/256):POKE 89,TEMP
  POKE 88,LOWADDR-256*TEMP
  PRINT CHR$(125);:REM this does the actual clear
  POKE 106,SVHI
* POKE 88,SVLOW1:POKE 89,SVLOW2

Some cautions are in order (as usual): 1) The screen editor thinks that it really has cleared the screen and homed the cursor. For safety’s sake, it is probably best to follow that code fragment with either a GRAPHICS statement or a real screen clear. 2) Since you can only specify the high (ending) address to the nearest page boundary, you have to be careful you aren’t wiping something else out.

For once, though, caution number (1) has a good side effect. If you follow the program fragment given above with a GRAPHICS statement, then locations 88 and 89 are going to get recalculated anyway! So the lines marked with asterisks may be omitted in such cases.

P.S.: If you have BASIC there is a much easier method, related to the way strings may be cleared. Given that you know LOWADDR and HIADDR, as in the fragment given above, you may clear the area via the following:

poke lowaddr,0:move lowaddr,lowaddr+1,hiaddr-lowaddr (And, wouldn’t you know it, another caution: the system gets very unhappy if hiaddr=lowaddr.)

Next month, we will teach you how to have your Atari take over the entire Bell telephone system. All you need is 37 billion dollars, 25000 miles of #0000 gauge copper wire, and a toothpick. Caution: if you try this you musssssssssssssssssttttttt tabbbbbbbbbbbbbeeeeee suuuuuurrrrrr asdf asetasyghxvnbaer6q23uqerngt1357 etaoin shrdlu

Program 1.
0000        0990        .PAGE "  EQUATES"
            1000 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1010 ;
            1020 ; EASYKEY  --
            1030 ;   A program to ease repetitive typing
            1040 ;   when using Atari BASIC, etc.
            1050 ;
            1060 ; Written by Bill Wilkinson
            1070 ;            Optimized Systems Software
            1080 ;
            1090 ; for the May, 1982, issue of COMPUTE!
            1100 ;
            1110 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1120 ;
            1130 ;
            1140 ; Equates to subroutines, etc., located
            1150 ; in the Atari OS ROMs
            1160 ;
            1170 ; CAUTION: these equates are for the
            1180 ;   revision 'A' ROMs.
            1190 ;
FCB3        1200 SWAP   =    $FCB3
FA88        1210 ERANGE =    $FA88
006B        1220 BUFCNT =    $6B
0054        1230 ROWCRS =    $54
0055        1240 COLCRS =    $55
006C        1250 BUFSTR =    $6C
F6E2        1260 KGETCH =    $F6E2
004C        1270 DSTAT  =    $4C
02FB        1280 ATACHR =    $2FB
009B        1290 CR     =    $9B
F66E        1300 EGETC2 =    $F66E
F67C        1310 EGETC3 =    $F67C
0063        1320 LOGCOL =    $63
F90A        1330 BELL   =    $F90A
F6AD        1340 DOSS   =    $F6AD
F634        1350 RETUR1 =    $F634
            1360 ;
            1370 ;
031A        1380 HATABS =    $31A
02E7        1390 SYSTEMLOMEM = $2E7
0000        1490        .PAGE
            1500 ;
            1510 ; EQUATES UNIQUE TO THIS ROUTINE
            1520 ;
0022        1530 QUOTE  =    $22       ; The " character
0000        1540 NUL    =    0         ; A nul...which becomes a CR
            1550 ;
00E6        1560 ZTEMP1 =    $E6       ; shared with fltg pt routines
            1570 ;
00FF        1580 LOW    =    $FF
0100        1590 HIGH   =    $100
0080        1600 KQUIT  =    $80       ;MSBit says quit
            1800 ;
            1810 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1820 ;
            1830 ; CHOOSE THIS ORIGIN TO FIT YOUR SYSTEM !!
            1840 ;
            1850 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1860 ;
1F00        1900 ORIGIN =    $1F00
2200        1910 NEWLOMEM =  ORIGIN+$300
0000        1920        *=   ORIGIN
1F00        1990        .PAGE "        Hooking the driver into OS"
            2000 ;
            2010 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2020 ;
            2030 ; The 'HOOKUP' routine --
            2040 ;
            2050 ; this portion of the code simply hooks
            2060 ; our replacement driver into the E:
            2070 ; handler vector table.
            2080 ;
            2090 ;
            2100 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            2110 ;
            2120 HOOKUP
1F00 A200   2130        LDX  #0        ; start at beginning
            2140 LOOKLOOP
1F02 BD1A03 2150        LDA  HATABS(X
1F05 C945   2160        CMP  #'E       ; looking for the E: driver
1F07 F007   2170        BEQ  EFND      ;got it
1F09 E0     2180        INX
1F0A E8     2190        INX
1F0B E8     2200        INX            ; to next name
1F0C D0F4   2210        BNE  LOOKLOOP  ; keep looking
1F0E F0F0   2220        BEQ  HOOKUP    ; actually, this is fatal error
            2230 ;
            2240 ; found the E driver
            2250 EFND
1F10 BD1B03 2260        LDA  HATABS+1,X ; get LSB of addr
1F13 85E6   2270        STA  ZTEMP1
1F15 BD1C03 2280        LDA  HATABS+2,X ; and MSB
1F18 85E7   2290        STA  ZTEMP1+1
1F1A A9C7   2300        LDA  #NEWETBLS&LOW
1F1C 9D1B03 2310        STA  HATABS+1,X ; replace with our table
1F1F A91F   2320        LDA  #NEWETBL/HIGH
1F21 9D1C03 2330        STA  HATABS+2,X
            2340 ;
1F24  A00F  2350        LDY  #15       ; all bytes of table
            2360 EMVLP
1F26 B1E6   2370        LDA  (ZTEMP1),Y ; get a byte of old tbl
1F28 99C71F 2380        STA  NEWETBL,Y ; to our table
1F2B 88     2390        DEY
1F2C 10F8   2400        BPL  EMVLP     ; and do more
            2410 :
            2420 ; E: handler vector table is moved
            2430 ;
1F2E A942   2440        LDA  #NEWEGETCH-1&LOW
1F30 8DCB1F 2450        STA  NEWETBLGC ? change the get character ptr
1F33 A91F   2460        LDA  #NEWEGETCH-1/HIGH
1F35 8DCC1F 2470        STA  NEWETBLGC+1
            2480 ;
            2500 ; tables, etc. are corrected
            2510 ;
            2520 ; move lomem
            2530 ;
1F38 A900   2540        LDA  #NEWLOMEM&LOW
1F3A 8DE702 2550        STA  SYSTEMLOMEM
1F3D A922   2560        LDA  #NEWLOMEM/HIGH
1F3F 8DE802 2570        STA  SYSTEMLOMEM+1
1F42 60     2580        RTS
1F43        2990        .PAGE "        The replacement E: driver"
            3000 ;
            3010 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            3020 ;
            3030 ; Begin the actual E: replacement routine
            3040 ;
            3050 ; This routine replaces E:'s get-a-character
            3060 ;   entry point.
            3070 ;
            3080 ;
            3090 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            3100 ;
            3110 NEWEGETCH
1F43 20B3FC 3120        JSR  SWAP      ;<< lines with comments starting
1F46 2088FA 3130        JSR  ERANGE    ;<< with "<<" are copied from
1F49 A56B   3140        LDA  BUFCNT    ;<< without change from ROM
            3150
1F4B D026   3160        BNE  GOEGETC3  ; was just EGETC3
            3170  ;
1F4D A554   3190        LDA  ROWCRS    ;<<
1F4F 856C   3200        STA  BUFSTR    ;<<
1F51 A555   3210        LDA  COLCRS    ;<<
1F53 856D   3220        STA  BUFSTR+1  ;<<
            3230 EGETC1
1F55 20791F 3240        JSR  NEWKGETCH ; What we really wanted to replace
1F58 844C   3250        STY  DSTAT     ;<<
1F5A ADFB02 3260        LDA  ATACHR    ;<<
1F5D C99B   3270        CMP  #CR       ;<<
            3280 ;
1F5F F015   3290        BEQ  GOEGETC2  ; was just EGETC2
            3300 ;
1F61 20ADF6 3310        JSR  DOSS
1F64 20B3FC 3320        JSR  SWAP      ;<<
1F67 A563   3330        LDA  LOGCOL    ;<<
1F69 C971   3340        CMP  #113      ;<<
1F6B D003   3350        BNE  EGETC6    ;<<
1F6D 200AF9 3360        JSR  BELL      ;<<
            3370 EGETC6
1F70 4C551F 3380        JMP  EGETC1    ;<<
            3390 ;
            3400 GOEGETC3
1F73 4C7CF6 3410        JMP  EGETC3    ;becuz branch can't get there
            3420
            3430 GOEGETC2
1F76 4C6EF6 3440        JMP  EGETC2    ;becuz branch can't get there
            3450 ;
1F79        3990        .PAGE "        The replacement KGETCH routine"
            4000 ;
            4010 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            4020 ;
            4030 ; This is the routine that replaces the
            4040 ; ROM-based KGETCH (Keyboard GET Character)
            4050 ; routine    This routine is designed
            4060 ; especially for NEWEGETCH, but could be
            4070 ; called    in place of KGETCH if the user
            4080 ; wished.
            4090 ;
            4100 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            4110 ;
            4120 NEWKGETCH
1F79 ADC61F 4130        LDA  KPTR+1    ; msb of ptr to multikey string
1F7C F024   4140        BEQ  NOSTRING  ; but no string, so normal KGETCH
1F7E 85E7   4150        STA  ZTEMP1+1  ; need to put it in zero page
1F80 ADC51F 4160        LDA  KPTR      ; ditto the LSB
1F83 85E6   4170        STA  ZTEMP1
1F85 EEC51F 4180        INC  KPTR      ; bump ptr for next time
1F88 D003   4190        BNE  NEWK1
1F8A EEC61F 4200        INC  KPTR+1    ; MSB also if needed
            4210 NEWK1
            4220 ;
1F8D A000   4230        LDY  #0
1F8F B1E6   4240        LDA  (ZTEMP1),Y ; get next char in string
1F91 1003   4250        BPL  NEWK2     ; last char has MSB on
1F93 8CC61F 4260        STY  KPTR+1    ; so reset pointer & flag
            4270 NEWK2
1F96 297F   4280        AND  #$7F      ; ensure only 7 bits
1F98 D002   4290        BNE  NEWK3     ; except...
1F9A A99B   4300        LDA  #CR       ; null becomes CR
            4310 NEWK3
1F9C 8DFB02 4320        STA  ATACHR    ; this is what KGETCH does
1F9F 4C34F6 4330 GORETURN JMP RETUR1   ; seems silly, but it works
            4340 ;
            4350 ; no string waiting for us...get a real key
            4360 ;
            4370 NOSTRING
1FA2 20E2F6 4380        JSR  KGETCH    ; a real key
1FA5 844C   4390        STY  DSTAT     ; why??? just in case
1FA7 C900   4400        CMP  #0        ; control-comma, the heart
1FA9 F017   4410        BEQ  REALCTL   ; handled special
1FAB C91B   4420        CMP  #27       ; one more than control-Z
IFAD B0F0   4430        BCS  GORETURN  ; a regular key
            4440 ;
            4450 ; if here, we have control-A through control-Z
            4460 ;
1FAF 0A     4470        ASL  A
1FB0 A8     4480        TAY            ; doubled value used as index
1FB1 B9FE1F 4490        LDA  STABLE-2,Y ; -2 becuz control-A is
1FB4 8DC51F 4500        STA  KPTR      ; value of 1 instead of zero
1FB7 B9FF1F 4510        LDA  STABLE-1,Y
1FBA 8DC61F 4520        STA  KPTR+1    ; MSB of addr of string
1FBD F0E0   4530        BEQ  GORETURN  ; oops...no string for this key
            4540
1FBF 4C791F 4550        JMP NEWKGETCH  ; and pass back first char of string
            4560
            4570
            4580 ; special handling for control-comma !!!!
            4590
            4600 REALCTL
1FC2 4CE2F6 4610        JMP KGETCH     ; it is used as an escape key
            4620 ;                     ; to allow real control chars !
            4630 ;
            4900 ;
            4910 ; MISCELLANEOUS RAM USAGE
            4920 ;
1FC5 0000   4930 KPTR   .WORD 0        ;master ptr to next char
            4940
1FC7 0000   4950 NEWETBL .WORD 0,0     ;to be filled in
1FC9 0000
1FCB 0000   4960 NEWETBLGC .WORD 0,0,0,0,0,0
1FCD 0000
1FCF 0000
1FD1 0000
1FD3 0000
1FD5 0000
            4970 ;
1FD7        4990        .PAGE "        The pointer table"
            5000 ;
            5010 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            5020 ;
            5030 ; The strings and String TABLE
            5040 ;
            5050 ; Each string in use has its address placed
            5060 ; in the corresponding location in STABLE
            5070 ; Any control character which should not be
            5080 ; translated should have $0000 as its
            5090 ; string address in STABLE
            5100 ;
            5110 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            5120 ;   (first, get on even boundary)
1FD7        5130        *=   ORIGIN+$100
            5140 STABLE
2000 0000   5150        .WORD SA,SB,SC,SD,SE,SF,SG,SH,SI
2002 0000
2004 0000
2006 3420
2008 0000
200A 0000
200C 0000
200E 0000
2010 0000
]***ERROR - 5-UNDEFINED
2012 0000   5160        .WORD SJ,SK,SL,SM,SN,SO,SP,SQ,SR
2014 0000
2016 0000
2018 0000
201A 0000
201C 0000
201E 7F20
2020 0000
2022 0000
}***ERROR - 5-UNDEFINED
2024 8620   5170        .WORD SS,ST,SU,SV,SW,SX,SY,SZ
2026 0000
2028 0000
202A 0000
202C 0000
202E 0000
2030 0000
2032 0000
}***ERROR - 5-UNDEFINED
2034        5990        .PAGE "      The soft key strings"
            6000 ;
            6010 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            6020 ;
            6030 ; The actual strings
            6040 ;
            6050 ; we need supply strings only for
            6060 ; the desired softkeys
            6070 ;
            6080 ; Note that inverse video is not
            6090 ; allowed, except that the last
            6100 ; character of a string has its
            6110 ; MSBit on, same as inverse video
            6120 ;
            6130 ; Note that zero bytes ('NUL') get
            6140 ; converted to RETURN ($9B) characters
            6150 ;
            6160 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            6170 ;
            6200 SD     .BYTE "CLOSE #7:OPEN #7,6,0,"
            6210        .BYTE QUOTE,"D:*.*",QUOTE
            6220        .BYTE ":FOR I=0 TO 256:GET #7,I"
            6230        .BYTE ":PRINT CHR$(I);:NEXT I"
            6240        .BYTE NUL+KQUIT
            6250
            6260 SP     .BYTE "PRINT ",KQUIT+'#
            6270 ;
            6280 SS     .BYTE "SETCOLO",KQUIT+'R
            6290 ;
            6300 ; Note the methods of getting quoted strings
            6310 ; in .BYTE statements and turning on the
            6320 ; MSBits of a character
            7000 ;
            7010 ; finally, we set up for LOAD-AND-GO
            7020 ;
            7030        *= $2E0
            7040        .WORD  HOOKUP
            7050 ;
            9999        .END
Program 2.
1000 REM *******
1010 REM *
1020 REM * EASYLOAD --
1030 REM * A BASIC PROGRAM THAT ALLOWS
1040 REM * THE USER TO MAKE HIS/HER OWN
1050 REM * SET OF "SOFTKEYS" FOR USE
1060 REM * WITH THE "EASYKEY" PROGRAM
1070 REM *
1080 REM *******
1090 REM
1100 REM :BEFORE RUNNING THIS PROGRAM, BE SURE
1110 REM :THAT "EASYKEY" HAS BEEN LOADED AND
1120 REM :RUN.  YOU MAY VERIFY THIS BY CHECKING
1130 REM :THE VALUE OF PEEK(744) -- IT SHOULD
1140 REM :MATCH THE NEWLOMEM PAGE VALUE IN
1150 REM :THE LISTING OF EASYKEY
1160 REM :[ PEEK(744) = 34 IF EASYKEY IS
1170 REM :ASSEMBLED AS GIVEN HERE ]
1180 REM
1190 REM == FIRST, SET UP ADDRESSES, ETC. ==
1200 STABLE=8192:REM STABLE = $2000
1210 STRINGS=STABLE+(2*26)
1220 REM
1230 DIM KEY$(130)
1240 REM
1250 REM FIRST, CLEAR OUT OLD SOFTKEY DEFINITIONS
1260 REM
1270 FOR ADDR=STABLE TO STRINGS-1:POKE ADDR,0:NEXT ADDR
1280 REM
1290 PRINT CHR$(125);
1300 PRINT "When prompted, enter a softkey string"
1310 PRINT "for the given control-key."
1320 PRINT
1330 PRINT "Remember: no inverse keys and use"
1340 PRINT "control-comma (the heart) to request"
1350 PRINT "a RETURN key code. Actual use of the"
1360 PRINT "Return key terminates the string for"
1370 PRINT "the given softkey."
1300 PRINT :PRINT "[just RETURN will undefine that key]"
1390 PRINT :PRINT
1400 ADDR=STRINGS:REM WHERE WE START STORING SOFTKEY STRINGS
2000 REM MAIN LOOP
2010 FOR KEY=0 TO 25
2020   PRINT "ConTRol- ";CHR$(65+KEY);:INPUT KEY$
2030   KLEN=LEN(KEY$):IF NOT KLEN THEN 2290
2040   KEY$(KLEN)=CHR$(ASC(KEY$(KLEN))+128):REM SET MSB OF LAST CHARACTER
2050   IF ADDR+KLEN>=STABLE+512 THEN PRINT "too much defined!";CHR$(253):END
2070   REM SET ADDRESS OF STRING IN STABLE
2080   TEMP=INT(ADDR/256):POKE STABLE+KEY+KEY+1,TEMP
2090   POKE STABLE+KEY+KEY,ADDR-256*TEMP
2100   FOR KPT=1 TO KLEN
2110     POKE ADDR,ASC(KEY$(KPT))
2120     ADDR=ADDR+1
2130     NEXT KPT
2200   REM END OF PROCESSING FOR THAT KEY
2290 NEXT KEY
3000 REM ********************************
3010 REM
3020 REM NOW WE ALLOW YOU TO SAVE THIS NEWLY DEFINED SET ON DISK
3030 REM
3040 REM SIMPLY HIT BREAK AT THE QUESTION IF YOU DON'T WANT TO SAVE
3050 REM
3100 PRINT :PRINT :PRINT "What set of softkeys should we  save"
3110 PRINT " this definition under (1-999) "?:INPUT SET
3120 IF SET<1 OR SET>999 OR SET<>INT(SET) THEN 3100
3150 KEY$="D:SOFTKEY.":KEY$(LEN(KEY$)+1)=STR$(SET)
3200 OPEN #1,8,0,KEY$
3210 FOR KPT=STABLE TO ADDR
3220 PUT #1,PEEK(KPT)
3230 NEXT KPT
3250 CLOSE #1
3280 PRINT :PRINT "==== normal end ===="
3290 END
Program 3.
30000 REM *********************************
30010 REM
30020 REM THIS PROGRAM OR PROGRAM FRAGMENT IS
30030 REM INTENDED TO BE USED TO RELOAD ONE
30040 REM THE SETS OF SOFTKEYS CREATED BY
30050 REM "EASYLOAD".
30060 REM
30100 STABLE=8192:REM STABLE=$2000
30110 DIM NAME$(20)
30200 PRINT "What set of softkeys should be"
30210 PRINT "  reloaded from disk (1-999) ";:INPUT SET
30220 NAME$="D:SOFTKEY.":NAME$(LEN(NAME$)+1)=STR$(SET)
30250 OPEN #1,4,0,NAME$
30290 TRAP 30400
30300 FOR KPT=STABLE TO STABLE+512
30310   GET #1,KEY:POKE KPT,KEY
30320   NEXT KPT
30400 CLOSE #1
30500 REM COULD RETURN FROM SUBROUTINE, IF DESIRED
COMPUTE! ISSUE 25 / JUNE 1982 / PAGE 162

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 double-sided, 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 available 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 40000
32020 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 A-register 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 VVTP+VNUM*8+3,TEMP1
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 lsb 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.

COMPUTE! ISSUE 26 / JULY 1982 / PAGE 165

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,0,junk$’ 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),QUANTITYORDERED(0)
110 OPEN #1,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 #1,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 $0A1F, 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<1F

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<0
from BASIC:  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.

COMPUTE! ISSUE 27 / AUGUST 1982 / PAGE 145

INSIGHT: Atari

Bill Wilkinson
Optimized Systems Software
Cupertino, CA

A Monthly Column

All computer users can benefit from this month’s column—many of Bill’s observations and hints are not specific to the Atari. If you’re thinking of translating a BASIC game program into machine language to achieve greater speed, you’ll find some valuable information below. For example, there’s a discussion of the “ball/boarder” problem which can be the must difficult puzzle to solve when programming certain kinds of games.

This month we return to the world of program writing. As I noted in my last column, there has been a growing demand for me to explain how to write graphics programs in assembly language. So I will begin a two or three-part series this month on converting BASIC programs to assembly language. Although the programs will be specifically written for the Atari computers, it won’t take too much imagination to convert them to Apple and Commodore machines.

The Bouncing BASIC Ball

Since we are going to try to build up this program in stages. we will start this month with the simplest possible form. Program 1 is an Atari BASIC program which bounces a “ball” around inside the rectangular screen. There is no scoring, no paddles, no sound, no players, no missiles, no intelligence.

In fact, perhaps the only thing which needs explaining is the frequent occurrence of the subexpression: INT(n*RND(0)). With Apple Integer BASIC, one could obtain the equivalent function by coding RND(n); and I have often wished that Atari had let us include that capability in the original specifications for Atari BASIC (oh, well, maybe in the next version of BASIC A+?). Anyway, the idea is to produce an integer random number in the range of 0 to n-1, inclusive.

So now let’s examine the program as a whole. (First, a contment: I have used the convention that X means “horizontal” and Y means “vertical.” This can have some strange implications. See below.) We start by establishing the least detailed graphics mode (which is, incidentally, roughly equivalent to Apple’s LO-RES mode). Then we set both of the variables XMOVE and YMOVE to a random number in the range -2 to +2, inclusive. (Do you see how? ‘INT(5*RND(0))’ gives a number from zero to four, inclusive, and we then subtract two from it.) But we don’t allow both values to be zero (line 400). (In a real “Pong” type game, you wouldn’t want the X-motion to ever be zero. Here, allowing XMOVE to be zero is instructive.)

We then give the ball a starting position with X in the range of 0 to 39 and with Y from 0 to 19. Note that both the current position (X and Y) and the to-be-made-current position (XNEW and YNEW) are set equal. This is simply to get things started evenly. Line 900 resets the system timer. (You will have to do something differently here if you are using an Apple.)

The main loop is almost as simple. First, we erase (COLOR 0) the old “ball” (note that we are erasing nothing if this is the first time through the loop). Then we PLOT the new ball with a convenient, visible color (COLOR 2). We update our current ball position (line 1300) and also our to-be-made-current position (line 1400).

It Gets A Bit Difficult

Here is where it begins to get tricky. If the ball will be at or beyond the edge(s) of the screen, we must reverse its movement, as appropriate (lines 1500 and 1700). But suppose that the movement has already carried it beyond the screen bounds; we must then bring it back inbounds (lines 1600 and 1800). Finally, for this simple demo, we simply do this loop until the clock ticks (4.26 seconds, roughly) and then start all over.

Even ignoring the limited goals of this program, there are a few significant flaws: (1) There is no visible border around the screen to tell you when and where the ball will “hit.” (2) There are no sound effects. (3) The ball isn’t round (or even remotely so). (4) Sometimes, the ball rebounds without hitting the wall. I am going to leave (1) and (2) for next time, and (3) can’t really be changed without using player-missile graphics. But flaw (4) is an interesting one, and worth some discussion.

The problem lies in the basic algorithm I chose for moving the ball: the X and Y movements can range from -2 to +2 units, independently, and I move the ball each time in both X and Y according to the current movement factors (XMOVE and YMOVE). Let’s take an example: suppose that the XMOVEment is zero and the YMOVEment is -2. And further suppose that the ball is currently at Y position +1 (one square from the edge of the screen). If I allow the ball to move to the new Y position determined by Y and YMOVE (YNEW=Y+YMOVE in line 1400), then it will be off the screen (YNEW will be -1). What to do?

One solution might be to pretend we have absorbent walls (IF YNEW<0 THEN YNEW=0). This will work, but will give strange flight paths for the ball. The solution I chose was to imagine that the ball hit the wall smack in the middle the two times I chose to make it visible. (Imagine: the ball is displayed at Y position +1. One-half of a time-tick later, it hits the wall and rebounds. Another one-half of a time-tick later, it has rebounded back out to Y position +1. We thus display it again at position +1, since we are displaying only at integral time-ticks.) This choice is reflected in the programming in lines 1600 and 1800.

Of course, all “motion” via a computer is no more true motion than is a motion picture or a television picture. In truth, you are simply seeing a series of still pictures flashed in front of your eyes so quickly that your brain perceives the result as motion. Thus, there is nothing inherently wrong with my solution. Except that, from BASIC, the time between pictures is so long that even my lazy brain can sometimes clearly see that the ball didn’t touch the wall. (Notice that if XMOVE is zero, so that we have only vertical ball movement, the effect is even easier to see.)

Can we do better? From BASIC, probably not. From assembly language, probably yes. If we choose a different algorithm, a different graphics mode, or make the pictures change faster, maybe we can give better illusions of motion. But that will wait for next time. This month, we will simply recode our BASIC routine in assembly language.

Having A Ball With Assembly Language

First note that the BASIC line numbers have been preserved, with line 100 in the assembly code having the label LINE100 and being followed, on line 101, with a remark containing the BASIC source for that line. (If you want to make your listings neat and readable, you might try the trick I used here: I placed a control-J [an ASCII line-feed character] both before and after the BASIC source. It can make your listing much more readable.)

Also note the inclusion of my graphics subroutines from the February issue of COMPUTE! (Issue #21). I have added a RaNDom function, to make the mainline code easier and more compatible with the BASIC original, Even if you choose not to type in the mainline assembly language this month, you should type in and preserve these routines. Or simply add RND to the listing you typed in from February (you did type all that in, of course). We will use these same routines in the later articles in this series, but the listing will not be repeated.

As much as possible, the assembly language is self-explanatory, especially when coupled with the BASIC source. For example, what could be clearer than the translation of “GRAPHICS 3” into “LDA #3” and “JSR GRAPHICS”? If you don’t understand why this works, you really need to get a good introductory book and read upon 6502 assembly language. For those of you into such things, you might note that when we convert from BASIC to assembly language, we tend to convert expressions by using reverse Polish notation. Thus, for example, line 300’s assembly language equivalent might be expressed in “pidgin-HP” (that is, in a parody of the keyboard language used In HP reverse Polish calculators) as something like this: 4 RND 2 - ENTER xmove STORE And those you into FORTH will presumably also see the obvious corollaries.

The assembly language coding here is not the best nor the most efficient. For example, lines 110 through 430 could be replaced by a simple “ORA XMOVE” (because the A-register already contains YMOVE and because we don’t really need the sum to find out if the two values are both zero). Rather, the idea here was to do as straightforward a translation as possible, allowing more of you to understand how simple assembly language can be.

Are there any tricky spots in the code? Not really. Though, if you are like me, you will have to pause each time you use a CMP and figure out if you really want BCS or BCC (or whether you also need a BEQ or…). Again, some of the CMP’s could have been made simpler (for example, by using ‘CMP #40’ on line 1630 and omitting line 1640). And, again. I opted for consistency with the BASIC program.

The program does work. Try it. It took me about three hours to type it in and debug it (including about an hour of debugging the debugger). This represents much less time than it would have taken if I had not had the BASIC program as a working model. You might omit lines 1930 to 1980 the first time you run it. I won’t tell you what will happen, but I will tell you that the lines are used to synchronize ball movement with the clock.

On Assembling And Debugging

You may have noted that the master origin (‘*=’) for this program is at $3000. If you use that origin and don’t do anything special, assembling the program will wipe out the source code and kablooey! What can you do? Personally, I prefer to direct the object code to disk when I assemble. (I usually use ‘ASM ,#R:,#D:file.OBJ’ where “file” is the same name as the source file and I use “R:” because I list to a DIABLO or DEC serial printer.) Then, with the source also safely LISTed to disk, I can use NEW and reLOAD the object and proceed to run and debug it. Using this method, it makes sense to place the origin somewhere fairly high in EASMD’s (or the Assembler/Editor’s) working memory.

An alternative method is to keep the object code in memory below all my source listing. With EASMD this is easy to do. For example, with this program, I simply used a ‘LOMEM 3800’ command to tell EASMD not to use any memory below $3800. With the Assembler/Editor cartridge, it is almost as easy: simply use BUG to issue “C2E5<00,38” and then “G A000”. ($02E5 is system LOMEM, which the Assembler picks up and uses for its own when it is coldstarted at $A000.) In both instances, make sure you have LISTed off any program in memory before changing the LOMEM bound, since it is the occurance of NEW which forces the change.

Actually. I often use both of the above measures. And even then I can run into problems. When I was working on this month’s program, for example, I could assemble and then load the program fine. But when I went to use “G3000” from BUG, the system looped madly. I’m still trying to figure out why, but I solved it by loading the OBJect file from the operating system and then reentering the Assembler via a cold start. BUG then worked fine. I hope that by next month I will have figured out the reason for this strange behavior and will report a fix to you. (To be fair, I am using a very early pre-release version of the cartridge…perhaps you won’t have this problem.

Breakpoint Setting

Possibly the biggest fault of BUG (both versions) is the lack of easy breakpoint capabilities. Changing instructions to BRKs ($00) and back often gets so tiresome that I tend to say the heck with it and try out an otherwise unchecked portion of code. When I’m lucky, it all works. When I’m not, I turn off the power and start again. Thank goodness I’m not trying to do this with just a cassette. The corollary? If you are using a cassette-only system, proceed with utmost caution and take the trouble to set lots of breakpoints.

That’s about it for this month. Next month we will add several complications to the bouncing ball program. We will also explore some news, trivia, and gossip. And, whatever you do, don’t believe everything that people say about the Atari and Atari BASIC: we may have some surprising benchmarks for you.

Program 1. Simple Bouncing Ball Program
100 GRAPHICS 3
200 XMOVE=INT(5*RND(0))-2
300 YMOVE=INT(5*RND(0))-2
400 IF XMOVE+YMOVE=0 THEN 200
500 X=INT(40*RND(0))
600 Y=INT(20*RND(0))
700 XNEW=X:YNEW=Y
900 POKE 19,0:POKE 20,0:REM RESET TIMER
1000 REM LOOP STARTS HERE
1100 COLOR 0:PLOT X,Y
1200 COLOR 2:PLOT XNEW,YNEW
1300 X=XNEW:Y=YNEW
1400 XNEW=X+XMOVE:YNEW=Y+YMOVE
1500 IF XNEW<=0 OR XNEW>=39 THEN XMOVE=-XMOVE
1600 IF XNEW<0 OR XNEW>39 THEN XNEW=X
1700 IF YNEW<=0 OR YNEW>=19 THEN YMOVE=-YMOVE
1800 IF YNEW<0 OR YNEW>19 THEN YNEW=Y
1900 IF PEEK(19)=0 THEN 1000
2000 RUN
Program 2. Bouncing Ball Initialization

0000        20  .PAGE  " initialization"
            30 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            40 ;
            50 ; A SIMPLE BOUNCING BALL PROGRAM
            60 ;
            70 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            80 ;
0000        90  *=  $3000
            0100 LINE100
            0101 ;>>>
                GRAPHICS 3

3000 A903   0110        LDA  #3
3002 20E830 0120        JSR  GRAPHICS
            0197 ;
            0198 ;;;;;;;
            0199 ;
            0200 LINE200
            0201 ;>>>
                XMOVE=INT(5*RND(0))-2
3005 A904   0210        LDA  #4
3007 205731 0220        JSR  RND       ; GET RANDOM NUMBER FROM 0 TO 4
300A 38     0230        SEC
300B E902   0240        SBC  #2        ; NOW IS RANDOM FROM -2 TO +2
300D 8DE230 0250        STA  XMOVE     ; AS IN BASIC PROGRAM
            0297 ;
            0298 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            0299 ;
            0300 LINE300
            0301 ;>>>
                YMOVE=INT(5*RND(0))-2
3010 A904   0310        LDA  #4
3012 205731 0320        JSR  RND       ; GET RANDOM NUMBER FROM 0 TO 4
3015 38     0330        SEC
3016 E902   0340        SBC  #2        ; NOW IS RANDOM FROM -2 TO +2
3018 8DE330 0350        STA  YMOVE     ; AS IN BASIC PROGRAM
            0397 ;
            0398 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            0399 ;
            0400 LINE400
            0401 ;>>>
                IF XMOVE+YMOVE=0 THEN 200

301B ADE230 0410        LDA  XMOVE
301E 18     0420        CLC
301F 6DE330 0430        ADC  YMOVE     ; XMOVE + YMOVE
3022 F0E1   0440        BEQ  LINE200   ; IF = 0 THEN 200

            0497 ;
            0498 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            0499 ;
            0500 LINE500
            0501 ;>>>
                X=INT(40*RND(0))

3024 A927   0510        LDA  #39
3026 205731 0520        JSR  RND       ; GET RANDOM NUMBER FROM 0 TO 39
3029 8DBE30 0530        STA  X         ; AND KEEP IT
            0597 ;
            0599 ;
            0600 LINE600
            0601 ;>>>
                Y=INT(20*RND(0))
302C A913   0610        LDA  #19
302E 205731 0620        JSR  RND        ; GET RANDOM NUMBER FROM 0 TO 19
3031 8DBF30 0630        STA  Y          ; AND KEEP IT
            0697 ;
            0698 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            0699 ;
            0700 LINE700
            0701 ;>>>
                XNEW=X:YNEW=Y
3034 ADDE30 0710        LDA  X
3037 8BE030 0720        STA  XNEW      ; XNEW = X
303A ADDF30 0730        LDA  Y
303D 8DE130 0740        STA  YNEW      ; YNEW = Y

            0897 ;
            0898 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            0899 ;
            0900 LINE900
            0901 ;>>>
                POKE 19,0:POKE 20,0
3040 A900   0910        LDA  #0
3042 8513   0920        STA  19
3044 8514   0930        STA  20        ; DON'T NEED TO DO LDA #0 TWICE
            0997 ;
            0998 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            0999 ;
            1000 LINE1000
            1001 ;>>>
                REM LOOP STARTS HERE
            1097 ;
            1098 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1099 ;
            1100 LINE1100
            1101  ;>>>
                COLOR 0:PLOT X,Y
3046 A900   1110        LDA  #0
3048 201531 1120        JSR  COLOR
304B AEDE30 1130        LDX  X
304E ACDF30 1140        LDY  Y         ; LOAD VALUES FOR SUBROUTINE CALL
3051 202031 1150        JSR  PLOT
            1197 ;
            1198 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1199 ;
            1200 LINE1200
            1201 ;>>>
                COLOR 2:PLOT XNEW,YNEW
3054 A902   1210        LDA  #2
3056 201531 1220        JSR  COLOR
3059 A900   1230        LDA  #0        ; (NEEDED FOR PLOT)
305B AEE030 1240        LDX  XNEW
305E ACE130 1250        LDY  YNEW
3061 202031 1260        JSR  PLOT
            1297 ;
            1298 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1299 ;
            1300 LINE1300
            1301 ;>>>
                X=XNEW:Y=YNEW
3064 ADE030 1310        LDA  XNEW
3067 8DDE30 1320        STA  X
306A ADE130 1330        LDA  YNEU
306D 8DDF30 1340        STA  Y
            1397 ;
            1398 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1399 ;
            1400 LINE1400
            1401 ;>>>
                XNEW=X+XMOVE:YNEW=Y+YMOVE
3070 ADDE30 1410        LDA  X
3073 18     1420        CLC
3074 6DE230 1430        ADC  XHOME
3077 8DE030 1440        STA  XNEW
307A AD0F30 1450        LDA  Y
307D 18     1460        CLC
307E 6DE330 1470        ADC  YMOVE
3081 8DE130 1480        STA  YNEW
            1497 ;
            1498 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1499 ;
            1500 LINE1500
            1501 ;>>>
                IF XNEW<=0 OR XNEW>=39 THEN XMOVE=-XMOVE
3084 ADE030 1510        LDA  XNEW
3087 3006   1515        BMI  THEN1500  ;XNEW < 0
3089 F004   1520        BEQ  THEN1500  ;XNEW = 0
308B C927   1525        CMP  #39
308D 9009   1530        BCC  LINE1600  ;XNEW NOT >= 39
            1550 THEN1500
308F A900   1555        LDA  #0
3091 38     1560        SEC
3092 EDE230 1565        SBC  XMOVE     ;GET 0 - XMOVE
3095 8DE230 1570        STA  XMOVE     ; TO XMOVE
            1597 ;
            1598 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1599 ;
            1600 LINE1600
            1601 ;>>>
                IF XNEW<0 OR XNEW>39 THEN XNEW=X
3098 ADE030 1610        LDA  XNEW
309B 3006   1620        BMI  THEN1600  ;XNEW < 0
309D C927   1630        CMP  #39
309F F008   1640        BEQ  LINE1700  ;XNEW = 39
30A1 9006   1650        BCC  LINE1700
            1660 THEN1600
30A3 ADDE30 1670        LDA  X
30A6 8DE030 1680        STA  XNEW      ; XNEW = X
            1697 ;
            1698 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1699 ;
            1700 LINE1700
            1701 ;>>>
                IF YNEW<=0 OR YNEW>=19 THEN YMOVE=-YMOVE
30A9 ADE130 1710        LDA  YNEW
30AC 3006   1715        BMI  THEN1700  ;YNEW < 0
30AE F004   1720        BEQ  THEN1700  ;YNEW = 0
30B0 C913   1725        CMP  #19
30B2 9009   1730        BCC  LINE1800  ;YNEW NOT >= 19
            1750 THEN1700
30B4 A900   1755        LDA  #0
30B6 38     1760        SEC
30B7 EDE330 1765        SBC  YMOVE     ;GET 0 - YMOVE
30BA 8DE330 1770        STA  YMOVE     ; TO YMOVE
            1797 ;
            1798 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1799 ;
            1800 LINE1800
            1801 ;>>>
                IF YNEW<0 OR YNEW>39 THEN YNEW=Y
30BD ADE130 1810        LDA  YNEW
30C0 3006   1820        BMI  THEN1800  ;YNEW < 0
30C2 C913   1830        CMP  #19
30C4 F008   1840        BEQ  LINE1900  ;YNEW = 39
30C6 9006   1850        BCC  LINE1900
            1860 THEN1800
30C8 ADDF30 1870        LDA  Y
30CB 8DE130 1880        STA  YNEW      ; YNEW = Y
            1897 ;
            1893 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1899 f
            1900 LINE1900
            1901 ;>>>
                IF PEEK(19)=0 THEN 1000
30CE A513   1910        LDA  19
3000 D009   1920        BNE  LXNE2000
            1930 ;==== LEAVE THESE 6 LINES OUT FIRST TIME =====
30D2 A514   1940        LDA  20        ; LSB OF CLOCK
            1950 CLOCKWAIT
30D4 C514   1960        CMP  20        ; CHANGED YET?
30D6 F0FC   1970        BEQ  CLOCKWAIT ; NO...WAIT
            1980 ;==== BUT ALWAYS KEEP LINE 1990 ====
30D8 4C4630 1990        JMP  LINE1000
            1997 ;
            1998 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1999 ;
            2000 LINE2000
            2001 ;>>>
                RUN
            2010 ;
            2020 ; to truly simulate RUN* we should store
            2030 ; zero to all variables. For this program
            2040 ; that is not necessary* since all variables
            2050 ; are reset in the besinnina of the program
            2060 ; anyway. In General? though* be careful
            2070 ; to check such things.
            2080 ;
30DB 4C0030 2090  JMP  LINE100
            3000 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            3010 ;
            3020 ;  !!! VARIABLES AND SUBROUTINES !!!
            3030 ;
            3040 ; There are no direct BASIC equivalents
            3050 ; for the following lines ... the BASIC
            3060 ; interpreter handles all this for you
            3070 ; automatically.
            3080 ;
            3090 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            3100 ;
            3110 ; first -- the variable declarations
            3120 ;
30DE 00     3130 X      .BYTE 0
30DF 00     3140 Y      .BYTE 0
30E0 00     3150 XNEW   .BYTE 0
30E1 00     3160 YNEW   .BYTE 0
30E2 00     3170 XMOVE  .BYTE 0
30E3 00     3180 YMOVE  .BYTE 0
            3190 ;
            3200 ; second -- the subroutines 3210 ;
            3220 ; these are mostly the same subroutines that
            3230 ; we presented in the Februaryr 1982r issue
            3240 ; though they have been modified slightly.
            3250 ; Note that SETCOLOR and LOCATE have been
            3260 ; dropped (not used in this program)
            3270 ; and a new function* RND? has been added.
            3280 ;
            3290 ;
30E4        3300  .INCLUDE #D:GRAPHICS.ASM

Equates, etc., for graphics subroutines
30E4        9000        .PAGE "Equates, etc., for graphics subroutines"
            9005 ;
            9010 ; CIO EQUATES
            9015 ;
E456        9020 CIO    =    $E456     ; Call OS thru here
0342        9025 ICCOM  =    $342      ; COMmand to CIO in IoCb
0344        9030 ICBADR =    $344      ; Buffer or filename ADdress
0348        9035 ICBLEN =    $348      ; Buffer LENgth
034A        9040 ICAUX1 =    $34A      ; AUXiliary byte # 1
034B        9045 ICAUX2 =    $34B      ; AUXiliary byte # 2
            9050 ;
0003        9055 COPEN  =   3          ; Command OPeN
000C        9060 CCLOSE  =  12         ; Command CLOSE
COMPUTE! ISSUE 28 / SEPTEMBER 1982 / PAGE 116

INSIGHT: Atari

Bill Wilkinson
Optimized Systems Software
Cupertino, CA

In addition to a continuation of the game development that I started last month, this month’s column will dive into the argument of what makes BASIC run, including a chip that makes Atari BASIC run better. But first…

FMS And Burst I/O, Yet Again

Well, July’s column was supposed to fix the mistakes I made in the May column. And then, lo and behold, I blew it again in July. On page 172 of issue 26 of COMPUTE! there is a listing of changes to be made to FMS to help burst I/O work properly in update mode. The assembly language listing and the BUG changes were correct. Unfortunately, the POKE’s from BASIC had one typo (my fault). The last POKE read POKE 2773,13 …WRONG! should be POKE 2773,31 …RIGHT!

Speed And BASIC

Personally, I have never been sure that it is necessary for an interpreted language (e.g., BASIC) to be fast. Typically, I choose to use an interpreter for ease of use and speed of debugging, for writing quickie little programs. and for creating utilities that can run at any speed at any time.

But an increasing number of people are trying to use BASIC for writing serious software, including games, utilities, and business applications. Now I maintain that the speed of BASIC is irrelevant when it is being used for utilities (who cares how fast a disk fixer-upper runs?) or business applications (the program is usually waiting for keyboard, printer, or disk I/O anyway). But for writing games and a certain category of other programs (e.g., sorts), speed is important. But then why use BASIC? Because it’s the easiest language to use? Because it can be made fast enough? Because it’s the only language the author knows?

Actually, those (and many others) are all valid reasons to choose BASIC, as long as the author doesn’t expect more than BASIC is capable of delivering. So what is BASIC capable of delivering? A lot of adequacy. After all, look at some of the very successful games that are written in BASIC (Crush, Crumble, and Chomp is the first one that comes to my mind). Or look at some games that should never have been written in BASIC and yet were (a lot come to mind, but I will refrain from naming any).

Certain other authors writing in another magazine have claimed that Atari BASIC is the slowest language ever created. My last impulse was to say, “Who cares? It is the easiest to use, and that’s more important.” But I simply couldn’t take that statement lying down, as it were. After all, if Atari BASIC is such a snail, how come all these programs seem to work just fine?

So I armed myself with five different BASIC interpreters: Applesoft, Atari BASIC, Atari Microsoft BASIC, BASIC A+, and Cromemco’s 32K Structured BASIC. Now OSS produced three of these five BASICs, so it might seem that I am prejudiced. Well…maybe a lot, but not too much. Some comments follow on what I decided to try to do.

I wanted to use a benchmark program that would, to some degree, show the fundamental speed of each BASIC. But I also wanted to see what impact such things as constants, variable names, and multi-statement lines would have. Luckily, at just about this same time, I happened upon a benchmark (as yet unpublished) which showed Atari faster than Applesoft in a very simple program. “Oh ho!” says I. “How can this be? Atari is the slowest machine ever, say certain voices,”

Anyway, I began experimenting with a small benchmark program, allowing various changes so that I could see the impact on speed. The most fundamental program was simply:

10 <start a timer>
20 A=0:B=12345.6
30 A=A+1.234567
40 IF A<B THEN 30
50 <print time used>

Obviously, the intent of this program is to cause a loop to execute 10,000 times. But what can be changed that will significantly affect the execution time without materially altering the program? Below I show all the versions of lines 20 and 30 that I tested. (Line 40 is not shown, but it followed line 20 in the naming of variables and otherwise remained unchanged.) The table also shows the times for the various languages, roundel to three significant figures.

Table 1. The Speed Matrix
Lines 20 and 30          Cromemco   Atari   Atari  BASIC A+  Apple
                         32K BASIC  BASIC   uSoft  (Atari)   soft

20 A=0:B=12345.6            37.0     72.6    270.   62.9     275.
30 A=A+1.234567                     (63.6)         (59.3)

20 A=0:B=12345.6            37.0     72.6    710.   62.9     350.
30 A=A+1.23456789                   (63.6)          (59.3)

20 A=0:B=12345.6:           37.0     73.1    56.3   63.4     50.8
   C=1.234567                       (64.1)         (59.8)
30 A=A+C

20 LONGVARIABLEA=0:   **    37.0     72.6    320.   62.9     can’t
   LONGVARIABLEB=12345.6            (63.6)         (59.3)     do
30 LONGVARIABLEA=
   LONGVARIABLEA+1.234567

20 LONGVARIABLEA=0:   **    37.0     72.6    752.   62.9     can’t
   LONGVARIABLEB=12345.6            (63.6)         (59.3)     do
30 LONGVARIABLEA =
   LONGVARIABLEA+1.23456789

20 LONGVARIABLEA=0:   **    37.0     73.1    106.   63.4     can’t
   LONGVARIABLEB=12345.6:           (64.1)         (59.8)     do
   C=1.234567
30 LONGVARIABLEA=
   LONGVARIABLEA+C

** These tests made using double precision variables in Cromenco BASIC and
   Atari Microsoft BASIC. Single precision times were shorter, but not significantly so.
() Times shown in parentheses are explained in the text.

In addition to the timings shown in Table 1, I also tried adding several variables to the programs. Adding 18 variables (in lines 11 and 12) added about five or six seconds to the Microsoft BASICs, about 1.5 seconds for Atari BASIC and BASIC A+, and nothing at all to Cromemco BASIC.

Also, I tried the effects of combining lines 30 and 40 into a single line. For example: 30 A = A + C: IF A<B THEN 30 The time savings were all in the area of one second, not surprisingly, so I have not detailed them here.

But, look at the surprises! Let’s look at the “foreigner,” Cromemco 32K BASIC, first. Nothing seems to make a difference to it! Actually, I knew that this would happen before I ran the tests. Of all the BASICs shown, Cromemco’s is the most like a compiler. I simply included it to give you an idea of what a truly properly structured interpreter can accomplish, but we must be fair and admit that the language is 26K bytes in its smallest usable incarnation.

For you Atari BASIC and BASIC A+ programmers, the happiest surprise is perhaps simply finding out that these languages do as well as they do. Also, note that the various program changes have only a small effect on the running times. So you don’t have to be too careful about how you write your programs. (But it is still true that putting subroutines and FOR/NEXT loops at the beginning of a program will make a noticeable speed difference. Don’t feel too bad: all Microsoft BASICs have this same quirk.)

And now to the Microsoft BASIC’s. Obviously, you pay a penally for using constants in a loop. Using double precision constants (1.23456789 in our examples) costs so much that you should try to avoid them. Watch for long variable names: 41 seconds to go from a one-character name to LONGVARIABLEA? Ouch! (Actually, I also tried three-character names and found the penalty there to be over seven seconds.) And there is a penalty for having lots of variable names in use. Hmmm… we need to use variable names instead of constants, because constants are so slow: but using lots of variable names costs time also, so…

How about the other side of the Microsoft coin? What can we do that will show off the Microsoft BASIC speed? Two answers: use integer variables and do some transcendental function calculations. It’s reasonably obvious why integer variables help: integer arithmetic is guaranteed to take less time than floating point. But why the transcendentals, if we just showed that the speeds are similar? Simple. I cheated. I used only addition, where the Atari BASIC floating point package shows up pretty good. But oh boy! Did we blow it when it comes to multiply! When using SIN, COS, etc.. Atari Microsoft BASIC is three to six times faster than Atari BASIC. Until now. But before I explain that “until.” let me make a few points.

Microsoft BASIC is definitely capable of more speed than Atari BASIC, but only if you are very careful and use lots of programming tricks. If you are an advanced programmer, this won’t bother you. But I still believe, as I did over three years ago when we designed Atari BASIC, that for most people (and especially for beginners and hackers like me) the ease of use that is the hallmark of Atari BASIC makes it a real standout. But of course I’m not the perfect, objective judge. So try all of the BASIC’s, if your budget can afford it, and judge for yourself.

Fast, Faster, Fastest

This section will explain that “until now” that I wrote in the next to last paragiaph. As I said, we (OSS and predecessors) blew it when it came to implementing the multiply algorithm, and as a result the transcendental routines take long enough for you to go out and get a cup of coffee. But…

Newell Industries (alias Wes Newell) of 3310 Nottingham, Piano, Texas (75074) has introduced the Fastchip. Actually, the Fastchip is a ROM which replaces the OS Floating Point ROM in an Atari 400 or 800. Major portions of the 2K bytes of ROM have been changed, resulting in several speed and/or accurracy improvements. The biggest changes were to the multiplication (ta da!) routine and floating-point to integer conversion (which is used all the time, by GOTO, POKE, SETCOLOR, XIO, OPEN, and many, many other statements and functions).

I have said that I will not normally review software, but I think the Fastchip deserves an exception to this rule on two points: it can he considered hardware. and it is a must for anyone contemplating heavy math usage with an Atari. Just as an example, note the times in parentheses in Table 1. These times are those recorded with a Fastchip installed. And this in a benchmark which does not make heavy use of FastChip’s best features!

Newell Industries has done some fairly complete timings of the various routines, so I won’t belabor that point here. I will, however, include my own small benchmark program, just to give you an idea of the improvements available.

As you will note, I have included the Microsoft timings, also. Quite frankly, comparing Microsoft with Atari BASIC in this benchmark is almost as ludicrous as the reverse comparisons in Table 1. Which perhaps says a lot about how worthwhile benchmark programs really are.

Anyway, note that using the Fastchip brings the Atari BASIC timings within striking range of the Microsoft timings. A most respectable performance when you consider that the Atari BASIC routines use six byte floating point while Microsoft uses a four byte floating point. Incidentally, the BASIC A+ timings were all only a small fraction of a second faster than the Atari BASIC times here, so I omitted them.

Enough hard work. On with the games!

Table 2. Transcendental Timings
              Atari   Atari  Atari BASIC
line 30     Microsoft BASIC with Fastchip
30 J=ABS(I)   1.15     1.53    1.48
30 J=SIN(I)   6.85    25.3    10.9
30 J=EXP(I)   6.75    33.7     9.93
30 J=I^I     12.4     74.0    20.8

10 <start timer>
20 FOR I=0 TO 6.3 STEP 0.02
30 J=<a function of I...see table>
40 NEXT I
50 <print elapsed time>

(program used with Table 2)

BOING … Part 2

Last month, we started with a simple program to bounce a ball around in a box. We noted some problems having to do with bouncing fast balls against a wall when the “clock” is slow; either the ball hits the wall “invisibly” or the bounce has to look strange. This month, we will extend that program into a real game and present an alternative method of moving the ball.

If you did type in last month’s program, you might try changing it so that you assign XMOVE and YMOVE instead of having the program pick random directions. I would suggest that you try values of 0, 0.5, 1.0. and 2.0 in various combinations. If you choose XMOVE=1 and YMOVE=0.5, you will accomplish roughly what this month’s program will use. Note, though, that the ball appears to jerk across the screen in strange directions. If you slow down the movement loop (put a delay in it), you will see that the ball really does go in as straight a line as it can (given the coarseness of the display we chose, Graphics 3). The jerkiness is simply an optical illusion, as far as I can tell, due to your eye expecting a certain movement and then being fooled.

The solution? Really, with finite pixel positioning, there is none. But you can greatly improve the situation by using a higher resolution graphics mode while retaining a relatively large ball: the jumps in the higher resolution mode are small in comparison to the ball and so are not perceived as readily. With an Atari, the easiest way to accomplish this is with Player/Missile Graphics; but I will not delve into that in this series of articles since the subject has been covered so thoroughly and well elsewhere. Rather, the intent of these articles is simply to give beginners to graphics and/or assembly language a start in converting ideas from paper to BASIC to assembler.

This month, though, there simply isn’t room or time to show and explain both the BASIC program and its assembly language counterpart. So the assembly language version will wait for next month, but I promise that it will be as closely related to this month’s BASIC as last month’s pair of programs were interrelated.

By the way, for those of you who simply want to play the game, just type it in as carefully as possible. Then simply RUN it for a two player Table Tennis-like game, using joysticks (not paddles—and, by the way, you must hold the joysticks turned 90 degrees left from normal position). For a one player game (not exciting, but a good demo), hold down the START key as you hit the RETURN key after typing RUN. And thus we start a skeletal explanation of how this program works.

What Makes BOING Ping?

First, note that YP(x) and SCORE(x) are simply the Y (vertical) paddle position of player “x” and a count of that same player’s misses (x is 0 or 1, only). SINGLE is a flag set by examining the console switches which creates either a two player or one player game. LASTWIN is a -1 or +1 flag which indicates who scored the last point (we initialize it randomly).

At line 2000, the real work begins. In Graphics mode 3, we draw top and bottom boundaries and left and right paddles and print the current score. If this is a single person game, we overlay the right paddle with another wall. Also, in line 2060, we initialize each player’s paddle position to 10, smack in the middle of each side. The ball is also initialized somewhere in the middle of the screen and given a starting shove.

At lines 2600 and 2700, we use my trick for reading the left and right joystick positions (this is the reason for turning the paddles), and we skip moving the paddle if the joystick is centered (and we never move the right paddle in a SINGLEs game). The method of moving a paddle is sheer simplicity: since each paddle is three units high, we erase the pixel on one end and create a new one on the other end. Presto, the paddle is moved. Oh, yes, we update YP(x).

Then, at line 3000, we start moving the ball. This is pretty much like last month, except that the XMOVE is always plus or minus one while the YMOVE is -1, -0.5, 0, +0.5, or +1. Note that if the ball won’t hit something on its next move, it is because it will miss a paddle, so someone (HITP) will lose a point.

But if the ball is hit by a paddle, its YMOVEment is not determined by simple reflection. Rather, if the ball hits the center of a paddle, it is reflected straight across the playing field (with YMOVE=0). If it hits directly on either side of center, it returns at a slight angle (YMOVE = -0.5 or +0.5). But if it just barely hits the edge of the paddle, it rebounds at a satisfactorily nasty angle (YMOVE=-1 or +1). All this is done in line 3080.

Finally, the “LOSE” and “SCORE” routines are fairly simple. We force the ball to continue its flight for two more steps and then make a nasty noise and a simple but flashy display. We award a hit point as appropriate and figure out who LASTWIN should be.

This is not a sophisticated game. It is not intended to awe you with the power and flexibility of the Atari computer. It is intended to be a simple enough game that most of you will be able to follow its logic. And it certainly is intended to be easily translated to assembly language. But that’s next month.

1000 REM *** STARTUP THE GAME ****
1010 DIM YP(1),SCORE(1):SCORE(0)=0:SCORE(1)=0
1020 SINGLE=(PEEK(53279)<>7)
1100 LASTWIN=1:IF RND(0)>=0.5 THEN LASTWIN=-LASTWIN
2000 REM *** PREPARE FOR A SERVE ***
2010 GRAPHICS 3:COLOR 2:PLOT 0,0:DRAWTO 39,0
2020 PLOT 0,19:DRAWTO 39,19
2030 PRINT :PRINT SCORE(1),,SCORE(0):PRINT "SCORE";
2035 IF SCORE(0)>20 OR SCORE(1)>20 THEN END
2040 COLOR 3:PLOT 0,9:DRAWTO 0,11:PLOT 39,9:DRAWTO 39,11
2050 IF SINGLE THEN COLOR 2:PLOT 39,0:DRAWTO 39,19
2060 YP(0)=10:YP(1)=10:REM VERTICAL POSITION
2070 IF SINGLE THEN LASTWIN=1
2100 REM SET UP BALL
2110 XMOVE=LASTWIN:YMOVE=INT(3*RND(0))-1:Y=INT(12*RND(0))+4
2120 YNEW=Y:X=19-5*XMOVE:XNEW=X
2500 REM *** MAIN PLAYING LOOP ***
2510 REM
2520 REM 1. CHECK AND MOVE PADDLES
2530 REM 2. SHOW NEW BALL POSITION
2540 REM 3. CHECK FOR COLLISIONS, ETC,
2550 REM
2590 REM *** FIRST CHECK AND MOVE PADDLES
2600 V0=PTRIG(0)-PTRIG(1):IF NOT V0 THEN 2700
2610 VP0=YP(0)-V0:IF VP0<2 OR VP0>17 THEN 2700
2620 COLOR 0:PLOT 0,YP(0)+V0:COLOR 3:PLOT 0,VP0-V0:YP(0)=VP0
2700 V1=PTRIG(2)-PTRIG(3):IF SINGLE OR V1=0 THEN 3000
2710 VP1=YP(1)-V1:IF VP1<2 OH VP1>17 THEN 3000
2720 COLOR 0:PLOT 39,YP(1)+V1:COLOR 3:PLOT 39,VP1-V1:YP(1)=VP1
3000 REM *** BALL CONTROL ***
3010 COLOR 0:PLOT X,Y
3020 COLOR 1:PLOT XNEW,YNEW
3030 X=XNEW:Y=YNEW
3040 XNEW=XNEW+XMOVE:YNEW=YNEW+YMOVE
3050 IF XNEW<38 AND XNEW>1 THEN 3200
3060 HITP=(XNEW>20):XHIT=39*HITP
3070 IF SINGLE THEN IF HITP THEN 3100
3080 YMSAVE=YMOVE:YNEW=INT(YNEW):YMOVE=(YNEW-YP(HITP))/2
3090 IF ABS(YMOVE)>1 THEN GOTO 4000
3100 XMOVE=-XMOVE
3200 IF YNEW=1 OR YNEW=18 THEN YMOVE=-YMOVE
3290 GOTO 2600
4000 REM *** THE 'LOSE' ROUTINE
4010 COLOR 0:PLOT X,Y
4020 COLOR 1:PLOT XNEW,YNEW
4030 FOR I=1 TO 10:NEXT I
4040 COLOR 0:PLOT XNEW,YNEW
4050 COLOR 2:PLOT XNEW+XMOVE,YNEW+YMSAVE
4130 SOUND 0,132,12,12:POKE 20,0
4140 SETCOLOR 1,0,PEEK(20)*4:IF PEEK(20)<32 THEN 4140
4150 SOUND 0,0,0,0
4200 REM *** SCORE IT ***
4210 SCORE(HITP)=SCORE(HITP)+1
4220 LASTWIN=1:IF HITP THEN LASTWIN=-LASTWIN
4990 GOTO 2000
COMPUTE! ISSUE 29 / OCTOBER 1982 / PAGE 178

INSIGHT: Atari

Bill Wilkinson
Optimized Systems Software
Cupertino, CA

A BASIC game is translated into machine language. The comments in the program will teach you how to PLOT, DRAWTO, COLOR, etc., in your own machine language games.

Last month marked the first anniversary of this column in COMPUTE!, and I didn’t even notice it. Which tells you how busy I am. We, like almost everyone in the software industry, are beginning to realize that survival comes only to those who diversify. So we are busily introducing new products and concepts. We think the net effect is beneficial to everyone: for us it means a chance to grow and try new approaches; for the user it means newer and better products with a wider choice than ever.

Of course, with the wider choice comes the obvious problem: which one of several competing packages should the user buy? I think I am asked that question only slightly less often than its predecessor: which computer should I buy? I usually sidestep the issue by saying something like this: “Find a software package that seems to do exactly what you want it to do. Ask for references from satisfied customers. When you are convinced that the software will suit your needs, buy the computer that is needed to run the particular software.”

The most common problem I see is people buying too little computer for the problem they want to tackle. And, while the problem is sometimes related to the speed of the chosen machine (let’s face it, you shouldn’t be doing realtime voiceprint analysis with an Atari), the more common problem is simply lack of memory—both kinds of memory, RAM and disk.

This month, I have several topics of interest to Atari aficionados. And, of course, the monster listing of the assembly language version of the “Boing” game (the BASIC version was published last month). Please—hear my disclaimer: I am not nor do I claim to be a game programmer. I am quite aware that Boing is not the epitome of the gamer’s art. Rather, I am here attempting to show the fundamentals of writing graphics games in assembly language. So don’t type this game in expecting a miracle program; use it for instructional purposes only. Add to it, experiment with it, and chalk it up to experience.

A Boo-Boo

Well, so far we’ve encountered only one substantial mistake in our book, Inside Atari DOS (published by COMPUTE!). The error occurs in the text on page 11 and in the diagram (Figure 2-3) on page 14. Both correctly indicate the contents of the last three bytes of a data sector (the “link” information), but both assign the wrong order to these bytes. The byte containing the “number of bytes used in sector” is the last byte of the sector (byte 127 in single density sectors), not byte 125 as shown. Then the bytes shown as 126 and 127 move up to become 125 and 126, respectively.

Our apologies for the misinformation; we hope it didn’t affect too many of you adversely. I think the mistake came about because of the comment in the listing at line 4312 on page 87, where the file number and sector link bytes are called “bytes 126, 127.” Well, they are, if you are numbering from 1 to 128. The tables, etc., in the book are all numbered from 0 to 127; but recall that sectors on the disk are numbered from 1 to 720 (instead of 0 to 719). I don’t know why we humans have such a hard time counting from zero, but we do. And computers have a hard time counting from any other number. Oh well.

Incidentally, the only other error in the diagrams that I have found occurs on page 21, where the labels “SABUFH” and “SABUFL” at the heads of the two columns are reversed.

CP/M For Atari?

I often get asked whether OS/A+ will run CP/M programs on the Atari (since externally OS/A+ looks very, very similar to CP/M—not an accident). But, you simply can’t run CP/M on a 6502 (the heart of any Atari or Commodore or Apple). So how do Apple II owners run CP/M? Simple. They plug a card into their machine that essentially disables the 6502 and runs a Z-80 CPU instead. Why not do the same with an Atari?

First, let me say that I don’t think that, as a practical matter, it is possible to replace the 6502 in the Atari 400/800 with another CPU (e.g., a Z-80). The reasons are many, but the primary one is the fact that the Atari peripheral chips (particularly Antic) seem somewhat permanently married to the 6502. However, there is no real reason that one could not put a co-processor board in the third slot of an 800 (the co-processor would probably have to have its own memory, though, to avoid interfering with the Atari’s DMA and interrupt processing). This is essentially how some manufacturers have added 8086 capability to Apple II’s. But it is expensive, since we now must pay not only for a CPU but also for 65K bytes of RAM and some sort of I/O to talk to the “main” 6502 CPU.

But doing this leaves you stuck with using the Atari serial bus to get data on and off a disk. And, aside from the slow speed, in my opinion an Atari 810 is really too small for practical CP/M work. So, what’s the solution, if any? Actually, I’ve heard of a couple and know of one that is now working.

The first CP/M solution is to simply treat the Atari as an intelligent terminal and hook it up to a CP/M system. While this sounds like overkill, remember that most CP/M systems do not come with a terminal (screen and keyboard), and none can offer the color graphics capabilities of the Atari. But Vincent Cate (alias USS Enterprises) of San Jose, California, has come out with a hardware/software package that does more than make an Atari into an intelligent terminal. His package also allows most CP/M based computers with a 19,200 baud serial port to effectively replace the disk(s) and printer of an Atari computer.

The CP/M system is turned on and started up first, and it fools the Atari into believing that it is an 810 disk drive (just as does the 850 Interface Module in diskless systems). It thus boots a mini-pseudo-DOS into the Atari which simply passes file requests over the serial bus to the CP/M system. A great idea for someone who has a CP/M system and wants either to get a graphics terminal or to justify buying a game machine.

The primary limitation of this system is simply that you won’t be able to read or write Atari-formatted diskettes, though it may be possible to CLOAD from an Atari cassette and then SAVE to the CP/M disk. You won’t be compatible with the rest of the Atari world, but for games you probably don’t care. At $150, this is the cheapest CP/M to Atari connection, but it does presume the prior purchase of a CP/M-based system.

L. E. Systems (alias David and Sandy Small, et al.) has another method of doing co-processing: remove the cover of your 800 and replace it and the OS ROM board with an extension of the Atari’s internal computer bus. On this bus one can stick more memory cards, disk controllers, and (of course) a Z80 card with its own 65K of memory. If your goal is to build a super powerful graphics machine, with access to the vast CP/M library, this is a workable approach (about $1900 with two disk drives, plus the cost of the Atari 800).

However, for about the same money, you could buy a real CP/M machine (such as the Cromemco C-10) with 80-column screen, full function keyboard, built-in printer interface, bigger disks, etc. And then, if you wished, you could hook up your Atari via Vincent Cate’s interface. The L. E. Systems’ approach, though, assures lightning fast data and control flow between the Z80 and the 6502. More importantly, it allows you to continue to buy and use Atari-compatible disk-based software.

Finally, my rumor mill says that by the time you read this there will be a product available which will function as a more or less conventional Atari-compatible disk controller (à la Percom). But, at the flip of a switch, it will instead boot up and run CP/M (internal to the controller box), treating the Atari as an intelligent terminal, much as Vincent Cate’s system does with more conventional CP/M computers.

Do I have any recommendations? Not really. Personally, I like my 128K Byte Cromemco (with 10 Megabyte hard disk and dual 1 Megabyte floppies) for serious software development. But when I think about it, I realize that the thing that makes this system so nice is not the CP/M compatibility (I almost never use CP/M, preferring to stick with Cromemco’s Cromix). Rather, it is simply nice to have all that disk space available on command. So why get CP/M? Because you want to get into exotic compiler languages or because you need some very sophisticated business packages. Fine. But for games? Home finances? Learning how to program in BASIC? Graphics? I suggest you avoid CP/M.

Going With Boing

At last, we have here the complete listing of Boing as written in assembly language. As much as practicable, I have done a direct one-for-one translation from BASIC to machine code, without taking advantage of most of the foibles of the machine. Perhaps the only major change I have introduced is also the most unnoticeable from a casual reading of the source: I have made all the variables (which are six-byte floating point numbers in BASIC) into single bytes. This is not always possible. Sometimes, when writing in assembler, one needs numbers greater than 255; then one “simply” uses two-byte integers (or three or four-byte integers, or floating point even).

Except that, on a 6502, that “simply” isn’t so simple. There are no 16-bit (or larger) instructions on a 6502, and one must simulate them using series of eight-bit loads, adds, stores, etc. For example, if this program were using Mode 8 graphics, where the horizontal position can vary from 0 to 319 (thus requiring a two-byte number to hold it), all of the code involving the “X…” variables would be larger and more complex. Lesson to be learned: use byte-size numbers whenever possible on a 6502.

Anyway, with regard to the listing of Boing, please note that I didn’t leave enough space between my BASIC line numbers to allow my assembly language to share the numbering scheme. So I have put the BASIC lines into the listing in a way that makes them stand out for ease of reading. Presuming that you have read my August and September columns, you will recognize the style and conversions that I have done. Statements such as PLOT, DRAWTO, COLOR, and others have been translated into JSRs to routines in my graphics package. (Note that the listing of the package has been omitted for space considerations. Simply include lines 9000 through 9999 of the listing in my August article.) I would, however, like to discuss a few points of interest.

Notice the coding of lines 2600 and 2700, where the BASIC program had used PTRIG(x)-PTRIG(x + 1) to obtain a + 1, 0, or -1 value from the joystick. But that requires turning the joystick 90 degrees from normal to play the game. As long as we are coding in assembly language, let’s do it right!

What we have here, then, is essentially the code that BASIC A+ uses for its HSTICK(n) function. I think the code is easy to follow if you remember that the switches in the joystick force a zero bit in locations STICKn when they are pushed. By masking to only the bits we want, and by then inverting the bits, we are able to treat an “on” bit in a more or less normal fashion.

By the way, note that here, as elsewhere in the code, we are also using one-byte numbers to hold both positive and negative values. This works only so long as the absolute value of the signed numbers does not exceed 127, so be careful when using this technique.

Note the simulation of the array YP(n). First, look at how easy it is to handle array elements with constant subscripts, as in BASIC line 1010 (listing lines 1210 to 1230). Even variable subscripts aren’t too hard when the array is byte sized and byte dimensioned. Look at BASIC line 4210 (listing lines 6030 and 6040). Admittedly, a true assembly language simulation of the BASIC line would probably go more like this:

LDX    HITP
LDA    SCORE,X
CLC
ADC    #1
LDX    HITP
STA    SCORE,X
        ;SCORE (HITP) = SCORE(HITP) + 1

But why not be a little smart when making conversions? Besides, if we were writing in some higher level languages, we could have written “INCREMENT SCORE(HITP)”.

Finally, the hardest part of this conversion needs some analysis. As we noted last month, in order to provide better movement and bounce characteristics for the ball, we allowed it to have movements (and positions!) of -1, -0.5, 0, +0.5, and + 1. But now we’re in assembly language using byte integers. How do we implement fractional movements? We can’t really, so we must choose an equivalent scheme.

Notice the variables in the program called “Q.Yxxx”. These variables all are used to hold values that represent half movements or positions. Example: if Q.YNEW contains 17, that means it is really representing position 8.5! Notice, then, that before plotting any point that is represented in this fashion, we must divide its value by 2 (by using a LSR instruction, c.f., listing lines 3820, 3930, etc.). Choosing this scheme has some interesting consequences: the last statement of BASIC line 3080 (listing lines 4500 through 4650) is, in some ways, the hardest part of this listing to understand, simply because of the implied “mixed-mode” arithmetic that is used. But it works!

Foibles Of The Assembler/Editor

Writing this article caused me to rediscover some of the foibles of the Atari Assembler/Editor cartridge (and EASMD, for that matter). For many of you, these quirks may seem normal, especially if you haven’t used several different assemblers on various machines. But, to others, these eccentricities can be annoying or puzzling.

First, beware of the “*=” pseudo-operator. It is not an origin operator (“ORG” in many assemblers), even though it is used as such! Any label associated with this pseudo-op will take on the value of the instruction counter before the operator is executed. This is necessary since “*= ” is also used to reserve storage (“DS” or “RMB” in some assemblers).

Examples:
   LABEL1 *= * + 5
     ; reserves five bytes of storage
     ; and assigns the label "LABEL 1"
     ; to the five bytes
    * = $4000
     ; sets the instruction counter
     ; to 4000 hex
   LABEL2 *= $5000
     ; assuming this line followed one
     ; above, assigns 4000 hex to
     ; "LABEL2" and sets instruction
     ; counter to 5000 hex!

Second, examine any references to location “CLOCK.LSB” in the Boing listing (e.g., line 5870). Notice that, even though CLOCK.LSB is in zero page, the assembler produced a three-byte instruction for all references to it. This is because the definition of CLOCK.LSB did not occur until after the first reference to it! Actually, the assembler/editor is being remarkably clever here. Remember that the cartridge is, like most assemblers, a two-pass program. It reads the source once to determine where things are and will be, and then it reads the source again to produce the listing and code. But, during the first pass through the source, it can’t possibly know whether CLOCK.LSB is in zero page or not, so it chooses the safe route and assumes non-zero page. Then, lo and behold, it discovers that we really wanted the label to be in zero page. What to do?

If we now assign that label to zero page, the second pass of the assembler will produce only two bytes of code here, and all references to labels past that point will be off by one byte. We will have the infamous “phase error.” So the assembler has a rule that states “once non-zero page, always non-zero page,” and it continues to generate three-byte references. For a simple assembler like the Atari cartridge, this is a big step. It is still possible to produce phase errors with the cartridge, but it is more difficult than with many 6502 assemblers.

Third and last, there is a problem with the assembler/editor when it comes to multiple forward references. Consider the following code fragment:

AAA = BBB
BBB = CCC
CCC = 5

There is no way for a two-pass assembler to determine what the value of AAA is! On the first pass, it says “AAA is undefined, because BBB hasn’t been defined yet.” And then it thinks “BBB is undefined, similarly because of CCC.” On the second pass, it should say “ERROR!!AAA is undefined, because BBB still hasn’t been defined yet.” But it can then produce “BBB is equal to 5 because that’s what CCC is equal to.”

Unfortunately, the assembler/editor doesn’t keep a separate flag meaning “label as yet undefined.” The “BBB = CCC” line is sufficient, from the assembler’s viewpoint, to establish the existence of “BBB.” So, on the second pass, it blindly puts the value of BBB (presumably zero) into AAA. Watch out for this trap! It has snared many a good programmer! I hope you realize that there would be no problems if you had coded that sequence in this order:

CCC = 5
BBB = CCC
AAA = BBB

That’s it for this month. Next month we will investigate the many languages available to the Atari programmer. We will discuss and fix the major bug in Atari’s 850 interface handler (the “Rn:” drivers). And maybe, just maybe, we will try to add cassette tape verification to BASIC.

0000        1000  .PAGE  "     === GAME STARTUP ==="
            1010 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1020 ;
            1030 ; This is the startup of BOING
            1040 ;
            1050 ;
            1060 ;
            1070 ; CAUTION:  set memory origin according to
            1080 ;           your system needs!
            1090 ;
            1100 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            1110 ;
0000        1120        *=   $6000
            1130 ;
            1140 BOING
            1150 ;

:BASIC: 1010 DIM YP(1),SCORE(1):SCORE(0)=0:SCORE(1)=0

6000 4C0760 1160        JMP AROUND. DIM
6003 00     1170  YP    .BYTE 0,0      ; y-position
6004 00
6005 00     1180  SCORE .BYTE 0,0      ; and score
6006 00
            1190 ;
            1200 AROUND.DIM
6007 A900   1210        LDA  #0
6009 8D0560 1220        STA  SCORE+0   ; SCORE(0)=0
600C 8D0660 1230        STA  SCORE+1   ; SCORE(1)=0
            1240 ;
            1250 ;

:BASIC: 1020 SINGLE=PEEK(53279)<>7)

600F AD1FD0 1260        LDA  53279     ; peek at console switches
6012 4907   1270        EOR  #$07      ; A=7? Then A=0. A<>7? Then A<>0.
6014 8DE062 1280        STA  SINGLE    ; set up our flag
            1290 ;

:BASIC: 1100 LASTWIN=1:IF RND(0)>=0.5 THEN LASTWIN=-LASWIN

6017 A001   1300        LDY  #1        ; use y as temp for lastwin
6019 AD0AD2 1310        LDA  RANDOM    ; get a random byte
601C 1002   1320        BPL  HALFCHANCE
601E 88     1330        DEY            ; 50-50 chance that we do this
601F 88     1340        DEY            ; ...makes Y * $FF, same as -1
            1350 HALFCHANCE
6020 8CE162 1360        STY  LASTWIN   ; store temp in final place
            1370 ;

:BASIC: 2000 REM prepare for a serve

            1380 LINE2000
            1390 ;

:BASIC: 2010 GR.3:COLOR 2:PLOT 0,0:DRAWTO 39,0

6023 A903   1400        LDA  #3
6025 20F362 1410        JSR  GRAPHICS  ; GR.3
            1420 ;
6028 A902   1430        LDA  12
602A 202063 1440        JSR  COLOR     ; COLOR 2
            1450 ;
602D A900   1460        LDA  #0
602F A8     1470        TAY
6030 AA     1480        TAX
6031 202B63 1490        JSR  PLOT      ; PLOT 0,0
            1500 ;
6034 A900   1510        LDA  #0
6036 A227   1520        LDX  #39
6038 A8     1530        TAY
6039 204463 1540        JSR  DRAWTO    ; DRAWTO 39,0
            1550 ;

:BASIC: 2020 PLOT 0,19:DRAWTO 39,19

603C A900   1560        LDA  #0
603E AA     1570        TAX
603F A013   1580        LDY  #19
6041 202B63 1590        JSR  PLOT      ; PLOT 0,19
            1600 ;
6044 A900   1610        LDA  #0
6046 A227   1620        LDX  #39
6048 A013   1630        LDY  #19
604A 204463 1640        JSR  DRAWTO    ; DRAWTO 39,19
            1650 ;

:BASIC: 2030 .... NOTE: We don't print the scores In this version ....

            1660 ;
            1670 ;

:BASIC: 2040 COLOR 3:PLOT 0,9:DRAWTD 0,11:PLOT 39,9:DRAWTO 39,11

604D A903   1680        LDA  #3
604F 202063 1690        JSR  COLOR     ; COLOR 3
            1700 ;
6052 A900   1710        LDA  #0
6054 AA     1720        TAX
6055 A009   1730        LDY  #9
6057 202B63 1740        JSR  PLOT      ; PLOT 0,9
            1750 ;
605A A900   1760        LDA  #0
605C AA     1770        TAX
605D A00B   1780        LDY  #11
605F 204463 1790        JSR  DRAWTO    ; DRAWTO 0,11
            1800 ;
6062 A900   1810        LDA  #0
6064 A227   1820        LDX  #39
6066 A009   1830        LDY  #9
6068 202B63 1840        JSR  PLOT      ; PLOT 39,9
            1850 ;
606B A900   1860        LDA  #0
606D A227   1870        LDX  #39
606F A00B   1880        LDY  #11
6071 204463 1890        JSR  ERWIX)    ; DRAWTO 39,11
            1900 ;
            1910 ;

:BASIC: 2050 IF SINGLE THEN COLOR 2:PLOT 39,0:DRAWTO 39,19

6074 ADE062 1920        LDA  SINGLE
6077 F016   1930        BEQ  NOTTHEN2050 ; not single player mode
            1940 ;
6079 A902   1950        LDA  #2
607B 202063 1960        JSR  COLOR     ; COLOR 2
            1970 ;
607E A227   1980        LDX  #39
6080 A900   1990        LDA  10
6082 A8     2000        TAY
6003 202B63 2010        JSR  PLOT      ; PLOT 39,0
            2020 ;
6086 A227   2030        LDX  #39
6088 A013   2040        LDY  #19
608A A900   2050        LDA  #0
608C 204463 2060        JSR  DRAWTO    ; DRAWTO 39,19
            2070 ;
            2080 NOTTHEN2050
            2090 ;
            2100 ;

:BASIC: 2060 YP(0)=10:YP(1)=10

608F A90A   2110        LDA  #10
6091 8D0360 2120        STA  YP  ; YP(0)=10
6094 8D0460 2130        STA  YP+1  ; YP(1)=10
            2140 ;

:BASIC: 2070 IF SINGLE THEN LASTWIN=1

6097 ADE062 2150        LDA  SINGLE
609A F005   2160        BEQ  LINE2100  ; NOT SINGLE
609C A901   2170        LDA  #1
609E 8DE162 2180        STA  LASTWIN   ; LASTWIN=1 BECUZ SINGLE<>0
            2190 ;

:BASIC: 2100 REM SET UP BALL

            2200 LINE2100
            2210 ;
            2220 ;

:BASIC: 2110 XMOVE=LASTWIN:YMOVE=INT(3*RND(0))-1:Y=INT(12*RND(0))+4

60A1 ADE162 2230        LDA  LASTWIN
60A4 8DE362 2240        STA  XMOVE     ; XMOVE=LASTWIN
            2250 ;
60A7 A902   2260        LDA  #2
60A9 206263 2270        JSR  RND       ; get random number from 0 to 2
60AC 8DE662 2280        STA  Q.YMOVE
60AF CEE662 2290        DEC  Q.YMOVE   ; then do the '-1'
60B2 0EE662 2300        ASL  Q.YMOVE   ; and convert to "half-moves"
            2310 ;
60B5 A90B   2320        LDA  #11
60B7 206263 2330        JSR  RND       ; get random number from 0 to 11
60BA 18     2340        CLC
60BB 6904   2350        ADC  #4        ; '+4' as above
60BD 0A     2360        ASL  A         ; double number of moves to get half-moves
60BE 8DE562 2370        STA  Q.Y       ; Again, this is a 'half-position' variable
            2380 ;
            2390 ;

:BASIC: 2120 YNEW=Y:X=19-5*XMOVE:XNEW=X

60C1 AE6562 2400        LDA  Q.Y
60C4 0DE762 2410        STA  Q.YNEW    ; YNEW=Y
            2420 ; Here, we take advantage of the fact that XMOVE
            2430 ; can only have values -1 or +1
60C7 A9FB   2440        LDA  #0-5      ; assume XMOVE = +1
60C9 ACE362 2450        LDY  XMOVE     ; does XMOVE = +1?
60CC 1002   2460        BPL  XMOVEPLUS ; yes
60CE A90 5  2470        LDA  #5        ; no., so -5*-1 = +5
            2480 XMOVEPLUS
60D0 18     2490        CLC
60D1 6913   2500        ADC  #19       ; 19-5 OR 19*5
60D3 8DE262 2510        STA  X
            2520 ;
60D6 ADE262 2530        LDA  X         ; but you can see we don't really need this
60D9 8DE462 2540        STA  XNEW      ; XNEW=X
            2550 ;
            2560 ;

:BASIC: 2500 REM MAIN PLAYING LOOP

            2570 ;
            2580 ;

:BASIC: 2600 V0=PTRIG(0)-PTRIG(1):IF NOT V0 THEN 2700

            2590 ; note that what we really want is V0=+1 if
            2600 ; stick is pushed one way and V0=-1 if
            2610 ; stick is pushed the other.
            2620 ;
            2630 LINE2600
60DC AD7B02 2640        LDA  STICK0    ; OS shadow location
60DF 2903   2650        AND  #3        ; look at just fud and backwd switches
60E1 4903   2660        ECR  #3        ; invert the sense
60E3 F006   2670        BEQ  GOTV0     ; if zero, stick not pushed
60E5 C901   2680        CMP  #1        ; EV/D pushed?
60E7 F002   2690        BEQ  GOTV0     ; good...what we wanted
60E9 A9FF   2700        LDA  #0-1      ; must be pulled back
            2710 GOTV0
60EB 8DEB62 2720        STA  V0        ; ta-da
            2730 ;
60EE ADEB62 2740        LDA  V0        ; so is stick pushed?
60F1 F03E   2750        BEQ  LINE2700  ; IF NOT V0 THEN 2700
            2760 ;
            2770 ;

:BASIC: 2610 VP0=YP(0)-V0:IF VP0<2 OR VP0>17 THEN 2700

60F3 AD0360 2780        LDA  YP+0      ; YP(0)
60F6 38     2790        SEC
60F7 EDEB62 2800        SBC  V0
60FA 8DED62 2810        STA  VP0       ; VP0=YP(0)-V0
            2820 ;
60FD C902   2830        CMP  #2
60FF 9030   2840        BCC  LINE2700  ; IF VP0<2 THEN 2700
6101 C912   2850        CMP  #18
6103 B02C   2860        BCS  LINE2700  ; or IF VP0>17 THEN 2700
            2870 ;
            2880 ;

:BASIC: 2620 COLOR 0:PLOT 0,YP(0)+V0:COLOR 3:PLOT 0,VP0-V0:YP(0)=VP0

6105 A900   2890        LDA  #0
6107 202063 2900        JSR  COLOR     ; COLOR 0
            2910 ;
610A AD0360 2920        LDA  YP+0
610D 18     2930        CLC
610E 6DEB62 2940        ADC  V0        ; YP(0)+V0
6111 A8     2950        TAY            ; is y position
6112 A900   2960        LDA  #0
6114 AA     2970        TAX
6115 202B63 2980        JSR  PLOT      ; PLOT 0,YP(0)+V0
            2990 ;
6118 A903   3000        LDA  #3
611A 202063 3010        JSR  COLOR     ; COLOR 3
            3020 ;
611D ADED62 3030        LDA  VP0
6120 38     3040        SEE
6121 EDEB62 3050        SBC  V0
6124 A8     3060        TAY
6125 A900   3070        LDA  #0
6127 AA     3080        TAX
6128 202363 3090        JSR  PLOT      ; PLOT 0,VPD+V0
            3100 ;
612B ADED62 3110        LDA  VP0
612E 8D0360 3120        STA  YP+0      ; YP(0)=VP0
            3130 ;
            3140 ;

:BASIC: 2700 V1=PTRIG(2)-PTRIG(3):IF SINGLE OR V1=0 THEN 3000

            3150 LINE2700
            3160 ;  note that what we  really want is V0=+1 if
            3170 ; stick is pushed one way and VI-1 if
            3160 ; stick is pushed the other.
            3190 ;
6131 AD7902 3200        LDA  STICK1    ; OS shadow location
6134 2903   3210        AND  #3        ; look at just fwd and backwd switches
6136 4903   3220        EOR  #3        ; invert the sense
6130 F006   3230        BEQ  GOTV1     ; if zero, stick not pushed
613A C901   3240        CMP  #1        ; FWD pushed?
613C F002   3250        BEQ  GOTV1     ; good...what we wanted
613E A9FF   3260        LDA  #0-1      ; must be pulled back
            3270 GOTO
6140 0DEC62 3280        STA  VI        ; ta-da
            3290 ;
6143 AEE062 3300        LDA  SINGLE
6146 D045   3310        BNE  LINE3000  ; IF SINGLE THEN 3000
6140 ADEC62 3320        LDA  VI        ; so is stick pushed?
614B F040   3330        BEQ  LINE3000  ; or IF VI =0 THEN 3000
            3340 ;
            3350 ;

:BASIC; 2710 VP1=YP(1)-V1: IF VP1<2 OR VP1>17 THEN 3000

614D AD0460 3360        LDA  YP+1      ; YP(1)
6150 30     3370        SEC
6151 EDEC62 3300        SBC  VI
6154 0DEE62 3390        STA  VP1       ; VP1=YP(1)-V1
            3400 ;
6157 C902   3410        CMP  #2
6159 9032   3420        BCC  LINE3000  ; IF VP1<2 THEN 3000
615B C912   3430        CMP  #18
6153 B02E   3440        BCS  LINE3000  ; or IF VP1>17 THEN 3000
            3450 ;
            3460 ;

:BASIC: 2720 COLOR 0:PLOT 39,YP(1)+V1:COLOR 3:PLOT 39,VP1-V1:YP(1)=VP1

615F A900   3470        LDA  #0
6161 202063 3480        JSR  COLOR     ; COLOR 0
            3490 ;
6164 AD0460 3500        LDA  YP+1
6167 16     3510        CLC
6168 6DEC62 3520        ADC  VI        ; YP(1)+V1
616B A8     3530        TAY            ; is y position
616C A900   3540        LDA  #0
616E A227   3550        LDX  #39
6170 202B63 3560        JSR  PLOT      ; PLOT 39,YP(1)+V1
            3570 ;
6173 A903   3580        LDA  #3
6175 202063 3590        JSR  COLOR     ; COLOR 3
            3600 ;
6170 AEEE62 3610        LDA  VP1
617B 38     3620        SEC
617C EDEC62 3630        SBC  VI
617F A8     3640        TAY
6180 A900   3650        LDA  #0
6182 A227   3660        LDX  #39
6184 202B63 3670        JSR  PLOT      ; PLOT 39,VP1+V1
            3680
6187 AD0E62 3690        LDA  VP1
618A 8D0460 3700        STA  YP+1      ; YP(1)=VP1
            3710 ;
            3720 ;
            3730 ;

:BASIC: 3000 REM *** BALL CONTROL ***

            3740 LINE3000
            3750 ;
            3760 ;

:BASIC: 3010 COLOR 0:PLOT X,Y

618D A900   3770        LDA  #0
618F 202063 3700        JSR  COLOR     ; COLOR 0
            3790 ;
6192 AEE262 3800        LDX  X
6195 ACE562 3810        LDA  Q.Y
6198 4A     3820        LSR  A         ; Divide half-position by 2 to get real pos'n
6199 A0     3830        TAY
619A A900   3840        LDA  #0
619C 202B63 3850        JSR  PLOT      ; PLOT X,Y
            3860 ;
            3870 ;

:BASIC: 3020 COLOR 1:PLOT XNEW,YNEW

619F A901   3880        LDA  #1
61A1 202063 3890        JSR  COLOR     ; COLOR 1
            3900 ;
61A4 AEE462 3910        LDX  XNEW
61A7 ADE762 3920        LDA  Q.YNEW
61AA 4A     3930        LSR  A         ; Divide half-position by 2 to get real pos'n
61AB A5     3940        TAY
61AC A900   3950        LDA  10
61AE 202B63 3960        JSR  PLOT      ; PLOT XNEW,YNEW
            3970 ;
            3980 ;

: BASIC: 3030 X=XNEW:Y=YNEW

61B1 ADE462 3990        LDA  XNEW
61B4 8DE262 4000        STA  X         ; X=XNEW
            4010 ;
61B7 ADE762 4020        LDA  Q.YNEW
61BA 8DE562 4030        STA  Q.Y       ; Y=YNEW
            4040 ?
            4050 ;

:BASIC: 3040 XNEW=XNEW+XMOVE:YNEW=YNEW+YMOVE

61BD ADE462 4060        LDA  XNEW
61C0 18     4070        CLC
61C1 6DE362 4080        ADC  XMOVE
61C4 8DE462 4090        STA  XNEW      ; XNEW=XNEW+XMOVE
            4100 ;
61C7 ADE762 4110        LDA  Q.YNEW
61CA 18     4120        CLC
61CB 6DE662 4130        ADC  Q.YMOVE
61CE 8DE762 4140        STA  Q.VNEW    ; YNEW=YNEW+YMOVE
            4150 ;
            4160 ;

:BASIC: 3050 IF XNEW<38 AND XNEW>1 THEN 3200

            4170 ;
61D1 AD6462 4180        LDA  XNEW
61D4 C926   4190        CMP  #38
61D6 B004   4200        BCS  NOTTHEN3050
61D8 C902   4210        CMP  #2
61DA B04C   4220        BCS  LINE3200  ; XNEW<38 AND XNEW>1, SO GO
            4230 ;
            4240 NOTTHEN3050
            4250 ;
            4260 ?

:BASIC: 3060 HITP=(XNEW>20):XHIT=39*HITP

61DC A200   4270        LDX  #0
61DE A000   4280        LDY  #0
61E0 ADE462 4290        LDA  XNEW
61E3 C914   4300        CMP  #20       ; XNEW>20 ?
61E5 9004   4310        BCC  XNEWLT20  ; NO
61E7 A001   4320        LDY  #1        ; YES...SO 'TRUE' IS 1
61E9 A227   4330        LDX  #39
            4340 XNEWLT20
61EB 8CE962 4350        STY  HITP
61EE 8EEA62 4360        STX  XHIT
            4370 ;
            4380 ;

:BASIC: 3070 IF SINGLE THEN IF HITP THEN 3100

61F1 AEE062 4390        LDA  SINGLE
61F4 FD05   4400        BEQ  LINE3080  ; NOT SINGLE
61F6 ADE962 4410        LDA  HITP
61F9 D024   4420        BNE  LINE3100  ; YES, SINGLE AND HITP
            4430 ;
            4440 ;

:BASIC: 3080 YMSAVE=YMOVE:YNEW=INT(YNEW):YMOVE=(YNEW-YP(HITP))/2

            4450 ;
            4460 LINE3080
61FB ADE662 4470        LDA  Q.YMOVE
61FE 8DE862 4480        STA  Q.YMSAVE  ; YMSAVE=YMOVE
            4490 ;
            4500 ; RD1EMBER: we are using half move increments in Q.Y...
            4510 ;  variables...so we really simply want to get
            4520 ;  rid of the lowest bit (the half step)
            4530 ;
6201 ATE762 4540        LDA  Q.YNEW
6204 29FE   4550        AND  #$FE      ; mask off last bit
6206 8DE762 4560        STA  Q.YNEW    ; YNEW=INT(YNEW)
            4570 ;
6209 AEE962 45B0        LDX  HITP      ; so X is either 0 or 1
620C 4A     4590        LSR  A         ; Q.YNEW / 2 gives the true YNEW
620D 38     4600        SEC
620E FD0360 4610        SBC  YP,X      ; YNEW-YP(HITP)
            4620 ; we don't need to divide by 2, because Q.YMOVE wants half-moves
6211 8DE662 4630        STA  Q.YMOVE   ; done
            4640 ;
            4650 ;

:BASIC: 3090 IF ABS(YMOVE)>1 THEN 4000

6214 ADE662 4660        LDA  Q.YMOVE
6217 C903   4670        CMP  #3        ; halfsteps, remember
6219 9004   4680        BOC  LINE3100  ; 0,1, or 2 halfsteps
621B C9FE   4690        CMP  #$FE
621D 902C   4700        BOC  LINE4000  ; aha...>2 halfsteps, <-2 halfsteps
            4710 ;
            4720 ?

:BASIC: 3100 XMOVE= -XMOVE

            4730 LINE3100
621F A900   4740        LDA  10
6221 38     4750        SBC
6222 EDE362 4760        SBC  XMOVE
6225 8DE362 4770        STA  XMOVE     ; xmove = -xmove
            4780 ;
            4790 ;

:BASIC: 3200 IF YNEW=1 OR YNEW=18 THEN YMOVE= -YMOVE

            4800 LINE3200
6228 AEE762 4810        LDA  Q.YNEW
622B C902   4820        CMP  #1+1      ; remember: half moves
622D F004   4830        BEQ  THEN3200
622F C924   4840        CMP  #18+18
6231 D009   4850        BNE  NOTTHEN3200
            4860 ;
            4870 THEN3200
6233 A900   4880        LDA  #0
6235 38     4890        SEC
6236 EDE662 4900        SBC  Q.YMOVE   ; 0-YMOVE
6239 8DE662 4910        STA  Q.YMOVE   ; is obviously the same as -YMOVE
            4920 ;
            4930 NOTTHEN3200
            4940 ;
            4950 ;

:BASIC: 3290 GOTO 2600

            4960 ;
            4970 ; if we simply jumped back to LINE2600 here, the game
            4980 ;  would play impossibly fast...
            4990 ;  so we put in a delay
            5000 ;
623C A900   5010        LDA  #0
623E 8D1400 5020        STA  CLOCK.LSB ; the 60th of a second ticker
            5030 DELAY1
6241 AD1400 5040        LDA  CLOCK.LSB
6244 C902   5050        CMP  #2        ; a 30th of a second?
6246 D0F9   5060        BNE  DELAY1
            5070 ;
6248 4CDC60 5080        JMP  LINE2600
            5090 ;
            5100 ;

:BASIC: 4000 REM *** the LOSE routine ***

            5110 LINE4000
            5120 ;
            5130 ; we will score the misses, even though we don't
            5140 ; display the results
            5150 ;
            5160 ;

:BASIC: 4010 COLOR 0:PLOT X,Y

624B A900   5170        LDA  #0
624D 202063 5180        JSR  COLOR     ; COLOR 0
            5190 ;
6250 AEE262 5200        LDX  X
6253 AEE562 5210        LDA  Q.Y       ; the half step
6256 4A     5220        LSR  A         ; becomes an integral step
6257 A8     5230        TAY
6258 A900   5240        LDA  #0
625A 202B63 5250        JSR  PLOT      ; PLOT X,Y
            5260 ;
            5270 ;

:BASIC: 4020 COLOR 1:PLOT XNEW,YNEW

625D A901   5280        LDA  #1
625F 202063 5290        JSR  COLOR     ; COLOR 1
            5300 ;
6262 AEE462 5310        LDX  XNEW
6265 ADE762 5320        LDA  Q.YNEW
6268 4A     5330        LSR  A         ; again, half step to full step
6269 A8     5340        TAY
626A A900   5350        LDA  #0
626C 202B63 5360        JSR  PLOT      ; PLOT XNEW, YNEW
            5370 ;
            5300 ;

:BASIC: 4030 FOR I=1 TO 10:NEXT I

            5390 ; shoddy, shoddy - using a for/next loop for timing!
            5400 ;
            5410 ; here, we do it right
626F A900   5420        LDA  #0
6271 8D1400 5430        STA  CLOCK.LSB
            5440 ;
            5450 DELAY2
6274 AD1400 5460        LDA  CLOCK.LSB
6277 C902   5470        CMP  #2        ; tick tock yet?
6279 D0F9   5480        BNE  DELAY2    ; nope, maybe just tick
            5490 ;
            5500 ;

:BASIC: 4040 COLOR 0:PLOT XNEW,YNEW

627B A900   5510        LDA  #0
627D 202063 5520        JSR  COLOR
            5530 ;
6280 AEE462 5540        LDX  XNEW
6283 AEE762 5550        LDA  Q.YNEW    ; starting to look familiar?
6286 4A     5560        LSR  A
6287 A8     5570        TAY
6288 A900   5580        LDA  #0
628A 202B63 5590        JSR  PLOT      ; PLOT XNEW,YNEW
            5600 ;
            5610 ;

:BASIC: 4050 COLOR 2:PLOT XNEW+XMOVE,YNEW+YMSAVE

628D A902   5620        LDA  #2
628F 202063 5630        JSR  COLOR     ; COLOR 2
            5640 ;
6292 AD6462 5650        LDA  XNEW
6295 18     5660        CLC
6296 6DE362 5670        ADC  XMOVE
6299 AA     5680        TAX            ; x register = XNEW+XMOVE
629A ADE762 5690        LDA  Q.YNEW
629D 18     5700        CLC
629E 6DE862 5710        ADC  Q.YMSAVE
62A1 4A     5720        LSR  A         ; integerize the sum
62A2 A8     5730        TAY            ; y register = YNEW+YMSAVE
62A3 A900   5740        LDA  #0
62A5 202B63 5750        JSR  PLOT      ; PLOT it
            5760 ;
            5770 ;

:BASIC: 4130 SOUND 0,132,12,12:POKE 20,0

62A8 A984   5780        LDA  #132
62AA 8D00D2 5790        STA  SOUND.FREQ ; implicitly channel 0
62AD A9CC   5800        LDA  #12*16+12
62AF 8D01D2 5810        STA  SOUND.CONTROL ; ,12,12 also for channel 0
62B2 A900   5820        LDA  #0
62B4 8D1400 5830        STA  CLOCK.LSB ; finally, BASIC did it right!
            5840 ;
            5850 ;

:BASIC: 4140 SETCOLOR 1,0,PEEK(20)*4:IF PEEK(20)<32 THEN 4140

            5860 LINE4140
62B7 AD1400 5870        LDA  CLOCK.LSB ; same as PEEK(20)
62BA 0A     5880        ASL  A
62BB 0A     5890        ASL  A         ; *4
62BC 8DC502 5900        STA  SETCOLOR1 ; control register number 1
            5910 ;
62BF C980   5920        CMP  #32*4     ; a little tricky...can you follow it?
62C1 90F4   5930        BOC  LINE4140  ; it works...really
            5940 ;
            5950 ;

:BASIC: 4150 SOUND 0,0,0,0

62C3 A900   5960        LDA  #0
62C5 8D00D2 5970        STA  SOKD.FREQ
62C8 8D01D2 5980        STA  SOUND.CCNTROL
            5990 ;
            6000 ;

:BASIC: 4200 REM *** SCORE IT ***

            6010 ;
            6020 ;

:BASIC: 4210 SCORE(HITP)=SCORE(HITP)+1

62CB AEE962 6030        LDX  HITP
62CE FE0560 6040        INC  SCORE,X   ; isn't assembler easy?
            6050 ;
            6060 ;

:BASIC: 4220 LASTWIN-1:IF HITP THEN LASTWIN=-LASTWIN

62D1 A901   6070        LDA  #1
62D3 AEE962 6080        LDX  HITP      ; if HITP?
62D6 F002   6090        BEQ  NOT. HITP ; no
62D8 A9FF   6100        LDA  #0-1      ; yes...so make it -1
            6110 NOT.HITP
62DA 8DE162 6120        STA  LASTWIN   ; that's all that is needed
            6130 ;
            6140 ;

:BASIC: 4990 GOTO 2000

            6150 ;
62DD 4C2360 6160        JMP  LINE2000
            6170 ;

BOING -- not quite up to PONG
GENERAL RAM USAGE

62E0        6180        .PAGE  "GENERAL RAM USAGE"
            6190 ;
62E0 00     6200 SINGLE BRK            ; flag for one-player game
62E1 00     6210 LASTWIN BRK           ; who won last time?
            6220 ;
            6230 ; the x moves 6240 ;
62E2 00     6250 X      BRK            ; current x position
62E3 00     6260 XMOVE  BRK            ; current x movement
62E4 00     6270 XNEW   BRK            ; new x position
            6280 ;
            6290 ; and the y positions and moves
            6300 ;
            6310 ; remember; the Q.Yxxx locations reference positions
            6320 ; or movements in terms of half steps
            6330 ;
62E5 00     6340 Q.Y    BRK            ; current y position
62E6 00     6350 Q.YMOVE BRK           ; current y movement
62E7 00     6360 Q.YNEW BRK            ; new y position
62E8 00     6370 Q.YMSAVE BRK          ; saved for LOSE routine only
            6380 ;
            6390 ; other misscellany
            6400 ;
62E9 00     6410 HITP   BRK            ; the HIT Person...who missed
62EA 00     6420 XHIT   BRK            ; where the miss occurred (x position)
            6430 ;
62EB 00     6440 V0     BRK            ; just a temporary
62BC 00     6450 VI     BRK            ; ditto
            6460 ;
62ED 00     6470 VP0    BRK            ; Vertical position of Paddle 0
62EE 00     6480 VP1    BRK            ; Vertical position of Paddle 1
            6490 ;
            6500 ; system equates
            6510 ;
0012        6520 CLOCK =     18        ; the system clock
0014        6530 CLOCK.LSB = CLOCK+2   ; the 60th of a second ticker
0278        6540 STICK0 =    $278      ; OS shadow read of first stick
0279        6550 STICK1 =    $279      ; ditto for second stick
D200        6560 SOUND.FREQ = $D200    ; port which controls channel 0 freq
D201        6570 SOUND.CONTROL = $D201 ; and control
            6580 ;
02C5        6590 SETCOLOR1 = $2C5  ; also known as COLPF1

BOING -- not quite up to PONG
The GRAPHICS subroutines

62EF        6600        .PAGE "The GRAPHICS subroutines"

6372        6630        .OPT LIST
6372        6640        .END
[Put the graphics subroutines from line 9000 on up (pg. 150, COMPUTE!, August 1982) here. ]
COMPUTE! ISSUE 30 / NOVEMBER 1982 / PAGE 186

INSIGHT: Atari

Bill Wilkinson
Optimized Systems Software
Cupertino, CA

A quick way to verify cassettes, a survey of languages available for the Atari, and a fix for a bug in Atari’s RS-232 handlers.

Well, I didn’t quite make it. I was trying to have a cassette verify program done in time for this month’s column, but the pressures of writing a couple of sections for the new COMPUTE!s Book of Atari Graphics, producing three major new OSS products, and answering literally hundreds of phone calls got to me. So, wait for next month. But in the meantime, at least I have a quickie verify method that might keep the frustrations away for a month.

Quick And Dirty

One of the major flaws of the Atari computers has always been the lack of a cassette verify capability. But there is an almost effortless way to simulate this missing capability.

The secret lies in the fact that, because of Atari’s superior operating system and because BASIC interfaces properly to it, you can LIST to any file or device. So, when you are ready to save your program to cassette, do not use CSAVE. Instead, Use LIST"C:" to produce an ATASCII listing on the cassette. Then you can rewind the tape and, without deleting or changing the program in memory, enter the following direct statements:

DIM Q$(256):OPEN #1,4,0,"C:"
FOR Q=0 TO 100000:INPUT #1,Q$:PRINT Q$:
NEXT Q

Do you see the reason for the trick? Atari makes no distinction between a listing file and a data file, even on a cassette, so we can simply read the listing as data and print what we read on the screen. If what appears on the screen is correct, the cassette was recorded correctly. Incidentally, the FOR/NEXT loop is only needed so that we can enter the statements in direct mode (without line numbers). The loop will execute more times than there are lines in the file, but the end of file error will stop the process anyway. (And it is a good idea to type “END” after getting the end of file error.)

For the adventuresome, there might be an even easier way. After using the LIST"C:", simply rewind the tape and type ENTER"C:". Remember, ENTER does not erase the program in memory, but instead merges the filed program with the current one. But in this case, since the two programs are the same (if the file was recorded correctly), the ENTER should have no visible effect. If there is an error in the tape, the ENTER will simply halt and no harm will be done. Theoretically. In truth, it is possible that one line could be destroyed (if it were partially ENTERed from one tape block and then blew up in the next block). I have not tested this exhaustively, so use it at your own risk.

Foreign Languages

What is the Atari language? What is the best language for doing the most things with an Atari computer? Is there such a thing? There may be no good answers to these questions, but trying to answer them may prove interesting, so let’s give it a shot.

The Atari now has a respectable complement of languages available for it. I will list those I know of here and I must apologize in advance for any omissions. The listings within each category are roughly in order of date of introduction of the product. An asterisk indicates a product no longer actively advertised, so check with the publisher for availability.

Assemblers
Cassette-Based Assembler – Quality Software*
Assembler/Editor Cartridge – Atari, Inc.
EASMD (Edit/ASseMble/Debug) – OSS, Inc.*
DATASM/65 – Datasoft*
Macro Assemblers
MAE (Macro Assembler/Editor) – Eastern House
Macro Assembler – ELCOMP
AMAC (Atari MACro assembler) – Atari, Inc
MAC/65 – OSS, Inc.
Interpreters
Atari BASIC – Atari, Inc.
BASIC A# – OSS, Inc.
LISP–Datasoft
PILOT–Atari, Inc.
tiny c – OSS, Inc.
Microsoft BASIC – Atari, Inc.
Pseudo-Compilers
QS FORTH – Quality Software
Atari FORTH – Atari Program Exchange
pns FORTH – Pink Noise Studios
PASCAL – Atari Program Exchange
ValForth – Valpar
Compilers
PASCAL –Atari Program Exchange
C/65–OSS, Inc.

I admit I hesitated over classifying FORTH as a pseudo-compiled language, but I was trying to group the products by speed and space considerations. Technically, FORTH is a “threaded” language, but that doesn’t imply anything about its implementation. Besides, I love to bug the FORTH aficionados. Anyway, to proceed.

The assemblers grow more numerous almost monthly, and it is obvious that most serious graphics work for the Atari is still being done in assembly language, even though the 6502 has one of the strangest assembly languages in existence. (There used to be others far stranger, but they’ve either died out or been relegated to the dedicated controller market. You know you’re an old-timer if you ever used a 4004, PPS-4, PPS-8, 8008, F-8, 2650, COPS, Ti1000, etc.)

Of course, Macro Assemblers are a step in the right direction, but I have yet to see any 6502 assembler system done “right,” with relocatable and linkable object modules, a symbolic debugger, and more. Yet. For those of you not familiar with macro-assembly techniques, I should point out that old macro hackers usually build up a library of their favorite macros and can easily plug together several variations on a utility program (for example) by simply picking and choosing from their assortment of macros.

I don’t really want to explore this subject in depth right now, but I would like to point out that, using some – or at least one – of the currently available macro assemblers for the Atari, you can write assembly language programs that look like this:

   OPEN 1,8,0,"D:NEWFILE"
LOOP
   INPUT 1,LINE
   IFERROR EXIT
   PRINT 0,LINE
   GOTO Loop
;
EXIT

It would seem to me that the percentage of Atari owners who will successfully dive into assembly language is too small to make any assembler become the dominant Atari language. Currently, though, there is no other way to write such marvels as Eastern Front, Frogger, and operating systems. So, at least for many software heavyweights, assembly is the language.

Compiling 6502 Code

I’d like to skip the interpreters for now and discuss both kinds of compilers. For starters, what’s the difference between a compiler and a pseudo-compiler? Software purists could argue this point for days, but I will use a simple rule here: if it produces output, it’s a compiler. If it produces tokens or words which must be interpreted, it’s a pseudo-compiler.

Now, quite honestly, on a 6502 there probably isn’t much advantage in one of these over the other. Generally, a pseudo-compiler produces fewer bytes of code, but requires a relatively massive runtime support module (the interpreter, including I/O routines, etc.). As a rule, on most computers, pseudo-compiled code will run slower than compiled code because of the overhead of the interpreter.

Unfortunately, most conventional language compilers for 6502-based machines will of necessity produce large and generally clumsy code. Consider the following statement, legal, with minor variation, in most higher level languages:

array(index) = value;

Given that all three variables shown are 16-bit globals, a really good compiler for a Z80 could produce as few as 15 bytes of code to execute it (and the one we wrote for Cromemco produces only 16 bytes).

A superb compiler for the 6502 could produce as few as 25 bytes, but only if it knew that “index” would not contain a value exceeding 127! And, oh yes, most pseudo-code compilers would probably produce 11 or 12 bytes of tokens for this same code.

So, you see, even a multi-pass optimizing compiler can at best coax the 6502 into using 1.5 to 2.5 times the amount of code that a Z80 needs. And, in truth, there aren’t any “superb,” “multipass,” “optimizing” compilers yet available for the Atari. So the code generated will be even bigger, perhaps as much as three to four times that needed by a Z80. (To be fair, an “average” Z80 compiler would produce 25 or so bytes of code, itself.)

So why did we digress through all of this? Simply to show that it is remarkable that there are any compilers at all for the Atari. Of the two compilers shown, the PASCAL is the more complete language, but it is a little difficult to work with, needs a huge support library, and requires two disk drives. Still, since it is an APX product, it is a remarkable bargain. C/65, on the other hand, is a subset of the full C language; it is a one-pass compiler (no optimizing here, obviously) which produces macro assembly language output. Its primary advantages: the assembly language can produce a listing with the original C code interspersed as comments, it uses a very small support library, and it can run on a single drive. But I think we may not have seen the end of compiler efforts on the 6502.

Interpreter Efficiency

But now we come to my favorite topic: interpreters. Despite its shortcomings as a compiled-for machine, the 6502 comports itself nicely when interpreting: it is fast and needs only relatively compact code to implement. Why? Simply because interpreters generally work on “lines” of input. But if we limit a line to 256 characters (a very reasonable limitation), we find that there are several modes of operation on the 6502 that just love working with such short character strings. (Especially, of course, the “indirect indexed” or “(zero page),Y” instructions.) The truth of the matter is that the designer of a 6502-based interpreter has a lot of leeway in prescribing how the language will run best.

So look at the wealth of interpreters available already! With more to come, I am sure. We find in these interpreters the most used of all Atari languages, Atari BASIC. Well, that’s not surprising, considering that it’s essentially a required ingredient in an Atari system. But let’s come back to it in a moment.

Naturally, PILOT is here. It’s a nice, simple language which can easily be interpreted. It was probably a joy to program; I would have loved being involved.

But there are some real powerhouse languages here, also. LISP has traditionally been an interpreter, the darling of the Artificial Intelligence people. And, finally, there is Microsoft BASIC and BASIC A+. Quite honestly, I feel that these last two languages provide the best and easiest access to the Atari’s features. Naturally, I am prejudiced towards BASIC A+, but the Microsoft BASIC has a few nice and unique features even if it isn’t quite as easy to use.

What’s The Atari Language?

So, after all that, just what is the Atari language? Well, I’m going to cop out and say that it’s Atari BASIC. Despite all the nasty things said about the poor thing, look at all the things written in BASIC. And they work.

Atari BASIC is an excellent starting point. The easiest next step is BASIC A+, but most people won’t have too much trouble learning other algebraic languages, such as PASCAL or C (the only real problem with these languages is that debugging is so much harder than with an interpreter). I consider PILOT and LISP useful languages in their own right, but much of what you learn in them is non-transportable to other languages.

The same is true of FORTH. FORTH enthusiasts would have you believe that FORTH is the only language you will ever need. Nonsense. Each language has its uses, its strong points, and its failings. (In my opinion, the major failings of FORTH are (1) that it operates independent of the host system’s DOS and (2) techniques learned in FORTH are often non-transportable to other languages, because of FORTH’s reverse-Polish notation. However, I respect the language for what it is: a hacker’s dream come true. And I’m a hacker.)

Personally, I like to collect languages the way other people collect games. Seldom will I find one that won’t teach me something new about how computers can be made to work. So try some “foreign” languages yourself soon and see how much fun they can be. (And pain and trouble and frustrating and educational and uplifting.)

System Reset And The 850

A couple of times in the past, I have presented in this column the “rules” for adding device drivers to Atari OS. Well, would you believe it, Atari itself broke the rules when they implemented the 850 (RS-232) handlers. The violation was a minor one, yet the consequences can be severe. To start with, let’s recap my rules:

  1. Locate the current value of system LOMEM (contents of $02E7).
  2. Load your driver into memory and relocate it to LOMEM.
  3. Adjust the contents of LOMEM to reflect the memory being used by your driver.
  4. Add your device’s name and handler address to the handler table (HATABS, at $031A).
  5. Get the current value of DOSINI (location $000C) and save it somewhere in your handler. Put your own initialization address into DOSINI.
  6. Whenever your initialization routine is called (i.e., when System Reset is pushed by a user), first call the initializer whose address was in DOSINI before you changed it. Then perform steps 3 and 4 again, since Reset will have changed LOMEM and reloaded the HATABS.

Now step 2 is the most difficult of these to accomplish, in practice, because it is hard to produce a relocatable module on the Atari. Many programs I have seen (and written) are actually assembled absolute at a “known” good location. This is okay, if you are writing for your own private system: you know what will be loaded when and where. But if you are producing a driver for sale, you really should follow the rule faithfully.

Atari’s 850 drivers do, indeed, relocate themselves beautifully. They add their name to the handler table. They adjust the system LOMEM pointer. So what do they do wrong? One minor thing: they do steps 3 and 4 before they call the old initialization routine (see step 6) instead of after!

The result: the 850 handler changes LOMEM to just above itself and then calls the DOS initialization, which resets LOMEM to just above DOS! Thus, the RS-232 handlers are not protected from programs which come in and quite properly use RAM starting at LOMEM. Generally, if you are running with Atari BASIC, this won’t affect you, since BASIC maintains its own pointer to LOMEM once it is initialized at power on. But if you return to DOS without MEM.SAV, or run some assembly language utility… well, there are just too many cases where this little faux pas can wipe you out.

I am currently working on a patch (ready by next month, I hope) to the handler (to be made via the handler loader) which will fix this problem. In the meantime, it might be a good idea to have your programs check for the existence of the “R” name in HATABS and avoid the appropriate amount of memory if it is found.

In December we’ll have some heavy assembly language stuff, what with the patch to the 850 handler and the cassette verify routine. I hope to return to some more BASIC stuff to start off the new year.

COMPUTE! ISSUE 31 / DECEMBER 1982 / PAGE 240

INSIGHT: Atari

Bill Wilkinson
Optimized Systems Software
Cupertino, CA

This month, I will follow through with at least one of my promises for some heavier assembly language stuff: the discussion and source for the fix to the 850 handler LOMEM problem. Unfortunately, I did not manage to complete the other promised project, the BASIC Cassette Verify program.

That program has proven more difficult to write than I had suspected it would, primarily because it’s hard to get the debugger and BASIC to cooperate. With some luck I will have the problem fixed very shortly.

In any case, I’ve also got a few little tidbits to share with you, so let’s tackle them first.

Atari-CP/M Revisited

First, I would like to clear up a misunderstanding (on my part) about the Vincent Cate (USS Enterprises) Atari-to-CP/M connection, mentioned a couple of issues ago. I stated that one problem with the system was that you would not be able to use standard Atari diskettes. Not totally true. If you have (or have access to) an Atari compatible 810 drive, you can copy programs from the 810 to the CP/M host. (Vincent claims that the system is even capable of properly simulating self-booting disk games, etc., though I would imagine that some of the heftier protection schemes might defy his standard system.)

Anyway, the address for USS Enterprises is 6708 Landerwood Lane, San Jose, CA 95120. I hope this doesn’t seem too much like an ad or endorsement: I have not used the system. I have, however, heard from people who have and who say it does what it claims to do.

In the same column, I mentioned a new product to be introduced soon which would function either as an Atari disk controller (810 emulator) and/or as a CP/M system in which the Atari console was a smart terminal. That project is apparently at the reality stage, so I guess in fairness I should now mention it by name.

The company producing the product is Software Publishers, Inc., of Arlington, Texas. (I know, I know. Software publishers?) The base price of the controller, I have been told, is about $500 without disk drive. The CP/M add-on will be (is?) about $250. Perhaps someone will soon give us a review of the viability of this concept.

Double No-Trouble

Speaking of viability: We have been using our Percom drives (one double density, one double sided and double density) for about three months now. We are more than satisfied with their reliability. And, of course, the new OS/A+ we produced for use on the larger drives allows considerable flexibility. Perhaps the Atari can be used as a business machine after all.

And to be sure that we don’t slight anyone, I need to mention that our MPC double density system has been here about a month now also and seems to be working fine.

So far, all the things we’ve tried seem better for most purposes than the 810 drives, though all of them seem to have trouble with some heavily protected diskettes. Moral: buy the drive, forget the diskettes. (Side issue and pet peeve: If it’s that heavily protected, it will have trouble even on a slightly out of speed Atari 810. So far, I have plunked down my scarce dollar only three times for copy-protected disks. I think I will try to be thriftier in the future.)

Percom DOS

By now it should be general knowledge that the “new and improved DOS” that Percom has been publicizing is none other than OS/A+. But it is a significant change from our “old” OS/A+. which is really just a CP/M-like keyboard interface hooked to the Atari DOS 2.0S File manager. Thanks to the efforts of Mark Rose, our youngest associate and a junior at Stanford University, we have managed to produce an all new, random access DOS designed to interface to any and all disk drives from 128 kilobytes to 16 megabytes. The “random access” description implies that you are not tied to the tyrany of NOTE any more (and POINT is now reasonable: you POINT to a byte position within a file, just like on the big guys’ systems, and better than CP/M),

This may sound like an advertisement for OSS and Percom, but it really isn’t. First of all, our profits aren’t really tied to the sales of this new DOS, so it isn’t really an ad for us. And second, it appears that OS/A+ will be used by all the other Atari-compatible drive manufacturers, so Percom is offering it first but not alone. Anyway, the real reason I brought this up (aside from wanting to pat Mark Rose on the back in public) is to pass on a few of the things that you should watch out for if you are thinking of moving to either more or larger drives.

LOMEM On The Tot-Mem Poll

I am sadly dismayed to see so many Atari-produced and Atari-compatible products being introduced nowadays which violate one of the prime rules for running on an Atari: don’t put anything lower in memory than LOMEM.

After all, the operating system provides these nice, convenient locations LOMEM and HIMEM, which contain the addresses of the bottom and top of usable memory. Why not use them?

But no, let us assume that we will run under Atari DOS 2.0S, with two single density drives, with our blinders on (so that we cannot see the future). Phooey. How about a little table to show the values of LOMEM under various DOS configurations, with various numbers of drives and files available?

LOMEM With Various DOS’s

  Dos            Number     Number   Contents
  Used           Of Drives  Of Files Of LOMEM

  Atari DOS 2.0S  2-S        3         $1C00
  Atari DOS 2.0S  4-S        7         $1F00
  Atari DOS 2.0S  2-D        3         $1E80
  Atari DOS 2.0S  2-S, 2-D   5         $2180
  Atari DOS 2.0S  4-D        7         $2380
  OS/A+ ver 2.0   2-S        3         $1F00
  OS/A+ ver 2.0   4-S        7         $2100
  OS/A+ ver 2.0   4-D        7         $2680
  OS/A+ ver 4.0   2-D        3         $2C00
  OS/A+ ver 4.0   4-DD       7         $3300

legend:-S means single density drives
       -D means double density drives
       -DD means double sided, double density

Surprised? It gets worse: if you load the RS-232 handler for the 850 Interface Module, you must add almost $700 to all the table figures! (And I left out K-DOS simply because I don’t know the correct figures there, but I understand that they are all over $3000.)

“But,” you say, “how come you show Atari DOS with double density drives?” Aha! You didn’t know that Atari DOS will handle double density drives for most user programs? (The menu can get confused, especially for duplicating disks, but BASIC—for example—runs just fine.)

We agonized a long time over coming out with OS/A+ version 4, the Percom (et al.) random access DOS, with its much higher LOMEM values. But then we realized that, given that you will use double density and larger disks, there is simply no way to stay completely compatible. So, if you’re going to do it, do it right.

Incidentally, Percom’s initial patches to Atari DOS 2.0S solved the problem in a different way: they moved the disk buffers to the top of memory and dropped HIMEM. Of course, then they ran into trouble with the programs that ignore HIMEM, Like BASIC A+? Wellllll, I guess we have to take our lumps, too. Sigh. But we’re working on it, honest.

So this has gone on long enough. The moral: if you’re writing assembly language programs, pay attention to the rules. If you’re stuck with an interpreter or compiler that does it wrong, go yell at the company that palmed it off on you.

Mishandler

Since I am ranting on about LOMEM anyway, let’s tackle the problem I presented last month: the Atari RS-232 handler for the 850 Interface Module does not handle the RESET key properly when the disk device (or other previously loaded handlers) is present.

The result is that LOMEM will be reset to what the disk handler thinks it is, rather than above the 850’s driver. And, of course, this means that any program which uses LOMEM properly will zap the RS-232 (Rn:) drivers. Which might not be so bad except that the Rn: name will still be recognized by CIO. Which might be a real disaster.

Why did all this come about? Because Atari didn’t follow their own advice. When you steal DOSINI from DOS, in order to link yourself into the RESET chain, the first thing you should do is call the old DOSINI. Instead, the 850 handler does all its initializing, resets LOMEM to above itself, and then calls the old DOSINI! (And, of course, poor old FMS doesn’t know that R: exists, so it moves LOMEM to just above itself. And, admittedly, you could fix the problem by having DOS change LOMEM only if the change is upward. This is left as an exercise to the reader.)

So what do we do about this bug? If you are using BASIC (or BASIC A+), forget about it. BASIC maintains its own LOMEM pointer, which is initialized only at BASIC coldstart time (e.g., at power-up). In fact, many system programs either do similar things or have been purposely assembled in higher memory to avoid all possible drivers. (Except see that good old table. Maybe they aren’t all high enough?)

However, if you need to fix this problem, chances are you need to fix it quickly and thoroughly. The machine language program below seems to do a reasonably good job of patching the mess. But, of course:

Caveats: (1) This program works as shown with my 850 Interface Module. I know for a fact that Atari has made more than one version of this beast, so I can not guarantee it will work on yours. (2) This program works by patching the AUTORUN.SYS (also known as AUTORUN.232 or RS232.OBJ or RS232.COM) file. If you are not using Atari DOS (or OS/A+, for RS232.OBJ or RS232.COM), then this will work only if you can load and execute this routine at the addresses shown in the listing.

So how does this program work? To understand it, we must first understand how the Rn: handler is loaded from the 850.

Here I Am

When the Atari computer is powered up, it finds out if a disk drive is attached by sending out a status request command (via SIO). If, indeed, disk drive number one is alive and well, then the disk boot proceeds. But if the 850 is alive and well, it is also sitting on the serial bus, looking at SIO sending status request command(s) to the disk. SIO will try 13 times to boot the disk before giving up. But here is where the 850 gets sneaky: if the disk doesn’t answer after about ten of those tries, the 850 jumps on the bus and says “Here I am! I’m the disk drive! Boot me!”

And, of course, the computer indeed “boots” the disk—whether it actually is the drive’s controller chip responding or whether it is an 850 in chip’s clothing. And that’s how those 1800 or so bytes of code get into the computer when all you have is an 850.

But how does that code get pseudo-booted when you do have a disk? Well, one way would have been to distribute the handler on the disk.

But why waste all that good code sitting out in the 850, just waiting to be executed? So AUTORUN.SYS (in any of its aliases) is a very small routine that performs just the right operations to load the 850’s serial handlers.

In building the program presented here, I have cheated. Quite frankly, I have not investigated why and how the code used in AUTORUN.SYS works. And quite franker, I don’t care. What I have done is simply build my program around that code. And here’s what my program does.

First, I get the current contents of DOSINI (presumably the address of the FMS initialization routine) and save them for later use. Then I fall through and let the 850’s code be loaded and initialized. If this process is successful, I then find the new contents of DOSINI (the Rn: driver’s initialization routine address) and save them also. And where do I save the two initialization addresses? In the middle of the patch to be applied to the 850 driver.

Then all I need do is move the patch into the middle of the driver and relink DOSINI to point to the patch. Now, the cute part of all this is: where do we put the patch? Why, right on top of the erroneous call to the FMS initialization. (The one that occurs after the 850 init, remember?)

Ummm, but I’m patching a JSR to the FMS init followed by a JMP to the 850 init. How does all that fit into the space of one (previous) JSR? And what about the code immediately preceding the patch? Here it comes, the kludge. The code we are replacing includes a check of the warmstart location, since the handler does not bother to call the FMS initialization if it doesn’t need to. Well, with our code patch, the FMS always gets called to init itself. But so what? It doesn’t hurt anything, just slows the loading of this 850 interface code an unnoticeable amount.

Anyway, if you can follow the code, you will note where the patch is being applied. The byte immediately before the patch location must be a CLC instruction. (Check it out by loading the RS-232 handlers and then using a debugger to list the code.) If it is not, then your 850 differs too much from mine to use this routine as is. (And if you figure out where to patch it, why not tell all of us.)

Last but not least, notice that the patch is intrinsically relocatable, just as is the 850 handler. It should work in virtually any memory and/or disk drive and/or DOS configuration.

Whew! That was lengthy and heavy, right? Well, cheer up, there’s more to come next month. Like how to add a default drive specifier to Atari DOS and OS/A+. If you have two drives, wouldn’t it be convenient to be able to specify that meant “D2:…” once in a while? Watch this space.

Atari 850 Fixer Upper
or: when in doubt, punt.

0000        1010        .PAGE " or: when in doubt, punt."
            1020 ;
            1030 ; Some equates
            1040 ;
0043        1050 FIXOFFSET = $43       ; read the text
000C        1060 DOSINI =    $0C       ; the cause of all this
            1070 ;
            1080 ;
            1090 ; This first code is simply to save the original
            1100 ;  contents of DOSINI for later use, like the
            1110 ;  850 code should have done in  the first
            1120 ;  place. Sigh.
            1130 ;
0000        1140        *=  $3800-10
            1150 NEW LOADER
37F6 A50C   1160        LDA  DOSINI    ; presumably, we  are  saving
37F8 8D773B 1170        STA  PATCH2+1  ; the FNS init vector for
37FB A50D   1180        LDA  DOSINI+1  ; later use, but  the  beauty  of
37FD 8D7B38 1190        STA  PATCH2+2  ; this: it works  w/o  FWS also
            1200 ;
            1210 ;
            1220 ; Now we begin the original Atari loader code.
            1230 ;
            1240 ; If your code doesn't agree with this, it
            1250 ;  is possible that your 850's internal
            1260 ;  is different also. If so, apply the
            1270 ;  patches with caution. Read the text.
            1280 ;
            1290 ; CAUTION: this code is uncommented, simply
            1300 ;  because I'm not sure exactly what it
            1310 ;  is doing. But who cares...it works.
            1320 ;
3800        1330        *=  $3800  ;  where the Atari code was found
            1340 LOADER
3800 A950   1350        LDA  #$50
3802 8D0003 1360        STA  $0300
3805 A901   1370        LDA  #$01
3807 8D0103 1380        STA  $0301
380A A93F   1390        LDA  #$3F
380C 8D0203 1400        STA  $0302
380F A940   1410        LDA  #$40
3811 8D0303 1420        STA  $0303
3814 A905   1430        LDA  #$05
3816 8D0603 1440        STA  $0306
3819 8D0503 1450        STA  $0305
381C A900   1460        LDA  #$00
381E 8D0403 1470        STA  $0304
3821 8D0903 1480        STA  $0309
3824 8DQA03 1490        STA  $030A
3827 8D0B03 1500        STA  $030B
382A A90C   1510        LDA  #$0C
382C 8D0803 1520        STA  $0308
382F 2059E4 1530        JSR  $E459
3832 1001   1540        BPL  $3835
3834 60     1550        RTS
3835 A20B   1560        LDX  #$0B
3837 BD0005 1570        LDA  $0500,X
383A 9D0003 1580        STA  $0300,X
383D CA     1590        DEX
383E 10F7   1600        BPL  $3837
3840 2059E4 1610        JSR  SE459
3843 3006   1620        BMI  $384B
3845 200605 1630        JSR  $0506
3848 4C4C38 1640        JMP  FIXIT     ; this WAS a 'JMP (DOSINI)'
384B 60     1650        RTS
            1660 ;
            1670 ; Now the 850 has loaded its code into memory...
            1680 ; so we can patch its boo-boos 1690 ;
            1700 ;
            1710 ;
            1720 FIXIT
384C A50C   1730        LDA  DOSINI    ; The 850 code has patched
384E 8D7A38 1740        STA  PATCH3+1  ; its init entry point into
3851 A50D   1750        LDA  DOSINI+1  ; 'DOSINI' ... we will jump
3853 8D7B38 1760        STA  PATCH3+2  ; to it at the end of our patch
            1770 ;
3856 A043   1780        LDY  #FIXOFFSET ; for my 850! read the text
3858 A200   1790        LDX  #0        ;  loop index
            1800 ;
            1810 ; We move our patch code into the 850's code
            1820 ;
            1830 PATCHLP
385A BD7538 1840        LDA  PATCH1,X  ; a byte of patch...
385D 91X    1850        STA  (DOSINI),Y ; into the 850 code
3B5F C8     1860        INY           ; next patchloc
3860 EB     1870        INX           ; next byte of patch
3861 E008   1880        CPX  #8       ; unless done
3863 D0F5   1890        BMC  PATCHLP
            1900 ;
3865 A944   1910        LDA  #FIXOFFSET+1 ; again, caution...read text
3867 18     1920        CLC
3868 650C   1930        ADC  DOSINI    ; we move DOSINI to point
386A 850C   1940        STA  DOSINI    ; to our patch...which in
386C A50D   1950        LDA  DOSINI+1  ; turn will jump back to
386E 6900   1960        ADC  #0        ; the 850's init code.
3870 8SOD   1970        STA  DOSINI+1
            1980 ;
3872 6C0C00 1990        JMP  (DOSINI)  ; and this actually goes to our patch!
            2000 ;
            2010 ;
            2020 ; This patch area has two addresses placed
            2030 ; in it and then it is moved en masse
            2040 ; into the 850 code, as a patch thereto
            2050 ;
            2060 PATCH1
3875 60     2070        RTS            ; gets rid of some unneeded code
            2080 PATCH2
3876 200000 2090        JSR  0         ; becomes JSR FMSINIT, or some such
            2100 PATCH3
3879 4COOOO 2110        JMP  0         ; to original reset point
387C 00     2120        BRK
            2130 ;
            2140 ; This is just to make it a LOAD AND GO file
            2150 ;
            2160 ; You might wish to use 52E2 instead if you
            2170 ; understand the implications thereof
            2180 ;
387D        2190        *=  $2E0
02E0 F637   2200        .WORD NEWLOADER
02E2        2210        .END