COMPUTE! ISSUE 20 / JANUARY 1982 / PAGE 120
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.
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.
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).
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.
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:
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.
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.
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).
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.
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.
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
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.
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.
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.
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:
|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
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 Type Description NMI Reset Button (the only uncontrollable interrupt) NMI Display List Interrupt NMI Vertical Blank Interrupt (60 times per second) IRQ BREAK key IRQ any other key IRQ Serial Input (for SIO communication with disk, etc.) IRQ Serial Output (ditto) IRQ Serial Transmission Completed (ditto) IRQ Timer #4 IRQ Timer #2 IRQ Timer #1 IRQ 6520 parallel port “A” IRQ 6520 parallel port “B” IRQ BRK 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.
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 value Name Description 255 ($FF) CMDR Repeat the entire sound command string 254 ($FE) CMDS Stop all sounds (do not end command string) 253 ($FD) CMDN Number of voices is specified in next byte (0 4) 252 ($FC) CMDTV Specify 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) CMDE End command, unhook from VVBLKD. Does not turn off sound, so is usually preceded by CMDS. any other … Any 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.
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.
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.
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
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
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.
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):
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.
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:
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:
(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.)
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:
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?
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.
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?)
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
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.
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.
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.
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.
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.
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 …
IF (A<>0) THEN …
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.
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.)
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
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.
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.
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?
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.
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
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.
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.
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)|
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.
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…”).
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
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
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.
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.
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.
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.
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.
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
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
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.
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).
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.
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.
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.
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
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…
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!
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.
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)
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.
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
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.
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.
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.
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!
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
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.
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.
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.
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.
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.
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.
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.)
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:
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
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.
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.
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.)
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.
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.
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.
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