Apple Assembly Line
Volume 1 -- Issue 9June 1981

In This Issue...

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 GeneratorsMark 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 6502Bob 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 MultiplicationsBob 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-BCFFBob 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 ReviewBob 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.)