Apple Assembly   Line
Volume 2 -- Issue 2 November 1981

In This Issue...

Things For Sale

Here is an up-to-date list of some of the things which I have that you might need: Notice that the prices on books, diskettes, and bags are below retail.

S-C ASSEMBLER II Version 4.0...........................$55.00
Source code on Disk for above assembler................$95.00
Cross Assembler Patches for 6809 (for 4.0 owners.......$20.00
Cross Assembler for 6800/6801/6802 (for 4.0 owners)....$22.50
Quarterly Disk #1 (source code from Oct 80 - Dec 80)...$15.00
Quarterly Disk #2 (source code from Jan 81 - Mar 81)...$15.00
Quarterly Disk #3 (source code from Apr 81 - Jun 81)...$15.00
Quarterly Disk #4 (source code from Jul 81 - Sep 81)...$15.00
Blank Diskettes (Verbatim, with hub rings, no labels,
                 plain white jackets, in cellophane
                 wrapper).................20 disks for $50.00
Zip-lock Bags (2-mil, 6"x9")...............100 bags for $8.50
Zip-lock Bags (2-mil, 9"x12").............100 bags for $13.00
Back Issues of "Apple Assembly Line"...............each $1.20
"Beneath Apple DOS", Don Worth & Peter Lechner.........$18.00
"What's Where in the Apple", William Luebbert..........$14.00
"6502 Assembly Language Programming", Lance Leventhal..$16.00

I add shipping charges to orders for books and bags. If you are in Texas, remember to add 5% sales tax on books, disks, and bags. Software isn't taxable in Texas.

Advertising Info

If you have a software or hardware product that you want to sell, you can reach over 500 serious Apple owners by advertising in AAL. A full page is only $20, and a half page $10. I print 1000 copies, because many orders for back issues come in.


Using Applesoft ROM's from Assembly Language Bob Sander-Cederlof

There are many useful entry points in the Applesoft ROM's. The problem is figuring out how to use them. John Crossley's article "Applesoft Internal Entry Points" (originally published in Apple Orchard Volume 1 Number 1 March 1980) gives a brief description of most of the usable subroutines. If you missed the article, you can still get it from the International Apple Corps. It has also recently been reprinted in "Call A.P.P.L.E. in Depth--All About Applesoft".

Now I want to show you how to use the floating point math subroutines. I won't cover every one of them, but enough to do most of the things you would ever need to do. This includes load, store, add, subtract, complement, compare, multiply, divide, print, and formatted-print.

Internal Floating Point Number Format

Applesoft stores floating point numbers in five bytes. The first byte is the binary exponent; the other four bytes are the mantissa: ee mm mm mm mm.

The exponent (ee) is a signed number in excess-$80 form. That is, $80 is added to the signed value. An exponent of +3 will be stored as $83; of -3, as $7D. If ee = $00, the entire number is considered to be zero, regardless of what the mantissa bytes are.

The mantissa is considered to be a fraction between $.80000000 and $.FFFFFFFF. Since the value is always normalized, the first bit of the mantissa is always "1". Therefore, there is no need to actually use that bit position for a mantissa bit. Instead, the sign of the number is stored in that position (0 for +, 1 for -). Here are some examples:

     -10.0   84 A0 00 00 00
     +10.0   84 20 00 00 00
     +1.0    81 00 00 00 00
     +1.75   81 60 00 00 00
     -1.75   81 E0 00 00 00
     +.1     7D 4C CC CC CD

The Applesoft math subroutines use a slightly different format for faster processing, called "unpacked format". In this format the leading mantissa bit is explicitly stored, and the sign value is stored separately. Several groups of page-zero locations are used to store operands and results. The most frequently used are called "FAC" and "ARG". FAC occupies locations $9D thru $A2; ARG, $A5 thru $AA.

Loading and Storing Floating Point Values

There are a handful of subroutines in ROM for moving numbers into and out of FAC and ARG. Here are the five you need to know about.

     AS.MOVFM   $EAF9   unpack (Y,A) into FAC
     AS.MOVMF   $EB2B   pack FAC into (Y,X)
     AS.MOVFA   $EB53   copy ARG into FAC
     AS.MOVAF   $EB63   copy FAC into ARG
     AS.CONUPK  $E9E3   unpack (Y,A) into ARG

All of the above subroutines return with the exponent from FAC in the A-register, and with the Z-status bit set if (A)<0.

Here is an example which loads a value into FAC, and then stores it at a different location.

     LDA #VAR1
     LDY /VAR1    ADDRESS IN (Y,A)
     JSR AS.MOVFM
     LDX #VAR2
     LDY /VAR2    ADDRESS IN (Y,X)
     JSR AS.MOVMF

Arithmetic Subroutines

Once a number is unpacked in FAC, there are many subroutines which can operate on it.

     AS.NEGOP   $EED0   FAC = -FAC

     AS.FOUT    $ED34   convert FAC to decimal ASCII string
                        starting at $0100

     AS.FCOMP   $EBB2   compare FAC to packed number at (Y,A)
                        return (A) = 1 if (Y,A) < FAC
                               (A) = 0 if (Y,A) = FAC
                               (A) =FF if (Y,A) > FAC

     AS.FADD    $E7BE   load (Y,A) into ARG, and fall into...
     AS.FADDT   $E7C1   FAC = ARG + FAC

     AS.FSUB    $E7A7   load (Y,A) into ARG, and fall into...
     AS.FSUBT   $E7AA   FAC = ARG - FAC

     AS.FMUL    $E97F   load (Y,A) into ARG, and fall into...
     AS.FMULT   $E982   FAC = ARG * FAC

     AR.FDIV    $EA66   load (Y,A) into ARG, and fall into...
     AS.FDIVT   $EA69   FAC = ARG / FAC

Here is an example which calculates VAR1 = (VAR2 + VAR3) / (VAR2 - VAR3).

     LDA #VAR2   VAR2+VAR3
     LDY /VAR2
     JSR AS.MOVFM    VAR2 INTO FAC
     LDA #VAR3
     LDY /VAR3
     JSR AS.FADD     + VAR3
     LDX #VAR1
     LDY /VAR1
     JSR AS.MOVMF    STORE SUM TEMPORARILY IN VAR1
     LDA #VAR3   VAR2-VAR3
     LDY /VAR3
     JSR AS.MOVFM    VAR3 INTO FAC
     LDA #VAR2
     LDY /VAR2
     JSR AS.FSUB     VAR2-VAR3
     LDA #VAR1
     LDY /VAR1
     JSR AS.FDIV     DIVIDE DIFFERENCE BY SUM
     LDX #VAR1
     LDY /VAR1
     JSR AS.MOVMF    STORE THE QUOTIENT

As you can see, it is easy to get confused when writing this kind of code. It is so repetitive, there are so many setups of (Y,A) and (Y,X) addresses, that I make a lot of typing mistakes. It would be nice if there was an interface program between my assembly language coding and the Applesoft ROMs. I would rather write the above program like this:

     JSR FP.LOAD   VAR2 INTO FAC
     .DA VAR2
     JSR FP.SUB    -VAR3
     .DA VAR3
     JSR FP.STORE  SAVE AT VAR1
     .DA VAR0
     JSR FP.LOAD   VAR2 INTO FAC
     .DA VAR2
     JSR FP.ADD    +VAR3
     .DA VAR3
     JSR FP.DIV    /(VAR2-VAR3)
     .DA VAR1
     JSR FP.STORE  STORE IN VAR1
     .DA VAR1

Easy Interface to Applesoft ROMs

The first step in constructing the "easy interface" is to figure out a way to get the argument address from the calling sequence. That is, when I execute:

     JSR FP.LOAD
     .DA VAR1
how does FP.LOAD get the address VAR1?

I wrote a subroutine called GET.ADDR which does the job. Every one of my FP. subroutines starts by calling GET.ADDR to save the A-, X-, and Y-registers, and to return with the address which followed the JSR FP... in the Y- and A-registers. In fact, I return the low-byte of the address in both the A- and X-registers. That way the address is ready in both (Y,A) and (Y,X) form.

GET.ADDR is at lines 4260-4480. I save A, X, and Y in three local variables, and then pull off the return address from the stack and save it also. (This is the return to whoever called GET.ADDR). Then I save the current TXTPTR value. This is the pointer Applesoft uses when picking up bytes from your program to interpret them. I am going to borrow the CHRGET subroutine, so I need to save the current TXTPTR and restore it when I am finished. Then I pull the next address off the stack and stuff it into TXTPTR. This address is the return address to whoever called the FP... subroutine. It currently points to the third byte of that JSR, one byte before the .DA address we want to pick up.

I next call GET.ADDR2, which uses CHRGET twice to pick up the next two bytes after the JSR and returns them in X and Y. Then I push the return address I saved at the beginning of GET.ADDR, and RTS back. Note that TXTPTR now points at the second byte of the .DA address. It is just right for picking up another argument, or for returning. If there is another argument, I get it by calling GET.ADDR2 again. When I am ready for the final return, I do it by JMPing to FP.EXIT.

FP.EXIT, at lines 4670-4790, pushes the value in TXTPTR on the stack. It is the correct return address for the JSR FP.... Then I restore the old value of TXTPTR, along with the A-, X-, and Y-registers. And the RTS finishes the job.

The Interface Subroutines

I have alluded above to the "FP..." subroutines. In the listing I have shown eight of them, and you might add a dozen more after you get the hang of it.

     FP.LOAD        load a value into FAC
     FP.STORE       store FAC at address
     FP.ADD         FAC = FAC + value
     FP.SUB         FAC = FAC - value
     FP.MUL         FAC = FAC * value
     FP.DIV         FAC = FAC / value
     FP.PRINT       print value the way Applesoft would
     FP.PRINT.WD    print value with D digits after decimal
                    in a W-character field

FP.LOAD, FP.STORE, FP.ADD, and FP.MUL are quite straightforward. All they do is call GET.ADDR to get the argument address, JSR into the Applesoft ROM subroutine, and JMP to FP.EXIT.

FP.SUB and FP.DIV are a little more interesting. I didn't like the way the Applesoft ROM subroutines ordered the operands. It looks to me like they want me to think in complements and reciprocals. Remember that AS.FDIV performs FAC = (Y,A) / FAC. It is more natural for me to think left-to-right, so my FP.DIV permorms FAC = FAC / value. Likewise for FP.SUB.

I reversed the sense of the subtraction after-the-fact, by just calling AS.NEGOP to complement the value in FAC. Reversing the division has to be done before calling AS.FDIV. I saved the argument address on the stack, called AS.MOVAF to copy FAC into ARG, called AS.MOVFM to get the argument into FAC, and then called AS.FDIVT.

FP.PRINT, at lines 1830-1930, is also quite simple. I call GET.ADDR to set up the argument address, and AS.MOVFM to load it into FAC. Then AS.FOUT converts it to an ASCII string starting at $0100. It terminates with a $00 byte. A short loop picks up the characters of this string and prints them by calling AS.COUT. I called AS.COUT, rather than $FDED in the monitor, so that Applesoft FLASH, INVERSE, and NORMAL would operate on the characters.

And now for the really interesting one....

Formatted Print Subroutine

FP.PRINT.WD expects three arguments: the address of the value to be printed, the field width to print it in, and the number of digits to print after the decimal point. Leading blanks and trailing zeroes will be printed if necessary. The Applesoft E-format will be caught and converted to the more civilized form. Fields up to 40 characters wide may be printed, which will accommodate up to 39 digits and a decimal point. If you try to print a number that is too wide for the field, it will try to fit it in by shifting off fractional digits. If it is still too wide, it will print a field of ">>>>" indicating overflow.

For example, look at how values 123.4567and 12345.67 would be printed for corresponding W and D:

      W    D     123.4567    12345.67
     ---------------------------------
     10    1    bbbbb123.4  bbb12345.6
     10    3    bbb123.456  b12345.670
     10    5    b123.45670  12345.6700
     10    7    123.456700  12345.6700
      7    1       bb123.4     12345.6
      4    1          123.        >>>>

Sound pretty useful? I can hardly wait to start using it! Now let's walk through the code.

Lines 2380-2410 pick up the arguments. The value is loaded into FAC, and converted to a string at $0100 by AS.FOUT. Then I get the W and D values into X and Y.

Lines 2420-2510 check W and D. W must not be more than 40; if it is, use 40. (I arbitrarily chose 40 as the limit. If you want a different limit, you can use any value less than 128.) I also make sure that D is less than W. I save W in WD.GT in case I later need to print a field full of ">". Lines 2520-2560 compute W-D-1, which is the number of characters in the field to the left of the decimal point. I save the result back in W.

Lines 2570-2590 check whether AS.FOUT converted to the Applesoft E-format or not. The decimal exponent printed after E is still in $9A as a binary value. Numbers formatted the civilized way are handled by lines 2600-3160. E-format numbers are restructured by lines 3200-3930.

Lines 2600-2750 scan the string at $0100 up to the decimal point (or to the end if no decimal point). In other words, I am counting the number of characters AS.FOUT put before the decimal point. If W is bigger than that, the difference is the number of leading blanks I need to print. Since W is decremented inside the loop, the leading blank count is all that is left in W. But what if W goes negative, meaning that the number is too big for the field? Then I reduce D and try again. If I run out of "D" also, then the field is entirely too small, so I go to PRINT.GT to indicate overflow. If there was no decimal point on the end, the code at lines 2790-2820 appends one to the string.

Lines 2870-2980 scan over the fractional digits. If there are more than D of them, I store the end-of-string code ($00) after D digits. I also decrement D inside this loop, so that when the loop is finished D represents the number of trailing zeroes that I must add to fill out the field. (If the string runs out before D does, I need to print trailing zeroes.)

At line 3020, the leading blanks are printed (if any; remember that W had the leading blank count). Then lines 3060-3110 print the string at $0100. And finally, line 3150 prints out D trailing zeroes (D might be zero).

E-formatted numbers are a little tougher; we have to move the decimal point left or right depending on the exponent. We also might have to add zeroes before the decimal point, as well as after the fraction. Lines 3200-3330 scan through the converted string at $0100; the decimal point (if any) is removed, and an end-of-string byte ($00) is put where the "E" character is. Now all we have at $0100 is the sign and a string of significant digits, without decimal point or E-field.

Lines 3350-3600 test the range of the decimal exponent. Negative exponents are handled at lines 3370-3660, and positive ones at lines 3700-3930.

Negative exponents mean that the decimal point must be printed first, then possibly some leading zeroes, and then some significant digits. Lines 3370-3410 compute how many leading zeroes are needed. For example, the value .00123 would be converted by AS.COUT as "1.23E-03". The decimal exponent is -3, and we need two leading zeroes. The number of leading zeroes is -(dec.exp+1).

There is a little coding trick at line 3370. I want to compute -(dec.exp+1), and dec.exp is negative. By executing the EOR #$FF, the value is complemented and one is added at the same time! Why? Because the 6502 uses 2's complement arithmetic. Negative numbers are in the form 256-value. EOR #$FF is the same as doing 255-value, which is the same as 256-(value+1). Got it?

Line 3430 prints the leading blanks; lines 3450-3460 print the decimal point. Lines 3480-3520 print the leading zeroes, decrementing D along the way. When all the leading zeroes are out, D will indicate how many significant digits need to be printed.

Lines 3540-3620 print as many significant digits as will fit in the remaining part of the field (maybe none). Of course, the field might be large enough that we also need trailing zeroes. If so, line 3650 prints them.

What if the exponent was positive? Then lines 3700-3710 see if the number will fit in the field. If not, PRINT.GT will fill the field with ">". If it will fit, then the exponent is the number of digits to be printed. The number of leading blanks will be W-dec.exp-1 (the -1 is for the decimal point). Note that line 3740 complements and adds one at the same time, to get -(exp+1).

Line 3770 prints the leading blanks, if any. Lines 3780-3830 print the significant digits from the string at $0100. Lines 3840-3890 print any zeroes needed between the significant digits and the decimal point. Lines 3900-3910 print the decimal point, and line 3920 prints the trailing zeroes.

Possible Modifications

You might like to add a dozen or so more FP... subroutines, and hand-compile your favorite Applesoft programs into machine language. You might want to revise the FP.PRINT.WD subroutine to work from Applesoft using the & statement, or using a CALL. This would give you a very effective way of formatting values. You also might want to make it put the result in an Applesoft string variable, rather than directly printing it. You might want to add a floating dollar sign capability, or comma insertion between every three digits. If you implement any of these, let me know. I would like to print them in future issues of AAL.

 1000  *---------------------------------
 1010  *      TEST
 1020  *---------------------------------
 1030  TEST   LDY #10      LOOP 10 TIMES
 1040         JSR FP.LOAD  VAR1 = 1.0
 1050         .DA AS.ONE
 1060         JSR FP.STORE
 1070         .DA VAR1
 1080         JSR FP.LOAD  VAR2 = 10.0
 1090         .DA AS.TEN
 1100         JSR FP.STORE
 1110         .DA VAR2
 1120  .1     JSR FP.LOAD  VAR1=(VAR1+1)/VAR2
 1130         .DA VAR1
 1140         JSR FP.ADD
 1150         JSR AS.ONE
 1160         JSR FP.DIV
 1170         .DA VAR2
 1180         JSR FP.STORE
 1190         .DA VAR1
 1200         JSR FP.LOAD  VAR2=VAR2-1
 1210         .DA VAR2
 1220         JSR FP.SUB
 1230         .DA AS.ONE
 1240         JSR FP.STORE
 1250         .DA VAR2
 1260         JSR FP.PRINT.WD
 1270         .DA VAR1,#8,#3
 1280         JSR FP.PRINT.WD
 1290         .DA VAR1,#19,#4
 1300         JSR MON.BLANKS  3 SPACES
 1310         JSR FP.PRINT
 1320         .DA VAR1
 1330         JSR MON.CROUT  PRINT CARRIAGE RETURN
 1340         DEY          NEXT TRIP AROUND THE LOOP
 1350         BNE .1
 1360         RTS          FINISHED
 1370  VAR1   .BS 5        MY VARIABLES
 1380  VAR2   .BS 5
 1390  *---------------------------------
 1400  *      ARITHMETIC PACKAGE
 1410  *---------------------------------
 1420  AS.FOUT.E  .EQ $9A
 1430  AS.TEMP1   .EQ $93 THRU $97
 1440  AS.TXTPTR  .EQ $B8,B9
 1450  *---------------------------------
 1460  AS.CHRGET  .EQ $00B1
 1470  AS.COUT    .EQ $DB5C
 1480  AS.FSUB    .EQ $E7A7 FAC=ARG-FAC
 1490  AS.FADD    .EQ $E7BE
 1500  AS.ONE     .EQ $E913 CONSTANT 1.0
 1510  AS.FMUL    .EQ $E97F
 1520  AS.TEN     .EQ $EA50 CONSTANT 10.0
 1530  AS.FDIVT   .EQ $EA69 DIVIDE ARG BY FAC
 1540  AS.MOVFM   .EQ $EAF9
 1550  AS.MOV1F   .EQ $EB21
 1560  AS.MOVMF   .EQ $EB2B
 1570  AS.MOVAF   .EQ $EB63 MOVE FAC TO ARG
 1580  AS.FOUT    .EQ $ED34
 1590  AS.NEGOP   .EQ $EED0 FAC = -FAC
 1600  *---------------------------------
 1610  MON.BLANKS .EQ $F948 PRINT 3 BLANKS
 1620  MON.CROUT  .EQ $FD8E PRINT CRLF
 1630  *---------------------------------
 1640  *      JSR FP.LOAD      LOAD VALUE INTO FAC
 1650  *      .DA <ADDR OF VALUE>
 1660  *---------------------------------
 1670  FP.LOAD
 1680         JSR GET.ADDR IN Y,X AND Y,A
 1690         JSR AS.MOVFM
 1700         JMP FP.EXIT
 1710  *---------------------------------
 1720  *      JSR FP.STORE     STORE FAC
 1730  *      .DA <ADDR TO STORE IN>
 1740  *---------------------------------
 1750  FP.STORE
 1760         JSR GET.ADDR IN Y,X AND Y,A
 1770         JSR AS.MOVMF
 1780         JMP FP.EXIT
 1790  *---------------------------------
 1800  *      JSR FP.PRINT     PRINT VALUE IN FREE FORMAT
 1810  *      .DA <ADDR OF VALUE TO BE PRINTED>
 1820  *---------------------------------
 1830  FP.PRINT
 1840         JSR GET.ADDR
 1850         JSR AS.MOVFM
 1860         JSR AS.FOUT
 1870         LDY #0
 1880  .1     LDA $100,Y
 1890         BEQ .2
 1900         JSR AS.COUT
 1910         INY
 1920         BNE .1       ...ALWAYS
 1930  .2     JMP FP.EXIT
 1940  *---------------------------------
 1950  *      JSR FP.ADD   FAC = FAC + VALUE
 1960  *      .DA <ADDR OF VALUE>
 1970  *---------------------------------
 1980  FP.ADD JSR GET.ADDR IN Y,X AND Y,A
 1990         JSR AS.FADD  FAC=ARG+FAC
 2000         JMP FP.EXIT
 2010  *---------------------------------
 2020  *      JSR FP.SUB   FAC = FAC - VALUE
 2030  *      .DA <ADDR OF VALUE>
 2040  *---------------------------------
 2050  FP.SUB JSR GET.ADDR
 2060         JSR AS.FSUB   FAC=ARG-FAC
 2070         JSR AS.NEGOP  FAC=-FAC
 2080         JMP FP.EXIT
 2090  *---------------------------------
 2100  *      JSR FP.MUL   FAC = FAC + VALUE
 2110  *      .DA <ADDR OF VALUE>
 2120  *---------------------------------
 2130  FP.MUL JSR GET.ADDR IN Y,X AND Y,A
 2140         JSR AS.FMUL  FAC=ARG*FAC
 2150         JMP FP.EXIT
 2160  *---------------------------------
 2170  *      JSR FP.DIV   FAC = FAC / VALUE
 2180  *      .DA <ADDR OF VALUE>
 2190  *---------------------------------
 2200  FP.DIV JSR GET.ADDR
 2210         PHA
 2220         TYA
 2230         PHA
 2240         JSR AS.MOVAF  MOVE FAC TO ARG
 2250         PLA
 2260         TAY
 2270         PLA
 2280         JSR AS.MOVFM
 2290         JSR AS.FDIVT
 2300         JMP FP.EXIT
 2310  *---------------------------------
 2320  *      JSR FP.PRINT.WD   PRINT VALUE WITH W.D FORMAT
 2330  *      .DA <ADDR OF VALUE>,#<W>,#<D>
 2340  *          D = # OF DIGITS AFTER DECIMAL POINT
 2350  *          W = # OF CHARACTERS IN WHOLE FIELD
 2360  *---------------------------------
 2370  FP.PRINT.WD
 2380         JSR GET.ADDR ADDRESS OF VALUE
 2390         JSR AS.MOVFM VALUE INTO FAC
 2400         JSR AS.FOUT  CONVERT TO STRING AT $100
 2410         JSR GET.ADDR2  (X)=W, (Y)=D
 2420         CPX #41      LIMIT FIELD WIDTH TO 40 CHARS
 2430         BCC .14
 2440         LDX #40
 2450  .14    STX W        # CHARACTERS IN WHOLE FIELD
 2460         STX WD.GT
 2470         CPY W        FORCE D<W
 2480         BCC .13
 2490         LDY W
 2500         DEY
 2510  .13    STY D
 2520         DEX          COMPUTE W-D-1
 2530         TXA
 2540         SEC
 2550         SBC D
 2560         STA W
 2570         LDA AS.FOUT.E  SEE IF E-FORMAT
 2580         BEQ .12      NO
 2590         JMP E.FORMAT
 2600  .12    LDY #0
 2610  *---------------------------------
 2620  *      SCAN TO "." OR END, DECREMENTING W
 2630  *---------------------------------
 2640  .1     LDA $100,Y   SCAN TO END OR DECIMAL POINT
 2650         BEQ .2       FOUND END, NO DECIMAL POINT
 2660         CMP #'.
 2670         BEQ .3       FOUND DECIMAL POINT
 2680         INY          COUNT STRING LENGTH
 2690         DEC W
 2700         BPL .1       ...UNLESS TOO MANY DIGITS FOR FIELD
 2710         LDA #0
 2720         STA W        NEED NO LEADING BLANKS
 2730         DEC D        BACK UP D IF POSSIBLE
 2740         BPL .1       TRY AGAIN
 2750         JMP PRINT.GT OVERFLOW
 2760  *---------------------------------
 2770  *      APPEND DECIMAL POINT SINCE NONE PRESENT
 2780  *---------------------------------
 2790  .2     LDA #'.      PUT DECIMAL POINT BACK ON END
 2800         STA $100,Y
 2810         LDA #0       END OF STRING CHAR
 2820         STA $101,Y
 2830  *---------------------------------
 2840  *      SCAN TO END, DECREMENTING D
 2850  *      (PUT EOS AFTER D DIGITS)
 2860  *---------------------------------
 2870  .3     INY          NEXT CHAR
 2880         LDA D
 2890         BEQ .5       NO FRACTIONAL DIGITS
 2900  .4     LDA $100,Y   COUNT FRACTIONAL DIGITS TO END
 2910         BEQ .6       END OF STRING
 2920         INY
 2930         DEC D
 2940         BNE .4       STILL NEED MORE DIGITS
 2950  *---------------------------------
 2960  .5     LDA #0       MAKE EOS
 2970         STA $100,Y
 2980         STA D        NEED NO TRAILING ZEROES
 2990  *---------------------------------
 3000  *      PRINT LEADING BLANKS AS NEEDED
 3010  *---------------------------------
 3020  .6     JSR LEADING.BLANKS
 3030  *---------------------------------
 3040  *      PRINT CONVERTED STRING
 3050  *---------------------------------
 3060  *      COMES HERE WITH (Y)=0
 3070  .8     LDA $100,Y
 3080         BEQ .9
 3090         JSR AS.COUT
 3100         INY
 3110         BNE .8       ...ALWAYS
 3120  *---------------------------------
 3130  *      PRINT TRAILING ZEROES AS NEEDED
 3140  *---------------------------------
 3150  .9     JSR TRAILING.ZEROES
 3160         JMP FP.EXIT
 3170  *---------------------------------
 3180  *      HANDLE NUMBERS WHICH COME IN E-FORMAT
 3190  *---------------------------------
 3200  E.FORMAT
 3210         LDX #0
 3220         LDY #0
 3230  .1     LDA $100,Y   SCAN TO "E", CHANGE TO EOS
 3240         CMP #'E
 3250         BEQ .3
 3260         CMP #'.      SHUFFLE DIGITS AFTER "."
 3270         BEQ .2       LEFT ONE POSITION
 3280         STA $100,X
 3290         INX
 3300  .2     INY
 3310         BNE .1       ...ALWAYS
 3320  .3     LDA #0       EOS
 3330         STA $100,X
 3340  *---------------------------------
 3350         LDA AS.FOUT.E  EXP AGAIN
 3360         BPL .12      EXP>0
 3370         EOR #$FF     -(EXP+1) IS # ZEROES
 3380         CMP D        SEE IF MORE THAN WE NEED
 3390         BCC .4       NO
 3400         LDA D        YES, JUST USE D
 3410  .4     TAX
 3420  *---------------------------------
 3430         JSR LEADING.BLANKS
 3440  *---------------------------------
 3450         LDA #'.      DECIMAL POINT
 3460         JSR AS.COUT
 3470  *---------------------------------
 3480  .7     LDA #'0      ZEROES
 3490         JSR AS.COUT
 3500         DEC D        REDUCE DIGIT COUNT
 3510         DEX
 3520         BNE .7       MORE ZEROES
 3530  *---------------------------------
 3540         LDY #0
 3550         LDA D        HOW MANY DIGITS?
 3560         BEQ .9       NONE
 3570  .8     LDA $100,Y   GET A DIGIT
 3580         BEQ .10      OUT OF DIGITS
 3590         JSR AS.COUT
 3600         INY
 3610         DEC D
 3620         BNE .8       MORE
 3630  .9     JMP FP.EXIT
 3640  *---------------------------------
 3650  .10    JSR TRAILING.ZEROES
 3660         JMP FP.EXIT
 3670  *---------------------------------
 3680  *      E-FORMAT WITH EXP>0
 3690  *---------------------------------
 3700  .12    CMP W        SEE IF ENOUGH ROOM
 3710         BCS PRINT.GT FILL FIELD WITH ">"
 3720         TAX
 3730         INX          # DIGITS AND TRAILING ZEROES
 3740         EOR #$FF     -(EXP+1)
 3750         ADC W        COMPUT # LEADING BLANKS
 3760         STA W
 3770         JSR LEADING.BLANKS
 3780  .13    LDA $100,Y   PRINT SIGNIFICANT DIGITS
 3790         BEQ .14
 3800         JSR AS.COUT
 3810         DEX
 3820         INY
 3830         BNE .13      ...ALWAYS
 3840  .14    LDA D        SAVE TRAILING ZERO CNT
 3850         PHA
 3860         STX D        SET UP ZEROES BEFORE "."
 3870         JSR TRAILING.ZEROES
 3880         PLA          RESTORE REAL TRAILING ZERO CNT
 3890         STA D
 3900         LDA #'.      PRINT DECIMAL POINT
 3910         JSR AS.COUT
 3920         JSR TRAILING.ZEROES
 3930         JMP FP.EXIT
 3940  *---------------------------------
 3950  *      PRINT (WD.GT) GREATER THAN SIGNS (">")
 3960  *---------------------------------
 3970  PRINT.GT
 3980         LDA #'>      OVERFLOW
 3990         LDY WD.GT
 4000         JSR PRINT.ACHAR.YTIMES
 4010         JMP FP.EXIT
 4020  *---------------------------------
 4030  *      OUTPUT (W) LEADING BLANKS
 4040  *---------------------------------
 4050  LEADING.BLANKS
 4060         LDA #$20     BLANK
 4070         LDY W        # TO PRINT
 4080         JMP PRINT.ACHAR.YTIMES
 4090  *---------------------------------
 4100  *      OUTPUT (D) TRAILING ZEROES
 4110  *---------------------------------
 4120  TRAILING.ZEROES
 4130         LDA #'0
 4140         LDY D
 4150  * FALL INTO PRINT.ACHAR.YTIMES
 4160  *---------------------------------
 4170  *      PRINT (Y) REPETITIONS OF (A)
 4180  *---------------------------------
 4190  PRINT.ACHAR.YTIMES
 4200         BEQ .2       (Y) IS 0, DON'T PRINT ANY
 4210  .1     JSR AS.COUT
 4220         DEY
 4230         BNE .1
 4240  .2     RTS
 4250  *---------------------------------
 4260  GET.ADDR
 4270         STA SAVE.A   SAVE A,X,Y REGISTERS
 4280         STX SAVE.X
 4290         STY SAVE.Y
 4300         PLA          SAVE GET.ADDR RETURN ADDRESS
 4310         STA RETLO
 4320         PLA
 4330         STA RETHI
 4340         LDA AS.TXTPTR  SAVE APPLESOFT TEXT POINTER
 4350         STA SAVE.T
 4360         LDA AS.TXTPTR+1
 4370         STA SAVE.T+1
 4380         PLA          POINT AT BYTES AFTER JSR FP.<WHATEVER>
 4390         STA AS.TXTPTR
 4400         PLA
 4410         STA AS.TXTPTR+1
 4420         JSR GET.ADDR2  GET FIRST TWO BYTES AFTER
 4430         LDA RETHI    RETURN
 4440         PHA
 4450         LDA RETLO
 4460         PHA
 4470         TXA          ADDR ALSO IN Y,A
 4480         RTS
 4490  *---------------------------------
 4500  GET.ADDR2
 4510         JSR AS.CHRGET  GET NEXT BYTE IN CALLING SEQUENCE
 4520         TAX
 4530         JSR AS.CHRGET  GET NEXT BYTE IN CALLING SEQUENCE
 4540         TAY
 4550         RTS
 4560  *---------------------------------
 4570  W      .BS 1
 4580  D      .BS 1
 4590  RETHI  .BS 1
 4600  RETLO  .BS 1
 4610  SAVE.A .BS 1
 4620  SAVE.X .BS 1
 4630  SAVE.Y .BS 1
 4640  SAVE.T .BS 2        TXTPTR
 4650  WD.GT  .BS 1
 4660  *---------------------------------
 4670  FP.EXIT
 4680         LDA AS.TXTPTR+1  GET HIGH BYTE
 4690         PHA
 4700         LDA AS.TXTPTR    GET LOW BYTE
 4710         PHA
 4720         LDA SAVE.T
 4730         STA AS.TXTPTR
 4740         LDA SAVE.T+1
 4750         STA AS.TXTPTR+1
 4760         LDA SAVE.A
 4770         LDX SAVE.X
 4780         LDY SAVE.Y
 4790         RTS

Poor Man's Disassembler James O. Church

I wanted a quick and cheap way to get machine language code into the S-C Assembler II Version 4.0, via a text file. I didn't need labels or other automatic features like those $25-$30 Two-Pass Disassemblers have. Or at least not badly enough to pay the price and wait for delivery.

There is a fundamental disassembler in the Apple Monitor ROM, which the "L" command invokes. The problems with it are that it only writes on the screen (not on a text file), and it is not in the correct format for the assembler to use. It has too many spaces between the opcode and operand fields, and there is and address rather than a line number at the beginning of each line.

I wrote a program in Applesoft that gets the starting address of the memory you want to disassemble, and then calls on the monitor "L" command as long as you like. The opcode and operand of each disassembled line are packed into a string array until you want to quit. Then you have the option to write the string array on a text file. The program squeezes out the two extra spaces mentioned above, and omits the hex address from each line. In place of the address and blanks which precede the opcode, this program inserts two control-I characters.

Later, when you use EXEC to get the text file into the S-C Assembler II, the first control-I will generate a line number, and the second one will tab over to the opcode column.

To speed it up a little, I wrote a machine language routine to move the second screen line into the string array. I used the last 15 lines of the Field Input Routine from the September, 1981, issue of AAL as a guide. (Thank you, Bob Potts!)

I chose to not use the already overworked "&" way to call my subroutine. Instead I just used CALL 768, followed by the string reference. It works just as well, as far as I'm concerned.

Also, rather than BLOADing such a short little program, I included it as a hexadecimal string inside the Applesoft program. I used an old technique from B. Lam (Call A.P.P.L.E., many moons ago) for passing the hex code to the monitor and thence into memory. (It's all in line 50.)

Line 100 sets up my array for 1280 lines. That's enough for about 2K of code at a time. Plenty. Make it bigger if you like.

Lines 110-120 ask for and process the starting memory address you want. If you type a negative value, I add 65536 to it to make it positive (from 0 thru 65535, rather than -32768 thru 32767). Then I test the range to make sure you ARE in that range.

Line 130 puts the address where the monitor "L" command wants to find it.

The CALL -418 on line 140 disassembles 20 lines. Line 150 shuffles the operand field two spaces left. Then CALL 768A$(X) puts the 11-byte string starting with the first character of the opcode on the second screen line, into A$(X). CALL -912 on line 180 scrolls the screen up one line, so the next line of disassembly is now on the second screen line. The process repeats until 20 lines have been processed.

Then you have the choice to continue or not. If not, you have the option to write A$() on a text file. If you choose to write it on a file, the file is OPENed, DELETEd, OPENed again, and primed for WRITE. Why the DELETE and extra OPEN? So that if the file was already there, it will be replaced with a new one. If a pre-existing file was longer than my new disassembly, the extra old lines would remain in the file.

You know, once the program is in the string array in text form, you could go ahead and scan it for particular addresses in the operand column. Then you could replace them with meaningful symbols. And you could add meaningful labels on lines that are branched to.... [James Church is a special agent for the Northwestern Mutual Life Insurance Agency; he lives in Trumbull, CT. Article ghost-written and program slightly modified by Bob Sander-Cederlof]

   40  HOME : VTAB 10: 
       HTAB 9: PRINT "POOR MAN'S DISASSEMBLER":
       HTAB 9: PRINT "-----------------------":
       HTAB 13: PRINT "JAMES O. CHURCH":
       HTAB 14: PRINT "SPECIAL AGENT"
   50  HEX$ = "300:20 E3 DF A9 0B 20 52 E4 A0 00 91 83 A5 71 C8 91 
       83 A5 72 C8 91 83 A2 94 A0 04 A9 0B 20 E2 E5 60 N D823G":
       FOR I = 1 TO  LEN (HEX$):
       POKE 511 + I, ASC ( MID$ (HEX$,I,1)) + 128:
       NEXT :
       POKE 72,0:
       CALL  - 144
  100  DIM A$(1280):
       X = 0
  110  HOME : VTAB 10:
       INPUT "START LOCATION IN DECIMAL: ";L$:
       L =  VAL (L$):
       IF L < 0 THEN L = L + 65536
  120  IF L < 0 OR L > 65535 THEN 110
  130  LH =  INT (L / 256):
       LL = L - LH * 256:
       POKE 58,LL:
       POKE 59,LH
  140  J = 0:
       HOME :
       CALL  - 418
  150  FOR I = 0 TO 6:
       POKE 1176 + I, PEEK (1178 + I):
       NEXT 
  160  CALL 768A$(X)
  170  X = X + 1:
       IF X > 1280 THEN  PRINT "ARRAY FULL":
       GOTO 210
  180  CALL  - 912:
       J = J + 1:
       IF J < 20 THEN 150
  190  PRINT : PRINT "CONTINUE? (Y/N) ";:
       GET A$:
       IF A$ = "Y" THEN 140
  200  HOME : VTAB 10
  210  PRINT "DO YOU WANT TO PUT IT IN A FILE? (Y/N) ";:
       GET A$:
       IF A$ <  > "Y" THEN  HOME :
       END 
  220  PRINT : INPUT "NAME OF FILE: ";F$
  230  D$ =  CHR$ (4):
       PRINT D$"OPEN"F$
  240  PRINT D$"DELETE"F$:
       PRINT D$"OPEN"F$:
       PRINT D$"WRITE"F$
  250  FOR J = 0 TO X - 1:
       PRINT  CHR$ (9); CHR$ (9);A$(J):
       NEXT 
  260  PRINT D$"CLOSE":
       END 

 

 1000  *---------------------------------
 1010  *      BUILD STRING FROM SECOND LINE ON SCREEN
 1020  *---------------------------------
 1030         .OR $300
 1040  *---------------------------------
 1050  PTRGET .EQ $DFE3  PUTS STRING POINTER ADDRESS IN $83,84
 1060  GETSPA .EQ $E452  PUTS ADDRESS OF STRING SPACE IN $71,72
 1070  MOVSTR .EQ $E5E2  MOVES DATA FROM (Y,X) TO STRING SPACE
 1080  *---------------------------------
 1090  SPCPTR .EQ $71,72 PNTR TO STRING SPACE RESERVED BY GETSPA
 1100  STRPTR .EQ $83,84 PNTR TO STRING VARIABLE PTRGET GOT
 1110  *---------------------------------
 1120  *      TO USE:
 1130  *          CALL 768A$(X)
 1140  *---------------------------------
 1150  GO     JSR PTRGET   GET ADDRESS OF STRING INTO $83,84
 1160         LDA #11      MOVE 11 BYTES
 1170         JSR GETSPA   GET SPACE FOR 11-BYTE STRING
 1180         LDY #0
 1190         STA (STRPTR),Y  PUT LENGTH IN STRING DESCRIPTOR
 1200         LDA SPCPTR   LOW BYTE OF STRING ADDRESS
 1210         INY
 1220         STA (STRPTR),Y
 1230         LDA SPCPTR+1 HIGH BYTE OF STRING ADDRESS
 1240         INY
 1250         STA (STRPTR),Y
 1260         LDX #$0494   START OF OPCODE ON SECOND SCREEN LINE
 1270         LDY /$0494       ADDRESS IN (Y,X)
 1280         LDA #11      11 BYTES LONG
 1290         JSR MOVSTR   MOVE IT IN
 1300         RTS

Loops Bob Sander-Cederlof

When you want to program repetitive code in, you write a FOR-NEXT loop or an IF loop. For example, you might write:

     10 FOR I = 1 TO 10     or:  10 I=0
     20 ...                      20 I=1+1 : IF I > 10 THEN 100
     30 NEXT I                   30 ...
                                 90 GO TO 20
                                 100

How do you do it in assembly language?

Loop Variable in X or Y

One of the simplest kind of loops holds the loop variable in the Y- or X-register, and decrements it once each trip.

     LOOP   LDY #10    Loop for Y = 10 to 1
     .1     ...
            DEY
            BNE .1

Note that the loop variable is in the Y-reigster, and that it counts from 10 to 1, backwards. When the DEY opcode changes Y from 1 to 0, the loop terminates.

If you want the loop to execute one more time, with Y=0, change it to this:

     LOOP   LDY #10    Loop for Y = 10 to 0
     .1     ...
            DEY
            BPL .1

Of course, a loop count of 129 or more would not work with this last example, because Y would look negative after each DEY until the value was less than 128.

If you want the loop variable to run up instead of down, like from 0 to 9, you need to add a comparison at the end of loop:

     LOOP   LDY #0     Loop for Y = 0 to 9
     .1     ...
            INY
            CPY #10
            BCC .1     Carry clear if Y < 10

All the examples above use the Y-register, but you can do the same thing with the X-register. In fact, using the X-register, you can nest one loop inside another:

     LOOPS  LDY #0     FOR Y = 0 TO 9
     .1     LDX #10    FOR X = 10 TO 1 STEP 1
     .2     ...
            DEX
            BNE .2     NEXT X
            ...
            INY
            CPY #10    NEXT Y
            BCC .1

Loop Variable on Stack

Sometimes X and Y are needed for other purposes, and so I use the stack to save my loop variable. Also, the step size can be larger than 1.

     LOOP   LDA #0    FOR VAR=5 TO 15 STEP 3
     .1     PHA       SAVE VAR ON STACK
            ...
            PLA       GET VAR FROM STACK
            CLC
            ADC #3    ADD STEP SIZE
            CMP #16
            BCC .1    VAR <= 15

In the Apple Monitor ROM there is a double loop using the stack to hold one of the variables. It is used just for a delay loop, with the length of delay depending on the contents of A when you call it. It is at $FCA8.

     WAIT   SEC
     .1     PHA       outer loop
     .2     SBC #1    ...inner loop
            BNE .2    ...next
            PLA
            SBC #1
            BNE .1    next
            RTS

The outer loop runs from A down to 1, and the inner loop runs from whatever the current value of the outer loop variable is down to 1. The delay time, by the way, is 5*A*A/2 + 27*A/2 + 13 cycles. (A cycle in the Apple II is a little less than one microsecond.)

16-bit Loop Variables

What if you need to run a loop from $1234 to $2345? That is a little trickier, but not too hard:

     LOOP  LDA #$1234    START AT $1234
           STA VARL
           LDA /$1234
           STA VARH
     .1    ....
           INC VARL      NEXT: ADD 1
           BNE .2
           INC VARH
     .2    LDA VARL
           CMP #$2346    COMPARE TO LIMIT
           LDA VARH
           SBC /$2346
           BCC .1        NOT FINISHED

A good example of this kind of loop is in the monitor ROMs also. The code for the end of loop incrementing and testing is at $FCB4-$FCC8. The memory move command ("M") at $FE2C-$FE35 uses this.

Conclusion

There are as many variations on the above themes as there are problems and programmers. Look around in the ROMs, and in programs published in AAL and other magazines; try to understand how the loops you find are working, and adapt them to your own needs.


Apple Assembly Line is published monthly by S-C SOFTWARE, P. O. Box 280300, Dallas, Texas 75228. Phone (214) 324-2050. Subscription rate is $12 per year in the U.S.A., Canada, and Mexico. Other countries add $12/year for extra postage. Back issues are available for $1.20 each (other countries add $1 per back issue for postage). All material herein is copyrighted by S-C SOFTWARE, all rights reserved. Unless otherwise indicated, all material herein is authored by Bob Sander-Cederlof. (Apple is a registered trademark of Apple Computer, Inc.)