Apple Assembly Line Volume 1 -- Issue 9 June, 1981 In This Issue... ---------------- Two Fancy Tone Generators . . . . . . . . . . . . . . . . . 2 More About Multiplying on the 6502 . . . . . . . . . . . . 5 Specialized Multiplications . . . . . . . . . . . . . . . . 7 Commented Listing of DOS 3.3 $B800-BCFF . . . . . . . . . . 10 Beneath Apple DOS -- A Review . . . . . . . . . . . . . . . 19 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 ,, 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 ,, 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 ---------- 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 ---------------------------------- 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 --------------------------- 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 --------------------------------------- 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. 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 ----------------------------- 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.