Volume 1 -- Issue 9 | June 1981

In This Issue...

- Two Fancy Tone Generators
- Correction to "Hiding Things Under DOS"
- More About Multiplying on the 6502
- Specialized Multiplications
- Commented Listing of DOS 3.3 $B800-BCFF
- Beneath Apple DOS -- A Review

New Quarterly Disk Ready

Remember that all the source programs which appear in the Apple Assembly Line are available on disk, ready to assembly with the S-C Assembler II Version 4.0. Every three months I collect it all on a Quarterly Disk, and you can get it for only $15.

QD#1 covers AAL issues 1-3 (October thru December 1980), QD#2 covers AAL issues 4-6 (January thru March 1981), QD#3 covers issues 7-9 (April thru June 1981). Copies of all back issues of the AAL newsletter are available for $1.20 each.

Another Way to Get 80-Columns

Those unpredictable Apple Parallel Interface ROMs! I wonder if even Apple knows how many different versions they have made, and why!

Anyway, as you know if you have one, some of them make it very difficult to get 80-column printout when you are using the S-C Assembler II. You should be able to type control-I and "80N", but the assembler sees control-I and does a tab. Plus you get a syntax error, and the printer is un-hooked.

You can type "$I80N" (where "I" means control-I). Or you can type "$579:50" (assuming slot 1).

Or, you can make the first line of your program do it. Type in this line so it will be the first line in your program:

         0000 *I80N

Then type the "MEM" command. It will tell you the memory address where your source program starts. Using monitor commands, display about 8 bytes at the beginning of the source program. Look for the pattern "49 38 30 4E". Change the "49" to "09", which is ASCII for control-I. When your program is LISTed or ASMed, the control-I will be caught by Apple's interface and put you into 80-column mode.

So, now you have at least three ways to make it work. Don't you wish you had the ROM version which is in my Apple Parallel card? It works right without ANY of the above! Now if I could only make it work with my screen printing program....

Two Fancy Tone Generators | Mark Kriegsman

I was not quite satisfied with the sound from Bob Sander-Cederlof's "Touch-Tone Simulator" (AAL February 1981, page 5,6). His method for making two simulataneous tones was to play one tone for a while and then the other one for a while, letting your ear put it all together. I have written the following DUAL.TONES program which mixes the two tones together in a more realistic way. I also wrote SINGLE.TONE which plays a given tone at 16 different volume levels. All out of the standard Apple speaker! Really!

The programs are accessed from Applesoft with the "&". (See lines 1510 and 1830.) SINGLE.TONE is called with &T followed by three expressions separated by commas. The three expressions are for the tone, duration, and volume, respectively. Tone is a value from 0 to 255, duration a value from 0 to 65535, and volume a value from 0 to 15. Experiment with different settings and you will see how it works. By making loops which change both pitch and volume, you can simulate the sound of a falling bomb or a passing car.

DUAL.TONES also needs three parameters: tone#1, duration, and tone#2, respectively. The two tone values must be between 0 and 255; duration is again a value from 0 to 65535. It is interesting to try two tone values very close together, to hear the beating effect, and two tones at harmonic intervals to hear the chords. I think &D 254,28000,255 sounds a little like a light saber. Again, a loop which varies both tone values can make some exciting sound effects!

Lines 1340-1400 are executed when you BRUN B.AMPERTONE; they set up the ampersand vector for Applesoft. Once this is done, an ampersand in your program or typed in as a direct command will start executing the AMPERTONE subroutine.

Lines 1440-1490 determine which & routine you are calling. The character following the "&" is in the A-register. If it is "T", SINGLE.TONE is called; if "D", DUAL.TONE is called; if neither, you get SYNTAX ERR.

Subroutines in the Applesoft ROMs are used to read the parameter expressions (lines 2190-2230). GTBYTC advances to the next character, and then evaluates the expression that starts there. If the value is between 0 and 255 it is returned in the X-register. (If not, you get RANGE ERR.) CHKCOM makes sure the next character is a comma; if it isn't, you get SYNTAX ERR. GETNUM is used in executing the POKE statement. It looks for an expression giving a value between 0 and 65535, then a comma, and then another expression giving a value between 0 and 255. The first value is stored at $50 and $51, and the second is returned in the X-register.

