Apple Assembly Line
Volume 5 -- Issue 9 June 1985

S-C Macro Assembler ProDOS

We will begin shipping the ProDOS version of the S-C Macro Assembler in July, so we are now accepting advance orders. There is more to the ProDOS version than just a change of operating systems. The new upgrade includes a couple of major new features:

    .INB (INclude Blocked) directive -- This works just like
         .IN, except that only one disk block at a time is 
         overlaid into memory. Allows assembly of much larger 
         files, with only a minor speed penalty.

    .AC  (Ascii Compressed) directive -- Generates compressed 
         strings from a string between delimiters, according 
         to rule tables.  Very complex, but worth the effort 
         if you have a lot of messages and need to save memory.

The price of the ProDOS version alone will be $100. The up- grade from DOS Version 2.0 to ProDOS will be $30. The upgrade from DOS Version 1.0 or 1.1 to ProDOS will be $50, and will include DOS Version 2.0. The initial purchase price of the DOS 3.3 and ProDOS versions together will be $120. These are introductory prices which may well be raised in a few months.

65802's Are Here!

After many months of manufacturing delays, Western Design Center is shipping 65802 and 65816 microprocessors. We recently received a final production '802, and it's now happily processing away in Bob's oldest Apple II (#219). You can order the chips from WDC for $95.00. Call (602) 962-4545.

Note About Alliance Computers

Back in January Alliance Computers advertised 65802's for $50.00, but couldn't fill the orders because the chips didn't exist yet. Some of their formerly unhappy customers tell me that their orders have now arrived, so Alliance is taking care of their customers. You can reach Alliance Computers at P.O. Box 408, Corona, NY 11368.

The Boyer-Morris String Search Algorithm Bob Bernard
Westport, CT

For years now, I have been working on a debugger for the Apple. Lately I have been adding a hex string search capability to it.

I needed one so I could look through the Apple IIc (ProDOS) utilites to see how it squirrels away in the alternate page screen holes user specified default settings for the serial ports. These are used at PR#1 or 2 time to simulate the dip switches on the Super Serial Card in a IIe. Without setting them you always get 9600 bps, etc. (Imagewriter settings, that is). I (and I assume other AAL readers) want a little routine for DOS 3.3 hello that will allow the user's defaults to be put away the same as the IIc utility does.

Well, that routine is not ready yet. However, the search utility is rather interesting in its own right.

I was just going to code up a straight hex search, but then I mentioned it to my computer science graduate son, David. He was horrified that I would waste my time on anything so crude. That's what I get for bringing up a programmer! David insisted that I should instead code an implementation of Boyer and Moore's algorithm, which appeared in the October 1977 issue of the Communications of the ACM. [A more recent reference is in the book "Algorithms", by Robert Sedgewick, (Addison-Wesley Publishing Co., 1983, 551 pages) on pages 249-252.]

Well, I read the article and it seemed like a challenge. Besides it looked like a real time saver, and could also be used for character string searches. The code here has been excerpted from my debugger, and then worked over by Bob S-C.

The "conventional" or "brute-force" search technique aligns the search pattern with the left end of the string to be searched through and compares one byte at a time, from left to right, until either the entire pattern is compared successfully or a mismatch occurs. In the latter case the search window is moved one byte to the right, and the comparing process is repeated.

Without any knowledge about the contents of the search pattern, the most the window can be moved is one place to the right. Boyer-Moore owes its speed advantage to the fact that it uses context (i.e. knowledge about the contents of the pattern to be searched for) to increase the distance that the search window can be advanced when a mismatch occurs. Thus efficiency increases as the length of the pattern increases, which does not happen in a conventional search.

The cost of this benefit (there always is a cost) is that a table (called DELTA1 in the CACM article and DELTA.TABLE in my program) is required to store this context information, 256 bytes in this implementation. One byte is needed in the table for every possible value of the characters in the string to be searched.

If a particular byte appears in the search pattern, then the corresponding DELTA table entry contains the distance that the rightmost occurrence of that byte is from the left end of the pattern. All other entries contain the value -1. When a mismatch occurs, the DELTA table entry corresponding to that byte from the text being searched is used to compute how far to advance the search window. If that byte does not appear anywhere in the pattern, then the search window can be advanced by the full length of the pattern.

Since moving the search window, and the associated testing for finished, take most of the time in any searching technique, saving time here can be extremely beneficial, and explains why Boyer and Moore should be complimented.

My program uses the control-Y monitor command, in the form

     adr1.adr2^Y <hexstring>

The two addresses specify the start and end of the area to be searched. "^Y" stands for "control-Y". The hex string may be separated from the control-Y by one or more spaces, if you desire. Since the control-Y doesn't show up on the screen, I usually type at least one space before the hex string. The hex string itself is a continuous string of hex digits, with no imbedded spaces. Here is an example that will search from $800 to $BFFF for "BERNARD":

     800.BFFF^Y 4245524E415244

The program will list the starting addresses of any and all complete matches that are found.

The maximum length of the hex string is limited by the monitor input buffer. Since the longest command you can type is less than 256, and you have to use around ten characters for the addresses and control-Y, that puts an upper limit of less than 246 hex digits in your command. Each byte of the search pattern (or "key") is made up of two hex digits, so the maximum hex string will be less than 123 bytes long.

I assigned DELTA.TABLE to the area $02D0.03CF. Since I scan and collect the search pattern right in the monitor keyboard buffer at $0200, after converting to hex bytes it will run no higher than $027F.

Actually, I only implemented a simplified version of Boyer and Moore's procedure. The CACM article also discusses a second table, DELTA2, which is filled with additional context information regarding "terminating substrings" of the search pattern. In cases where a partial mismatch occurs, it may be possible to advance the search window farther than the DELTA1 table would indicate. However, since such situations occur in less than 20% of the cases, David allowed that the potential additional speed did not justify the time and effort and the additional table and code space that would have been required, and he gave me a passing grade on my effort without it. The incorporation of this additional capability, and changes to make the program an ASCII search, are left "as a exercise for the reader."

My program must go through several steps. First it has to find and pack up the search key. Next it must build the DELTA table. And finally the search can be performed.

Lines 1290-1360 will be executed when you BRUN the program. They install the control-Y vector and jump into the monitor, just as though you entered with CALL-151.

