In This Issue...
About five minutes ago, about two hours before this issue goes to the printer, our UPS driver delivered one review copy of Programming the 65816, by David Eyes and Ron Lichty. IT'S HERE! And it looks excellent. I have time and space for only a few words here; we expect to have a complete review of this book and Fischer's next month.
Eyes and Lichty have given use over 600 pages of introduction, architecture, tutorial, application, and reference information, including a 60-page chapter listing and describing in detail the source code for a rudimentary 65816 Tracer/Debugger. This book has complete information on ALL the 65x family processors, from the original 6502 up through these latest 16-bit versions.
We expect delivery of our inventory within the next week or two and we'll start shipping your copies as soon as we get them. Remember, our price is $21 + shipping.
Bob's new QUIT code for ProDOS is nice! There is one minor error at line 3390, which causes an extra character to be printed. Change "BNE .1" to "BPL .1" and it will work better. The extra character was only noticeable when the filename was 15 characters long, or had been RENAMEd to a shorter name than it previously had, because filenames are normally filled out with null characters.
You can also save one byte by changing line 4290 from "JMP MSG" to "BNE MSG". The BNE will always branch in this case, and is one byte shorter than the JMP.
In the July 1986 issue of Dr. Dobb's Journal, Robert Pirko gave an algorithm and a program to calculate the square root of a 16-bit integer. (Letter, pages 10-12; program, pages 60-66.) His program was written in 8088 assembly language for the IBM PC. I decided to try writing the same algorithm in 6502 code.
As with most square root algorithms, Pirko's depends on the so-called Newton-Raphson iteration. In simple algebra, this means that a "pretty good" approximation to the square root of N can be made "much better" by the formula:
1 N much better = ---( pretty good + ----------- ) 2 pretty good
How many times you have to use the formula depends on how close your first guess is, and how much precision you need. By the definition of "integer square root", we are looking for the largest 8-bit integer whose square is less than or equal to the 16-bit argument N. Therefore, we need only 8-bit precision.
The formula refines the answer rapidly, roughly doubling the number of precise bits each time. So if we start with a close enough initial guess, once through the formula might be plenty.
What Pirko did was experiment to find a set of rules to get a "good enough" initial guess. Once through Newton-Raphson refined the guess to 8-bit precision, except that the truncation errors of integer arithmetic resulted in some roots being too large by 1. By squaring the calculated root and comparing the original number, Pirko could decrement those which needed it.
Pirko's program was pretty fast. He tested speed by taking the square roots of all integers from 0 to 65535, with the average time being 91 microseconds on his IBM PC (he didn't say which version, or what clock frequency). Any time I see a timed program like this, my benchmarking blood begins to bubble. However, the Apple is not as fast this time, because of the need for a division and a multiplication. The 8088 has opcodes for multiply and divide, while the 6502 does not.
Here are Pirko's rules for obtaining an initial guess:
Argument Range Root or Initial Guess --------------------------------- 0000-0001 root = N 0002-00FF guess = N/16 + 3 0100-0FFF guess = Nhigh*4 + 13 1000-7FFF guess = Nhigh + 50 8000-D7FF guess = Nhigh + 40 D800-FEFF guess = 255 FF00-FFFF root = 255
After playing with various versions of the program, I settled on a slightly different set of rules. The average time was reduced a little by changing the third and fourth ranges:
0100-08FF guess = Nhigh*4 + 13 0900-7FFF guess = Nhigh + 50
It is not difficult to separate out the seven cases and calculate the initial guess. Lines 1130-1460 in the program below do all that work. Lines 1480-1530 perform the Newton-Raphson formula once. Lines 1540-1630 decrement the root if its square exceeds the original argument.
Since the 6502 does not have any multiply or divide opcodes, I coded these two operations as subroutines. My division subroutine divides the 16-bit value in ARGHI,ARGLO by the 8-bit value in GUESS, leaving the answer in QUOT and REM. My multiplication subroutine multiplies the 8-bit value in the A-register by the 8-bit value in ROOT to get a 16-bit product in PROD.
The program named MT (lines 3390-3850) does a complete test of the square root subroutine. MT calls on SQR for each possible argument from 0 through 65535 ($FFFF), then tests the value of ROOT by squaring it. If the square is larger than the argument, there was an error. If not, I increment the root and square it again. If this square is not larger than the argument, there was an error. If either type of error occurs, I display the argument and the root.
Once I was sure SQR was producing correct answers, I wrote a timing routine (lines 2110-2430) which runs through all values 0-65535 ten times. By changing line 1190 to "1190 SQR RTS" I timed the overhead of the 655360 calls (13.2 seconds). When I changed line 1190 back, the additional time was 273.3 seconds. This is an average of 417 microseconds per root, over 4 times slower than the IBM program.
I wanted to see what the 6502 could do IF it had multiply and divide opcodes in hardware. Assuming each would take no more than 12 cycles, I changed lines 1740 and 1940 to RTS's. The resulting time averaged 74.3 microseconds per root. If only we had DIV and MUL, we could easily beat the PC in this benchmark.
We can come a little closer by playing a dirty trick. Since we know that the test program runs through all the arguments in sequence, we know that the squaring operation will usually be doing the same number over and over. For example, the root $FE comes out of the Newton-Raphson formula over 500 times! By giving the MUL routine a memory, we can save the root squared last time. Lines 1950-1970 are all we need to add. Simply remove the asterisks and re-assemble. With this trick, the average root took only 248 microseconds. We can also save about 24 microseconds per root by putting the MUL and DIV subroutines "in-line", eliminating the JSR and RTS (with the dirty trick, in-line code only saves an additional 12 microseconds).
Some time ago, Technical Education Research Centers (TERC) published information on the Hitachi 6203 multiply/divide chip and how to interface it to the Apple. We mentioned this in AAL a few years back. That chip allows you to code the division in my program to run in 33 cycles, and the squaring in 30 cycles. The result would still be a little slower than the PC. By the way, all I know about the 6203 I got from TERC's newsletter. I have never been able to get any information whatsoever out of Hitachi about it, and I have never actually seen the chip. I sure would like to! (If you are interested in TERC, try calling Bob Tinker or his associates at (617) 547-0430, or write to TERC, 1696 Massachusetts Avenue, Cambridge, MA 02138.)
I am not finished yet. I spent another half day investigating a different technique for getting the integer square root. This is a binary adaptation of the mysterious method we learned and forgot in high school. Can you remember how we did it? I know at least a few of you can, because you are in high school right now. And others, because you are high school teachers. Anyway, it is too difficult to explain in these few lines. The same algorithm in binary is pretty straightforward. I present it in lines 2490-3020, without much comment. It works, but it takes an average of about 880 microseconds per root. The test routine in lines 3040-3350 takes the square root of evey value from 0-65535 using both SQR and NEWSQR, and compares the results. If they are different, it prints the data.
It is now nearly a month since I wrote all the preceding paragraphs in this article. Today the latest Dr. Dobbs Journal arrived, and a pair of letters referred to some old articles about integer square roots. I looked them up, and learned an easier way to apply the "high school" method. The resulting 6502 program won't fit in this issue, but is included on the Monthly and Quarterly Disks. This version takes an average of 737 cycles per root. It looks like a 65802 native mode version would be significantly faster.
Naturally, I expect at least one of you to come up with a significantly better and faster program than any of mine.
1000 *SAVE S.INTEGER.SQRT 1010 .LIST CON 1020 *-------------------------------- 1030 ARGLO .EQ 0 1040 ARGHI .EQ 1 1050 GUESS .EQ 2 1060 QUOT .EQ 3 1070 REM .EQ 4 1080 ROOT .EQ 5 1090 PROD .EQ 6,7 1100 TRIPS .EQ 8 1110 PREVIOUS.ROOT .EQ 9 1120 BITHI .EQ 10 1130 BITLO .EQ 11 1140 SUBHI .EQ 12 1150 SUBLO .EQ 13 1160 WORKHI .EQ 14 1170 WORKLO .EQ 15 1180 *-------------------------------- 1190 SQR 1200 LDA ARGHI 1210 BMI .1 32768 OR MORE 1220 BNE .2 256...32767 1230 LDA ARGLO 1240 LSR 1250 BEQ .8 ...ARG= 0 OR 1 1260 LSR 1270 LSR 1280 LSR 1290 ADC #3 (2...5 IS OKAY HERE 1300 BNE .4 ...ALWAYS 1310 *---32768...65535---------------- 1320 .1 CMP #255 1330 BEQ .7 (A)=255=ROOT 1340 ADC #40 (35...44 OKAY) 1350 BCC .4 1360 LDA #255 1370 BNE .4 ...ALWAYS 1380 *---256...32767------------------ 1390 .2 CMP #$09 1400 BCS .3 $0900...$7FFF 1410 ASL $0100...$08FF 1420 ASL 1430 ADC #13 1440 BNE .4 ...ALWAYS 1450 *---$0900...$7FFF---------------- 1460 .3 ADC #49 ADDS 50 (50...61 OKAY) 1470 *---DO NEWTON ONCE--------------- 1480 .4 STA GUESS 1490 JSR DIV 1500 CLC 1510 LDA QUOT 1520 ADC GUESS 1530 ROR 1540 *---SQUARE THE RESULT------------ 1550 STA ROOT 1560 JSR MUL 1570 *---DECREMENT ROOT IF TOO BIG---- 1580 LDA ARGLO 1590 CMP PROD 1600 LDA ARGHI 1610 SBC PROD+1 1620 LDA ROOT 1630 SBC #0 DECREMENT IF TOO BIG 1640 *-------------------------------- 1650 .7 STA ROOT 1660 RTS 1670 *-------------------------------- 1680 .8 ROL RESTORE 0 OR 1 1690 BCC .7 ...ALWAYS 1700 *-------------------------------- 1710 * DIVIDE (ARGLO,ARGHI) BY (GUESS) 1720 * LEAVE ANSWER IN QUOT,REM 1730 *-------------------------------- 1740 DIV 1750 LDY #8 1760 LDA ARGLO 1770 STA QUOT 1780 LDA ARGHI 1790 .1 ASL QUOT 1800 ROL 1810 BCS .15 1820 CMP GUESS 1830 BCC .2 1840 .15 SBC GUESS 1850 INC QUOT 1860 .2 DEY 1870 BNE .1 1880 STA REM 1890 RTS 1900 *-------------------------------- 1910 * MULTIPLY (ROOT) BY (A-REGISTER) 1920 * PUT RESULT IN PROD,PROD+1 1930 *-------------------------------- 1940 MUL 1950 *** CMP PREVIOUS.ROOT "DIRTY TRICK" 1960 *** BEQ .7 ditto 1970 *** STA PREVIOUS.ROOT ditto 1980 STA GUESS 1990 LDA #0 2000 LDY #8 2010 .5 LSR GUESS 2020 BCC .6 2030 CLC 2040 ADC ROOT 2050 .6 ROR 2060 ROR PROD 2070 DEY 2080 BNE .5 2090 STA PROD+1 SAVE HI-BYTE OF SQUARE 2100 .7 RTS 2110 *-------------------------------- 2120 T 2130 LDA #10 2140 STA TRIPS 2150 LDA #0 2160 STA ARGHI 2170 STA ARGLO 2180 STA PREVIOUS.ROOT 2190 *-------------------------------- 2200 .1 JSR SQR 2210 .DO 0 2220 LDA ARGHI 2230 JSR $FDDA 2240 LDA ARGLO 2250 JSR PRB 2260 LDA ROOT 2270 JSR PRB 2280 LDA PROD+1 2290 JSR $FDDA 2300 LDA PROD 2310 JSR PRB 2320 JSR $FD8E 2330 .FIN 2340 *-------------------------------- 2350 INC ARGLO 2360 BNE .1 2370 INC $7F7 2380 INC ARGHI 2390 BNE .1 2400 INC $7F5 2410 DEC TRIPS 2420 BNE .1 2430 RTS 2440 *-------------------------------- 2450 PRB JSR $FDDA 2460 LDA #" " 2470 JMP $FDED 2480 *-------------------------------- 2490 NEWSQR 2500 LDY #8 Loop 8 times for an 8-bit root 2510 LDA ARGHI 2520 STA WORKHI Save working copy of argument 2530 LDA ARGLO 2540 STA WORKLO 2550 LDA #0 SUB0 = $4000 2560 STA SUBLO BIT0 = $4000 2570 STA BITHI 2580 LDA #$40 2590 STA SUBHI 2600 STA BITHI 2610 *-------------------------------- 2620 .1 SEC Trial subtraction 2630 LDA WORKLO 2640 SBC SUBLO 2650 TAX Save lo-byte of difference 2660 LDA WORKHI 2670 SBC SUBHI 2680 BCC .2 ...WORKi < SUBi 2690 STA WORKHI Save new value for WORK 2700 STX WORKLO 2710 .2 PHP Save carry status (next ROOT bit) 2720 ROL ROOT ROOT = ROOT*2 + CARRY 2730 LDA SUBHI SUB = (SUB .EOR. BIT)/2 2740 EOR BITHI 2750 LSR 2760 STA SUBHI 2770 LDA SUBLO 2780 EOR BITLO 2790 ROR 2800 STA SUBLO 2810 PLP 2820 BCC .3 ...WORK was less than SUB 2830 LDA SUBHI SUB = SUB .EOR. BIT 2840 EOR BITHI 2850 STA SUBHI 2860 LDA SUBLO 2870 EOR BITLO 2880 STA SUBLO 2890 .3 LSR BITHI BIT = BIT/4 2900 ROR BITLO 2910 LSR BITHI 2920 ROR BITLO 2930 LDA SUBHI SUB = SUB .EOR. BIT 2940 EOR BITHI 2950 STA SUBHI 2960 LDA SUBLO 2970 EOR BITLO 2980 STA SUBLO 2990 *-------------------------------- 3000 DEY 3010 BNE .1 3020 RTS 3030 *-------------------------------- 3040 TT 3050 LDA #0 3060 STA ARGHI 3070 STA ARGLO 3080 STA PREVIOUS.ROOT 3090 *-------------------------------- 3100 .1 JSR NEWSQR 3110 LDA ROOT 3120 PHA 3130 JSR SQR 3140 PLA 3150 CMP ROOT 3160 BEQ .11 3170 PHA 3180 .DO 1 3190 LDA ARGHI 3200 JSR $FDDA 3210 LDA ARGLO 3220 JSR PRB 3230 LDA ROOT 3240 JSR PRB 3250 PLA 3260 JSR PRB 3270 JSR $FD8E 3280 .FIN 3290 *-------------------------------- 3300 .11 INC ARGLO 3310 BNE .1 3320 INC $7F7 3330 INC ARGHI 3340 BNE .1 3350 RTS 3360 *-------------------------------- 3370 * COMPLETE TEST OF ALL POSSIBLE ARGUMENTS 3380 *-------------------------------- 3390 MT 3400 LDA #0 3410 STA ARGLO 3420 LDA #$00 3430 STA ARGHI 3440 *-------------------------------- 3450 .1 JSR SQR 3460 LDA ROOT 3470 JSR MUL 3480 LDA ARGLO 3490 CMP PROD 3500 LDA ARGHI 3510 SBC PROD+1 3520 BCC .9 ROOT TOO LARGE 3530 INC ROOT 3540 BEQ .2 3550 LDA ROOT 3560 JSR MUL 3570 DEC ROOT 3580 LDA ARGLO 3590 CMP PROD 3600 LDA ARGHI 3610 SBC PROD+1 3620 BCS .9 ROOT TOO SMALL 3630 *-------------------------------- 3640 .2 INC ARGLO 3650 BNE .1 3660 INC ARGHI 3670 LDA ARGHI 3680 LSR 3690 LSR 3700 LSR 3710 LSR 3720 ORA #"0" 3730 STA $7F7 3740 LDA ARGHI 3750 BNE .1 3760 RTS 3770 *-------------------------------- 3780 .9 LDA ARGHI 3790 JSR $FDDA 3800 LDA ARGLO 3810 JSR PRB 3820 LDA ROOT 3830 JSR $FDDA 3840 JSR $FD8E 3850 JMP .2 3860 *-------------------------------- 3870 ERRHI .EQ 16 3880 ERRLO .EQ 17 3890 *-------------------------------- 3900 * METHOD DERIVED FROM 68000 CODE IN DDJ MAY 85 3910 * 6502 VERSION AVERAGES 737 CYCLES 3920 *-------------------------------- 3930 SQR3 LDA ARGHI Save working copy of argument 3940 STA WORKHI 3950 LDA ARGLO 3960 STA WORKLO 3970 LDA #0 3980 STA ROOT Start with ROOT = 0 3990 STA ERRHI and ERR = 0 4000 STA ERRLO 4010 *-------------------------------- 4020 LDY #8 8 pairs of bits in argument 4030 .1 ASL WORKLO Two bits out of WORK into ERR 4040 ROL WORKHI 4050 ROL ERRLO 4060 ROL ERRHI 4070 ASL WORKLO 4080 ROL WORKHI 4090 ROL ERRLO 4100 ROL ERRHI 4110 ASL ROOT ROOT = ROOT*2 4120 LDA ROOT BIT = ROOT*2 4130 ASL 4140 STA BITLO 4150 LDA #0 4160 ROL 4170 STA BITHI 4180 LDA ERRLO (CARRY IS CLEAR) 4190 SBC BITLO COMPUTE: ERR-BIT-1 4200 TAX SAVE LO DIFFERENCE 4210 LDA ERRHI 4220 SBC BITHI 4230 BCC .2 ERR < BIT 4240 STA ERRHI 4250 STX ERRLO 4260 INC ROOT ROOT = ROOT+1 4270 .2 DEY 4280 BNE .1 4290 RTS 4300 *-------------------------------- |
I am not sure how it happened, but I seem to have botched up the table on page 20 of the November 1985 issue. As I now understand it, the relationship between the PRODOS file image (which loads at $2000) and the image of ProDOS after it is loaded is as follows (the lines marked with * are the changed lines):
2000-287E ProDOS Installer Code 287F-28FE zeroes 28FF-293C Installer for /RAM Driver 293D-29FF zeroes 2A00-2BFF Aux 200-3FF /RAM/ Driver * 2C00-2C99 FF00.FF99 /RAM/ Driver 2C7F-2CFF zeroes * 2D00-4DFF DE00-FEFF MLI Kernel 4E00-4EFF BF00-BFFF System Global Page * zeroes D700-DDFF 4F00-4F7C D742-D7BE Thunderclock driver 4F80-4FFF FF80-FFFF Interrupt Code * 5000-56FF D000-D6FF Device Drivers 5700-59FF Alt D100-D3FF QUIT Code
Looking at the same information from the viewpoint of the finished product, here is a map of ProDOS after it is loaded:
4E00-4EFF BF00-BFFF System Global Page * 5000-56FF D000-D6FF Device Drivers * zeroes D700-DDFF 4F00-4F7C D742-D7BE Thunderclock driver * 2D00-4DFF DE00-FEFF MLI Kernel * 2C00-2C99 FF00-FF99 /RAM/ Driver 4F80-4FFF FF80-FFFF Interrupt Code 5700-59FF Alt D100-D3FF QUIT Code
Back in March, 1984 Bob S-C wrote up a modification to the DOS VERIFY command processor to make that command do a hex dump of a file. This can be a very useful tool, giving us the ability to see exactly what's stored in any file of any type, so of course I wanted to include it in the DOS I've been building for the UniDisk 3.5. In his version Bob provided a 40-column hex-only display and invited the reader to expand it to 80 columns with ASCII as well. That's what I've done here, as well as adding pause and abort features. I didn't get as far as his suggestion to allow paging through the file either backwards or forwards, so there's still something left for you tinkerers.
As written here DUMP runs in 158 bytes of page 3. I got involved in making it better, rather than making it smaller, so it still won't fit inside of DOS yet. Maybe next time around.
There is a side effect of modifying VERIFY. Both SAVE and BSAVE call VERIFY after they're done, to make sure all went well. The first time I got this patch working and then SAVEd the file I got a nice dump all over the screen at the end of the SAVE. That's why I ended up having the DUMP command call a little routine that patches into the VERIFY code, calls it, and then de-patches itself. This means that if you RESET out of a DUMP, or if you get an I/O ERROR, the DUMP patch will still be in place and you'll probably want to disconnect it. You can do that either by doing a successful DUMP command, or by calling the disconnect routine directly ($329G or CALL 809).
So, on to the code. By the way, I use a couple of 65C02 opcodes in this program, but the equivalent 6502 instructions are shown {in curly brackets} in the comment field.
INSTALL points the command vector to my routine and changes the command name from VERIFY to DUMP. Normally when you change a DOS command name the new name must have the same number of letters as the old one, but since VERIFY is the last name in the list we can truncate it and fill the ending bytes with zeroes.
PATCH is called when we type the DUMP command. Here we patch into the VERIFY processor to call DUMP in place of the call to get the next sector of the file. It then calls VERIFY as usual and then puts it back. This way we can get the sector and display it when it's our command, but leave the normal VERIFY operation undisturbed so SAVE and BSAVE can function normally.
DUMP first does the GET.SECTOR call we patched in over, exiting when we hit the end of the file. The next step is to prepare the display so we print a blank line and display the relative sector number in the file. Note that this is not a disk track or sector number; this is the position in the track/sector list. We then set the Y-register to the beginning of the sector buffer and branch into the code to display one line.
The line display routines starting at line 1600 normally begin by printing four spaces to allow for the sector number printed on the first line. After that we print the offset into the sector from the Y-register and a separating dash. Notice that the PRINT.BYTE and PRINT.DASH routines also output a trailing space. Now it's time to get the next 16 bytes from the file, stash them in a buffer for later ASCII printing, and display them in hexadecimal. (The stash buffer is inside the secondary filename buffer, which is used only during a RENAME command.) The odd code in lines 1730-1780 has the effect of printing an extra space after every four bytes, to separate the display into columns and improve readability.
The last steps in displaying a line are to recover the 16 bytes from the buffer, make them printable and replace any control characters with underlines, and send them on out. (Most programs seem to replace control characters with periods, but I chose the underline so I could more easily see the real periods in the file. Underlines are much rarer in typical text.) After that we check the keyboard for a pause or abort and check to see if we've finished the sector yet. If necessary we branch back and do the next line.
PAUSE is lifted straight from the SHOW command article in AAL July, 1982. As a matter of fact, since I always install SHOW I just call PAUSE at $AE8E rather than including it in DUMP. This is a very useful routine to keep around.
1000 *SAVE S.DUMP 1010 .OP 65C02 1020 *-------------------------------- 1030 * 1040 * Patch DOS to change 1050 * VERIFY into DUMP 1060 * 1070 *-------------------------------- 1080 POINTER .EQ $42 1090 VECTOR .EQ $9D54 1100 VERIFY .EQ $A27D 1110 VFY.NAME .EQ $A902 1120 BUFFER .EQ $AAA0 1130 READ.CALL .EQ $AD1C 1140 GET.SECTOR .EQ $B0B6 1150 SECTOR .EQ $B5E4 1160 KEYBOARD .EQ $C000 1170 STROBE .EQ $C010 1180 MON.PRNTAX .EQ $F941 1190 MON.PRBL2 .EQ $F94A 1200 MON.CROUT .EQ $FD8E 1210 MON.PRBYTE .EQ $FDDA 1220 MON.COUT .EQ $FDED 1230 *-------------------------------- 1240 .OR $300 1250 * .TF B.DUMP 1260 *-------------------------------- 1270 INSTALL LDA /PATCH-1 point DOS vector table 1280 STA VECTOR+1 to my patch 1290 LDA #PATCH-1 1300 STA VECTOR 1310 LDX #5 1320 .1 LDA COMMAND,X change VERIFY command 1330 STA VFY.NAME,X name to DUMP 1340 DEX 1350 BPL .1 1360 RTS 1370 COMMAND .AT /DUMP/ 1380 .HS 0000 1390 *-------------------------------- 1400 PATCH LDA #DUMP hook into VERIFY command 1410 STA READ.CALL 1420 LDA /DUMP 1430 STA READ.CALL+1 1440 JSR VERIFY call it 1450 LDA #GET.SECTOR restore normal VERIFY 1460 STA READ.CALL 1470 LDA /GET.SECTOR 1480 STA READ.CALL+1 1490 RTS 1500 *-------------------------------- 1510 DUMP JSR GET.SECTOR read next sector 1520 BCS .7 end of file 1530 JSR MON.CROUT start sector with <CR> 1540 LDA SECTOR+1 1550 LDX SECTOR 1560 JSR MON.PRNTAX display sector position 1570 LDY #0 start at beginning of sector 1580 BEQ .2 ...always 1590 1600 .1 LDX #4 print 4 blanks 1610 JSR MON.PRBL2 so columns look neater 1620 .2 TYA 1630 JSR PRINT.BYTE print byte count 1640 JSR PRINT.DASH separator 1650 LDX #15 16 bytes per line 1660 .3 LDA (POINTER),Y get byte from file 1670 STA BUFFER,X stash it 1680 JSR PRINT.BYTE print as hex value 1690 INY next byte 1700 DEX 1710 BMI .4 done with this line? 1720 1730 TXA check X 1740 INC {EOR #%00000011} 1750 AND #%00000011 1760 BNE .3 every fourth byte 1770 JSR PRINT.SPACE skip a space 1780 BRA .3 {JMP .3} 1790 1800 .4 JSR PRINT.DASH separator 1810 LDX #15 16 bytes 1820 .5 LDA BUFFER,X get stashed value 1830 ORA #%10000000 hi-bit on 1840 CMP #" " 1850 BCS .6 filter out controls 1860 LDA #"_" substitute "_" 1870 .6 JSR MON.COUT print as ASCII 1880 DEX 1890 BPL .5 back for more 1900 1910 JSR MON.CROUT next line 1920 JSR PAUSE check for pause or 1930 BEQ .7 abort 1940 CPY #0 done with sector? 1950 BNE .1 1960 CLC normal exit 1970 RTS 1980 .7 SEC EOF or abort exit 1990 RTS 2000 *-------------------------------- 2010 PRINT.DASH 2020 LDA #"-" 2030 JSR MON.COUT 2040 BRA PRINT.SPACE {JMP PRINT.SPACE} 2050 PRINT.BYTE 2060 JSR MON.PRBYTE 2070 PRINT.SPACE 2080 LDA #" " 2090 JMP MON.COUT 2100 *-------------------------------- 2110 * RETURN .EQ. IF ABORT 2120 * .NE. IF CONTINUE 2130 *-------------------------------- 2140 PAUSE LDA KEYBOARD any key pressed? 2150 BPL .2 no, continue 2160 STA STROBE yes, clear strobe 2170 CMP #$8D abort? 2180 BEQ .2 yes, return .EQ. status 2190 .1 LDA KEYBOARD no, pause 'til keypress 2200 BPL .1 none pressed yet 2210 STA STROBE clear strobe 2220 CMP #$8D abort? 2230 .2 RTS .EQ. if abort 2240 .LIF |
We borrowed a Laser-128 (popular clone of the Apple //c) the other day. It had been rumored that our software would not run on it, in spite of Central Point Software's sanguine claims. Sure enough, the S-C Macro Assembler would not operate, under either DOS or ProDOS. They boot and load, but no more.
A little investigation revealed what we expected: our software uses at least a half-dozen entry points into the Apple monitor which are not supported in the Laser-128 monitor. Most of them have to do with our "$" command, which lets you perform monitor commands without leaving the S-C environment. These patches will disable the "$" command and repair the "MEM" command. The addresses shown are for our current release disks.
DOS 3.3 $1000 version 1AE6:4C B3 1B 20 40 F9 A9 AD 4C ED FD 124A:E9 1A (was 99 FD) 125D:E9 1A (was 99 FD) DOS 3.3 $D000 version DAE6:4C B3 DB 20 40 F9 A9 AD 4C ED FD D24A:39 DA (was 99 FD) D25D:E9 DA (was 99 FD) ProDOS version 8B45:4C 24 8C 20 40 F9 A9 AD 4C ED FD 8450:48 8B (was 99 FD) 8463:48 8B (was 99 FD)
Make a backup copy of the disk, and then boot the backup copy. When the assembler version you choose has loaded, type the letter X and the RETURN key. This should BRK out of the assembler into the Laser-128 monitor. Make the patches as shown above, and then type "3D0G" or control-RESET to get back into the assembler. It should be working correctly now. If you are fixing the DOS 3.3 version, you can now BSAVE the patched code on the file you originally loaded.
If you are fixing the ProDOS version, you now should BLOAD the type SYS file called SCASM.SYSTEM. The same patches you just made to the assembler should now be applied to the image of the SYS file, and then BSAVE the image on the disk:
:BLOAD SCASM.SYSTEM,TSYS,A$2000 :MNTR *2D45:4C 24 8C 20 40 F9 A9 AD 4C ED FD *2650:48 8B *2663:48 8B *3D0G :BSAVE SCASM.SYSTEM,TSYS,A$2000,L17920
One incompatibility remains for which we never found the cause: the esc-L shorthand command, to turn a CATALOG line into a LOAD command, does not work in 80-column mode. It does work just fine in 40-column mode. If any of you try these patches and find other problems, we would like to hear about them.
One more item: we found the Laser-128 monitor incorrectly disassembles the PLX command as PHX.
In the January '86 AAL, Bob presented a routine to convert lo-res to regular hi-res. He pointed out that "it is not possible to exactly reproduce the lo-res colors on the hi-res screen (unless I used //e or //c double hi-res)." Bob's routine used patterns to "come close to the same color." He also wrote that he didn't have a color monitor and didn't know how close he got. I have an RGB color monitor, and patterns result in most unsatisfactory conversion. This article offers three satisfactory conversion routines that, of course, use double hi-res.
Regular color double hi-res, which will work on a color TV, a composite color monitor, or an RGB monitor (mode 2 or 3), has a resolution of 140 by 192. Lo-res has a resolution of 40 by 48. Since 140 is not evenly divisible by 40, regular color double hi-res is not capable of displaying a satisfactory representation of a lo-res picture. You can reproduce all sixteen colors, but you can't get satisfactory resolution of 40 lo-res columns. An RGB monitor, driven by an AppleColor (or compatible) card, can display a PERFECT double hi-res version of a lo-res picture two different ways. Two of my routines perform these color conversions and work ONLY for RGB. Without RGB they look lousy. The third routine, included for completeness, performs a monochrome conversion and will work on an RGB monitor (mode 1) or a monochrome monitor.
I'm writing this article so I may present examples of how the RGB-only video modes work. The two color routines use modes that are not described in the AppleColor card manual. The AppleColor card comes with demo disks that show foreground/ background hi-res; a Video-7 demo disk I have shows "160 Mode". Except for these two simple demos and a collection of programs which I have written, I know of no programs, commercial or otherwise, using these modes. By the way, while I do have a //c, I don't have an RGB interface for it, so I don't know if Video-7's (or anyone else's) //c RGB Adapter supports these modes or is compatible with the AppleColor card. I would be interested in hearing from any of you on these subjects at: 41 Putnam Park Road, West Redding, CT 06896.
F/B hi-res works much like monochrome regular hi-res. Main memory bytes define the screen at seven pixels each (bit 7 is ignored - no half shifting), but instead of each 1 bit producing an "on" pixel and each 0 bit an "off" pixel, each 1 bit produces a "foreground" pixel and each 0 bit a "background" pixel. Auxiliary memory bytes define the F/B colors of the seven pixels at the same address in main memory. The high nybbles contain the foreground colors and the low nybbles the background colors. For example, aux $2000:12 and main $2000:55 would produce magenta, dark blue, magenta, dark blue, magenta, dark blue, and magenta beginning at the top left. In effect you get 280x192 resolution with sixteen colors! Of course, since F/B colors are defined for each group of seven pixels, not each pixel, you can only get two colors per seven pixels.
With "160 Mode", the "given" of using only seven bits of every byte for pixel information is gone. All eight bits of both the auxiliary and main memory pages are used. It seems (to me) to be more closely related to lo-res than hi-res, only "sideways" and with sixteen times the resolution. Low nybbles define the even columns and high nybbles the odd columns, while in lo-res the correspondence is between nybbles and rows. Auxiliary memory pairs precede main memory pairs. For example, aux $2000:21 and main $2000:43 would produce: magenta, dark blue, purple, and dark green beginning at the top left. Effective resolution is 160x192 with sixteen colors and no restrictions!
Both the RGB modes offer superior pictures compared to regular hi-res and (regular) double hi-res. They also offer other advantages important to programmers. We can manipulate F/B hi-res images pixel data separately from the color data. "160 Mode" images can be drawn in 80 different horizontal positions without any shifting, and the other 80 are only a 4 bit shift (or look up) away using whole bytes. Best of all, the bits that define a colored pixel don't ever fall in different bytes!
The three routines may be tested by entering at labels "a", "b" and "c". These are extensions the demo "T" in Bob's program. I have also kept his "PLOT" and "PAUSE.FOR.ANY.KEY" routines. I've rearranged Bob's code to handle some //e (and //c) stuff, the double hi-res video modes, etc. You should refer back to his article, and compare our code. One thing that I didn't carry forward from Bob's code is his stepping through the screen memory using Cartesian coordinates. My routines crank along sequentially. The video mode switching is all handled by these test routines.
F/B mode is active any time the 80 column switch is off and annunciator 3 is off. The other double hi-res RGB modes are activated by clocking two data bits and a "1" bit into a shift register on the AppleColor card. The value of the data bits determine which mode is activated. The data bits and the "1" bit are the setting of the 80 column switch at the time the shift register is clocked. The shift register is clocked by lowering the annunciator 3 switch.
ConvertLoResToFBHiRes first sets the main memory pixel buffer to a pattern of 24 black horizontal regions (0, "off") with 24 white bars (1, "on") interspaced. The "off" (excuse me, background) areas correspond to the even lo-res rows, and the "on" (foreground) to the odd rows. This works out to filling main $2XXX w/$00 and main $3XXX w/$7F. By using this pattern, I avoided having to manipulate the lo-res data before storing it in auxiliary memory. To understand this you must remember that even lo-res rows are stored in low nybbles and odd lo-res rows in high nybbles. After setting up main memory, the lo-res data is expanded 8:1 into the auxiliary memory color buffer. For example: $400 gets copied to aux $2000, $2400, $2800, $2C00, $3000, $3400, $3800, and $3C00. If this is viewed on a non-RGB monitor, all you'll see is 24 bars.
ConvertLoResToDoubleHiRes160 expands the lo-res data nybbles 16:1. Each lo-res byte is split into low and high nybbles. Each nybble is in turn duplicated in the other half of 8 bits and then copied into the main and auxiliary memory locations that correspond to the lo-res pixel the nybble used to be. For example: the low nybble of $400 gets copied to both nybbles of aux $2000, $2400, $2800, $2C00, and main $2000, $2400, $2800, $2C00; the high nybble of $400 gets copied to both nybbles of aux $3000, $3400, $3800, $3C00, and main $3000, $3400, $3800, $3C00. If you look at this on a monochrome monitor you'll see only 7/8ths of the data; it looks strange. Viewed on a non-RGB color screen, you'll get a mess of wrong colors.
The last routine, ConvertLoResToDoubleHiRes560, draws double hi-res monochrome patterns (NOT color patterns). I carefully studied the patterns lo-res makes when viewed on a monochrome monitor, writing down the constituent binary numbers. From that, I typed in the table "Monos", extending the patterns to include both even and odd offsets, and dividing them into auxiliary and main bytes. Once the table was defined, the routine just fell together ("automatically")! It uses each nybble and its Y-index LSB (NOT its Y-coord, but it works) to look up patterns from "Monos", and stores them into double hi-res memory.
If you would like to look at an attempt to convert lo-res to regular color double hi-res, you can change the monochrome routine to perform in color. First you should move the STA Column80On in "c" up four lines so that it is before the first STA AN3Off instead of the last one. This turns on mode 2 instead of mode 1. You will also need to include two more lines in subroutine ".2" of "ConvertLoResToDoubleHiRes560", and add a small table. I would put the table just before "Monos". Because the sample colors drawn by "Plot" (being 4 pixels wide) do convert correctly, be sure that there is a lot of text on the screen when you run the modified code. Change as follows:
.2 TAX LDA Spin,X ROL (.2 moved up from this line) ...rest of s/r... Spin >hs 00.08.01.09.02.0A.03.0B >hs 04.0C.05.0D.06.0E.07.0F
I've put the F/B hi-res mode to use many times to run programs like Asteroid Field (an oldie, but a goodie) and MousePaint on my RGB monitor. These programs were intended to be viewed on a monochrome monitor and, without help, they display unintended colors on RGB. By setting up F/B hi-res to display white on black (grey on brown looks good too) and then booting, I save having to turn on the monochrome NEC and turning my head to the left while playing (or painting). Here's how:
*C001:0 N C055:0 N C057:0 N 400:F0 N 401<400.4000M C005:0 N 4000<2000.4000M C004:0 N C054:0 N C000:0 N C05E:0 N 6^P or *1/400:f0 n 1/401<1/400.6000m c05e 6^p (EDM ONLY)
These Monitor commands also write auxiliary text page 1. This is because there is a F/B 40-column text mode too!
1000 .LIST MOFF 1010 *SAVE dcj.Lo.to.HiRes 1020 LBas .eq $26,$27 1030 HBas .eq $2A,$2B 1040 ctr .eq $2E,$2F!!! 1050 Color .eq $30 1060 Store80Off .eq $C000 1070 Store80On .eq $C001 1080 Column80Off .eq $C00C 1090 Column80On .eq $C00D 1100 ReadColumn80 .eq $C01F 1110 MainPage .eq $C054 1120 AuxPage .eq $C055 1130 AN3Off .eq $C05E 1140 AN3On .eq $C05F 1150 *-------------------------------- 1160 a JSR Plot 1170 PHA save 40/80 state 1180 .1 JSR Pause2 1190 STA AN3Off (double (?) hi-res on) 1200 LDA $C057 hi-res (RGB F-B Hi-Res) 1210 JSR ConvertLoResToFBHiRes 1220 JSR Pause 1230 BNE .1 branch always 1240 *-------------------------------- 1250 b JSR Plot 1260 PHA save 40/80 state 1270 .1 JSR Pause2 1280 STA Column80On RGB MODE "4" 1290 STA AN3Off (160 Res 16 Color 1300 STA AN3On Double Hi-Res) 1310 STA Column80Off 1320 STA AN3Off 1330 STA AN3On 1340 STA Column80On 1350 STA AN3Off (double hi-res on) 1360 LDA $C057 hi-res 1370 JSR ConvertLoResToDoubleHiRes160 1380 JSR Pause 1390 BNE .1 branch always 1400 *-------------------------------- 1410 c JSR Plot 1420 sty $c07e //c... 1430 PHA save 40/80 state 1440 .1 JSR Pause2 1450 STA AN3Off RGB MODE 1 1460 STA AN3On (560 Res Monochrome 1470 STA AN3Off Double Hi-Res) 1480 STA AN3On 1490 STA Column80On 1500 STA AN3Off (double hi-res on) 1510 LDA $C057 hi-res 1520 JSR ConvertLoResToDoubleHiRes560 1530 JSR Pause 1540 BNE .1 branch always 1550 *-------------------------------- 1560 Pause2 LDA $C056 lo-res 1570 STA Column80Off 1580 STA AN3On (double hi-res off) 1590 LDA /$400 1600 STA LBas+1 1610 STA ctr page counter too 1620 Pause inc $400 lo-res active mark rqrd for "a" 1630 LDA $C000 wait for any key 1640 BPL Pause ...not yet 1650 STA $C010 clear strobe 1660 CMP #$8D clear Z-flag 1670 BNE .2 if NOT <return> 1680 PLA pop return address 1690 PLA 1700 STA AN3On (double hi-res off) 1710 LDA $C051 text 1720 PLA recover 40/80 state 1730 BMI .1 --->was 80-col 1740 STA Column80Off rqrd for "b" and "c" 1750 STA Store80off too 1760 RTS w/80 store off for 40-cols 1770 .1 STA Column80On / 1780 .2 RTS w/80 store and 80-cols on 1790 ConvertLoResToFBHiRes 1800 LDA /$2000 hi-res page 1 1810 STA HBas+1 1820 tya LDA #$00 even GR rows all 0 (background) 1830 JSR .3 1840 LDA #$7F odd GR rows all 1 (foreground) 1850 JSR .3 1860 .1 JSR RBSC picks lo-res too 1870 LDX #8 hi-res lines/lo-res row pairs ctr 1880 BIT AuxPage fill aux mem w/color nybbles 1890 .2 JSR NxtH store hi-res F-B pair/row pairs 1900 DEX 1910 BNE .2 1920 BIT MainPage 1930 INY 1940 BNE .1 1950 INC LBas+1 1960 DEC ctr 1970 BNE .1 loop for whole screen 1980 RTS 1990 .3 LDX #$10 page counter 2000 .4 STA (HBas),Y write main pixels 2010 INY 2020 BNE .4 2030 INC HBas+1 2040 DEX 2050 BNE .4 2060 RTS 2070 ConvertLoResToDoubleHiRes160 2080 .1 JSR RBSC picks lo-res too 2090 AND #$0F isolate even row nybble 2100 STA Color 2110 LSR line-up to left of msb 2120 JSR .2 2130 LDA (LBas),Y pick again 2140 AND #$F0 isolate odd row nybble 2150 STA Color 2160 JSR .2 2170 INY 2180 BNE .1 2190 INC LBas+1 2200 DEC ctr 2210 BNE .1 loop for whole screen 2220 RTS 2230 .2 ROR 2240 ROR 2250 ROR 2260 ROR 2270 ORA Color dup in other nybble 2280 LDX #4 hi-res lines/lo-res row ctr 2290 .3 BIT AuxPage 2300 STA (HBas),Y write aux $2XXX (or $3XXX) 2310 BIT MainPage 2320 JSR NxtH write main $2XXX (or $3XXX) too 2330 DEX 2340 BNE .3 2350 RTS 2360 * and for completeness: 2370 ConvertLoResToDoubleHiRes560 2380 .1 TYA 2390 ROR "column" lsb to carry 2400 PHP save for odd rows 2410 JSR RBSC picks lo-res too 2420 AND #$0F isolate even row nybble 2430 JSR .2 2440 LDA (LBas),Y pick again 2450 LSR isolate odd row nybble in LSN 2460 LSR 2470 LSR 2480 LSR 2490 PLP recover "column" lsb 2500 JSR .2 2510 INY 2520 BNE .1 2530 INC LBas+1 2540 DEC ctr 2550 BNE .1 loop for whole screen 2560 RTS 2570 .2 ROL double, merging "column" bit 2580 ASL double again for 4 bytes each 2590 TAX and shove it in an index reg 2600 LDA #4 hi-res lines/lo-res row ctr 2610 STA ctr+1 another counter loc... 2620 .3 BIT AuxPage 2630 LDA Monos,X 2640 STA (HBas),Y write aux $2XXX (or $3XXX) 2650 BIT MainPage 2660 LDA Monos+1,X 2670 JSR NxtH write main $2XXX (or $3XXX) too 2680 DEC ctr+1 should I have used Color? 2690 BNE .3 2700 RTS 2710 .ma hs ".gen (off)" macro 2720 .hs ]1 2730 .em 2740 Monos >hs 00.00.00.00 0--black 2750 >hs 11.22.44.08 1--magenta 2760 >hs 22.44.08.11 2--dark blue 2770 >hs 33.66.4C.19 3--purple 2780 >hs 44.08.11.22 4--dark green 2790 >hs 55.2A.55.2A 5--grey (1) 2800 >hs 66.4C.19.33 6--meduim blue 2810 >hs 77.6E.5D.3B 7--light blue 2820 >hs 08.11.22.44 8--brown 2830 >hs 19.33.66.4C 9--orange 2840 >hs 2A.55.2A.55 A--grey 2 (tan!) 2850 >hs 3B.77.6E.5D B--pink 2860 >hs 4C.19.33.66 C--light green 2870 >hs 5D.3B.77.6E D--yellow 2880 >hs 6E.5D.3B.77 E--aquamarine 2890 >hs 7F.7F.7F.7F F--white 2900 Plot LDA #$CC start @ top-right 2910 LDY #16 2920 .1 CLC 2930 LDX #3 2940 STA Color 00, 44, 88, CC 2950 .2 DEY 2960 STA $400,Y GR rows 0-3 2970 STA $480,Y 2980 ADC #$11 11, 55, 99, DD 2990 STA $500,Y GR rows 4-7 3000 STA $580,Y 3010 ADC #$11 22, 66, AA, EE 3020 STA $600,Y GR rows 8-11 3030 STA $680,Y 3040 ADC #$11 33, 77, BB, FF 3050 STA $700,Y GR rows 12-15 3060 STA $780,Y 3070 LDA Color 00, 44, 88, CC 3080 DEX 3090 BPL .2 3100 ADC #-$44 end, 00, 44, 88 3110 BCS .1 ...more 3120 com... STY HBas Y-reg zero! 3130 STY LBas 3140 LDA $C052 solid (40 x 48 pixels) 3150 STA Store80On page 1 (main) assumed 3160 LDA $C050 graphics 3170 LDA ReadColumn80 3180 RTS 3190 RBSC LDA LBas+1 Bob's trick 3200 EOR /$2000^$400 3210 STA HBas+1 3220 LDA (LBas),Y pick lo-res even-odd row pair 3230 RTS 3240 NxtH STA (HBas),Y store hi-res... 3250 INC HBas+1 new-old trick 3260 INC HBas+1 3270 INC HBas+1 3280 INC HBas+1 3290 RTS 3300 *-------------------------------- |
This morning I turned on my phone answering machine to play back the overnight messages. An anonymous caller calmly said, "This is an anonymous tip about the the BRUN problem under DOS. You can just BLOAD the program. Then CALL 41876, and the program will run properly and return nicely. You don't have to know the exact load address."
I checked it out, and it looks like our caller is correct. The BRUN processor inside DOS looks like this:
A38E- JSR BLOAD A391- JSR IOHOOK A394- JMP ($AA72)
By doing the complete BLOAD first, we have solved the problem of the trailing carriage return being printed after running our BRUNnable code. By the CALL 48176 (48176=$A394), we solve the problem of not knowing where the code loaded. In effect we have created a new BRUN command, which leaves out the call to IOHOOK and completes its own echoing before executing our code.
Thank you, Mr. Anonymous, whoever you are!
These are three new instructions for pushing data on the stack which need more explanation than you find in the data sheet of these new chips. Furthermore, some mis-information has gotten out about them.
All three push two bytes of data on the stack. Furthermore, it makes no difference whether you are in Emulation or Native mode, except for a minor problem which I will mention later.
PEA (Push Effective Address) pushes the two bytes which follow the PEA opcode onto the stack. It is, in effect, a "push 16-bit-immediate-value" instruction. Here is an example, with the equivalent in old-fashioned 6502 code also shown:
0800- F4 34 12 PEA $1234 0800- A9 12 LDA /$1234 0802- 48 PHA 0803- A9 34 LDA #$1234 0805- 48 PHA |
Notice that PEA takes 3 bytes and 5 cycles, while the equivalent 6502 code takes 6 bytes and 10 cycles.
If you were in native mode with the m-status-bit cleared (that is, with A-register in 16-bit mode), and forgot about the PEA opcode, you might do this:
0800- A9 34 12 LDA ##$1234 0803- 48 PHA
This approach takes 4 bytes and 7 cycles, and has the advantage or dis-advantage of leaving the data also in the A-register.
PEI (Push Effective Indirect) pushes two bytes from the direct page onto the stack. You might think of it as "push (dir)". The byte at dir+1 is pushed first, and then the byte at dir. Here is an example, with equivalent 6502 code:
Assume D=$0000, $0055 contains $34 $0056 contains $12 0800- D4 55 PEI $55 0800- A5 56 LDA $56 0802- 48 PHA 0803- A5 55 LDA $55 0805- 48 PHA |
PEI takes 2 bytes and 6 cycles, and the equivalent 6502 code takes 6 bytes and 12 cycles. In Native 16-bit mode, you could use LDA $55 to pick up both bytes, and a single PHA to push both of them. This would take 3 bytes and 8 cycles. PER (Push Effective Relative) is very similar to the PEA instruction. By analogy, PER is to PEA as BRL is to JMP. The processor adds the value in the two bytes following the opcode to the contents of the PC-register, and pushes the result (high-byte first) onto the stack. Before the addition takes place, the PC-register will have already been advanced to the first byte of the next instruction. Here is an example:
0800- 62 52 A2 PER $AA55 0803- |
The value of PC after reading the PER instruction will be $0803. Adding $0803 to $A252 gives $AA55.
There is an idiosyncrasy (bug?) in the 65802 and I presume in the 65816 in the emulation mode, having to do with instructions which push two bytes on the stack. The three instructions discussed here (PEA, PEI, and PER), as well as the PHD instruction, all exhibit this problem. If the stack pointer is at $00 (remember, in emulation mode the stack pointer is only 8-bits wide and always points into page 1), you would expect the two bytes to be stored at $0100 and $01FF. However, the processor stores the "high" byte at $0100 as it should, and then stores the "low" byte at $00FF instead of $01FF. The following code will prove it to you, if you have the chip in your computer:
LDX #$00 STX $FF STX $1FF TXS PEA $1234 LDA $FF JSR $FDDA LDA $100 JSR $FDDA LDA $1FF JSR $FDDA JMP $3D0 |
The program above will print out "341200". This means that the PEA instruction pushed the value $34 into $00FF, rather than $01FF. If you plan to use these instructions in emulation mode, be sure you are aware of this un-expected case.
Apple Assembly Line is published monthly by S-C SOFTWARE CORPORATION, P.O. Box 280300, Dallas, Texas 75228. Phone (214) 324-2050. Subscription rate is $18 per year in the USA, sent Bulk Mail; add $3 for First Class postage in USA, Canada, and Mexico; add $14 postage for other countries. Back issues are available for $1.80 each (other countries add $1 per back issue for postage).
All material herein is copyrighted by S-C SOFTWARE CORPORATION,
all rights reserved. (Apple is a registered trademark of Apple Computer, Inc.)