[Mark Kriegsman is a 15-year-old Apple expert living in Summit, New Jersey. I wrote the article above based on two letters and a program he sent. (Bob Sander-Cederlof)]

     1000  *---------------------------------
     1010  *      DUAL TONE, AND TONE WITH VOLUME CONTROL
     1020  *---------------------------------
     1030  *      WRITTEN BY MARK KRIEGSMAN.......5-22-81
     1040  *      REVISED BY BOB SANDER-CEDERLOF..5-29-81
     1050  *---------------------------------
     1060         .OR $300
     1070         .TF B.AMPERTONES
     1080  *---------------------------------
     1090  *      ROM SUBROUTINES USED
     1100  *---------------------------------
     1110  CHKCOM .EQ $DEBE    MUST SEE COMMA
     1120  SYNERR .EQ $DEC9    SYNTAX ERROR
     1130  GTBYTC .EQ $E6F5    EAT CHAR, GET BYTE IN X
     1140  GETNUM .EQ $E746    GET TWO-BYTE VALUE IN $50,51
     1150  *                   THEN COMMA AND ONE-BYTE VALUE IN X
     1160  *---------------------------------
     1170  *      PAGE-ZERO VARIABLES
     1180  *---------------------------------
     1190  DURATION   .EQ $50 AND $51
     1200  TONE1.CNT  .EQ $FB
     1210  TONE2.CNT  .EQ $FC
     1220  TONE1      .EQ $FD
     1230  TONE2      .EQ $FE
     1240  VOLUME     .EQ $FF
     1250  *---------------------------------
     1260  *      I/O ADDRESSES
     1270  *---------------------------------
     1280  SPKR       .EQ $C030
     1290  *---------------------------------
     1300  AMPERSAND.VECTOR    .EQ $3F5 THRU $3F7
     1310  *---------------------------------
     1320  *      INITIALIZE AMPERSAND VECTOR
     1330  *---------------------------------
     1340  INIT   LDA #$4C     JMP OPCODE
     1350         STA AMPERSAND.VECTOR
     1360         LDA #AMPERTONE
     1370         STA AMPERSAND.VECTOR+1
     1380         LDA /AMPERTONE
     1390         STA AMPERSAND.VECTOR+2
     1400         RTS
     1410  *---------------------------------
     1420  *      AMPERSAND ENTRY POINT
     1430  *---------------------------------
     1440  AMPERTONE
     1450         CMP #'T      IS IT TONE?
     1460         BEQ SINGLE.TONE
     1470         CMP #'D      IS IT DUAL?
     1480         BEQ DUAL.TONES
     1490         JMP SYNERR   NEITHER, SO SYNTAX ERROR
     1500  *---------------------------------
     1510  *      &T <TONE>,<DURATION>,<VOLUME>
     1520  *---------------------------------
     1530  SINGLE.TONE
     1540         JSR GET.PARAMS
     1550         TXA          LIMIT VOLUME
     1560         AND #15      TO 0-15
     1570         STA VOLUME
     1580         LDA TONE1
     1590         STA TONE1.CNT
     1600  .1     DEC TONE1.CNT
     1610         BNE .5
     1620         LDA SPKR     TOGGLE SPEAKER
     1630         LDA TONE1    RESET COUNTER
     1640         STA TONE1.CNT
     1650         LDY VOLUME
     1660  .3     NOP
     1670         NOP
     1680         DEY
     1690         BPL .3
     1700         LDA SPKR     TOGGLE SPEAKER AGAIN
     1710         LDY VOLUME   EQUALIZE VOLUME DELAY
     1720  .4     NOP
     1730         INY
     1740         CPY #16
     1750         BCC .4
     1760  .5     LDY #10      SHORT ADDITIONAL DELAY
     1770  .6     DEY
     1780         BNE .6
     1790         JSR DECREMENT.DURATION
     1800         BCC .1
     1810         RTS
     1820  *---------------------------------
     1830  *      &D <TONE1>,<DURATION>,<TONE2>
     1840  *---------------------------------
     1850  DUAL.TONES
     1860         JSR GET.PARAMS
     1870         STX TONE2
     1880         LDA TONE1
     1890         STA TONE1.CNT
     1900         LDA TONE2
     1910         STA TONE2.CNT
     1920  .1     DEC TONE1.CNT
     1930         BEQ .2       TIME TO TOGGLE
     1940         LSR VOLUME   TO EQUALIZE TIME
     1950         LDA VOLUME   TO EQUALIZE TIME
     1960         BPL .3       ...ALWAYS
     1970  .2     LDA SPKR     TOGGLE SPEAKER
     1980         LDA TONE1    RESET COUNTER
     1990         STA TONE1.CNT
     2000  .3     DEC TONE2.CNT
     2010         BEQ .4
     2020         LSR VOLUME   TO EQUALIZE TIME
     2030         LDA VOLUME   TO EQUALIZE TIME
     2040         BPL .5       ...ALWAYS
     2050  .4     LDA SPKR     TOGGLE SPEAKER
     2060         LDA TONE2    RESET COUNTER
     2070         STA TONE2.CNT
     2080  .5     JSR DECREMENT.DURATION
     2090         BCC .1
     2100         RTS
     2110  *---------------------------------
     2120  *      GET THREE PARAMETERS AFTER &T OR &D
     2130  *      1. 8-BIT VALUE, STORE IN TONE1
     2140  *      2. COMMA
     2150  *      3. 16-BIT VALUE, STORE IN DURATION
     2160  *      4. COMMA
     2170  *      5. 8-BIT VALUE, RETURN IN X-REGISTER
     2180  *---------------------------------
     2190  GET.PARAMS
     2200         JSR GTBYTC   GET TONE
     2210         STX TONE1
     2220         JSR CHKCOM
     2230         JMP GETNUM   GET DURATION AND VOLUME
     2240  *---------------------------------
     2250  *      DECREMENT DURATION
     2260  *      RETURN CARRY CLEAR IF NOT FINISHED
     2270  *---------------------------------
     2280  DECREMENT.DURATION
     2290         LDA DURATION FINISHED YET?
     2300         BNE .2
     2310         LDA DURATION+1
     2320         BNE .1
     2330         SEC
     2340         RTS          FINISHED
     2350  .1     DEC DURATION+1
     2360  .2     DEC DURATION
     2370         CLC
     2380         RTS

Correction to "Hiding Things Under DOS" | Bob Sander-Cederlof
When I typed Rick Hatcher's code for "Hiding Things Under DOS", AAL April, 1981, page 10, I goofed. Change line 110 of the little Applesoft code from "110 POKE 40194,211" to "110 POKE 40192,211". Better yet, to reserve NP pages between the current bottom of DOS and DOS's buffers, use this code before any files are opened:
         100 POKE 40192,PEEK(40192)-NP
         110 CALL 42964

More About Multiplying on the 6502 | Bob Sander-Cederlof

You will remember Brooke Boering's article on this subject in MICRO last December; I mentioned it in AAL#5, and printed his 16x16 multiply subroutine. Now Leo J. Scanlon, author of 6502 Software Design, published an eight-page article "Multiplying by 1's and 0's" in Kilobaud Microcomputing, June 1981, pages 110-120.

If you are serious and really want to learn, this article gets down to the nuts and bolts level. Work your way through it, and you will have learned not only how to multiply, but also a lot about machine language in general. Subroutines are listed for 8x8, 16x16, and NxM multiplication, for both signed and unsigned operands.