When you enter the search command, the Apple monitor parses the command line up to and including the control-Y, and then branches to my code at line 1380. The two addresses will have been converted and stuffed into A1 ($3C,3D) and A2 ($3E,3F). A variable named YSAV (at $34) contains the index to the next character following the control-Y.

Lines 1400-1440 skip over any blanks you may have typed between the control-Y and the first hex digit. Actually, the Y-register gets incremented once too often, so lines 1460-1470 decrement Y and save it; now YSAV points to the first hex digit in the search key.

The next problem I had to solve was to differentiate odd from even length strings and arrange them properly, adding a leading zero when an odd number of hex digits is input. Lines 1490-1530 search for the end of the hex string; if there are no digits at all, we are finished and line 1530 returns for the next monitor command.

This is a nice place to insert a brief description of the NXTCHAR subroutine, found in lines 2460-2590. NXTCHAR picks up the next character from the input buffer, and tests to see if it is a hex digit. If so, it returns either $00-09 or $FA-FF in the A-register, and carry will be clear. If not a hex digit, it returns with carry set. If we got a digit, the Y-register indexing the input buffer will have been advanced.

Lines 1550-1590 compute the key length. Since two digits make a byte, the number of digits in the hex string divided by two gives the number of bytes. But I actually want to use the byte-count-minus-one. Also I need to adjust for odd or even length strings. Lines 1600-1650 take care of these details. If the count was odd, I jump into the middle of the packing loop so that a leading zero gets inserted.

Lines 1670-1800 comprise the packing loop. NXTCHAR will return with carry set when we try to get a digit beyond the end of the key, so line 1680 is the only test in the loop. Lines 1670-1730 retrieve a left-hand digit and store it in the buffer. Lines 1740-1800 do the same for right-hand digits. Key bytes are stored starting at $0200, so they never catch up to the advancing retrieval of digits.

Line 1810 sets YSAV to point to the first character past the end of the hex string. This will usually be a carriage return, or another monitor command. Unless it is beyond $2CF, the monitor will correctly continue parsing whatever is in the buffer when we are through searching. At $2D0 and beyond, the DELTA table will clobber any further characters.

Now we come to the Boyer-Moore part. Lines 1820-1870 initialize the DELTA table to all -1 values, which is what we want for any bytes not present in the key. When the loop finishes, X=0 again.

Lines 1880-1970 scan through the search key from left to right, and store into DELTA the index of the rightmost occurrence of each value in the key. For example, if the key is "4245524E415244" ("BERNARD" again), the DELTA values will be:

     DELTA+$41:  4
     DELTA+$42:  0
     DELTA+$44:  6
     DELTA+$45:  1
     DELTA+$4E:  3
     DELTA+$52:  5 (also at 2, but 5 is rightmost)
    all others:  -1

We'll continue with this example after a brief look at the rest of the code.

Lines 1980-2040 back up the end pointer, which has been patiently waiting all this time in A2L and A2H. We subtract the key length (in bytes, not digits) from the end pointer, so that we will not try to match the key any further than necessary. We could do this inside the search loop, but it will run faster if we do it once before the loop.

Lines 2050-2440 perform the search. I inserted lines 2070-2110 inside the loop to printout the search window start address each time through the loop. This helps me to make sure it is working, and to explain how. Of course you should remove these five lines before using the routine for real problems. Notice they are all marked "<<<DEBUG>>>".

Lines 2120-2170 check whether the beginning of the search window has moved past the end of the area to be searched. If so, we are finished.

Lines 2180-2240 compare bytes from the key and the search window. If the entire key matches, we fall out of the loop into lines 2250-2300, where the address of the match will be printed. After a successful match the search window will be moved one byte to the right by lines 2370-2430, and we will begin the SEARCH.LOOP again.

Notice that the key is compared from right-to-left, not left- to-right. This is a critical part of the Boyer-Moore method. If a key byte does not match a search-window byte, we branch to line 2320. The byte from the search window is in the A-register. Lines 2320-2370 compute how far we can advance the search window, based on just what character we DID find in the search window, and how far into the key we had already matched.

To see how this works, let's continue the "BERNARD" example. Suppose the text we are searching is "THERE ARE FEW ST. BERNARDS IN SAN BERNARDINO." The key will be BERNARD, entered in hex as shown above. We first try to match BERNARD at the beginning of the text. We start at the right end, matching the "D" of the key with "A" of the text. The match fails, so we look up the "A" value in the DELTA table, which is 4. We subtract the delta value (4) from the current key index (6) and add the result (6-4=2) to the search window address. Note that this has the result of aligning the "A" of BERNARD with the "A" in the text.

Back to the top, and we now try to match the "D" of BERNARD to the "E" at the end of "ARE". Failure again! This time the DELTA value is 1, and we are still at position 6 in the key: index-delta is 5, so we advance the window by 5. This lines up the "E" of BERNARD with the E of the text. The next attempted match will find a blank in the text, which does not occur in the key at all. The delta value for blank is -1: 6-(-1)=7, so we will advance the window by 7. Now the window is up to "ST. BER" in the text.

When we compare "D" of BERNARD to "R" in the text, we fail again. The delta value for R is 5. There are two R's in BERNARD, but the rightmost one is at index 5. We can move the search window by 6-5=1. Next we try "D" against "N". The delta value of "N" is 3, so we can move the window 6-3=3 bytes. This time we have found "BERNARD"!

If you count it all up, we have compared the "D" of BERNARD with only six characters, and already we are at the first occurrence of the whole key in the text. A conventional search would have tried to match the first character of the key ("B") with all 18 characters in the text which precede the first "B" of the text. We have saved 13 times around the main loop! Of course, our loop is a tiny bit longer, but the end result is faster.

Here is a step-by-step picture of the entire search, which finds BERNARD twice:

                       BERNARD  (success!)
                                       BERNARD  (success!)
                                               BER... (end)

I have tacked two more examples onto the end of the source code, at lines 2620-2690. You can play with them. The five <<<DEBUG>>> lines will print out the window address at each step, so you can see how the search progresses. Remember to take those lines out before you make a production version of the program.

If you decide to include this search algorithm in your own private debugger program, like I am, you might want to add the ability to use an ASCII string for the key. You could use a quotation mark after the control-Y to signal the packer loop that an ASCII string follows. You might also want to add single-byte wildcard characters, and/or the ability to ignore the high-order bit of each byte matched.

