In This Issue...
Demise of Bailey's DataPhile Digest
Unfortunately, we no sooner sent out last month's AAL than we received a letter from the Baileys saying that they have ceased to publish the DataPhile Digest.
Quarterly Disk 13
QD 13 is now ready, and it includes both installments of ProDOS commented source code as listed last month and this. The code is in the format used by the S-C Macro Assembler. (Since the disk also includes the CONVERT S-C TO TEXT program in this issue, all of you can use it!) Quarterly Disks are $15 each, or $45 for a year's subscription.
Subscription Rates
Remember, subscriptions to Apple Assembly Line will be increasing to $18/year effective January 1. Since some of you may not receive this issue (or your renewal notice) until after that date, we'll extend the deadline to January 15 for renewals.
Last month I printed the commented listing of the disk reading subroutines. This month's selection covers disk writing, track positioning, and interrupt handling. Together the two articles cover all the code between $F800 and $FFFF.
Several callers have wondered if this is all there is to ProDOS. No! It is only a small piece. In my opinion, this is the place to start in understanding ProDOS's features: A faster way of getting information to and from standard floppies. But remember that ProDOS also supports the ProFILE hard disk, and a RAM disk in the extended Apple //e memory.
Further, ProDOS has a file structure exactly like Apple /// SOS, with a hierarchical directory and file sizes up to 16 megabytes.
Further, ProDOS includes support for a clock/calendar card, 80-columns with Smarterm or //e, and interrupts.
ProDOS uses or reserves all but 255 bytes of the 16384 bytes in the language card area (both $D000-DFFF banks and all #E000-FFFF). The 255 bytes not reserved are from $D001 through $D0FF in one of the $D000 banks. The byte at $D000 is reserved, because ProDOS uses it to distinguish which $D000 bank is switched on when an interrupt occurs. The space at $BF00-BFFF is used by ProDOS for system linkages and variables (called the System Global Page).
In addition, if you are using Applesoft, ProDOS uses memory from $9600-BEFF. This space does not include any file buffers. When you OPEN files, buffers are allocated as needed. CLOSEing automatically de-allocates buffers. Each buffer is 1024 bytes long. As you can see, with ProDOS in place your Applesoft program has less room than ever.
Track Seeking: $F90C-F995
The SEEK.TRACK subroutine begins at $F90C. The very first instruction multiplies the track number by two, converting ProDOS logical track number to a physical track number. If you want to access a "half-track" position, you could either store a NOP opcode at $F90C, or enter the subroutine at $F90D.
A table is maintained of the current track position for each of up to 12 drives. I call it the OLD.TRACK.TABLE. The subroutine GET.SSSD.IN.X forms an index into OLD.TRACK.TABLE from slot# * 2 + drive#. There are no entries in the table for drives in slots 0 or 1, which is fine with me. ProDOS uses these slots as pseudo slots for the RAM-based pseudo-disk and for ProFILE, if I remember correctly.
The code in SEEK.TRACK.ABSOLUTE is similar but not identical to code in DOS 3.3. The differences do not seem to be significant.
Disk Writing: $FD00-FE9A
The overall process of writing a sector is handled by code in RWTS, which was listed last month. After the desired track is found, RWTS calls PRE.NYBBLE to build a block of 86 bytes containing the low-order two bits from each byte in the caller's buffer. PRE.NYBBLE also stores a number of buffer addresses and slot*16 values inside the WRITE.SECTOR subroutine. Next RWTS calls READ.ADDRESS to find the sector, and then WRITE.SECTOR to put the data out.
WRITE.SECTOR is the real workhorse. And it is very critically timed. Once the write head in your drive is enabled, every machine cycle is closely counted until the last byte is written. First, five sync bytes are written (ten bits each, 1111111100). These are written by putting $FF in the write register at 40 cycle intervals. Following the sync bytes W.S writes a data header of D5 AA AD.
Second, the 86-byte block which PRE.NYBBLE built is written, followed by the coded form of the rest of your buffer. WRITE.SECTOR picks up bytes directly from your buffer, keeps a running checksum, encodes the high-order six bits into an 8-bit value, and writes it on the disk...one byte every 32 cycles, exactly. Since your buffer can be any arbitrary place in memory, and since the 6502 adds cycles for indexed instructions that cross page boundaries, WRITE.SECTOR splits the buffer in parts before and after a page boundary. All the overhead for the split is handled in PRE.NYBBLE, before the timed operations begin.
Finally, the checksum and a data trailer of DE AA EB FF are written.
Empty Space: $FEBE-FF9A
This space had no code in it. Nearly a whole page here.
Interrupt & RESET Handling: $FF9B-FFFF
If the RAM card is switched on when an interrupt or RESET occurs, the vectors at $FFFA-FFFF will be those ProDOS installed rather than the ones permanently coded in ROM. It turns out the non-maskable interrupt (NMI) is still vectored down into page 3. But the more interesting IRQ interrupt is now vectored to code at $FF9B inside ProDOS.
The ProDOS IRQ handler performs two functions beyond those built-in to the monitor ROM. First, the contents of location $45 are saved so that the monitor can safely clobber it. Second, a flag is set indicating which $D000 bank is currently switched on, so that it can be restored after the interrupt handler is finished. (The second step is omitted if the interrupt was caused by a BRK opcode.)
If the IRQ was not due to a BRK opcode, a fake "RTI" vector is pushed on the stack. This consists of a return address of $BF50 and a status of $04. The status keeps IRQ interrupts disabled, and $BF50 is a short routine which turns the ProDOS memory back on and jumps up to INT.SPLICE at $FFD8:
BF50- 8D 8B C0 STA $C08B BF53- 4C D8 FF JMP $FFD8
Of course, before coming back via the RTI, ProDOS tries to USE the interrupt. If you have set up one or more interrupt vectors with the ProDOS system call, they will be called.
INT.SPLICE restores the contents of $45 and switches the main $D000 bank on. Then it jumps back to $BFD3 with the information about which $D000 bank really should be on. $BFD3 turns on the other bank if necessary, and returns to the point at which the interrupt occured.
The instruction at $FFC8 is interesting. STA $C082 turns on the monitor ROM, so the next instruction to be executed is at $FFCB in ROM. This is an RTS opcode, so the address on the stack at that point is used. There are two possible values: $FA41 if an IRQ interrupt is being processed, or $FA61 if a RESET is being processed. This means the RTS will effectively branch to $FA42 or $FA62.
Uh Oh! At this point you had better hope that you are not running with the original Apple monitor ROM. The Apple II Plus ROM (called Autostart Monitor) and the Apple //e ROM are fine. $FA42 is the second instruction of the IRQ code, and $FA62 is the standard RESET handler. But the original ROM, like I have in my serial 219 machine, has entirely different code there.
I have an $FF at $FA42, followed by code for the monitor S (single step) command. And $FA62 is right in the middle of the S command. There is no telling what might happen, short of actually trying it out. No thanks. Just remember that RESET, BRK, and IRQ interrupts will not work correctly if they happen when the RAM area is switched on and you have the old original monitor in ROM.
There is another small empty space from $FFE9 through $FFF9, 17 bytes.
Perhaps I should point out that the listings this month and last are from the latest release of ProDOS, which may not be the final released version. However, I would expect any differences in the regions I have covered so far to be slight.
[ In this web edition I have included the entire code, instead of just the pieces not printed in the November 1983 issue.]
1000 .TI 76,PRODOS F800-FFFF.....COMMENTED BY RBS-C 11-8-83............ 1010 *SAVE S.PRODOS F800-FFFF 1020 *-------------------------------- 1030 RUNNING.SUM .EQ $3A 1040 TBUF.0 .EQ $3A 1050 BYTE.AT.BUF00 .EQ $3B 1060 BYTE.AT.BUF01 .EQ $3C 1070 LAST.BYTE .EQ $3D 1080 SLOT.X16 .EQ $3E 1090 INDEX.OF.LAST.BYTE .EQ $3F 1100 *-------------------------------- 1110 RWB.COMMAND .EQ $42 1120 RWB.SLOT .EQ $43 DSSSXXXX 1130 RWB.BUFFER .EQ $44,45 1140 RWB.BLOCK .EQ $46,47 0...279 1150 *-------------------------------- 1160 BUFF.BASE .EQ $4700 DUMMY ADDRESS FOR ASSEMBLY ONLY 1170 *-------------------------------- 1180 SAVE.LOC45 .EQ $BF56 1190 SAVE.D000 .EQ $BF57 1200 INTAREG .EQ $BF88 1210 INTBANKID .EQ $BF8D 1220 IRQXIT.3 .EQ $BFD3 1230 *-------------------------------- 1240 DRV.PHASE .EQ $C080 1250 DRV.MTROFF .EQ $C088 1260 DRV.MTRON .EQ $C089 1270 DRV.ENBL.0 .EQ $C08A 1280 DRV.Q6L .EQ $C08C 1290 DRV.Q6H .EQ $C08D 1300 DRV.Q7L .EQ $C08E 1310 DRV.Q7H .EQ $C08F 1320 *-------------------------------- 1330 * <<<COMPUTED >>> 1340 MODIFIER .EQ $60 <<<SLOT * 16>>> 1350 *-------------------------------- 1360 .OR $F800 1370 .TA $800 1380 *-------------------------------- 1390 * READ/WRITE A BLOCK 1400 * 1410 * 1. ASSURE VALID BLOCK NUMBER (0...279) 1420 * 2. CONVERT BLOCK NUMBER TO TRACK/SECTOR 1430 * TRACK = INT(BLOCK/8) 1440 * BLOCK SECTORS 1450 * ----- --------- 1460 * 0 0 AND 2 1470 * 1 4 AND 6 1480 * 2 8 AND 10 1490 * 3 12 AND 14 1500 * 4 1 AND 3 1510 * 5 5 AND 7 1520 * 6 9 AND 11 1530 * 7 13 AND 15 1540 * 3. CALL RWTS TWICE 1550 * 4. RETURN WITH ERROR STATUS 1560 *-------------------------------- 1570 RWB 1580 LDA RWB.BLOCK BLOCK MUST BE 0...279 1590 LDX RWB.BLOCK+1 1600 STX RWTS.TRACK 1610 BEQ .1 ...BLOCK # LESS THAN 256 1620 DEX 1630 BNE .5 ...BLOCK # MORE THAN 511 1640 CMP #$18 1650 BCS .5 ...BLOCK # MORE THAN 279 1660 .1 LDY #5 SHIFT 5 BITS OF TRACK # 1670 .2 ASL RWTS.TRACK A-REG 1680 ROL RWTS.TRACK ---------- -------- 1690 DEY 00TTTTTT ABC00000 1700 BNE .2 1710 ASL TRANSFORM BLOCK # INTO SECTOR # 1720 BCC .3 ABC00000 --> 0000BC0A 1730 ORA #$10 1740 .3 LSR 1750 LSR 1760 LSR 1770 LSR 1780 PHA 1790 JSR RWTS R/W FIRST SECTOR OF BLOCK 1800 PLA 1810 BCS .4 ...ERROR 1820 INC RWB.BUFFER+1 1830 ADC #2 1840 JSR RWTS R/W SECOND SECTOR OF BLOCK 1850 DEC RWB.BUFFER+1 1860 .4 LDA RWTS.ERROR 1870 RTS 1880 *---BLOCK NUMBER > 279----------- 1890 .5 LDA #$27 I/O ERROR 1900 SEC 1910 RTS 1920 *-------------------------------- 1930 * READ/WRITE A GIVEN SECTOR 1940 *-------------------------------- 1950 RWTS 1960 LDY #1 TRY SEEKING TWICE 1970 STY SEEK.COUNT 1980 STA RWTS.SECTOR 1990 LDA RWB.SLOT 2000 AND #$70 0SSS0000 2010 STA SLOT.X16 2020 JSR WAIT.FOR.OLD.MOTOR.TO.STOP 2030 JSR CHECK.IF.MOTOR.RUNNING 2040 PHP SAVE ANSWER (.NE. IF RUNNING) 2050 LDA #$E8 MOTOR STARTING TIME 2060 STA MOTOR.TIME+1 ONLY HI-BYTE NECESSARY 2070 LDA RWB.SLOT SAME SLOT AND DRIVE? 2080 CMP OLD.SLOT 2090 STA OLD.SLOT 2100 PHP SAVE ANSWER 2110 ASL DRIVE # TO C-BIT 2120 LDA DRV.MTRON,X START MOTOR 2130 BCC .1 ...DRIVE 0 2140 INX ...DRIVE 1 2150 .1 LDA DRV.ENBL.0,X ENABLE DRIVE X 2160 PLP SAME SLOT/DRIVE? 2170 BEQ .3 ...YES 2180 PLP DISCARD ANSWER ABOUT MOTOR GOING 2190 LDY #7 DELAY 150-175 MILLISECS 2200 .2 JSR DELAY.100 DELAY 25 MILLISECS 2210 DEY 2220 BNE .2 2230 PHP SAY MOTOR NOT ALREADY GOING 2240 .3 LDA RWB.COMMAND 0=TEST, 1=READ, 2=WRITE 2250 BEQ .4 ...0, MERELY TEST 2260 LDA RWTS.TRACK 2270 JSR SEEK.TRACK 2280 .4 PLP WAS MOTOR ALREADY GOING? 2290 BNE .6 ...YES 2300 .5 LDA #1 DELAY 100 USECS 2310 JSR DELAY.100 2320 LDA MOTOR.TIME+1 2330 BMI .5 ...WAIT TILL IT OUGHT TO BE 2340 JSR CHECK.IF.MOTOR.RUNNING 2350 BEQ .14 ...NOT RUNNING YET, ERROR 2360 .6 LDA RWB.COMMAND 2370 BEQ .17 CHECK IF WRITE PROTECTED 2380 LSR .CS. IF READ, .CC. IF WRITE 2390 BCS .7 ...READ 2400 JSR PRE.NYBBLE ...WRITE 2410 .7 LDY #64 TRY 64 TIMES TO FIND THE SECTOR 2420 STY SEARCH.COUNT 2430 .8 LDX SLOT.X16 2440 JSR READ.ADDRESS 2450 BCC .10 ...FOUND IT 2460 .9 DEC SEARCH.COUNT 2470 BPL .8 ...KEEP LOOKING 2480 LDA #$27 I/O ERROR CODE 2490 DEC SEEK.COUNT ANY TRIES LEFT? 2500 BNE .14 ...NO, I/O ERROR 2510 LDA CURRENT.TRACK 2520 PHA 2530 ASL SLIGHT RE-CALIBRATION 2540 ADC #$10 2550 LDY #64 ANOTHER 64 TRIES 2560 STY SEARCH.COUNT 2570 BNE .11 ...ALWAYS 2580 .10 LDY HDR.TRACK ACTUAL TRACK FOUND 2590 CPY CURRENT.TRACK 2600 BEQ .12 FOUND THE RIGHT ONE 2610 LDA CURRENT.TRACK WRONG ONE, TRY AGAIN 2620 PHA 2630 TYA STARTING FROM TRACK FOUND 2640 ASL 2650 .11 JSR UPDATE.TRACK.TABLE 2660 PLA 2670 JSR SEEK.TRACK 2680 BCC .8 ...ALWAYS 2690 .12 LDA HDR.SECTOR 2700 CMP RWTS.SECTOR 2710 BNE .9 2720 LDA RWB.COMMAND 2730 LSR 2740 BCC .15 ...WRITE 2750 JSR READ.SECTOR ...READ 2760 BCS .9 ...READ ERROR 2770 .13 LDA #0 NO ERROR 2780 .HS D0 "BNE"...NEVER, JUST SKIPS "SEC" 2790 .14 SEC ERROR 2800 STA RWTS.ERROR SAVE ERROR CODE 2810 LDA DRV.MTROFF,X STOP MOTOR 2820 RTS RETURN 2830 *-------------------------------- 2840 .15 JSR WRITE.SECTOR 2850 .16 BCC .13 ...NO ERROR 2860 LDA #$2B WRITE PROTECTED ERROR CODE 2870 BNE .14 ...ALWAYS 2880 .17 LDX SLOT.X16 CHECK IF WRITE PROTECTED 2890 LDA DRV.Q6H,X 2900 LDA DRV.Q7L,X 2910 ROL 2920 LDA DRV.Q6L,X 2930 JMP .16 GIVE ERROR IF PROTECTED 2940 *-------------------------------- 2950 SEEK.TRACK 2960 ASL GET PHYSICAL TRACK # 2970 STA HDR.TRACK ...SAVE HERE 2980 JSR CLEAR.PHASES (CARRY WAS CLEAR) 2990 JSR GET.SSSD.IN.X 3000 LDA OLD.TRACK.TABLE,X 3010 STA CURRENT.TRACK 3020 LDA HDR.TRACK 3030 STA OLD.TRACK.TABLE,X 3040 JSR SEEK.TRACK.ABSOLUTE 3050 *-------------------------------- 3060 CLEAR.PHASES 3070 LDY #3 3080 .1 TYA 3090 JSR PHASE.COMMANDER 3100 DEY 3110 BPL .1 3120 LSR CURRENT.TRACK BACK TO LOGICAL TRACK # 3130 CLC SIGNAL NO ERROR 3140 RTS 3150 *-------------------------------- 3160 SEEK.TRACK.ABSOLUTE 3170 STA TARGET.TRACK SAVE ACTUAL TRACK # 3180 CMP CURRENT.TRACK ALREADY THERE? 3190 BEQ .7 ...YES 3200 LDA #0 3210 STA STEP.CNT # STEPS SO FAR 3220 .1 LDA CURRENT.TRACK 3230 STA CURRENT.TRACK.OLD 3240 SEC 3250 SBC TARGET.TRACK 3260 BEQ .6 ...WE HAVE ARRIVED 3270 BCS .2 CURRENT > DESIRED 3280 EOR #$FF CURRENT < DESIRED 3290 INC CURRENT.TRACK 3300 BCC .3 ...ALWAYS 3310 .2 ADC #$FE .CS., SO A=A-1 3320 DEC CURRENT.TRACK 3330 .3 CMP STEP.CNT GET MINIMUM OF: 3340 BCC .4 1. # OF TRACKS TO MOVE LESS 1 3350 LDA STEP.CNT 2. # OF STEPS SO FAR 3360 .4 CMP #9 3. EIGHT 3370 BCS .5 3380 TAY 3390 SEC TURN NEW PHASE ON 3400 .5 JSR .7 3410 LDA ONTBL,Y DELAY 3420 JSR DELAY.100 3430 LDA CURRENT.TRACK.OLD 3440 CLC TURN OLD PHASE OFF 3450 JSR PHASE.COMMANDER 3460 LDA OFFTBL,Y DELAY 3470 JSR DELAY.100 3480 INC STEP.CNT # OF STEPS SO FAR 3490 BNE .1 ...ALWAYS 3500 .6 JSR DELAY.100 3510 CLC TURN PHASE OFF 3520 .7 LDA CURRENT.TRACK 3530 *-------------------------------- 3540 * (A) = TRACK # 3550 * .CC. THEN PHASE OFF 3560 * .CS. THEN PHASE ON 3570 *-------------------------------- 3580 PHASE.COMMANDER 3590 AND #3 ONLY KEEP LOWER TWO BITS 3600 ROL 00000XXC 3610 ORA SLOT.X16 0SSS0XXC 3620 TAX 3630 LDA DRV.PHASE,X 3640 LDX SLOT.X16 RESTORE SLOT*16 3650 RTS 3660 *-------------------------------- 3670 * VALUE READ FROM DISK IS INDEX INTO THIS TABLE 3680 * TABLE ENTRY GIVES TOP 6 BITS OF ACTUAL DATA 3690 * 3700 * OTHER DATA TABLES ARE IMBEDDED IN THE UNUSED 3710 * PORTIONS OF THE BYTE.TABLE 3720 *-------------------------------- 3730 BYTE.TABLE .EQ *-$96 3740 .HS 0004FFFF080CFF101418 3750 BIT.PAIR.LEFT 3760 .HS 008040C0 3770 .HS FFFF1C20FFFFFF24282C 3780 .HS 3034FFFF383C4044 3790 .HS 484CFF5054585C606468 3800 BIT.PAIR.MIDDLE 3810 .HS 00201030 3820 DATA.TRAILER 3830 .HS DEAAEBFF 3840 .HS FFFFFF6CFF70 3850 .HS 7478FFFFFF7CFFFF 3860 .HS 8084FF888C9094989CA0 3870 BIT.PAIR.RIGHT 3880 .HS 0008040C 3890 .HS FFA4A8ACFFB0B4B8BCC0 3900 .HS C4C8FFFFCCD0D4D8 3910 .HS DCE0FFE4E8ECF0F4 3920 .HS F8FC 3930 *-------------------------------- 3940 BIT.PAIR.TABLE 3950 .HS 00000096 3960 .HS 02000097 3970 .HS 0100009A 3980 .HS 0300009B 3990 .HS 0002009D 4000 .HS 0202009E 4010 .HS 0102009F 4020 .HS 030200A6 4030 .HS 000100A7 4040 .HS 020100AB 4050 .HS 010100AC 4060 .HS 030100AD 4070 .HS 000300AE 4080 .HS 020300AF 4090 .HS 010300B2 4100 .HS 030300B3 4110 .HS 000002B4 4120 .HS 020002B5 4130 .HS 010002B6 4140 .HS 030002B7 4150 .HS 000202B9 4160 .HS 020202BA 4170 .HS 010202BB 4180 .HS 030202BC 4190 .HS 000102BD 4200 .HS 020102BE 4210 .HS 010102BF 4220 .HS 030102CB 4230 .HS 000302CD 4240 .HS 020302CE 4250 .HS 010302CF 4260 .HS 030302D3 4270 .HS 000001D6 4280 .HS 020001D7 4290 .HS 010001D9 4300 .HS 030001DA 4310 .HS 000201DB 4320 .HS 020201DC 4330 .HS 010201DD 4340 .HS 030201DE 4350 .HS 000101DF 4360 .HS 020101E5 4370 .HS 010101E6 4380 .HS 030101E7 4390 .HS 000301E9 4400 .HS 020301EA 4410 .HS 010301EB 4420 .HS 030301EC 4430 .HS 000003ED 4440 .HS 020003EE 4450 .HS 010003EF 4460 .HS 030003F2 4470 .HS 000203F3 4480 .HS 020203F4 4490 .HS 010203F5 4500 .HS 030203F6 4510 .HS 000103F7 4520 .HS 020103F9 4530 .HS 010103FA 4540 .HS 030103FB 4550 .HS 000303FC 4560 .HS 020303FD 4570 .HS 010303FE 4580 .HS 030303FF 4590 *-------------------------------- 4600 TBUF .BS 86 4610 *-------------------------------- 4620 RWTS.TRACK .HS 07 4630 RWTS.SECTOR .HS 0F 4640 RWTS.ERROR .HS 00 4650 OLD.SLOT .HS 60 4660 CURRENT.TRACK .HS 07 4670 .HS 00 4680 *-------------------------------- 4690 OLD.TRACK.TABLE .EQ *-4 4700 .HS 0000 SLOT 2, DRIVE 0--DRIVE 1 4710 .HS 0000 SLOT 3 4720 .HS 0000 SLOT 4 4730 .HS 0000 SLOT 5 4740 .HS 0E00 SLOT 6 4750 .HS 0000 SLOT 7 4760 *-------------------------------- 4770 .HS 00 4780 *-------------------------------- 4790 SEARCH.COUNT .BS 1 4800 SEEK.COUNT .BS 1 4810 STEP.CNT .EQ * 4820 SEEK.D5.CNT .EQ * 4830 X1X1X1X1 .BS 1 ALSO STEP.CNT & SEEK.D5.CNT 4840 CHECK.SUM .BS 1 4850 HDR.CHKSUM .BS 1 4860 HDR.SECTOR .BS 1 4870 HDR.TRACK .EQ * 4880 MOTOR.TIME .BS 2 ALSO HDR.TRACK & HDR.VOLUME 4890 CURRENT.TRACK.OLD .BS 1 4900 TARGET.TRACK .BS 1 4910 *-------------------------------- 4920 * DELAY TIMES FOR ACCELERATION & DECELERATION 4930 * OF TRACK STEPPING MOTOR 4940 *-------------------------------- 4950 ONTBL .HS 01302824201E1D1C1C 4960 OFFTBL .HS 702C26221F1E1D1C1C 4970 *-------------------------------- 4980 * DELAY ABOUT 100*A MICROSECONDS 4990 * RUN DOWN MOTOR.TIME WHILE DELAYING 5000 *-------------------------------- 5010 DELAY.100 5020 .1 LDX #17 5030 .2 DEX 5040 BNE .2 5050 INC MOTOR.TIME 5060 BNE .3 5070 INC MOTOR.TIME+1 5080 .3 SEC 5090 SBC #1 5100 BNE .1 5110 RTS 5120 *-------------------------------- 5130 READ.ADDRESS 5140 LDY #$FC TRY 772 TIMES TO FIND $D5 5150 STY SEEK.D5.CNT (FROM $FCFC TO $10000) 5160 .1 INY 5170 BNE .2 ...KEEP TRYING 5180 INC SEEK.D5.CNT 5190 BEQ .11 ...THAT IS ENUF! 5200 .2 LDA DRV.Q6L,X GET NEXT BYTE 5210 BPL .2 5220 .3 CMP #$D5 IS IT $D5? 5230 BNE .1 ...NO, TRY AGAIN 5240 NOP ...YES, DELAY 5250 .4 LDA DRV.Q6L,X GET NEXT BYTE 5260 BPL .4 5270 CMP #$AA NOW NEED $AA AND $96 5280 BNE .3 ...NO, BACK TO $D5 SEARCH 5290 LDY #3 (READ 3 BYTES LATER) 5300 .5 LDA DRV.Q6L,X GET NEXT BYTE 5310 BPL .5 5320 CMP #$96 BETTER BE... 5330 BNE .3 ...IT IS NOT 5340 SEI ...NO INTERRUPTS NOW 5350 LDA #0 START CHECK SUM 5360 .6 STA CHECK.SUM 5370 .7 LDA DRV.Q6L,X GET NEXT BYTE 5380 BPL .7 1X1X1X1X 5390 ROL X1X1X1X1 5400 STA X1X1X1X1 5410 .8 LDA DRV.Q6L,X GET NEXT BYTE 5420 BPL .8 1Y1Y1Y1Y 5430 AND X1X1X1X1 XYXYXYXY 5440 STA HDR.CHKSUM,Y 5450 EOR CHECK.SUM 5460 DEY 5470 BPL .6 5480 TAY CHECK CHECKSUM 5490 BNE .11 NON-ZERO, ERROR 5500 .9 LDA DRV.Q6L,X GET NEXT BYTE 5510 BPL .9 5520 CMP #$DE TRAILER EXPECTED $DE.AA.EB 5530 BNE .11 NO, ERROR 5540 NOP 5550 .10 LDA DRV.Q6L,X 5560 BPL .10 5570 CMP #$AA 5580 BNE .11 NO, ERROR 5590 CLC 5600 RTS 5610 .11 SEC 5620 RTS 5630 *-------------------------------- 5640 READ.SECTOR 5650 TXA SLOT*16 ($60 FOR SLOT 6) 5660 ORA #$8C BUILD Q6L ADDRESS FOR SLOT 5670 STA .9+1 STORE INTO READ-DISK OPS 5680 STA .12+1 5690 STA .13+1 5700 STA .15+1 5710 STA .18+1 5720 LDA RWB.BUFFER PLUG CALLER'S BUFFER 5730 LDY RWB.BUFFER+1 ADDRESS INTO STORE'S 5740 STA .17+1 PNTR FOR LAST THIRD 5750 STY .17+2 5760 SEC PNTR FOR MIDDLE THIRD 5770 SBC #84 5780 BCS .1 5790 DEY 5800 .1 STA .14+1 5810 STY .14+2 5820 SEC PNTR FOR BOTTOM THIRD 5830 SBC #87 5840 BCS .2 5850 DEY 5860 .2 STA .11+1 5870 STY .11+2 5880 *---FIND $D5.AA.AD HEADER-------- 5890 LDY #32 MUST FIND $D5 WITHIN 32 BYTES 5900 .3 DEY 5910 BEQ .10 ERROR RETURN 5920 .4 LDA DRV.Q6L,X 5930 BPL .4 5940 .5 EOR #$D5 5950 BNE .3 5960 NOP 5970 .6 LDA DRV.Q6L,X 5980 BPL .6 5990 CMP #$AA 6000 BNE .5 6010 NOP 6020 .7 LDA DRV.Q6L,X 6030 BPL .7 6040 CMP #$AD 6050 BNE .5 6060 *---READ 86 BYTES INTO TBUF...TBUF+85---------- 6070 *---THESE ARE THE PACKED LOWER TWO BITS-------- 6080 *---FROM EACH BYTE OF THE CALLER'S BUFFER.----- 6090 LDY #170 6100 LDA #0 INIT RUNNING EOR-SUM 6110 .8 STA RUNNING.SUM 6120 .9 LDX DRV.Q6L+MODIFIER READ NEXT BYTE 6130 BPL .9 6140 LDA BYTE.TABLE,X DECODE DATA 6150 STA TBUF-170,Y 6160 EOR RUNNING.SUM 6170 INY 6180 BNE .8 6190 *---READ NEXT 86 BYTES------------------------- 6200 *---STORE 1ST 85 IN BUFFER...BUFFER+84--------- 6210 *---SAVE THE 86TH BYTE ON THE STACK------------ 6220 LDY #170 6230 BNE .12 ...ALWAYS 6240 *-- 6250 .10 SEC I/O ERROR EXIT 6260 RTS 6270 *-- 6280 .11 STA BUFF.BASE-171,Y 6290 .12 LDX DRV.Q6L+MODIFIER READ NEXT BYTE 6300 BPL .12 6310 EOR BYTE.TABLE,X DECODE DATA 6320 LDX TBUF-170,Y MERGE LOWER 2 BITS 6330 EOR BIT.PAIR.TABLE,X 6340 INY 6350 BNE .11 6360 PHA SAVE LAST BYTE (LATER BUFFER+85) 6370 *---READ NEXT 86 BYTES----------- 6380 *---STORE AT BUFFER+86...BUFFER+171------------ 6390 AND #$FC MASK FOR RUNNING EOR.SUM 6400 LDY #170 6410 .13 LDX DRV.Q6L+MODIFIER READ NEXT BYTE 6420 BPL .13 6430 EOR BYTE.TABLE,X DECODE DATA 6440 LDX TBUF-170,Y MERGE LOWER 2 BITS 6450 EOR BIT.PAIR.TABLE+1,X 6460 .14 STA BUFF.BASE-84,Y 6470 INY 6480 BNE .13 6490 *---READ NEXT 84 BYTES------------------------- 6500 *---INTO BUFFER+172...BUFFER+255--------------- 6510 .15 LDX DRV.Q6L+MODIFIER READ NEXT BYTE 6520 BPL .15 6530 AND #$FC 6540 LDY #172 6550 .16 EOR BYTE.TABLE,X DECODE DATA 6560 LDX TBUF-172,Y MERGE LOWER 2 BITS 6570 EOR BIT.PAIR.TABLE+2,X 6580 .17 STA BUFF.BASE,Y 6590 .18 LDX DRV.Q6L+MODIFIER READ NEXT BYTE 6600 BPL .18 6610 INY 6620 BNE .16 6630 AND #$FC 6640 *---END OF DATA------------------ 6650 EOR BYTE.TABLE,X DECODE DATA 6660 BNE .20 ...BAD CHECKSUM 6670 LDX SLOT.X16 CHECK FOR TRAILER $DE 6680 .19 LDA DRV.Q6L,X 6690 BPL .19 6700 CMP #$DE 6710 CLC 6720 BEQ .21 ...GOOD READ! 6730 .20 SEC ...SIGNAL BAD READ 6740 .21 PLA STORE BYTE AT BUFFER+85 6750 LDY #85 6760 STA (RWB.BUFFER),Y 6770 RTS 6780 *-------------------------------- 6790 UPDATE.TRACK.TABLE 6800 JSR GET.SSSD.IN.X 6810 STA OLD.TRACK.TABLE,X 6820 RTS 6830 *-------------------------------- 6840 CHECK.IF.MOTOR.RUNNING 6850 LDX SLOT.X16 6860 CHECK.IF.MOTOR.RUNNING.X 6870 LDY #0 6880 .1 LDA DRV.Q6L,X READ CURRENT INPUT REGISTER 6890 JSR .2 ...12 CYCLES... 6900 PHA ...7 MORE CYCLES... 6910 PLA 6920 CMP DRV.Q6L,X BY NOW INPUT REGISTER 6930 BNE .2 SHOULD HAVE CHANGED 6940 LDA #$28 ERROR CODE: NO DEVICE CONNECTED 6950 DEY BUT TRY 255 MORE TIMES 6960 BNE .1 ...RETURN .NE. IF MOVING... 6970 .2 RTS ...RETURN .EQ. IF NOT MOVING... 6980 *-------------------------------- 6990 GET.SSSD.IN.X 7000 PHA SAVE A-REG 7010 LDA RWB.SLOT DSSSXXXX 7020 LSR 7030 LSR 7040 LSR 7050 LSR 0000DSSS 7060 CMP #8 SET CARRY IF DRIVE 2 7070 AND #7 00000SSS 7080 ROL 0000SSSD 7090 TAX INTO X-REG 7100 PLA RESTORE A-REG 7110 RTS 7120 *-------------------------------- 7130 WRITE.SECTOR 7140 SEC IN CASE WRITE-PROTECTED 7150 LDA DRV.Q6H,X 7160 LDA DRV.Q7L,X 7170 BPL .1 ...NOT WRITE PROTECTED 7180 JMP WS.RET ...PROTECTED, ERROR 7190 *-------------------------------- 7200 .1 LDA TBUF 7210 STA TBUF.0 7220 *---WRITE 5 SYNC BYTES----------- 7230 LDA #$FF 7240 STA DRV.Q7H,X 7250 ORA DRV.Q6L,X 7260 LDY #4 7270 NOP $FF AT 40-CYCLE INTERVALS LEAVES 7280 PHA TWO ZERO-BITS AFTER EACH $FF 7290 PLA 7300 .2 PHA 7310 PLA 7320 JSR WRITE2 7330 DEY 7340 BNE .2 7350 *---WRITE $D5 AA AD HEADER------- 7360 LDA #$D5 7370 JSR WRITE1 7380 LDA #$AA 7390 JSR WRITE1 7400 LDA #$AD 7410 JSR WRITE1 7420 *---WRITE 86 BYTES FROM TBUF------------------- 7430 *---BACKWARDS: TBUF+85...TBUF+1, TBUF.0------ 7440 TYA =0 7450 LDY #86 7460 BNE .4 7470 .3 LDA TBUF,Y 7480 .4 EOR TBUF-1,Y 7490 TAX 7500 LDA BIT.PAIR.TABLE+3,X 7510 LDX SLOT.X16 7520 STA DRV.Q6H,X 7530 LDA DRV.Q6L,X 7540 DEY 7550 BNE .3 7560 LDA TBUF.0 7570 *---WRITE PORTION OF BUFFER------ 7580 *---UP TO A PAGE BOUNDARY-------- 7590 LDY #*-* FILLED IN WITH LO-BYTE OF BUFFER ADDRESS 7600 WS...5 EOR BUFF.BASE,Y HI-BYTE FILLED IN 7610 AND #$FC 7620 TAX 7630 LDA BIT.PAIR.TABLE+3,X 7640 WS...6 LDX #MODIFIER 7650 STA DRV.Q6H,X 7660 LDA DRV.Q6L,X 7670 WS...7 LDA BUFF.BASE,Y HI-BYTE FILLED IN 7680 INY 7690 BNE WS...5 7700 *---BRANCH ACCORDING TO BUFFER BOUNDARY CONDITIONS----- 7710 LDA BYTE.AT.BUF00 7720 BEQ WS..17 ...BUFFER ALL IN ONE PAGE 7730 LDA INDEX.OF.LAST.BYTE 7740 BEQ WS..16 ...ONLY ONE BYTE IN NEXT PAGE 7750 *---MORE THAN ONE BYTE IN NEXT PAGE-------------------- 7760 LSR ...DELAY TWO CYCLES 7770 LDA BYTE.AT.BUF00 PRE.NYBBLE ALREADY ENCODED 7780 STA DRV.Q6H,X THIS BYTE 7790 LDA DRV.Q6L,X 7800 LDA BYTE.AT.BUF01 7810 NOP 7820 INY 7830 BCS WS..12 7840 WS...8 EOR BUFF.BASE+256,Y HI-BYTE FILLED IN 7850 AND #$FC 7860 TAX 7870 LDA BIT.PAIR.TABLE+3,X 7880 WS...9 LDX #MODIFIER 7890 STA DRV.Q6H,X 7900 LDA DRV.Q6L,X 7910 WS..10 LDA BUFF.BASE+256,Y HI-BYTE FILLED IN 7920 INY 7930 WS..11 EOR BUFF.BASE+256,Y HI-BYTE FILLED IN 7940 WS..12 CPY INDEX.OF.LAST.BYTE 7950 AND #$FC 7960 TAX 7970 LDA BIT.PAIR.TABLE+3,X 7980 WS..13 LDX #MODIFIER 7990 STA DRV.Q6H,X 8000 LDA DRV.Q6L,X 8010 WS..14 LDA BUFF.BASE+256,Y HI-BYTE FILLED IN 8020 INY 8030 BCC WS...8 8040 BCS .15 ...3 CYCLE NOP 8050 .15 BCS WS..17 ...ALWAYS 8060 *---WRITE BYTE AT BUFFER.00--------------------------- 8070 WS..16 .DA #$AD,BYTE.AT.BUF00 4 CYCLES: LDA BYTE.AT.BUF00 8080 STA DRV.Q6H,X 8090 LDA DRV.Q6L,X 8100 PHA 8110 PLA 8120 PHA 8130 PLA 8140 WS..17 LDX LAST.BYTE 8150 LDA BIT.PAIR.TABLE+3,X 8160 WS..18 LDX #MODIFIER 8170 STA DRV.Q6H,X 8180 LDA DRV.Q6L,X 8190 LDY #0 8200 PHA 8210 PLA 8220 *---WRITE DATA TRAILER: $DE AA EB FF---------- 8230 NOP 8240 NOP 8250 .19 LDA DATA.TRAILER,Y 8260 JSR WRITE3 8270 INY 8280 CPY #4 8290 BNE .19 8300 CLC SIGNAL NO ERROR 8310 WS.RET LDA DRV.Q7L,X DRIVE TO SAFE MODE 8320 LDA DRV.Q6L,X 8330 RTS 8340 *-------------------------------- 8350 WRITE1 CLC 8360 WRITE2 PHA 8370 PLA 8380 WRITE3 STA DRV.Q6H,X 8390 ORA DRV.Q6L,X 8400 RTS 8410 *-------------------------------- 8420 PRE.NYBBLE 8430 LDA RWB.BUFFER PLUG IN ADDRESS TO LOOP BELOW 8440 LDY RWB.BUFFER+1 8450 CLC 8460 ADC #2 8470 BCC .1 8480 INY 8490 .1 STA PN...6+1 8500 STY PN...6+2 8510 SEC 8520 SBC #$56 8530 BCS .2 8540 DEY 8550 .2 STA PN...5+1 8560 STY PN...5+2 8570 SEC 8580 SBC #$56 8590 BCS .3 8600 DEY 8610 .3 STA PN...4+1 8620 STY PN...4+2 8630 *---PACK THE LOWER TWO BITS INTO TBUF------------- 8640 LDY #170 8650 PN...4 LDA BUFF.BASE-170,Y ADDRESS FILLED IN 8660 AND #3 8670 TAX 8680 LDA BIT.PAIR.RIGHT,X 8690 PHA 8700 PN...5 LDA BUFF.BASE-84,Y 8710 AND #3 8720 TAX 8730 PLA 8740 ORA BIT.PAIR.MIDDLE,X 8750 PHA 8760 PN...6 LDA BUFF.BASE+2,Y 8770 AND #3 8780 TAX 8790 PLA 8800 ORA BIT.PAIR.LEFT,X 8810 PHA 8820 TYA 8830 EOR #$FF 8840 TAX 8850 PLA 8860 STA TBUF,X 8870 INY 8880 BNE PN...4 8890 *---DETERMINE BUFFER BOUNDARY CONDITIONS---------- 8900 *---AND SETUP WRITE.SECTOR ACCORDINGLY------------ 8910 LDY RWB.BUFFER 8920 DEY 8930 STY INDEX.OF.LAST.BYTE 8940 LDA RWB.BUFFER 8950 STA WS...5-1 8960 BEQ .7 8970 EOR #$FF 8980 TAY 8990 LDA (RWB.BUFFER),Y 9000 INY 9010 EOR (RWB.BUFFER),Y 9020 AND #$FC 9030 TAX 9040 LDA BIT.PAIR.TABLE+3,X 9050 .7 STA BYTE.AT.BUF00 =0 IF BUFFER NOT SPLIT 9060 BEQ .9 9070 LDA INDEX.OF.LAST.BYTE 9080 LSR 9090 LDA (RWB.BUFFER),Y 9100 BCC .8 9110 INY 9120 EOR (RWB.BUFFER),Y 9130 .8 STA BYTE.AT.BUF01 9140 .9 LDY #$FF 9150 LDA (RWB.BUFFER),Y 9160 AND #$FC 9170 STA LAST.BYTE 9180 *---INSTALL BUFFER ADDRESSES IN WRITE.SECTOR------ 9190 LDY RWB.BUFFER+1 9200 STY WS...5+2 9210 STY WS...7+2 9220 INY 9230 STY WS...8+2 9240 STY WS..10+2 9250 STY WS..11+2 9260 STY WS..14+2 9270 *---INSTALL SLOT*16 IN WRITE.SECTOR--------------- 9280 LDX SLOT.X16 9290 STX WS...6+1 9300 STX WS...9+1 9310 STX WS..13+1 9320 STX WS..18+1 9330 RTS 9340 *-------------------------------- 9350 WAIT.FOR.OLD.MOTOR.TO.STOP 9360 EOR OLD.SLOT SAME SLOT AS BEFORE? 9370 ASL (IGNORE DRIVE) 9380 BEQ .2 ...YES 9390 LDA #1 LONG MOTOR.TIME 9400 STA MOTOR.TIME+1 (COUNTS BACKWARDS) 9410 .1 LDA OLD.SLOT 9420 AND #$70 9430 TAX 9440 BEQ .2 ...NO PREVIOUS MOTOR RUNNING 9450 JSR CHECK.IF.MOTOR.RUNNING.X 9460 BEQ .2 ...NOT RUNNING YET 9470 LDA #1 DELAY ANOTHER 100 USECS 9480 JSR DELAY.100 9490 LDA MOTOR.TIME+1 9500 BNE .1 KEEP WAITING 9510 .2 RTS 9520 *-------------------------------- 9530 .BS $FF9B-* <<<<EMPTY SPACE>>>> 9540 *-------------------------------- 9550 IRQ 9560 PHA SAVE A-REG 9570 LDA $45 SAVE LOC $45 9580 STA SAVE.LOC45 9590 PLA SAVE A-REG AT LOC $45 9600 STA $45 9610 PLA GET STATUS BEFORE IRQ 9620 PHA 9630 AND #$10 SEE IF "BRK" 9640 BNE .2 ...YES, LET MONITOR DO IT 9650 LDA $D000 SAVE $D000 BANK ID 9660 EOR #$D8 9670 BEQ .1 9680 LDA #$FF 9690 .1 STA INTBANKID 9700 STA SAVE.D000 9710 LDA #$BF PUSH FAKE "RTI" VECTOR WITH 9720 PHA IRQ DISABLED 9730 LDA #$50 AND SET TO RETURN TO $BF50 9740 PHA 9750 LDA #4 9760 PHA 9770 .2 LDA #$FA PUSH "RTS" VECTOR FOR MONITOR 9780 PHA 9790 LDA #$41 9800 PHA 9810 CALL.MONITOR 9820 STA $C082 SWITCH TO MOTHERBOARD 9830 *-------------------------------- 9840 RESET 9850 LDA RESET.VECTOR+1 9860 PHA PUSH "RTS" VECTOR FOR MONITOR 9870 LDA RESET.VECTOR 9880 PHA 9890 JMP CALL.MONITOR 9900 *-------------------------------- 9910 RESET.VECTOR 9920 .DA $FA61 MON.RESET-1 9930 *-------------------------------- 9940 INT.SPLICE 9950 STA INTAREG 9960 LDA SAVE.LOC45 9970 STA $45 9980 LDA $C08B SWITCH TO MAIN $D000 BANK 9990 LDA SAVE.D000 10000 JMP IRQXIT.3 10010 *-------------------------------- 10020 .BS $FFFA-* <<<<<EMPTY SPACE>>>>> 10030 *-------------------------------- 10040 V.NMI .DA $03FB 10050 V.RESET .DA RESET 10060 V.IRQ .DA IRQ 10070 *-------------------------------- |
In the October '83 issue of AAL, Robert F. O'Brien presented a way to create a text file containing the assembly listing of a large program. (See also "Assembly Listing Into a Text File", by Bill Morgan, July '83 AAL.) Actually, he created several text files; one for each .IN directive in the root file. You can't put the whole listing into one text file by using one .TF directive because of the way the .IN directive affects the DOS I/O hooks.
Robert's method for obtaining assembly listing text files is good, but I found a different way to create the text files of assembly listings that doesn't involve creating separate SYMBOLS sections, deleting duplicate labels, and putting up with "EXTRA DEFINITIONS ERROR" messages. It's a fairly simple approach and hinges on the fact that the problem presented by the .IN directive affects the source file containing the .IN, but not the source file to which the .IN refers. Instead of putting one .TF directive in the root file, put a .TF in each source file pointed to by a .IN directive.
For example:
ROOT FILE 1000 .DU 1010 .IN PART 1 1020 .IN PART 2 1030 .ED PART 1 1000 .TF LISTING 1 1010 (source for part 1) PART 2 1000 .TF LISTING 2 1010 (source for part 2)
From here on, follow Bill Morgan's original instructions. What follows is a summary of those instructions.
After deleting all other .TF directives, or turning them into comments by inserting "*" at the beginning of the line, typing ASM will create two binary files named LISTING 1 and LISTING 2. Each of these contains the assembly listing of PART 1 and PART 2 respectively, in text form. These binary files will not have starting address and length in the first four bytes. DO NOT attempt to BLOAD these files. You could really clobber DOS. To obtain true text files, make the following patch to the S-C Assembler before you assemble the program:
$1000 versions: $29DF:0 (original value is 04) $D000 versions: $C083 C083 EAF9:0 N C083
After the patch is made, assemble the program and restore the original value to $29DF ($EAF9).
For really large programs, it could get very tedious adding a .TF directive to each sub-file to obtain a text file listing and then deleting those .TF directives to prevent messing up the object file the next time the program is assembled. Fortunately, the S-C Macro Assembler's conditional assembly feature makes our work a lot easier. By placing an equated flag in the root file and surrounding each .TF with .DO and .FIN, we only have to change one line to set up our program for text file output or object file creation. For example:
ROOT FILE 1000 LSTOUT .EQ 0 TO ASSEMBLE OBJECT 1010 * 1 TO OUTPUT TEXT FILES 1020 .DO LSTOUT 1040 .DU 1050 .ELSE 1060 .TF OBJECT FILE 1070 .FIN 1080 .IN PART 1 1090 .IN PART 2 1100 .DO LSTOUT 1110 .ED 1120 .FIN PART 1 1000 .DO LSTOUT 1010 .TF LISTING 1 1020 .FIN 1030 (source for part 1) PART 2 1000 .DO LSTOUT 1010 .TF LISTING 2 1020 .FIN 1030 (source for part 2)
Don't forget to patch $29DF ($EAF9 for the language card version) with 0 to output true text files and back to 04 create object files. The last thing to remember is to use .LIST ON during the assembly. You won't write any text files if the assembler isn't producing a listing.
I just talked to the people at Manx Software about the ProDOS version of their C compiler, and this time they assured me that owners of the current Apple DOS version will be able to purchase the ProDOS version at a reduced upgrade price. That is enough to tip the balance in favor of buying the compiler right now, so I have ordered some. List price is $199: we'll have them for $180 + shipping.
Tim Mowchanuk, a lecturer at Brisbane College in Australia, sent the following suggestion:
"How can I implement a named GOTO or GOSUB routine? There are numerous routines that implement computed GOTO/GOSUB, but I consider that a futile exercise. Computed GOTO/GOSUB mess up renumbering utilities, and violate modern trends toward structured programming.
"What I really want is something that will handle BASIC like
100 & GOSUB NAME$where NAME$ holds the name of a subroutine. I envision subroutine names being defined by a special REM statement of the form
200 REM "SUBROUTINE NAME"The &GOSUB or &GOTO processor can search through the program for a line beginning with a REM token. If the first non-blank after the REM token is a quotation mark, the processor can compare the characters to the string value. If there is an exact match, the line containing the REM is the target for the &GOTO or &GOSUB."
The problem sounded just the right size for an interesting AAL article, so I started trying to write some code.
I published an &GOSUB routine back in April 1981 of the type that Tim thinks futile. The following program combines the two "futile" computed &GOSUB and &GOTO routines with two new ones that allow the computed value to be a string expression. If the expression after &GOTO or &GOSUB is numeric, the processor will search for a matching line number. If the result is a string, the processor will search for a REM label as Tim described above.
Only REM's at the beginning of a numbered line will be considered as labels. The label must be included in quotation marks. Spaces are OK between the word REM and the first quotation mark. Anything after the second quotation mark will be ignored.
You can now write a menu program that uses the actual command word as the name of a subroutine, and cease worrying about line numbers. The accompanying Applesoft program is an example of just such a technique.
100 PRINT CHR$ (4)"BLOAD B.LABELLED GO'S": CALL 768 1000 DATA SEND,RECEIVE,EDIT,LOAD,SAVE,EXIT,. 1010 I = 0 1020 I = I + 1: READ A$(I): IF A$(I) < > "." THEN 1020 1030 N = I - 1 1100 INPUT C$:I = 0 1110 I = I + 1: IF C$ = A$(I) THEN & GOSUB C$: GOTO 1100 1120 IF I < N THEN 1110 1130 PRINT "NO SUCH COMMAND": GOTO 1100 2000 REM "SEND" 2010 PRINT "SEND NOT YET IMPLEMENTED" 2020 RETURN 2500 REM "RECEIVE" 2510 PRINT "RECEIVE IS NOT READY" 2520 RETURN 3000 REM "EDIT" 3010 PRINT "MAYBE YOU CAN EDIT LATER" 3020 RETURN 3500 REM "LOAD" 3510 PRINT "LOAD WHAT, WHERE, HOW?" 3520 RETURN 4000 REM "SAVE" 4010 PRINT "SAVE WHAT, WHERE, HOW" 4020 RETURN 4500 REM "EXIT" 4510 PRINT "AH! THAT I CAN DO!" 4520 POP : END
1000 *SAVE S.LABELLED GO'S 1010 *-------------------------------- 1020 * & GOTO <STR EXP> 1030 * & GOSUB<STR EXP> 1040 * REM "<LABEL>" 1050 * 1060 * AS SUGGESTED BY TIM MOWCHANUK 1070 *-------------------------------- 1080 AS.VALTYP .EQ $11 1090 AS.TEMPPT .EQ $52,53 1100 INDEX.REM .EQ $5E 1110 INDEX.GO .EQ $5F 1120 PRGBOT .EQ $67,68 1130 AS.CURLIN .EQ $75,76 1140 PNTR .EQ $9B,9C 1150 STRLEN .EQ $9D 1160 STRADR .EQ $9E,9F 1170 VPNT .EQ $A0,A1 1180 TXTPTR .EQ $B8,B9 1190 *-------------------------------- 1200 TKN.GOTO .EQ $AB 1210 TKN.GOSUB .EQ $B0 1220 TKN.REM .EQ $B2 1230 *-------------------------------- 1240 AMPERSAND.VECTOR .EQ $3F5 ... 3F7 1250 *-------------------------------- 1260 AS.CHRGET .EQ $00B1 1270 AS.CHRGOT .EQ $00B7 1280 AS.MEMCHK .EQ $D3D6 1290 AS.NEWSTT .EQ $D7D2 1300 AS.GOTO1 .EQ $D941 1310 AS.GOTO.3 .EQ $D95E 1320 AS.UNDERR .EQ $D97C 1330 AS.FRMEVL .EQ $DD7B 1340 AS.SYNERR .EQ $DEC9 1350 AS.FRETMP .EQ $E604 1360 AS.GETADR .EQ $E752 1370 *-------------------------------- 1380 .OR $300 1390 .TF B.LABELLED GO'S 1400 *-------------------------------- 1410 SETUP LDA #LABELLED.GOTO.AND.GOSUB 1420 STA AMPERSAND.VECTOR+1 1430 LDA /LABELLED.GOTO.AND.GOSUB 1440 STA AMPERSAND.VECTOR+2 1450 RTS 1460 *-------------------------------- 1470 LABELLED.GOTO.AND.GOSUB 1480 JSR AS.CHRGOT 1490 CMP #TKN.GOTO 1500 BEQ .3 1510 CMP #TKN.GOSUB 1520 BEQ .2 ...GOOD SYNTAX SO FAR 1530 .1 JMP AS.SYNERR 1540 *---SETUP GOSUB RETURN DATA------ 1550 .2 LDA #3 1560 JSR AS.MEMCHK 1570 LDA TXTPTR+1 1580 PHA 1590 LDA TXTPTR 1600 PHA 1610 LDA AS.CURLIN+1 1620 PHA 1630 LDA AS.CURLIN 1640 PHA 1650 LDA #TKN.GOSUB 1660 PHA 1670 BNE .4 ...ALWAYS 1680 *---SETUP FOR GOTO--------------- 1690 .3 PLA POP RETURN TO "NEWSTT" 1700 PLA 1710 *---FIND LABEL AFTER TOKEN------- 1720 .4 JSR AS.CHRGET 1730 BEQ .1 1740 JSR AS.FRMEVL EVALUATE EXPRESSION 1750 BIT AS.VALTYP $00 IF NUMERIC, $FF IF STRING 1760 BMI .5 ...STRING 1770 *---NUMERIC EXPRESSION----------- 1780 JSR AS.GETADR CONVERT TO INTEGER 1790 JSR AS.GOTO1 1800 JMP AS.NEWSTT 1810 *---FREE ANY TEMP STRINGS-------- 1820 .45 LDA AS.TEMPPT+1 1830 LDY #0 1840 JSR AS.FRETMP 1850 .5 LDA AS.TEMPPT 1860 CMP #$56 EMPTY? 1870 BCS .45 ...NO, FREE A STRING 1880 *---COPY STRING LENGTH/ADDRESS--- 1890 LDY #2 1900 .55 LDA (VPNT),Y 1910 STA STRLEN,Y 1920 DEY 1930 BPL .55 1940 *---SEARCH PROGRAM FOR LABEL----- 1950 LDA PRGBOT+1 POINT TO BEGINNING 1960 LDX PRGBOT OF PROGRAM 1970 *---LOOK AT NEXT LINE------------ 1980 .6 STA PNTR+1 UPDATE PNTR TO NEXT LINE 1990 STX PNTR 2000 LDY #1 HI-BYTE OF FWD PNTR 2010 LDA (PNTR),Y 2020 BEQ .11 ...END OF PROGRAM 2030 *---CHECK FOR 'REM "'------------ 2040 LDY #4 2050 LDA (PNTR),Y 2060 CMP #TKN.REM 2070 BNE .10 ...NOT REM STATEMENT 2080 .7 INY NEXT BYTE OF LINE 2090 LDA (PNTR),Y 2100 CMP #' ' IGNORE BLANKS BEFORE " 2110 BEQ .7 2120 CMP #'"' " YET? 2130 BNE .10 ...NO, NOT A LABEL 2140 *---COMPARE LABEL---------------- 2150 STY INDEX.REM 2160 LDA #-1 2170 STA INDEX.GO 2180 .8 INC INDEX.REM 2190 LDY INDEX.REM 2200 LDA (PNTR),Y 2210 BEQ .1 ...EARLY END OF LABEL 2220 INC INDEX.GO 2230 LDY INDEX.GO 2240 CMP #'"' LEGAL END OF LABEL? 2250 BEQ .9 ...YES 2260 CMP (STRADR),Y 2270 BEQ .8 ...KEEP MATCHING 2280 BNE .10 ...DOESN'T MATCH 2290 .9 CPY STRLEN CORRECT LENGTH? 2300 BNE .10 ...NO, KEEP SEARCHING 2310 *---FOUND LABEL, SO GO TO IT----- 2320 JSR AS.GOTO.3 2330 JMP AS.NEWSTT 2340 *---DOESN'T MATCH, TRY NEXT LINE- 2350 .10 LDY #0 GET FORWARD POINTER 2360 LDA (PNTR),Y LO-BYTE 2370 TAX 2380 INY HI-BYTE 2390 LDA (PNTR),Y 2400 BNE .6 ...NOT END OF PROGRAM YET 2410 *---END OF PROGRAM, UNDEF LBL---- 2420 .11 JMP AS.UNDERR |
It may come as a surprise (it did to me), but there are apparently now only three calendar/clocks still on the market for the Apple II, II Plus, //e. The others, and there were a lot of them, seemed to have dropped off the map. And even one of the three (Mountain Computer) does not advertise anywhere I can find.
Another surprise: the most expensive clock has the fewest features, and the least expensive has the most features.
Mountain Computer Apple Clock:
$280 in current catalog listing; most recent ad I could find was in Jan 1980 Byte, at $199. Features below are guessed at from ad and conversations with Dan Pote. Works with BASIC only, does not include any DOS Dater or ProDOS support.
Gives month, day of month, hour, minute, second, millisecond
Interrupt available: Second, Millisecond
Thunderware Thunderclock Plus:
Gives month, day of month, day of week, hour, minute, second.
$150 with BASIC software for DOS or ProDOS $ 29 extra for Pascal software $ 29 extra for DOS-DATER/DEMO disk
Interrupts available: 64, 256, or 2048 times per second
Applied Engineering Timemaster:
$129 includes Applesoft support for DOS or ProDOS includes Pascal and CP/M support includes DOS Dater
Gives year, month, day of month, day of week, hour, minute, second
Interrupts available: Millisecond, Second, Minute, Hour. Switchable to either NMI or IRQ interrupt line.
For some reason they have not chosen to explain, the wizards at Apple who created ProDOS decided to "wire in" support for the Thunderclock (and ONLY Thunderclock). A system call reads the time and date from Thunderclock, calculates the year from the given information, and stores year-month-day-hour-minute in a packed format at $BF90...BF93. ProDOS automatically records time/date of creation and time/date of last modification.
In order to get the year with these dates, ProDOS goes through a calculation to derive year from given day of month, month, and day of week information. The calculation involves remaindering and table lookup...but it only works from 1982 through 1987. I suppose by 1988 they will have generated a new version which works beyond, or else we won't care anymore. Better yet, by 1988 maybe they will have driver-ized the clock support so we can use Dan's card directly.
Dan Pote sent me a Timemaster to play with, in hopes that I would figure out how to make it look like a Thunderclock to ProDOS. I did, so if you buy one now it will be completely compatible with ProDOS. You select by DIP Switch which page of the onboard EPROM will be mapped into the $CN00 space (where N is slot 1-7). One setting selects the ProDOS section, and the others select various versions designed for use with DOS and Applesoft.
You can talk to Dan's card directly, as well as through the EPROM. If you don't like the way his firmware works (unlikely), you can either ignore it or change it.
(By the way.... Call A.P.P.L.E., a club/magazine with a penchant for value and quality, has chosen to offer another one of Applied Engineering's boards in its latest catalog: the Viewmaster 80. Their price is $140, which is 20% below normal retail.)
Last night (Monday, Nov 28th) I took home an Apple to do some spreadsheet work. I took home the most portable one, but first swapped RAM cards. I took the STB-128 out of my oldest Apple and put it into the Apple II Plus with the fewest attachments.
When I plugged it in at home and booted the spreadsheet program, all appeared to be well. But it wasn't. I loaded in a model, and during the re-calculation the spreadsheet program hit a BRK opcode and died. I pressed RESET and looked at the partially re-calculated sheet: it was sprinkled with nonsense characters, and the keyboard was locked up. I played with various combinations for an hour or so, including other programs which use the RAM card. Everything pointed to there being a bad bit somewhere in the card.
Of course the RAM card test program was back at the office. I decided to write another one rather than face the two mile round trip.
The 128K space on the STB-128 is divided into 8 banks. You select a bank by storing a bank number (0-7) at any address in the $C080+slot*16 space which has bit 2 = 1. For slot 0, that means store in $C080, $C081, $C082, $C083, $C088, $C089, $C08A, or $C08B. The card has three green LEDs on top which show which bank is currently selected.
Each 16K bank is further divided to fit into the 12K address space between $D000 and $FFFF. The softswitch controlled by bit 3 in the $C08x address selects which of two 4K banks will be enabled at $D000-DFFF. The other 8K always sits at $E000-FFFF. A red LED signals which $D000 bank is selected.
The low-order two bits of the $C08x address control the mode of the RAM card. Accessing $C080 or $C088 write protects the card, and read enables it. This means the $D000-FFFF references the RAM card rather than the motherboard ROM. Accessing $C082 or $C08A write protects the RAM card and disables reading it; in other words, it switches on the motherboard ROM.
$C081 or $C089 also turn on the mother board ROM for reading, but if you access one of these twice in a row it will write enable the RAM card. In this mode reads reference the motherboard ROM, but writes write into the RAM card. This mode is used when loading the RAM card so that monitor and Applesoft routines which are in motherboard ROM can be used for the loading process.
Accessing $C083 or $C08B once read enables the STB-128 card and write protects it. A second access write enables the card. This is the mode we use for a memory test.
Thinking about how to test such a card, I wrote down the following "flow chart":
For Bank = 0 to 7 Store Bank in $C083 Access $C083 again to write enable Test $D000-DFFF Access $C08B twice Test $D000-FFFF Next Bank
I broke the actual testing of a range of memory into four parts. First I stored zeroes into every location, and checked to be sure I read zeroes back. Then I did the same with $FF. Then, $55. Then, $AA. This is certainly not an exhaustive test, but I hoped it would be sufficient.
The tricky part was informing myself of the locations and values involved of any memory errors found during the test. I could not conveniently use the monitor subroutines to write addresses and values on the screen, because the monitor only existed in the motherboard ROM and it was switched off! So, I wrote a quick and dirty display routine.
The routine for display in the listing below is not quite so "quick and dirty". The program starts by clearing the screen using the monitor HOME subroutine at $FC58. Then it switches to the RAM card and runs the test. The program pokes test failure data directly to the screen. I direct the data for each of the 8 banks to a different line. When a failure occurs, I print the address, the value that should have been there, the actual value found, and the exclusive-or of the two values. The exclusive-or shows me which bit or bits was incorrect.
After running the test it was obvious that the least significant bit in banks 5 and 6 was not working. When it should be zero it was sometimes one, and vice versa.
I did not know which chip on the STB-128 card belonged to which bit slice or which bank, so I guessed. I was lucky, and guessed right the first time. I pulled out the chip I thought might be the bad one, and re-ran the test. This time the test indicated the least significant bit of banks 4-7 was missing. (It happened to be the chip in the lower-left corner when looking at the face of the card.)
I put the chip back in, hoping that it would miraculously heal itself. Then I looked at the back of the board to see if anything looked suspicious there. Sure enough! STB did not trim off the excess length of the socket pins after soldering the board. One of those long pins had bent over and was possibly shorted to another, on the lower left socket. I straightened the pin and re-ran the test. Voila! It passed!
After I finished patting myself on the back I tried to run the spreadsheet again. It still failed! This morning I put the cards back in their usual homes, and everything works fine.
Tuesday Afternoon....Lo and behold, the card is still bad. I found the STB Systems diskette, and ran their RAM test program. It identified the same chip as being bad. But after running the test for several hours, the errors stopped. Obviously the chip's problems are intermittent.
Wednesday Morning....The chip is still giving errors. I called STB and they said to bring the board by. Wednesday afternoon....STB replaced the chip, and all is well.
1000 .LIF 1010 *SAVE S.TEST STB-128 1020 *-------------------------------- 1030 * TEST STB-128 1040 *-------------------------------- 1050 YSAVE .EQ 0 1060 LIMIT .EQ 1 1070 ADDR .EQ 2,3 1080 BANK .EQ 4 1090 BYTE .EQ 5 1100 SCREEN .EQ 6,7 1110 *-------------------------------- 1120 SELECT .EQ $C080 1130 *-------------------------------- 1140 TTTT JSR TEST 1150 JSR TEST 1160 JSR TEST 1170 JSR TEST 1180 RTS 1190 *-------------------------------- 1200 TEST LDA #0 1210 STA BANK 1220 STA ADDR 1230 JSR $FC58 CLEAR SCREEN 1240 LDA #$04 1250 STA SCREEN+1 1260 LDA #$28 1270 STA SCREEN 1280 *---SELECT BANK------------------ 1290 .1 LDA BANK 1300 STA SELECT+$07 1310 ORA #$B0 CONVERT TO SCREEN ASCII 1320 LDY #0 1330 STA (SCREEN),Y 1340 LDA SELECT+$03 1350 *---TEST D000...DFFF------------- 1360 LDA #$E0 1370 STA LIMIT 1380 JSR TEST.ZEROS 1390 JSR TEST.ONES 1400 JSR TEST.FIVES 1410 JSR TEST.AYES 1420 *---SWITCH TO OTHER D000--------- 1430 LDA SELECT+$0B 1440 LDA SELECT+$0B 1450 *---TEST D000...FFFF------------- 1460 LDA #0 1470 STA LIMIT 1480 JSR TEST.ZEROS 1490 JSR TEST.ONES 1500 JSR TEST.FIVES 1510 JSR TEST.AYES 1520 *---NEXT BANK-------------------- 1530 LDA SCREEN 1540 EOR #$80 1550 STA SCREEN 1560 BMI .2 1570 INC SCREEN+1 1580 .2 INC BANK 1590 LDA BANK 1600 CMP #8 1610 BCC .1 1620 *---SWITCH TO ROMS--------------- 1630 LDA SELECT+$01 1640 RTS 1650 *-------------------------------- 1660 TEST.ZEROS 1670 LDA #0 1680 .HS 2C 1690 TEST.ONES 1700 LDA #$FF 1710 .HS 2C 1720 TEST.FIVES 1730 LDA #$55 1740 .HS 2C 1750 TEST.AYES 1760 LDA #$AA 1770 STA BYTE 1780 LDA #$D0 1790 STA ADDR+1 1800 .1 JSR FILL 1810 JSR COMPARE 1820 INC ADDR+1 1830 LDA ADDR+1 1840 CMP LIMIT 1850 BNE .1 1860 RTS 1870 *-------------------------------- 1880 FILL LDY #0 1890 LDA BYTE 1900 .1 STA (ADDR),Y 1910 INY 1920 BNE .1 1930 RTS 1940 *-------------------------------- 1950 COMPARE 1960 LDY #0 1970 .1 LDA (ADDR),Y 1980 CMP BYTE 1990 BNE .3 2000 .2 INY 2010 BNE .1 2020 RTS 2030 .3 PHA SAVE ACTUAL DATA 2040 STY YSAVE SAVE Y-REG 2050 LDA ADDR+1 PRINT ADDRESS OF FAILURE 2060 LDY #2 2070 JSR CONBYTE 2080 LDA YSAVE LO-BYTE OF ADDRESS 2090 JSR CONBYTE 2100 INY 2110 LDA BYTE WHAT DATA SHOULD HAVE BEEN 2120 JSR CONBYTE 2130 INY 2140 PLA WHAT DATA REALLY WAS 2150 PHA KEEP ON STACK TOO 2160 JSR CONBYTE 2170 INY 2180 PLA FIGURE WHICH BITS WERE WRONG 2190 EOR BYTE 2200 JSR CONBYTE 2210 LDY #0 DELAY LOOP TO SLOW THINGS DOWN 2220 .4 DEY FOR OBSERVATION 2230 BNE .4 2240 LDY YSAVE 2250 JMP .2 REJOIN TEST 2260 *-------------------------------- 2270 CONBYTE 2280 PHA 2290 LSR 2300 LSR 2310 LSR 2320 LSR 2330 JSR CONNYBBLE 2340 PLA 2350 CONNYBBLE 2360 AND #$0F 2370 CMP #10 2380 BCC .1 2390 ADC #6 2400 .1 ADC #$B0 2410 STA (SCREEN),Y 2420 INY 2430 RTS 2440 *-------------------------------- |
Strangely enough, there are some of you who still do not own an S-C Assembler. And some of you buy or would like to buy our Quarterly Disks or the Applesoft Docu-Mentor disks.
These disks contain source files which are only usable by the S-C Macro Assembler. However, it is possible (even without an S-C Assembler) to convert them to regular text files so as to be readable by another brand assembler/editor.
The files appear in the catalog as type "I", which is supposed to mean Integer BASIC. Of course the contents has nothing to do with Integer BASIC, but making them "I-files" has several advantages:
There are also some dis-advantages:
Which brings us back to the point of this article.
To make the procedure simple, you need at least a 64K Apple. If you have an Apple //e, you are all set. An older Apple needs a "language card", or "RAM card".
The first step in the conversion process is to load the file into memory and find out where it is. Start by booting with your DOS 3.3 System Master disk, which loads Integer BASIC into the RAM card. Then LOAD the S-C source file which you want to convert. Integer BASIC will be switched on, but don't try to LIST or RUN!
Enter the Monitor by typing "CALL -151". At this point you will get an asterisk prompt. Look at locations $4C, $4D, $CA, and $CB. You can do it like this:
*4C.4D CA.CB 004C- 00 96 00CA- 58 73
Interpret the above as meaning that the source code begins in memory at $7358 and ends one byte before $9600.
If you use the monitor commands to look at the first 30 or 40 bytes (or more), you will discover how the source lines are stored. Each line begins with a byte count, which if added to the address will give the address of the first byte of the next line. Each line ends with a 00 byte. The byte count includes both of these bytes, and all in between. Here is a sample line:
0F E8 03 41 42 43 84 4C 44 41 81 23 24 35 00
The second and third bytes are the binary form of the line number. As usual in 6502 domain, the number is stored low-byte first. $3E8 means the line above is line 1000.
The fourth byte and beyond are ASCII codes for the text of the line, with two exceptions. If the bytes are less than $80, they are plain ASCII. If they are in the range from $81 through $BF, they represent a series of blanks. $81 means one blank, $84 means four blanks, and so on. The line above now decodes to:
1000 ABC LDA #$5
The other exception is not illustrated above, but here is one:
08 F2 03 2A C0 20 2D 00
The token $C0 means "repeated character". The next byte after $C0 gives the number of repetitions, and the byte after that tells what character to repeat. Above the C0 20 2D means 32 "-" characters, so the whole line looks like this:
1010 *--------------------------------
Armed with all that information, you can probably see how to write a simple Applesoft program to convert the memory image of the S-C source file to plain text and then write it on a text file.
In fact, here is just such a program:
100 REM CONVERT MEMORY IMAGE OF S-C SOURCE 110 REM TO ORDINARY TEXT FILE 200 HM = PEEK (76) + 256 * PEEK (77) 210 PP = PEEK (768) + 256 * PEEK (769) 220 HIMEM: PP 300 REM OPEN A TEXT FILE 310 D$ = CHR$ (4) 320 PRINT D$"OPEN TEXTFILENAME": PRINT D$"DELETE TEXTFILENAME" 330 PRINT D$"OPEN TEXTFILENAME": PRINT D$"WRITE TEXTFILENAME" 400 L = PP 410 IF L = HM THEN PRINT D$"CLOSE": END 420 GOSUB 500: REM DO ONE LINE 430 GOTO 410 500 REM DO ONE SOURCE LINE 510 N = PEEK (L) 520 LN = PEEK (L + 1) + 256 * PEEK (L + 2): PRINT LN" ";:L = L + 2 530 L = L + 1:C = PEEK (L): IF C = 0 THEN PRINT :L = L + 1: RETURN 540 IF C < 128 THEN PRINT CHR$ (C);: GOTO 530 550 IF C < 192 THEN FOR I = 1 TO C - 128: PRINT " ";: NEXT : GOTO 530 560 IF C = 192 THEN FOR I = 1 TO PEEK (L + 1): PRINT CHR$ ( PEEK (L + 2));: NEXT I:L = L + 2: GOTO 530 570 PRINT : PRINT D$"CLOSE": PRINT "***ERROR IN SOURCE AT "L"*** ": END
Here is a blow-by-blow description of how to use the program.
If you add a line at 315 to turn on MONCIO, you can see the text as it is produced.
Many thanks to all of you who responded to my questions about 68000, C, and the future of Apple Assembly Line.
Your answers ran about eight to one in favor of including 68000 information in AAL. Several writers suggested starting with a few pages, and possibly splitting off a separate newletter someday. That sounds like a good plan, so we'll start a regular section next issue. Those of you who already know 68000 can now start teaching the rest of us. Bob Urschel has already sent in a brief article and program! He has a QWERTY Q68 board like that we reviewed last month, and speaks very highly of it.
Interest in Macintosh (Apple 32?) is growing rapidly: the announcement is expected at the Apple shareholder meeting in mid-January. Some reports claim that some developers have had Mac for up to 18 months now. We haven't been among those so privileged, but I hope to be the first on my block with one. (Unless the thing turns out to have some fatal flaw, like no expansion slots. That was one rumor!)
Several of you also expressed an interest in C, but not even a majority. More like 30%. It looks like a number of people are curious, but feel that too much coverage would dilute AAL. Stephen Bach said it best, "... don't spread yourselves too thin and try to do C also." I expect to do occasional reviews and mentions of books and other aids to learning C, and to report on anything specifically related to C on Apple computers, but not much more.
Apple Assembly Line is published monthly by S-C SOFTWARE CORPORATION, P.O. Box 280300, Dallas, Texas 75228. Phone (214) 324-2050. Subscription rate is $15 per year in the USA, sent Bulk Mail; add $3 for First Class postage in USA, Canada, and Mexico; add $13 postage for other countries. Back issues are available for $1.50 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.)