Not to be outdone, I have written my own subroutine to multiply an M-byte multiplicand by a N-byte multiplier (both unsigned), producing a product of M+N bytes. It is written for clarity, not for size or speed (nevertheless, it is two bytes shorter than Scanlon's subroutine!).

The basic idea is to examine the bits of the multiplier one-by-one, starting on the right. If the multiplier bit = 1, the multiplicand is added in to the product, at the left end of the product register. In either case, the product register is then shifted right one bit position. The process is repeated until the multiplier is used up.

I wrote subroutines to shift the product register right one bit position, to shift the multiplier right one bit position returning the bit shifted out in the CARRY status bit, and to add the multiplicand to the product register. There is no reason these have to be subroutines; they could be coded in line, because they are only called from one place. I did it to make the overall program easier for you to follow.

The multiplication loop is coded as two loops: an outer loop for the number of bytes in the multiplier, and an inner loop for the number of bits in a byte. This allows me to have up to 255 bytes in the multiplier, just so the product (M+N bytes) is not more than 256 bytes. (Of course, if you want variables that long, you will have to move them out of page zero.)

There is one little trick you might not notice. After ACCUMULATE.PARTIAL.PRODUCT, carry will be set if the sum overflows. Then SHIFT.PRODUCT.RIGHT shifts the carry bit back into the product register, maintaining the right answer.

     1000  *---------------------------------
     1010  *      M-BYTE BY N-BYTE MULTIPLY
     1020  *---------------------------------
     1030  M      .EQ $00      # BYTES IN MULTIPLICAND
     1040  N      .EQ $01      # BYTES IN MULTIPLIER
     1050  PSIZE  .EQ $02      # BYTES IN PRODUCT
     1060  I      .EQ $03      LOOP COUNTER
     1070  J      .EQ $04      LOOP COUNTER
     1080  MULTIPLICAND .EQ $90 THRU ...
     1090  MULTIPLIER   .EQ $A0 THRU ...
     1100  PRODUCT      .EQ $B0 THRU ...
     1110  *---------------------------------
     1120  MXN.MPY
     1130  *---------------------------------
     1140  *      CLEAR THE PRODUCT REGISTER
     1150  *---------------------------------
     1160         LDY M        # BYTES IN MULTIPLICAND
     1170         STY PSIZE
     1180         LDA #0
     1190  .1     STA PRODUCT,Y
     1200         DEY
     1210         BPL .1
     1220  *---------------------------------
     1230  *      FOR I=M TO 1 STEP -1
     1240  *      PSIZE = PSIZE + 1
     1250  *      FOR J=8 TO 1 STEP -1
     1260  *---------------------------------
     1270         LDA N        # BYTES IN MULTIPLIER
     1280         STA I
     1290  .2     INC PSIZE
     1300         LDA #8
     1310         STA J
     1320  *---------------------------------
     1330  *      ACCUMULATE PARTIAL PRODUCT FOR NEXT BIT
     1340  *---------------------------------
     1350  .3     JSR SHIFT.MULTIPLIER.RIGHT
     1360         BCC .4       ZERO-BIT
     1370         JSR ACCUMULATE.PARTIAL.PRODUCT
     1380  .4     JSR SHIFT.PRODUCT.RIGHT
     1390  *---------------------------------
     1400  *      NEXT J : NEXT I
     1410  *---------------------------------
     1420         DEC J
     1430         BNE .3
     1440         DEC I
     1450         BNE .2
     1460         RTS
     1470  *---------------------------------
     1480  *      SHIFT MULTIPLIER RIGHT
     1490  *---------------------------------
     1500  SHIFT.MULTIPLIER.RIGHT
     1510         LDY N        # BYTES IN MULTIPLIER
     1520         LDX #0
     1530  .1     ROR MULTIPLIER,X
     1540         INX
     1550         DEY
     1560         BNE .1
     1570         RTS
     1580  *---------------------------------
     1590  *      SHIFT PRODUCT RIGHT
     1600  *---------------------------------
     1610  SHIFT.PRODUCT.RIGHT
     1620         LDY PSIZE    # BYTES IN PRODUCT
     1630         LDX #0
     1640  .1     ROR PRODUCT,X
     1650         INX
     1660         DEY
     1670         BPL .1
     1680         RTS
     1690  *---------------------------------
     1700  *      ACCUMULATE PARTIAL PRODUCT
     1710  *---------------------------------
     1720  ACCUMULATE.PARTIAL.PRODUCT
     1730         LDY M
     1740         DEY
     1750         CLC
     1760  .1     LDA MULTIPLICAND,Y
     1770         ADC PRODUCT,Y
     1780         STA PRODUCT,Y
     1790         DEY
     1800         BPL .1
     1810         RTS

Specialized Multiplications | Bob Sander-Cederlof

Sometimes you need a multiplication routine that is not general at all. For example, when you are converting from decimal to binary, you need a routine that will multiply be ten. When you are computing the memory address of a character at a particular position on a particular line on the Apple Screen, you need to be able to multiply by 40 and 128. Other cases may come to your mind.

The subroutine BASCALC in the Apple Monitor computes the address in screen memory. Here is what it is really doing, written in Integer BASIC:

         100 ADDR = 1024 + (LINE MOD 8)*128 + (LINE/8)*40

To do all that using a generalized multiply routine would take hundreds of microseconds; BASCALC takes only 40 microseconds. Here is Woz's code, with a few extra comments:

     1000  *---------------------------------
     1010  *      BASCALC FROM APPLE MONITOR
     1020  *---------------------------------
     1030  BASL   .EQ $28
     1040  BASH   .EQ $29
     1050  *---------------------------------
     1060  BASCALC
     1070         PHA          ARG = 000ABCDE
     1080         LSR          (A) = 0000ABCD, E IN CARRY
     1090         AND #3       (A) = 000000CD
     1100         ORA #4       (A) = 000001CD
     1110         STA BASH     HI-BYTE OF ADDRESS
     1120         PLA          (A) = 000ABCDE
     1130         AND #$18     (A) = 000AB000
     1140         BCC .1       MERGE IN E FROM CARRY
     1150         ADC #$7F     (A) = E00AB000
     1160  .1     STA BASL     BASL = E00AB000
     1170         ASL          (A) = 00AB0000, E IN CARRY AGAIN
     1180         ASL          (A) = 0AB00000, CARRY CLEAR
     1190         ORA BASL     (A) = EABAB000
     1200         STA BASL     LO-BYTE OF ADDRESS
     1210         RTS

A subroutine to multiply by ten usually takes advantage of the fact that ten in binary is "1010". That is, 10*X is the same as 8*X + 2*X, or 2*(4*X+X). In fact, even in machines that have hardware multiply instructions, it is usually faster to multiply by ten using "shift-twice-and-add" than using the built in MPY opcode!

Here is a short piece of code which multiplies a two-byte value by ten, storing the result back in the same bytes.

     1000  *---------------------------------
     1010  *      MULTIPLY TWO BYTES BY TEN
     1020  *---------------------------------
     1030  B0     .EQ $00
     1040  B1     .EQ $01
     1050  BY.TEN LDA B1       SAVE HI-BYTE ON STACK
     1060         PHA
     1070         LDA B0       GET LO-BYTE IN A
     1080         ASL B0       DOUBLE THE TWO-BYTE VALUE
     1090         ROL B1
     1100         ASL B0       DOUBLE IT AGAIN
     1110         ROL B1
     1120         CLC          ADD IN THE ORIGINAL VALUE
     1130         ADC B0
     1140         STA B0       LO-BYTE
     1150         PLA          HI-BYTE
     1160         ADC B1
     1170         STA B1
     1180         ASL B0       DOUBLE 5*B TO GET 10*B
     1190         ROL B1
     1200         RTS          RETURN TO CALLER

Another way, much less sophisticated, to multiply by ten is to simply add the number to itself nine times. If you have the S-C ASSEMBLER II Version 4.0, disassemble from $114A through $117A. You will find my subroutine for converting line numbers to binary. It is not elegant, but it does the job reasonably fast in a small amount of memory. A counter is initialized to 10; the next digit is read from the input line and converted from ASCII to binary; the number accumulator is added to the digit ten times, and the sum placed back into the number accumulator. The counter is in $52, and the number accumulator is in $50,51.

When you are converting from binary to decimal, you need to divide by ten. Or multiply by one-tenth. One-tenth written as a binary fraction is ".0001100110011001100....". Does the repetitive pattern here suggest to you a short-cut way to multiply by one-tenth? Maybe it would become even easier if we write one-tenth as 4/30 - 1/30. In decimal, to 8 places, that looks like .13333333 - .03333333 = .10000000. In binary, to 18 bits, it looks like .001000100010001000 - .000010001000100010 = 0.000110011001100110. See what you can come up with for a fast way to multiply a 16-bit number by one-tenth. I'll print the best version in AAL!

Commented Listing of DOS 3.3 $B800-BCFF | Bob Sander-Cederlof

As I promised last month, here are the innermost routines of DOS 3.3. These are the ones which actually read and write the hardware, and are the most significantly different routines between DOS 3.2.1 and DOS 3.3.

The major difference between the two versions of DOS is the way in which data bytes are coded on the disk. DOS 3.2.1 maps 256 8-bit bytes into 410 5-bit "nybbles". DOS 3.3 maps 256 8-bit bytes into 342 6-bit "nybbles". (The term "nybble" usually means 4 bits, but Apple uses nybble to mean 5- and 6-bits also.)

The two routines PRE.NYBBLE and POST.NYBBLE convert between memory format and disk format. The DOS 3.3 versions are much shorter and simpler than those of DOS 3.2.1, but they are still hard to visualize and explain.

To write a sector on the disk, RWTS calls PRE.NYBBLE and WRITE.SECTOR. Here is what happens:

1. The most significant 6 bits of each byte in the buffer are copied into $BB00-BBFF and right-justified with two zero-bits on the left.
2. The least significant 2 bits of each buffer byte are mapped into $BC00-BC55.
3. Each 6-bit nybble is used as an index into the NYBBLE.TABLE to pick up a corresponding 8-bit disk code. (The codes in NYBBLE.TABLE always have the first bit = 1, and never have more than two zero-bits in a row.)

      SECTOR BUFFER	       RWTS.BUFFER.1           RWTS.BUFFER.2
     7 6 5 4 3 2 1 0      7 6 5 4 3 2 1 0         7 6 5 4 3 2 1 0
     ---------------      ---------------         ---------------
    |           | | |00  |   |           | BB00  |   |#|#| | | | | BC00
    |           |A|B|    |   |           |       |   | | | | | | |
    |           | | |    |   |           |       |   | | | | | | |
    |           |||||    |   |           |       |00 |^|^|^|^|^|^|
    |      G    |v|v|    |00 |  G        |       |   |||||||||||||
    |           | | |    |   |           |       |   | | | | | | |
    |           | | |    |   |           |       |   |F|E|D|C|B|A|
    |           | | |    |   |           |       |   | | | | | | |
    |           | | |    |   |           |       |   | | | | | | | BC55
    |           |_|_|55  |   |           |        ---------------
    |           | | |56  |   |           |
    |           |C|D|    |   |           |
    |           | | |    |   |           |
    |           |||||    |   |           |
    |           |v|v|    |   |           |
    |           | | |    |   |           |
    |           | | |    |   |           |
    |           | | |    |   |           |
    |           |_|_|AB  |   |           |
    |           | | |AC  |   |           |
    |           |E|F|    |   |           |
    |           | | |    |   |           |
    |           |||||    |   |           |
    |           |v|v|    |   |           |
    |           | | |    |   |           |
    |           | | |    |   |           |
    |           | | |    |   |           |
    |           | | |FF  |   |           | BBFF
     ---------------      ---------------

To read a sector from the disk, RWTS calls READ.SECTOR and POST.NYBBLE. Here is what happens:

1. Each disk byte is converted to a 6-bit nybble and copied into the buffer from $BB00 through $BC55.
2. The nybbles in $BB00-BBFF become the most significant 6-bits of the buffer bytes.
3. 3. The nybbles in $BC00-BC55 supply the least significant 2-bits for each buffer byte. This is the reverse of the process above.

WRITE.ADDRESS is called from FORMAT, when you are initializing a 16-sector disk. This subroutine was embedded inside FORMAT in DOS 3.2.1. READ.ADDRESS, READ.SECTOR, and WRITE.SECTOR are almost identical to the DOS 3.2.1 versions.

Short as they are, I noticed that both PRE. and POST.NYBBLE can be written more efficiently. Can you see how to save three bytes in PRE.NYBBLE, and two bytes in POST.NYBBLE?

     1000  *      .LIF
     1010  *---------------------------------
     1020  *      DOS 3.3 DISASSEMBLY        $B800 - $BCFF
     1030  *      COMMENTS BY BOB SANDER-CEDERLOF  5-25-81
     1040  *---------------------------------
     1050         .OR $B800
     1060         .TA $0800
     1070  *---------------------------------
     1080  BUF.PNTR            .EQ $3E,3F
     1090  CONST.AA            .EQ $3E
     1100  FMT.SECTOR          .EQ $3F
     1110  VOLUME              .EQ $41
     1120  TRACK.CNTR          .EQ $44
     1130  CURRENT.TRACK       .EQ $0478
     1140  *---------------------------------
     1150  *      DISK CONTROLLER ADDRESSES
     1160  *---------------------------------
     1170  PHOFF  .EQ $C080    PHASE-OFF
     1180  PHON   .EQ $C081    PHASE-ON
     1190  MTROFF .EQ $C088    MOTOR OFF
     1200  MTRON  .EQ $C089    MOTOR ON
     1210  DRV0EN .EQ $C08A    DRIVE 0 ENABLE
     1220  DRV1EN .EQ $C08B    DRIVE 1 ENABLE
     1230  Q6L    .EQ $C08C    SET Q6 LOW
     1240  Q6H    .EQ $C08D    SET Q6 HIGH
     1250  Q7L    .EQ $C08E    SET Q7 LOW
     1260  Q7H    .EQ $C08F    SET Q7 HIGH
     1270  *
     1280  *      Q6    Q7     USE OF Q6 AND Q7 LINES
     1290  *     ----  ----    ----------------------
     1300  *     LOW   LOW     READ (DISK TO SHIFT REGISTER)
     1310  *     LOW   HIGH    WRITE (SHIFT REGISTER TO DISK)
     1320  *     HIGH  LOW     SENSE WRITE PROTECT
     1330  *     HIGH  HIGH    LOAD SHIFT REGISTER FROM DATA BUS
     1340  *---------------------------------
     1350  *
     1360  *---------------------------------
     1370  *      CONVERT 256 BYTES TO 342 6-BIT NYBBLES
     1380  *---------------------------------
     1390  PRE.NYBBLE
     1400         LDX #0
     1410         LDY #2
     1420  .1     DEY
     1430         LDA (BUF.PNTR),Y  NEXT REAL BYTE FROM BUFFER
     1440         LSR
     1450         ROL RWTS.BUFFER.2,X
     1460         LSR
     1470         ROL RWTS.BUFFER.2,X
     1480         STA RWTS.BUFFER.1,Y
     1490         INX
     1500         CPX #86
     1510         BCC .1
     1520         LDX #0
     1530         TYA
     1540         BNE .1
     1550         LDX #85      CLEAR TOP BITS OUT OF BUFFER
     1560  .2     LDA RWTS.BUFFER.2,X
     1570         AND #$3F
     1580         STA RWTS.BUFFER.2,X
     1590         DEX
     1600         BPL .2
     1610         RTS
     1620   .PG
     1630  *---------------------------------
     1640  *      WRITE A SECTOR ON THE DISK FROM RWTS.BUFFER
     1650  *---------------------------------
     1660  WRITE.SECTOR
     1670         SEC          SET IN CASE OF ERROR RETURN
     1680         STX $27      SAVE SLOT #
     1690         STX $0678    HERE, TOO
     1700         LDA Q6H,X    Q6 HIGH, Q7 LOW,
     1710         LDA Q7L,X    TO READ WRITE PROTECT STATUS
     1720         BMI .5       DISK IS WRITE PROTECTED
     1730         LDA RWTS.BUFFER.2  FIRST NYBBLE OF DATA
     1740         STA $26      SAVE IT
     1750         LDA #$FF     SYNC BYTE
     1760         STA Q7H,X    Q6H,Q7H: (A) TO SHIFT REGISTER
     1770         ORA Q6L,X    Q6L,Q7H: WRITE ON DISK
     1780         PHA          TIME DELAYS
     1790         PLA
     1800         NOP
     1810         LDY #4       WRITE FOUR MORE SYNC BYTES
     1820  .1     PHA          WASTE TIME
     1830         PLA
     1840         JSR WRT2     WRITE (A) ON DISK
     1850         DEY
     1860         BNE .1       UNTIL 4 OF THEM
     1870         LDA #$D5     WRITE DATA HEADER
     1880         JSR WRT1
     1890         LDA #$AA
     1900         JSR WRT1
     1910         LDA #$AD
     1920         JSR WRT1
     1930         TYA          A=0
     1940         LDY #86      WRITE 86 NYBBLES
     1950         BNE .3       ...ALWAYS
     1960  .2     LDA RWTS.BUFFER.2,Y  GET CURRENT NYBBLE AND
     1970  .3     EOR RWTS.BUFFER.2-1,Y  EOR WITH PREVIOUS NYBBLE
     1980         TAX          USE AS OFFSET INTO TABLE
     1990         LDA NYBBLE.TABLE,X  MAP 6-BITS TO 8-BITS
     2000         LDX $27      GET SLOT AGAIN
     2010         STA Q6H,X    Q6H,Q7H: (A) TO SHIFT REGISTER
     2020         LDA Q6L,X    Q6L,Q7H: WRITE ON DISK
     2030         DEY
     2040         BNE .2       UNTIL ALL BYTES FROM THIS BLOCK DONE
     2050         LDA $26      GET FIRST NYBBLE
     2060         NOP
     2070  .4     EOR RWTS.BUFFER.1,Y  EOR WITH CURRENT NYBBLE
     2080         TAX          INDEX INTO TABLE
     2090         LDA NYBBLE.TABLE,X  MAP TO 8-BIT VALUE
     2100         LDX $0678    SLOT # AGAIN
     2110         STA Q6H,X    Q6H,Q7L: (A) TO SHIFT REGISTER
     2120         LDA Q6L,X    Q6L,Q7H: WRITE ON DISK
     2130         LDA RWTS.BUFFER.1,Y  GET NYBBLE
     2140         INY
     2150         BNE .4       MORE TO DO
     2160         TAX          LAST NYBBLE
     2170         LDA NYBBLE.TABLE,X  MAP TO 8 BITS
     2180         LDX $27      SLOT # AGAIN
     2190         JSR WRT3     WRITE CHECK SUM ON DISK
     2200         LDA #$DE     WRITE TRAILER
     2210         JSR WRT1
     2220         LDA #$AA
     2230         JSR WRT1
     2240         LDA #$EB
     2250         JSR WRT1
     2260         LDA #$FF
     2270         JSR WRT1
     2280         LDA Q7L,X    Q7L
     2290  .5     LDA Q6L,X    Q6L
     2300         RTS
     2310  *---------------------------------
     2320  WRT1   CLC          WAIT 2 CYCLES
     2330  WRT2   PHA          WAIT 3 CYCLES
     2340         PLA          WAIT 4 CYCLES
     2350  WRT3   STA Q6H,X    Q6H,Q7H: (A) TO SHIFT REGISTER
     2360         ORA Q6L,X    Q6L,Q7H: WRITE ON DISK
     2370         RTS
     2380   .PG
     2390  *---------------------------------
     2400  *      CONVERT 342 6-BIT NYBBLES TO 256 BYTES
     2410  *      (THEY ARE NOW RIGHT-JUSTIFIED IN RWTS.BUFFER)
     2420  *---------------------------------
     2430  POST.NYBBLE
     2440         LDY #0
     2450  .1     LDX #86
     2460  .2     DEX
     2470         BMI .1
     2480         LDA RWTS.BUFFER.1,Y
     2490         LSR RWTS.BUFFER.2,X
     2500         ROL
     2510         LSR RWTS.BUFFER.2,X
     2520         ROL
     2530         STA (BUF.PNTR),Y
     2540         INY
     2550         CPY $26      (RWTS PUT 0 IN $26)
     2560         BNE .2
     2570         RTS
     2580  *---------------------------------
     2590  *      READ SECTOR INTO RWTS.BUFFER
     2600  *---------------------------------
     2610  READ.SECTOR
     2620         LDY #32      MUST FIND $D5 WITHIN 32 BYTES
     2630  .1     DEY
     2640         BEQ ERROR.RETURN
     2650  .2     LDA Q6L,X    READ SHIFT REGISTER
     2660         BPL .2       WAIT FOR FULL BYTE
     2670  .3     EOR #$D5     SEE IF FOUND $D5
     2680         BNE .1       NOT YET
     2690         NOP          DELAY BEFORE NEXT READ
     2700  .4     LDA Q6L,X    READ SHIFT REGISTER
     2710         BPL .4       WAIT FOR FULL BYTE
     2720         CMP #$AA     SEE IF $AA
     2730         BNE .3       NO
     2740         LDY #86      BYTE COUNT FOR LATER
     2750  .5     LDA Q6L,X    READ SHIFT REGISTER
     2760         BPL .5       WAIT FOR FULL BYTE
     2770         CMP #$AD     IS IT $AD?
     2780         BNE .3       NO
     2790  *---------------------------------
     2800         LDA #0       BEGIN CHECKSUM
     2810  .6     DEY
     2820         STY $26
     2830  .7     LDY Q6L,X    READ SHIFT REGISTER
     2840         BPL .7       WAIT FOR FULL BYTE
     2850         EOR BYTE.TABLE,Y  CONVERT TO NYBBLE
     2860         LDY $26      BUFFER INDEX
     2870         STA RWTS.BUFFER.2,Y
     2880         BNE .6
     2890  .8     STY $26
     2900  .9     LDY Q6L,X    READ SHIFT REGISTER
     2910         BPL .9       WAIT FOR FULL BYTE
     2920         EOR BYTE.TABLE,Y  CONVERT TO NYBBLE
     2930         LDY $26
     2940         STA RWTS.BUFFER.1,Y
     2950         INY
     2960         BNE .8
     2970  .10    LDY Q6L,X    READ CHECKSUM
     2980         BPL .10
     2990         CMP BYTE.TABLE,Y
     3000         BNE ERROR.RETURN
     3010  .11    LDA Q6L,X    READ TRAILER
     3020         BPL .11
     3030         CMP #$DE
     3040         BNE ERROR.RETURN
     3050         NOP
     3060  .12    LDA Q6L,X
     3070         BPL .12
     3080         CMP #$AA
     3090         BEQ GOOD.RETURN
     3100  ERROR.RETURN
     3110         SEC
     3120         RTS
     3130   .PG
     3140  *---------------------------------
     3150  *      READ ADDRESS
     3160  *---------------------------------
     3170  READ.ADDRESS
     3180         LDY #$FC     TRY 772 TIMES (FROM $FCFC TO $10000)
     3190         STY $26
     3200  .1     INY
     3210         BNE .2
     3220         INC $26
     3230         BEQ ERROR.RETURN
     3240  .2     LDA Q6L,X    READ SHIFT REGISTER
     3250         BPL .2       WAIT FOR FULL BYTE
     3260  .3     CMP #$D5     SEE IF $D5
     3270         BNE .1       NO
     3280         NOP          DELAY
     3290  .4     LDA Q6L,X    READ SHIFT REGISTER
     3300         BPL .4       WAIT FOR FULL BYTE
     3310         CMP #$AA     SEE IF $AA
     3320         BNE .3       NO
     3330         LDY #3       READ 3 BYTES LATER
     3340  .5     LDA Q6L,X    READ SHIFT REGISTER
     3350         BPL .5
     3360         CMP #$96     SEE IF $96
     3370         BNE .3       NO
     3380         LDA #0       START CHECK SUM
     3390  .6     STA $27
     3400  .7     LDA Q6L,X    READ REGISTER
     3410         BPL .7
     3420         ROL
     3430         STA $26
     3440  .8     LDA Q6L,X    READ REGISTER
     3450         BPL .8       WAIT FOR FULL BYTE
     3460         AND $26      MERGE THE NYBBLES
     3470         STA $2C,Y    $2C -- CHECK SUM
     3480         EOR $27      $2D -- SECTOR
     3490         DEY          $2E -- TRACK
     3500         BPL .6       $2F -- VOLUME
     3510         TAY          TEST CHECK SUM
     3520         BNE ERROR.RETURN
     3530  .9     LDA Q6L,X    READ REGISTER
     3540         BPL .9       WAIT FOR FULL BYTE
     3550         CMP #$DE     TEST FOR VALID TRAILER
     3560         BNE ERROR.RETURN
     3570         NOP
     3580  .10    LDA Q6L,X    READ REGISTER
     3590         BPL .10
     3600         CMP #$AA
     3610         BNE ERROR.RETURN
     3620  GOOD.RETURN
     3630         CLC
     3640         RTS
     3650         .PG
     3660  *---------------------------------
     3670  *      TRACK SEEK
     3680  *---------------------------------
     3690  SEEK.TRACK.ABSOLUTE
     3700         STX $2B      CURRENT SLOT*16
     3710         STA $2A      SAVE TRACK #
     3720         CMP CURRENT.TRACK  COMPARE TO CURRENT TRACK
     3730         BEQ .9       ALREADY THERE
     3740         LDA #0
     3750         STA $26      # OF STEPS SO FAR
     3760  .1     LDA CURRENT.TRACK  CURRENT TRACK NUMBER
     3770         STA $27
     3780         SEC
     3790         SBC $2A      DESIRED TRACK
     3800         BEQ .6       WE HAVE ARRIVED
     3810         BCS .2       CURRENT > DESIRED
     3820         EOR #$FF     CURRENT < DESIRED
     3830         INC CURRENT.TRACK  INCREMENT CURRENT
     3840         BCC .3       ...ALWAYS
     3850  .2     ADC #$FE     CARRY SET, SO A=A-1
     3860         DEC CURRENT.TRACK  DECREMENT CURRENT TRACK
     3870  .3     CMP $26      GET MINIMUM OF:
     3880         BCC .4          1. # OF TRACKS TO MOVE LESS 1
     3890         LDA $26         2. # OF ITERATIONS SO FAR
     3900  .4     CMP #12         3. ELEVEN
     3910         BCS .5
     3920         TAY
     3930  .5     SEC          TURN PHASE ON
     3940         JSR .7
     3950         LDA ONTBL,Y  GET DELAY TIME
     3960         JSR DLY100   DELAY 100*A MICROSECONDS
     3970         LDA $27      TRACK NUMBER
     3980         CLC          TURN PHASE OFF
     3990         JSR .8
     4000         LDA OFFTBL,Y
     4010         JSR DLY100
     4020         INC $26      # OF STEPS SO FAR
     4030         BNE .1       ...ALWAYS
     4040  *---------------------------------
     4050  .6     JSR DLY100
     4060         CLC          TURN PHASE OFF
     4070  .7     LDA CURRENT.TRACK
     4080  .8     AND #3       ONLY KEEP LOW-ORDER 2 BITS
     4090         ROL          (0000 0XX0)
     4100         ORA $2B      (0SSS 0XX0) MERGE SLOT
     4110         TAX          USE AS INDEX FOR PHASE-OFF
     4120         LDA PHOFF,X  PHASE-OFF
     4130         LDX $2B
     4140  .9     RTS
     4150  *---------------------------------
     4160         .HS AAA0A0   FILLER:  NOT USED IN DOS 3.3
     4170  *---------------------------------
     4180  *      SHORT DELAY SUBROUTINE
     4190  *---------------------------------
     4200  DLY100 LDX #17      100*A MICROSECONDS
     4210  .1     DEX
     4220         BNE .1
     4230         INC $46
     4240         BNE .2
     4250         INC $47
     4260  .2     SEC
     4270         SBC #1
     4280         BNE DLY100
     4290         RTS
     4300  *---------------------------------
     4310  *      DELAY TIMES FOR STEPPING MOTOR
     4320  *---------------------------------
     4330  ONTBL  .HS 01302824201E1D1C1C1C1C1C
     4340  OFFTBL .HS 702C26221F1E1D1C1C1C1C1C
     4350         .PG
     4360  *---------------------------------
     4370  *      NYBBLE TABLE
     4380  *---------------------------------
     4390  NYBBLE.TABLE
     4400         .HS 96979A9B9D9E9FA6A7ABACADAEAF
     4410         .HS B2B3B4B5B6B7B9BABBBCBDBEBFCB
     4420         .HS CDCECFD3D6D7D9DADBDCDDDEDFE5
     4430         .HS E6E7E9EAEBECEDEEEFF2F3F4F5F6
     4440         .HS F7F9FAFBFCFDFEFF
     4450  *---------------------------------
     4460  *      FILLER:  $BA69 THRU $BA95 NOT USED BY DOS 3.3
     4470  *---------------------------------
     4480         .BS 45
     4490  *---------------------------------
     4500  *      BYTE TABLE
     4510  *---------------------------------
     4520  BYTE.TABLE .EQ *-$96
     4530         .HS 0001989902039C040506A0A1A2A3
     4540         .HS A4A50708A8A9AA090A0B0C0DB0B1
     4550         .HS 0E0F10111213B81415161718191A
     4560         .HS C0C1C2C3C4C5C6C7C8C9CA1BCC1C
     4570         .HS 1D1ED0D1D21FD4D52021D8222324
     4580         .HS 25262728E0E1E2E3E4292A2BE82C
     4590         .HS 2D2E2F303132F0F1333435363738F8393A3B3C3D3E3F
     4600  *---------------------------------
     4610  *      342-BYTE BUFFER FOR NYBBLES
     4620  *---------------------------------
     4630  RWTS.BUFFER.1 .BS 256  $BB00 - BBFF
     4640  RWTS.BUFFER.2 .BS 86   $BC00 - BC55
     4650  *---------------------------------
     4660         .PG
     4670  *---------------------------------
     4680  *      WRITE ADDRESS HEADER (CALLED BY FORMAT)
     4690  *---------------------------------
     4700  WRITE.ADDRESS
     4710         SEC          SET IN CASE OF ERROR RETURN
     4720         LDA Q6H,X    Q6 HIGH, Q7 LOW,
     4730         LDA Q7L,X    TO READ WRITE PROTECT STATUS
     4740         BMI .2       DISK IS WRITE PROTECTED
     4750         LDA #$FF     SYNC BYTE
     4760         STA Q7H,X    Q6H,Q7H: (A) TO SHIFT REGISTER
     4770         CMP Q6L,X    Q6L,Q7H: WRITE ON DISK
     4780         PHA          TIME DELAYS
     4790         PLA
     4800  .1     JSR .3       12 CYCLE DELAY
     4810         JSR .3       12 CYCLE DELAY
     4820         STA Q6H,X    WRITE ON DISK
     4830         CMP Q6L,X
     4840         NOP
     4850         DEY
     4860         BNE .1
     4870         LDA #$D5     WRITE D5 AA 96
     4880         JSR WRITE.BYTE.3
     4890         LDA #$AA
     4900         JSR WRITE.BYTE.3
     4910         LDA #$96
     4920         JSR WRITE.BYTE.3
     4930         LDA VOLUME   WRITE VOLUME, TRACK, AND SECTOR
     4940         JSR WRITE.BYTE.1
     4950         LDA TRACK.CNTR
     4960         JSR WRITE.BYTE.1
     4970         LDA FMT.SECTOR
     4980         JSR WRITE.BYTE.1
     4990         LDA VOLUME   COMPUTE CHECKSUM
     5000         EOR TRACK.CNTR
     5010         EOR FMT.SECTOR
     5020         PHA          WRITE CHECKSUM
     5030         LSR
     5040         ORA CONST.AA   #$AA, FOR TIMING
     5050         STA Q6H,X
     5060         LDA Q6L,X
     5070         PLA
     5080         ORA #$AA
     5090         JSR WRITE.BYTE.2
     5100         LDA #$DE     WRITE DE AA EB
     5110         JSR WRITE.BYTE.3
     5120         LDA #$AA
     5130         JSR WRITE.BYTE.3
     5140         LDA #$EB
     5150         JSR WRITE.BYTE.3
     5160         CLC
     5170  .2     LDA Q7L,X
     5180         LDA Q6L,X
     5190  .3     RTS
     5200  *---------------------------------
     5210  *      SUBROUTINES TO WRITE BYTE ON DISK
     5220  *---------------------------------
     5230  WRITE.BYTE.1
     5240         PHA          ADDRESS BLOCK FORMAT
     5250         LSR
     5260         ORA CONST.AA
     5270         STA Q6H,X
     5280         CMP Q6L,X
     5290         PLA
     5300         NOP
     5310         NOP
     5320         NOP
     5330         ORA #$AA
     5340  WRITE.BYTE.2
     5350         NOP
     5360  WRITE.BYTE.3
     5370         NOP
     5380         PHA
     5390         PLA
     5400         STA Q6H,X
     5410         CMP Q6L,X
     5420         RTS
     5430  *---------------------------------
     5440  *      $BCDF THRU $BCFF IS NOT USED BY DOS 3.3
     5450  *---------------------------------
     5460         .PG

Beneath Apple DOS -- A Review | Bob Sander-Cederlof

If you have any interest whatsoever in DOS, be sure to buy this book! It costs $19.95 (plus shipping), from Quality Software, 6660 Reseda Blvd., Suite 105, Reseda, CA 91335. Call them up at (213) 344-6599 and give them your Master Charge or VISA number. Do it now!

Or better yet, send your check for $18 to S-C SOFTWARE, P. O. Box 5537, Richardson, TX 75080. I'll mail you a copy postpaid right away! Saves you both time and money!

The authors of Beneath Apple DOS are Don Worth and Pieter Lechner. You may know Don from his adventure-like program, "Beneath Apple Manor", or from his LINKER program (both available from Quality Software).

The book is published with a plastic comb binding, and is about the same dimensions as the "Apple Assembly Line". There are 156 pages, organized into 8 chapters and 3 appendices. A comprehensive Quick Reference Card for DOS 3.3 is included. There are cartoon sketches throughout which both amuse and aid comprehension, as well as more traditional diagrams and charts and tables. A four page index helps you find whatever you need to know.

Though the book focuses on DOS 3.3, it covers all the major differences found in earlier versions. Chapter 2 is called "The Evolution of DOS", and traces features and differences from Versions 3, 3.1, 3.2, 3.2.1, and 3.3. At other points throughout the book, wherever the various versions differ, the details for each version are explained.

Chapter 3 covers diskette formatting, in much more detail than the Apple DOS manual: how bits are recorded, how 256 bytes are converted to 410 or 342 shorter bytes, how those shorter bytes are converted to encoded bytes ready to be written, how the checksum is computed and tested, how the sectors are identified around a track, all about self-sync bytes, and how sectors are interleaved.

Chapter 4 covers diskette organization: the DOS image, the Volume Table of Contents, the catalog, track/sector lists, and the format of each type of file. Some guidelines for repairing damaged diskettes are given.

Chapter 5 outlines the overall structure of DOS. The booting process is explained in a fair amount of detail. If you need more information on DOS internals, chapter 8 is for you.

Chapter 6 gives clear instructions for using RWTS from machine language programs. You may already be quite familiar with this, because: 1) it is fairly well explained in the DOS manual; 2) many articles have been published in magazines and newsletters telling you how; and 3) you have gone ahead and tried it yourself. But there is another way to get into DOS which treats files as files, but without the normal DOS overhead. Apple's FID utility uses this way in, through the so-called File Manager. Chapter 6 goes into great detail describing the File Manager, and some examples showing how to use it are given. This information has never been published before, and is well worth the price of the entire book. Chapter 6 also shows you how to talk to the disk drive directly, without any DOS at all.