Perhaps the Boyer-Moore algorithm would be even more useful in a data base program, a word processor, or other context in which you are searching through huge quantities of text for relatively interesting keys. My example should get you started, and my son will be proud of you!

  1010 *--------------------------------
  1030 *      BY BOB BERNARD, MAY 17, 1985
  1040 *      MODIFIED BY BOB S-C, MAY 27TH
  1060 *      ("^Y" MEANS CONTROL-Y)
  1070 *
  1110 *
  1120 *--------------------------------
  1130 YSAV        .EQ $34
  1140 A1L         .EQ $3C,3D   START OF SEARCH AREA
  1150 A2L         .EQ $3E,3F   END OF SEARCH AREA
  1160 KEY.LENGTH  .EQ $40      (MONITOR'S A3L)
  1170 *--------------------------------
  1180 KBDBUF      .EQ $0200 THRU $2CF
  1190 DELTA.TABLE .EQ $02D0 THRU $3CF
  1200 USRADR      .EQ $03F8   CTL-Y JUMPS HERE
  1210 *--------------------------------
  1220 PRINTAX     .EQ $F941
  1230 CROUT       .EQ $FD8E   NEW LINE
  1240 MONZ        .EQ $FF69   MONITOR, NO BELL
  1250 *--------------------------------
  1260          .OR $0800  
  1270          .TF B.HEX.SEARCH 
  1280 *--------------------------------
  1290 HEX.SEARCH 
  1300          LDA #$4C       JMP OPCODE
  1320          LDA #SEARCH      LO ADR
  1330          STA USRADR+1
  1340          LDA /SEARCH      HI ADR
  1350          STA USRADR+2
  1360          JMP MONZ         MONITOR, NO BELL
  1370 *--------------------------------
  1380 SEARCH
  1390 *---SKIP LEADING BLANKS----------
  1400          LDY YSAV         NEXT VALID KBDBUF CHAR
  1410 .1       LDA KBDBUF,Y     GET CHAR FROM
  1420          INY              KEYBOARD BUFFER
  1430          CMP #" "         SKIP LEADING BLANKS
  1440          BEQ .1
  1450 *---MARK KEY START---------------
  1460          DEY
  1470          STY YSAV         WHERE SCAN STARTS
  1480 *---FIND END OF KEY--------------
  1490 .2       JSR NXTCHAR
  1500          BCC .2           ...HEX DIGIT
  1510          CPY YSAV         CHECK FOR NULL KEY
  1520          BNE .3           ...NOT NULL
  1530          RTS              NULL KEY
  1540 *---COMPUTE KEY LENGTH-----------
  1550 .3       TYA
  1560          SBC YSAV
  1580          LSR 
  1590          STA KEY.LENGTH
  1600          LDY YSAV
  1610          LDX #0
  1620          STX KBDBUF       (IN CASE ODD COUNT)
  1630          BCS .5           ...ODD NUMBER OF BYTES
  1640 *---ADJUST FOR EVEN LENGTH-------
  1660 *---LEFT NYBBLE------------------
  1670 .4       JSR NXTCHAR
  1680          BCS .6           END OF KEY
  1690          ASL
  1700          ASL
  1710          ASL
  1720          ASL
  1730          STA KBDBUF,X     LEFT HALF DEST CHAR 
  1740 *---RIGHT NYBBLE-----------------
  1750 .5       JSR NXTCHAR
  1760          AND #$0F
  1770          ORA KBDBUF,X     MERGE HI NIBBLE
  1780          STA KBDBUF,X
  1790          INX
  1800          BNE .4           ...ALWAYS
  1810 .6       STY YSAV
  1820 *---INIT ALL DELTAS=-1 ----------
  1830          LDX #0
  1840          LDA #-1
  1850 .7       STA DELTA.TABLE,X 
  1860          INX
  1870          BNE .7     ...256 OF THEM
  1880 *---DELTA(KEY(I))=I--------------
  1890          LDY #0           FOR I=0 TO KEYLEN
  1910          TAX                  OF RIGHT-MOST OCCURENCE OF
  1920          TYA                  8-BIT VALUE "K" IN KEY.
  1930          STA DELTA.TABLE,X
  1940          INY              NEXT I
  1950          CPY KEY.LENGTH
  1960          BCC .8
  1970          BEQ .8
  1980 *---ADJUST END OF SEARCH---------
  1990          SEC
  2000          LDA A2L
  2010          SBC KEY.LENGTH
  2020          STA A2L
  2030          BCS SEARCH.LOOP
  2040          DEC A2L+1
  2050 *--------------------------------
  2070        LDA A1L+1    <<<DEBUG>>>
  2080        LDX A1L      <<<DEBUG>>>
  2090        JSR PRINTAX  <<<DEBUG>>>
  2100        LDA #"-"     <<<DEBUG>>>
  2110        JSR $FDED    <<<DEBUG>>>
  2120          LDA A2L          CHECK AGAINST
  2130          CMP A1L           UPPER BOUND
  2140          LDA A2L+1         FOR SEARCH
  2150          SBC A1L+1
  2160          BCS .1           A1<=A2, NOT FINISHED
  2170          RTS              A1>A2, FINISHED
  2190 .1       LDY KEY.LENGTH   FOR I=KEYLEN TO 0
  2200 .2       LDA (A1L),Y      CHECK BYTES FROM
  2210          CMP KBDBUF,Y        RIGHT TO LEFT
  2220          BNE .3           ...DID NOT MATCH
  2230          DEY              NEXT I
  2240          BPL .2
  2250 *---MATCH FOUND------------------
  2260          LDX A1L          PRINT ADR
  2270          LDA A1L+1        WHERE MATCH
  2280          JSR PRINTAX         WAS FOUND
  2290          JSR CROUT        NEW LINE
  2300          JMP .4
  2310 *---ADVANCE SEARCH POINTER-------
  2320 .3       TAX              STRING CHAR JUST LOOKED AT 
  2330          TYA              I
  2340          CLC
  2350          SBC DELTA.TABLE,X   
  2360          BPL .5           ...VALUE IS POSITIVE
  2370 .4       LDA #0           ...ADVANCE BY 1
  2380 .5       SEC              COMPENSATE
  2390          ADC A1L
  2400          STA A1L
  2410          BCC SEARCH.LOOP
  2420          INC A1L+1
  2440          RTS              ...RAN OFF THE END OF MEMORY
  2450 *--------------------------------
  2460 NXTCHAR
  2470          LDA KBDBUF,Y     NEXT ACTIVE CHAR
  2480          INY
  2490          EOR #$B0         CONVERT ASCII TO DIGIT
  2500          CMP #10          0..9?
  2510          BCC .1           YES  
  2520          ADC #$88         SHIFT RANGE FOR A-F TEST
  2530          CMP #$FA         A..F?
  2540          BCS .1           YES. EXIT CC
  2550          SEC              NOT HEX CHAR
  2560          DEY              BACK UP INDEX
  2570          RTS
  2580 .1       CLC              SIGNAL HEX CHAR
  2590          RTS
  2600 *--------------------------------
  2610 END    .BS $A00-*
  2640 * TRY A00.A1F^Y  43414341434143
  2650 * SHOULD GET A09-A0B-A0D-A0F-A11-A13-A15-A17-A19
  2660 *--------------------------------
  2680 * TRY A20.A53^Y48494E47
  2690 *--------------------------------

Short Integer Square Root Subroutine Bob Sander-Cederlof

In some graphics situations you need a square root subroutine (it is probably the fault of Pythagoras). Since the screen coordinates are integers, a short and fast integer square root subroutine can be handy.

The following program is probably not in the "fast" category, but it is indeed short. It can produce the integer value of the square root of any integer from 0 through 65535. The program uses the method of subtracting successive odd numbers.

Every perfect square (N*N, where N is an integer) is the sum of a series of odd numbers from 1 through 2*N-1. Thus 4=1+3, 25=1+3+5+7+9, etc.

The program starts by subtracting 1, then 3, then 5, and so on until the remainder is negative. When the remainder goes negative, the last odd number subtracted was 2*N+1, so we can get the square root by dividing that odd number by 2.

I set up the routine so I could test it with an Applesoft pro- gram. You can POKE the low 8-bits of a number at 768 ($300), the high 8-bits at 769, and CALL 772. Upon return, PEEK(770)+ 256*PEEK(771) gives you the integer value of the square root.

I used a couple of tricks in the code. For one, the variable ODD is always an even number. Since I preface the subtraction with CLC, a "borrow" is assumed, so it has the effect of sub- tracting the odd number which is one larger than the even number in ODD. This save a LDA #1 instruction after line 1090.

In lines 1190-1230, I add 2 to the even number in ODD. But you can see that line 1200 is ADC #1. This adds 2 because carry happens to be set.

  1000 *SAVE S.SQRT16
  1010 *--------------------------------
  1020        .OR $300
  1030 ARG    .BS 2
  1040 ODD    .BS 2
  1050 *--------------------------------
  1060 SQRT   LDX ARG+1    X = HI BYTE   HI
  1070        LDY ARG      Y = LO BYTE   LO
  1080        LDA #0       START ODD=0
  1090        STA ODD+1
  1100 .1     STA ODD
  1110        CLC          BORROW ON, SUBTRACT (ODD+1)
  1120        TYA          LO
  1130        SBC ODD
  1140        TAY
  1150        TXA          HI
  1160        SBC ODD+1
  1170        TAX
  1180        BCC .2       ...ODD>REMAINDER, FINISHED
  1190        LDA ODD      CARRY SET, ADD 2 TO ODD
  1200        ADC #1
  1210        BCC .1       ...NEXT
  1220        INC ODD+1
  1230        BNE .1       ...ALWAYS     ...ALWAYS
  1240 .2     LSR ODD+1    SQRT IS (ODD/2)
  1250        ROR ODD
  1260        RTS
  1270 *--------------------------------

You can use the following Applesoft program to test the square root subroutine.

     1000  REM TEST SQRT16
     1005 W = 256:A = 768:B = 769:C = 770:D = 771:E = 772
     1010  FOR XH = 0 TO 255: PRINT XH"-";: FOR XL = 0 TO 255
     1030  POKE A,XL: POKE B,XH: CALL E
     1040 Y =  PEEK (D) * W +  PEEK (C)
     1050  IF Y <  >  INT ( SQR (XH * W + XL)) THEN  PRINT : PRINT XH * 
           W + L,Y
     1060  NEXT : NEXT 

Note on the TXS instruction in the 65802 Bob Sander-Cederlof

Sandy Greenfarb wrote the other day that he had received a 65802 and plugged it into his Basis 108 with success.

He has been trying various permutations of the new opcodes and modes, and discovered some stones are better left unturned:

"The following programs should both print the letter "A" on the screen. However, the one on the left works, while the one on the right hangs up the computer."
       Works           Hangs Up
       --------        --------
       CLC             CLC
       XCE             XCE
       LDA #"A         LDA #"A
       JSR $FDF0       JSR $FDED
       SEC             SEC
       XCE             XCE
       RTS             RTS

The only difference in the two programs is that the unsuccessful one weaves its way through DOS. I looked at the DOS code it goes through, and at first glance it appears there should be NO PROBLEMS associated with executing all this code in 65802 mode, since both 16-bit modes are off.

However, for some reason it still hangs up. Actually, it might not always hang: it depends on what is in page zero at the corresponding position as the stack pointer in page one.

I do not know why, but the TXS instruction transfers the entire 16-bit value of X to S when you are in the 65802 mode, regardless of the status of the M and X bits. Since M and X are both 1, the high byte of the X-register is 00. Therefore the TXS instruction at $9FB9 in DOS clears the high byte of the S-register. The RTS at $9FC4 then uses a return address from page zero, rather than page one.

I tried various experiments to see how TXS and TSX worked, and also examined TXA and TAX. In my humble opinion, the 65802 is inconsistent here. If you are in 65802 mode with M and X = 1, TXA does not modify the high byte of the A-register. This is what I expect and what I want. But TXS does modify the high byte of the S-register, contrary to my expectations.

Of course, as long as you know exactly how the chip works it really doesn't matter a lot. The problems come when we ASSUME we know how it works, but are wrong. The best antidote for these kind of assumptions, at least until a definitive reference manual for the chip is published, is trial and error.

I have had my 65802 for about six months now, and still have had no problems whatsoever with compatibility as long as I stay in normal 6502 mode. If I leave it in 65802 and go charging through a program written for the 6502 mode, I expect I will run into trouble.

Interrupt Trace Charles H. Putney
Dublin, Ireland

Have you ever wondered what's happening when the Apple goes off into nothingness? If your answer is yes, then this short utility will help you find out.

I was recently debugging an assembly language program and ran into this problem. The program seemed to work for almost all the input data, but occasionally would hang. After several frustrating hours trying to simulate the event, I decided that an interrupt trace utility would solve my problems. Later when I had this utility working, it was easy to see why the program was hanging.

This utility consists of a pushbutton addition to the Apple which connects to the interrupt request line (IRQ) of the 6502 and an interrupt service routine which is in page three. When the interrupt pushbutton is depressed, the interrupt service routine displays the program counter and all the registers on the bottom line of the screen. It also displays a flashing cursor and waits for an "S" or "G" from the keyboard to stop or resume execution.

I have mounted a pushbutton switch at the upper right hand side of the keyboard in the center of the styling surface. For a temporary installation I suggest leaving the pushbutton on a flexible lead. The wiring is easily done with 30 gage wire wrap wire. One side of the pushbutton is connected to ground. You may solder a wire to any convenient ground point on the top of the circuit board. Or, for a temporary installation, you could stick a wire into pin 8 of the game I/O connector.

The other side of the pushbutton is connected to the IRQ signal. I found that signal at pin 4 of the 6502. Remove the 6502 from the socket and strip the insulation from the end of the 30 gage wire. Insert it in the socket for the 6502 in pin 4 and replace the 6502 to retain the wire. Route the wire along the chips for a neat installation.

For a temporary hookup, Bob S-C suggests folding a 3-by-5 card in half, and triming it so that the folded edge just fits into an empty slot. Then, while power to the Apple is off, slip one wire into the space between the card and pin 26 (ground) and the other wire between the card and pin 30 (IRQ). Both of these wires will be on the power-supply side of the card: pin 26 is at the back edge, and pin 30 is the fifth from the back. Once the wires are inserted, you may wish to tape them down.

Enter the routine at address $300 and BSAVE it. When you want to debug a hanging program, first BRUN the INTERRUPT TRACE utility. This installs the utility at address $300 to $3CA. Pressing the pushbutton will cause an immediate display of the current program counter and registers. The utility will wait with a blinking cursor for a "G" or "S" from the keyboard to continue or enter monitor.

Sometimes the program you're investigating may not respond to the pushbutton. This is because somewhere in the program interrupts have been disabled with the SEI command ($78). You must search through the entire program and replace these with a CLI instruction ($58). Make sure that each $78 found is not data in the program and is a valid instruction before you replace it.

The next time that you have a problem with your Apple "hanging" for no apparent reason, use this utility to see where the 6502 "is". It may help solve those "hard to debug programs".

When you run the program, the SETUP routine (from $300 to $30B) sets the interrupt vector location and then enables interrupts. When the pushbutton is depressed, the IRQ line (pin 4 on the 6502) is pulled low. At the completion of the current instruction, the program counter high, program counter low, and processor status are pushed on the stack. Interrupt disable is automatically set and the program counter is loaded with the contents of $FFFE and $FFFF. In the Autostart monitor ROM the program counter is set to $FA40 where the monitor interrupt service routine is located. (In the old monitor the identical routine is at $FA86) This routine saves the accumulator in $45 and examines the processor status register to see if the inter- rupt was caused by a BRK command. Remember, the BRK command shares the same vector location with the interrupt for software simulation of interrupts. If the interrupt was not caused by BREAK then a JMP indirect to location ($3FE) is performed.

Lines 1280-1290 save the X- and Y-registers. The accumulator has already been saved by the monitor interrupt routine.

Lines 1300-1350 copy the register display titles to the bottom of the screen. Of course, if your program happened to be in one of the full-screen graphics modes, this line will not be visible. If you have a //e, you can add code to sense the graphics mode, save it, switch to text mode; then you will have to restore it all when you type "G" to continue after the interrupt. The new enhanced //e ROMs automatically handle saving and restoring all the bank switched memory, but they still leave the graphics modes up to the programmer.

Lines 1360-1510 convert the values of the five registers and store them into the bottom line. I add 3 to the S-register value before displaying it, so you see the value before the IRQ code pushed PC and S onto the stack. I start with the Y-register pointing at the point on the bottom line where the A-register should be displayed. The DISPLAY.HEX subroutine advances the Y-register by 5, so it is always ready for displaying the next register.

Lines 1520-1590 display the PC-register. This value is taken from the stack, where the IRQ automatically saved it.

Lines 1600-1750 wait for you to type "G" or "S". While waiting, the last character on the bottom line is flashed to remind you to type. If you type "G", lines 1760-1800 restore the registers and return to the interrupted program. If you type "S", line 1820 takes you to the monitor.

  1010 *--------------------------------
  1030 *
  1040 *      BY: CHARLES H. PUTNEY
  1050 *          18 QUINNS ROAD
  1060 *          SHANKILL
  1070 *          COUNTY DUBLIN
  1080 *          IRELAND
  1090 *--------------------------------
  1100 A.REG       .EQ $45      A-REG SAVE AREA USED BY MONITOR
  1110 STACK       .EQ $100     STACK PAGE
  1140 *--------------------------------
  1150 KEYBD  .EQ $C000    KEYBOARD DATA
  1170 MNTR   .EQ $FF69    MONITOR ENTRY POINT (CALL -151)
  1180 *--------------------------------
  1190        .OR $300     PAGE THREE
  1200 *--------------------------------
  1220        STA INTVEC   LOW BYTE
  1230        LDA /INT
  1240        STA INTVEC+1 HIGH BYTE
  1250        CLI          ALLOW IRQ'S
  1260        RTS
  1270 *--------------------------------
  1290        STY YREG     SAVE Y
  1300 *---DISPLAY REG TITLES-----------
  1310        LDX #39      PUT UP MESSAGE LINE
  1340        DEX
  1350        BPL .1       DONE ?
  1360 *---DISPLAY REG VALUES-----------
  1370        LDY #10      START OF REG DISPLAY AREA
  1380        LDA A.REG    ...A-REG
  1390        JSR DISPLAY.HEX
  1400        LDA XREG     ...X-REG
  1410        JSR DISPLAY.HEX
  1420        LDA YREG     ...Y-REG
  1430        JSR DISPLAY.HEX
  1440        TSX          GET STACK POINTER
  1450        INX          POINT AT PROCESSOR STATUS
  1460        LDA STACK,X  ...P-REG
  1470        JSR DISPLAY.HEX
  1480        INX          ADJUST S-REG
  1490        INX
  1500        TXA          ...S-REG AS WAS BEFORE INTERRUPT
  1510        JSR DISPLAY.HEX
  1520 *---DISPLAY PC-REG---------------
  1530        LDY #0       START OF PC-REG DISPLAY
  1540        LDA STACK,X  GET PC HIBYTE
  1550        JSR DISPLAY.HEX
  1560        DEX
  1570        LDY #2
  1580        LDA STACK,X  GET PC LOBYTE
  1590        JSR DISPLAY.HEX
  1600 *---WAIT FOR "S" OR "G"----------
  1610 .2     LDA KEYBD    KEY PRESSED ?
  1620        BPL .3       NO
  1630        STA KEYSTB   CLEAR THE KEY
  1640        CMP #"G"     GO AHEAD ?
  1650        BEQ .4       YES
  1660        CMP #"S"     STOP ?
  1670        BEQ .5       YES
  1680 .3     DEX          BLINK CURSOR
  1690        BNE .3
  1700        DEY          LONGER DELAY
  1710        BNE .3
  1730        EOR #$80     INVERT IT
  1740        STA BOTTOM.LINE+39  REPLACE IT
  1750        BNE .2       BRANCH ALWAYS
  1760 *---"G" TYPED, RETURN------------
  1780        LDX XREG
  1790        LDY YREG
  1800        RTI          BACK TO WORK
  1810 *---"S" TYPED, SO STOP-----------
  1820 .5     JMP MNTR     ENTER THE MONITOR
  1830 *--------------------------------
  1850        PHA          SAVE THE A-REG
  1860        LSR          SHIFT INTO LOWER NIBBLE
  1870        LSR
  1880        LSR
  1890        LSR
  1900        JSR DIGIT    MAKE IT A DIGIT
  1920        PLA          GET A-REG AGAIN 
  1930        AND #$0F     MASK IT
  1940        JSR DIGIT    MAKE IT A DIGIT
  1960        INY
  1970        INY
  1980        INY
  1990        RTS
  2000 *--------------------------------
  2010 DIGIT  INY
  2020        ORA #$B0     ADD NUMBER ZERO
  2030        CMP #$BA     IS IT A LETTER
  2040        BCC .1       NO - DONE
  2050        ADC #$6      6 PLUS CARRY MAKES A
  2060 .1     RTS
  2070 *--------------------------------
  2080 TITLES .AS -/     -   A=   X=   Y=   P=   S=         /
  2090 *--------------------------------
  2100 XREG   .DA #*-*     X REGISTER SAVE AREA
  2110 YREG   .DA #*-*     Y REGISTER SAVE AREA
  2120 *--------------------------------

Improving the Single-Byte Converter Bruce Love
New Zealand

Bob's s;ingle byte converter (see Jan 85 issue, pages 31-32) can be shortened by one byte. The left column is from Bob's code, the right a shorter version:

1040 .1  LDX #"0"
1050 .2  CMP DECTBL,Y
1060     BCC .3
1070     SBC DECTBL,Y
1080     INX
1090     BNE .2
 .1  LDX #"0"-1
     BCS .2

I also tried a different approach, using the decimal mode to count tens, then printing the tens as a hex value with the monitor routine at $FDDA and the remainder (units digit) with $FDED. This routine takes longer time, but does not need to use the X-register.

  1010 *--------------------------------
  1020        .LIST OFF
  1030 *--------------------------------
  1040 BYTE   .EQ $00
  1050 COUT   .EQ $FDED
  1060 CROUT  .EQ $FD8E
  1080 *--------------------------------
  1090 *      COMMAND
  1100 *--------------------------------
  1110 P      LDA #0
  1120        STA BYTE
  1130 .1     JSR PRINT.000.255
  1140        JSR CROUT
  1150        INC BYTE
  1160        LDA BYTE
  1170        BNE .1
  1180        RTS
  1190 *--------------------------------
  1200 *      PRINT.000.255
  1210 *--------------------------------
  1220 PRINT.000.255
  1230        LDY #0
  1240        SEC
  1250 .1     SBC #10
  1260        PHP
  1270        PHA
  1280        TYA
  1290        SED
  1300        ADC #0
  1310        TAY
  1320        PLA
  1330        PLP
  1340        BCS .1
  1350        ADC #"0+10
  1360        PHA
  1370        TYA
  1380        JSR PRBYTE
  1390        PLA
  1400        JMP COUT
  1410 *--------------------------------
  1420 SC
  1430        LDY #"0"
  1440        TAX
  1450        BEQ .3
  1460        LDA #0
  1470        SED
  1480 .2     CLC
  1490        ADC #1
  1500        BCC .25
  1510        INY
  1520 .25    DEX
  1530        BNE .2
  1540        CLD
  1550 .3     PHA
  1560        TYA
  1570        JSR COUT
  1580        PLA
  1590        JMP $FDDA
  1600 *--------------------------------
  1610 T      LDA #0
  1620 .1     STA BYTE
  1630        JSR SC
  1640        LDA #" "
  1650        JSR $FDED
  1660        LDA BYTE
  1670        JSR $FDDA
  1680        JSR CROUT
  1690        LDA $C000
  1700        BPL .2
  1710        STA $C010
  1720 .3     LDA $C000
  1730        BPL .3
  1740 .2     STA $C010
  1750        LDA BYTE
  1760        CLC
  1770        ADC #1
  1780        BNE .1
  1790        RTS
  1800 *--------------------------------

AppleVisions, a Glimpse Bob Sander-Cederlof

Here is a very elementary introduction to Apple Assembly Language programming by that old master Bob Bishop, along with Linda Gross- berger and Harry Vertelney. This 150+ page book and its companion diskette gently and humorously guide the beginning programmer into the realm of machine code. A "Cardboard Computer" introduces the concepts of registers, machine instructions, addressing, and branching. This background is then applied to the Apple's 6502 and the surrounding computer. AppleVisions is a nice place for the absolute beginner to start, especially the younger programmers interested in finding out what assembly language is.

AppleVisions. Addison-Wesley, 1985. $39.95 including diskette.

Two ROM Sets in One Apple //e Bob Sander-Cederlof

If and when you decide to upgrade to the new enhanced //e ROMs (which Apple sells for $70 along with a 65C02), you will probably have to turn your old ROMs over to the store that makes the switch. Reportedly, Apple is binding the stores with a contract that forces them to collect all the old chips.

That is VERY unfortunate. It could lead to wild shouting and panic, when you discover some of your favorite old software no longer works.

The upgrade consists of three parts:

The new firmware does NOT use any of the new features in the 65C02, so you could use it without the new cpu chip. Furthermore, there is no absolute requirement to have the new character generator installed. The new firmware is much better than the old, having lost some bugs and speeded up the 80-column scrolling and added lower-case support to Applesoft (among other things). It is compatible with the 6502, the 65C02, and the new 65802.

I personally do not yet have any use for the mouse characters, and do not expect to. Don Lancaster, in the June 1985 issue of "Computer Shopper", tells how to connect a 2764 EPROM in the character generator socket. The 2764 can hold two complete character sets, because it has twice the capacity of the 2732 normally in that socket. However, the socket has only 24 holes and the 2764 has 28 pins! Don shows how to wire this up with a socket adapter, and use a toggle switch to select either half.

And now Apple has "sort of" released an even more enhanced set of firmware, with debugging stuff built in. You may not see them on the open market for some time, but I like them even better than the standard enhanced ROMs. The "debug" ROMs add an absolute RESET (ctrl-RESET with solid apple), 16-byte hex display in the monitor when in 80-column mode, display of both hex and ASCII values of each byte in a memory dump, and the ability to use all monitor commands on both main and auxiliary memory. The disassembler and miniassembler are both present, and enhanced to include the 65C02 extensions.

The CD and EF ROM sockets are compatible with 2764 EPROMs. You can also use 27128s, which have twice the space. Pin 26 on the 2764 is always tied to +5 volts. On a 27128, pin 26 selects the top or bottom half of the 16K bytes inside. You can burn one set of firmware in one half, and the other set in the other half. Then bend out pin 26 a little, so that it does not go into the socket when you insert the chip. Attach a clip lead to the bent-out pin, and connect the other end to either +5 volts or ground, to select the half you want at any given time.

You can connect it to a toggle switch, or just stick the bare end of a wire into the game paddle connector. If you use the game socket on the motherboard, pin 1 is +5 volts and pin 8 is ground. Or stick a wire into one of the annunciator outputs (pins 12, 13, 14, and 15) so you can flip back and forth between firmware sets by software control.

It can be a little tricky to make a copy of the ROM firmware and get it into RAM or on a disk, so that you can later burn it in your own EPROM. Especially in the Cxxx part. My approach, since I have more than one Apple, is to put my SCRG PromGramer card in a different machine. Then one by one I can read the //e ROMs and burn them into the appropriate 27128s. This a lot faster than trying to figure out how to flip all the //e soft switches so as to get at the different banks of Cx ROM code.

I have recently seen 27128s priced as low as $5 and as high as $20, in the back of Byte magazine. It is well worth it to invest in a PromGramer, at $140, and an EPROM eraser ($50 to $100 from Logical Devices in Florida, see Byte ads). You can keep your Apple standard for commercial software, and still have your own private firmware on the motherboard at the flip of a switch!

A CALL Utility for Applesoft David C. Johnson
Applied Engineering

Anyone who has ever used Applesoft eventually realizes that the most powerful statement in the language is CALL. It allows you to get to the Monitor for instance (however, Extended Debugging Monitor users have a better way). When writing a program in BASIC, you invariably will want to do something that is at best difficult and often impossible to code using the other Applesoft statements.

The solution to this type of situation is to speak to your Apple in its native tongue. There are several way this can be done. Ampersand (&) routines are a popular technique. The USR( function even has its uses. The most logical way, for me, is the CALL statement.

Using CALL neatly transfers control from the Applesoft interpreter to whatever you want to do in machine language. The one disadvantage to CALL is that the processor's registers do not contain useful data when your machine code gets control.

The CALL utility presented in this article will allow you to specify, as part of the CALL statement, the contents of any or all of the registers upon entry of your machine language subroutine. You assign the register contents with LET-like structures. Obviously you can only fit an 8 bit value into the 8 bit registers and the program counter value will probably be a 16 bit number. Here's how the CALL statement should be written:

         CALL 768,PC=word,A=byte,X=byte,Y=byte,P=byte

The expressions "word" and "byte" may be any valid Applesoft numeric expression. This is because the utility calls routines internal to Applesoft to evaluate the expressions. If an expression results in a value larger than the register to which it is being assigned, or isn't numeric, or is invalid, you will get one of the usual errors. The commas shown separating the register assignments are required (syntax error if comma missing). The equals characters ("=") are also required. The register names ( PC, A, X, Y, & P) must be upper case on older Apples, while the newer firmware will convert lower case for you (or in spite of you). The register assignments may appear, after the first comma, in any order and need not all be specified. Unspecified registers will be loaded with their last used value. Previously unused registers default to zero, except the P-register which defaults to $04 in order to set the interrupt disable flag.

The program is well commented, but I'll add one more note of caution. Readers with Apples containing reqular 6502s (not 65C02s or 65802s) should avoid re-assembling the code with the label PC.Sav's bytes falling across a page boundary ($XXFF).

The program was written using the ProDOS version of the S-C Macro Assembler 2.0, while I was beta testing it for Bob. It works GREAT!

  1020 * 6/13/85 dcj
  1040 * CALL 768{,pc=word,a=byte,x=byte,y=byte,p=byte}
  1060 *--------------------------------
  1080        .OR $300
  1090        .TF CU
  1110 EQ.TOK .EQ $D0      Applesoft '=' token
  1130 CHRGET .EQ $B1 -$C8 advance TXTPTR & fetch chr
  1140 CHRGOT .EQ $B7      just fetch chr
  1160 FRMNUM .EQ $DD67    evaluate FP expression (FAC)
  1170 SYNCHR .EQ $DEC0    require chr in Acc syntax @ TXTPTR
  1180 SYNERR .EQ $DEC9    syntax error
  1190 GETBYT .EQ $E6F8    evaluate 8 bits @ TXTPTR (X-reg)
  1200 GETADR .EQ $E752    convert FAC to 16 bits in Acc & Y-reg (hi/lo)
  1220 *--------------------------------
  1240 CALL.UTIL
  1260        JSR CHRGOT   get chr after call adr expression
  1270        CMP #','     comma indicates more stuff follows
  1280        BEQ .1       =>go continue parsing
  1290        LDA P.SAV    load registers
  1300        PHA           (P-reg via stack)
  1310        LDA ACC.SAV
  1320        LDX X.SAV
  1330        LDY Y.SAV
  1340        PLP
  1350        JMP (PC.SAV) go 4 it!
  1370 * we got something to parse
  1390 .1     JSR CHRGET   get chr after comma
  1400        CMP #'A'     (as in 'Acc')
  1410        BEQ .2       =>go get '=' & byte for Acc
  1420        CMP #'X'     (as in 'X-reg')
  1430        BEQ .3       =>go get '=' & byte for X-reg
  1440        CMP #'Y'     (as in 'Y-reg')
  1450        BEQ .4       =>go get '=' & byte for Y-reg
  1460        CMP #'P'     (as in P-reg or Program Counter)
  1470        BEQ .5       =>go get '=' or 'C='...
  1480        JMP SYNERR   razz
  1500 * pickup Acc byte
  1520 .2     JSR .7       require '=' (@ next) & fetch byte exp
  1530        STX ACC.SAV  stuff it
  1540        BVC CALL.UTIL ...always
  1560 * pickup X-reg byte
  1580 .3     JSR .7       require '=' (@ next) & fetch byte exp
  1590        STX X.SAV    stuff it
  1600        BVC CALL.UTIL ...always
  1620 * pickup Y-reg byte
  1640 .4     JSR .7       require '=' (@ next) & fetch byte exp
  1650        STX Y.SAV    stuff it
  1660        BVC CALL.UTIL ...always
  1680 * Finish parsing 'P=' or 'PC='
  1700 .5     JSR CHRGET   advance to next chr position & fetch it
  1710        CMP #'C'     (as in 'Program Counter')
  1720        BEQ .6       =>go get '=' & 16 bits for PC
  1740 * pickup P-reg byte
  1760        JSR .10      require '=' @ current chr position
  1770        JSR .8       fetch byte expression
  1780        STX P.SAV    stuff it
  1790        BVC CALL.UTIL ...always
  1810 * pickup PC word
  1830 .6     JSR .9       require '=' @ next chr position
  1840        JSR FRMNUM   fletch FP expression
  1850        JSR GETADR   convert FP expression to Acc & Y-reg (hi/lo)
  1860        STY PC.SAV   stuff 'em
  1870        STA PC.SAV+1
  1880        JMP CALL.UTIL no flag known...
  1900 .7     JSR .9       require '=' @ next chr position
  1920 .8     JSR GETBYT   fetch byte expression (2 X-reg)
  1930        CLV          to force branch
  1940        RTS
  1960 .9     JSR CHRGET   1st advance to next chr position
  1980 .10    LDA #EQ.TOK  require '=' before register expressions
  2010 *--------------------------------
  2030 ACC.SAV    .DA #$00
  2040 X.SAV      .DA #$00
  2050 Y.SAV      .DA #$00
  2060 P.SAV      .DA #$04
  2070 PC.SAV     .DA $0000
  2090 *--------------------------------

Some Final DP18 Subroutines Bob Sander-Cederlof

Gerald Ferrier (Princeton, Minnesota) wrote to point out that we somewhow omitted a double-handful of subroutines from our lengthy series on 18-digit arithmetic for Applesoft. With apologies to you all, and thanks to Gerald, here they are:

  2020 *--------------------------------
  2040 *--------------------------------
  2050 MOVE.YA.DAC.1
  2060        STA PNTR
  2070        STY PNTR+1
  2080        LDY #10      MOVE 11 BYTES
  2090 .1     LDA (PNTR),Y
  2100        STA DAC,Y
  2110        DEY
  2120        BPL .1
  2130        LDA DAC.EXPONENT
  2140        STA DAC.SIGN
  2150        AND #$7F
  2160        STA DAC.EXPONENT
  2170        RTS
  2180 *--------------------------------
  2200 *--------------------------------
  2210 MOVE.YA.ARG.1
  2220        STA PNTR
  2230        STY PNTR+1
  2240        LDY #10      MOVE 11 BYTES
  2250 .1     LDA (PNTR),Y
  2260        STA ARG,Y
  2270        DEY
  2280        BPL .1
  2290        LDA ARG.EXPONENT
  2300        STA ARG.SIGN
  2310        AND #$7F
  2320        STA ARG.EXPONENT
  2330        RTS
  2340 *--------------------------------
  2360 *--------------------------------
  2370 MOVE.DAC.YA.1
  2380        STA PNTR
  2390        STY PNTR+1
  2400        LDA DAC.EXPONENT
  2410        BPL .0
  2430 .0     BIT DAC.SIGN
  2440        BPL .1       POSITIVE
  2450        ORA #$80     NEGATIVE
  2460 .1     LDY #0
  2470 .2     STA (PNTR),Y
  2480        INY
  2490        LDA DAC,Y
  2500        CPY #11      11 BYTES
  2510        BCC .2
  2520        RTS
  2530 *--------------------------------
  2550        LDA #DP.TEMP1
  2560        LDY /DP.TEMP1
  2570        JMP MOVE.DAC.YA.1
  2580 *--------------------------------
  2600        LDA #DP.TEMP1
  2610        LDY /DP.TEMP1
  2620        JMP MOVE.YA.ARG.1
  2630 *--------------------------------
  2650        LDA #DP.TEMP1
  2660        LDY /DP.TEMP1
  2670        JMP MOVE.YA.DAC.1
  2680 *--------------------------------
  2700        LDA #DP.TEMP2
  2710        LDY /DP.TEMP2
  2720        JMP MOVE.DAC.YA.1
  2730 *--------------------------------
  2750        LDA #DP.TEMP2
  2760        LDY /DP.TEMP2
  2770        JMP MOVE.YA.DAC.1
  2780 *--------------------------------
  2800        LDA #DP.TEMP2
  2810        LDY /DP.TEMP2
  2820        JMP MOVE.YA.ARG.1
  2830 *--------------------------------
  2850        LDA #DP.TEMP3
  2860        LDY /DP.TEMP3
  2870        JMP MOVE.YA.DAC.1
  2880 *--------------------------------
  2900        LDA #DP.TEMP3
  2910        LDY /DP.TEMP3
  2920        JMP MOVE.YA.ARG.1
  2930 *--------------------------------
  2950        LDA #DP.TEMP3
  2960        LDY /DP.TEMP3
  2970        JMP MOVE.DAC.YA.1
  2980 *--------------------------------

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