Chapter 7 explains how to customize DOS, and gives the patches for four nice custom features: avoiding the language card reload, making space between DOS and its buffers, removing the pause during a long CATALOG, and changing the HELLO file start-up from RUN to BRUN or EXEC.

Chapter 8, 42 pages long, describes EVERY routine in DOS. It starts with the disk controller ROM (at C600 of your controller is in slot 6), and goes from 9D00 through BFFF subroutine by subroutine. The descriptions are in text form: no disassembled code, and no flowcharts. If you put the book beside a disassembled section of DOS, it is easily understood. Data sections are outlined also, so that you can tell what every byte is there for. The last page of chapter 8 lists all the zero-page variables used by DOS, and explains each use.

Appendix A contains five sample programs which can be used to examine and repair diskettes. They also illustrate the use of RWTS and the File Manager.

Appendix B briefly explains the philosophy of disk protection schemes. Someday someone will write a whole book on this subject. This Appendix is only four pages, so you won't find out how to create the uncrackable disk, or even how to crack it if you did.

Appendix C is an excellent glossary of terms used in the book. I estimate that about 160 words are defined.

The authors list five good reasons why they wrote Beneath Apple DOS; no, six:

1. To show direct assembly language access to DOS.
2. To help you fix clobbered diskettes.
3. To correct errors and ommissions in the Apple manuals.
4. To provide complete infomation on diskette formatting and DOS internal operation.
5. To allow you to customize DOS to fit your needs.
6. To make the authors a lot of money.

They have done an excellent job with the first five objectives, and I think number 6 will be met as well.

Apple Assembly Line is published monthly by S-C SOFTWARE, P. O. Box 5537, Richardson, TX 75080. Subscription rate is $12/year, in the U.S.A., Canada, and Mexico. Other countries add $6/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.)