Apple Assembly Line
Volume 6 -- Issue 7 April 1986

In This Issue...

65816 Books

The race is on! "Programming the 65816", by David Eyes from Prentice-Hall, originally scheduled for publication last October, is now expected in late April. "65816/65802 Assembly Language Programming", by Michael Fischer from Osborne/McGraw-Hill, scheduled for May publication, is now also due in late April. We have plenty of copies of these books on order, and a long list of patient people waiting for complete information on programming these powerful new chips. Coming Soon...

More Memory Expansion

We'd like to call your attention to the new ad for Applied Engineering's RamFactor board. This is a "Slinky" style memory expansion card for any standard slot of an Apple II, II+, or //e. We've been doing some of the firmware for this product, and it's been a delight to work with.

One thing the ad doesn't really emphasize is the power and flexibility of the program switcher firmware. You can set up the card with a variable number of variable-sized partitions and then switch between them almost instantly. Any partition can be based on any operating system, or on your own program. Couple this with the battery backup option (it's really more of an uninterruptible power suppply for the card) and you have what amounts to a hard disk operating at RAM speed!

Tools for Restoring Lost Catalogs Bob Sander-Cederlof

From time to time it happens. One way or another I manage to clobber a catalog track on a disk. I have done it three times to Volume 1 in the DOS partition on my 10-megabyte Sider. (All it takes is "INIT HELLO,V1", forgetting that the last slot I accessed was the Sider's.)

All of the other tracks are still intact, but there is no way to get to them because the catalog is totally wiped out. One solution would be to have an accurate backup floppy for each Sider volume. This should be especially easy for Volume 1, because it is mostly standard Sider utilities. Mostly.... I have modifed several of them, and somehow I almost always have several programs-under-development that end up in V1. Of course, I could just as easily destroy the catalog track on any other volume, or any floppy for that matter.

It is for mistakes like mine that the program FIXCAT in "Bag of Tricks" was invented. FIXCAT looks over a diskette, finds all the sectors which look like they contain track/sector lists, and tries to piece together a new catalog track. Even though it is fairly automatic, I find it very difficult to use. I am always getting confused between old (deleted) copies of files and the current ones, and my disks usually have at least 2 or 3 dozen active files.

Recently it happened again. In fact, while I was working on one of the other articles in this issue of AAL. I decided to write a couple of utilities to help me make more effective use of FIXCAT. My new tools turn out to be useful even without FIXCAT, and you might enjoy just playing with them.

I assume you have a copy of "Beneath Apple DOS", or some other reference work which explains the format of DOS disks, catalog tracks, and track/sector lists.

The first tool I wrote looks through the tracks and sectors of a damaged disk for any sectors containing what could be track/sector lists. When one is found, I display the location of the supposed TS-list, all of the track/sectors in the list, and the first 64 bytes of the first data sector of the supposed file. Here is an example of the display:

03-5:  03-4 03-3 03-2
       07 02 09 E8 03 81 2E 4C 49 46 00 16 F2 03 2A C0 ...h...LIF..r.*@
       06 08 53 41 56 45 81 42 43 44 81 4D 41 47 49 43 ..SAVE.BCD.MAGIC
       00 08 FC 03 2A C0 20 2D 00 05 06 04 54 00 0B 10 ..|.*@ -....T...
       04 87 4C 44 41 81 23 30 00 0C 1A 04 2E 31 85 53 ..LDA.#0.....1.S

The first 64 bytes are displayed in both hexadecimal and in ASCII, with periods being substituted for unprintable characters.

Having this information on paper before starting up FIXCAT is a big help. I can peacefully analyze the data at my desk, without the fear and panic associated with making "life and death" decisions at the keyboard. The first few bytes of a file will usually reveal what type of file it is.

If it is a source code file for the S-C Macro Assembler, Integer BASIC, or Applesoft, it will begin with a two-byte length for the file. Binary files begin with the load address, then the length. Text files start right in with data in ASCII, normally with all the high bits on. Since I almost always have a line near the beginning of my source files which contains the file name, I can usually read that file name in the dump of the first 64 bytes.

The FIND.TS.LISTS program is fairly short and simple. Starting from the bottom, the subroutine READTS at lines 2370-2430 calls on RWTS to read a particular track and sector. I elected to use my own IOB, rather than the one inside DOS at $B7E8. For simplicity's sake I assembled in the slot, drive, and volume information in my IOB. READTS only has to store the desired track and sector numbers, and call RWTS. I limited error handling to just re-calling RWTS, in the hopes of eventually succeeding. Should this begin to be a problem, I could print out an error message and either quit or continue with the next sector.

The subroutine READ.NEXT.SECTOR, lines 2200-2350, is used to scan through the disk from beginning to end. TS-lists cannot be in track 0, so I start with track 1. Since DOS allocates sectors in a track starting with sector $0F and going backwards to sector $00, I decided to scan the same way. This makes the files found list more closely to the same order as they were in the original catalog. I first advance the track/sector to the next one, then read it. Thus after reading, CUR.TRACK and CUR.SECTOR are pointing to the one we just read.

Now back to the top. Lines 1100-1130 start CUR.TRACK and CUR.SECTOR at 0. The first call to READ.NEXT.SECTOR will advance them to track 1, sector $0F. Successive calls will read the rest of track 1, then advance to track 2, and so on until we have finished track $22. When we try to read track $23, which does not exist, READ.NEXT.SECTOR will return with carry set and our program will end.

Lines 1170-1290 examine the data in the sector just read to see if it might be a track/sector list. The method I use is to require that there be at least one TS-pair, at BUF+12. I also require that all of the bytes beyond BUF+12 are within the range of valid track-sector pairs. If any bytes are out of range, I assume the current sector is not a TS-list. My tests seem to be adequate, because with every disk I have used it on it found all and only the TS-lists.

Having found TS-list, I call DISPLAY.TS.LIST to display it. Lines 1450-1540 display the location of the TS-list. The subroutine PR.TS prints the track and sector numbers from the A- and X-registers in the form "TT-S". Lines 1550-1720 list the TS-pairs in the TS-list, stopping at the first pair with a track number of zero. Up to 8 pairs are listed on a line.

Lines 1330-1430 read the first data sector of the supposed file, and display the first 64 bytes in hex and ASCII. This display is done by calling DISPLAY.NEXT.16 four times.

As it happens, I did have a fairly recently made backup of the clobbered disk. I thought I should also run my program against this good disk, and comparing the two displays would enable me to pinpoint each active file. However, what I really want from the GOOD disk is the information in the CATALOG. I decided to modify FIND.TS.LISTS to be driven from the catalog track, rather than from a search for TS-lists. The result was another useful tool, BIG.CATALOG.DISPLAY.

BIG.CATALOG.DISPLAY has the same kind of output that FIND.TS.LISTS does, except that it also lists the file type, file name, and sector count from the catalog. Information is included for deleted files for which entries are found in the catalog, as well as all the active files.

The subroutines DISPLAY.TS.LIST, DISPLAY.NEXT.16, SEVEN.SPACES, PR.TS, and READTS are used without any changes from the FIND.TS.LISTS program. Instead of READ.NEXT.SECTOR, I have now READ.NEXT.CATALOG.SECTOR. This starts at track $11, sector $0F, and works back as far as sector $01. A better way might be to follow the actual chain, beginning in the VTOC sector, but the current scheme is easier and works with most of my disks.

Lines 1140-1180 set up the initial catalog track and sector. Lines 1190-1210 read the catalog sector. If the returned status is positive we did read a sector, and continue processing; if not, we are finished. Lines 1220-1250 set up the buffer address in the IOB for reading TS-lists and data sectors: we do not want to read them over the top of the catalog sector we are working with.

Lines 1270-1320 set up a loop for processing each of the seven file entries in the current catalog sector. The "NEXT" part of the loop is at lines 1350-1440. Each catalog entry takes 35 bytes, so lines 1350-1440 add 35 to the pointer.

DISPLAY.DATA.FOR.ONE.FILE first checks for a zero entry, meaning the end of the catalog. A catalog is initialized to all zeroes, so as soon as we find a zero entry we know there are no more files. Next, at lines 1520-1550, I check for a deleted file. If the track number is negative, it is a deleted file. The actual track number of a deleted file is saved on top of the 30th character of the file name, so I pick it up there. Lines 1560-1590 save the track and sector of the TS-list, so I can read it later. Lines 1600-1650 display the file type as a hex value, followed by two dashes.

Lines 1660-1700 print the first 29 characters of the file name. I don't print the last character because for a deleted file it will have been clobbered by saving the track number there. Probably what I should do here is print either the last character for an active file, or some special symbol for a deleted file. You can add that code if you like.

Lines 1710-1770 pick up the file size, in number of sectors, and print it as a hex value. The sector count includes the sector for the TS-list.

Lines 1780-1860 read the track/sector list for the file. If either the track number or the sector number is out of range, nothing is read and we skip any further processing for this file.

Lines 1870-1940 read in the first data sector for the file. Again, if either the track or sector number is out of range, we don't try to read it. Finally, lines 1950-2000 display the first 64 bytes of the file.

I hope you find these new tools as useful as I have. Of course, I could hope you will never NEED them, but that would prabably be a vain hope. I also hope you have "Bag of Tricks" or some similar utility to put it all back together after you get the information my tools provide. And if I ever clobber Volume 1 on my Sider again (perish the thought), I intend to modify my copies of DOS so they will not allow me to INIT a volume on the Sider.

  1010 *--------------------------------
  1020 CUR.SECTOR .EQ 0
  1030 CUR.TRACK  .EQ 1
  1040 *---------------------------------
  1050 COUT   .EQ $FDED
  1060 CROUT  .EQ $FD8E
  1080 ENTER.RWTS .EQ $3D9
  1090 *--------------------------------
  1110        LDA #0
  1120        STA CUR.SECTOR
  1130        STA CUR.TRACK
  1140 .1     JSR READ.NEXT.SECTOR
  1150        BCC .2       GOT A SECTOR, CHECK IT
  1160        RTS          END OF DISK, QUIT
  1170 *---CHECK IF THIS IS T/S LIST----
  1190        BEQ .1       ...NO, TRY NEXT ONE
  1200        LDY #12
  1210 .3     LDA BUF,Y
  1220        CMP #35
  1230        BCS .1       ...NOT VALID TRACK
  1240        INY
  1250        LDA BUF,Y
  1260        CMP #16
  1270        BCS .1       ...NOT VALID SECTOR
  1280        INY
  1290        BNE .3       ...MORE IN SECTOR TO CHECK
  1300 *---DISPLAY THE T/S LIST---------
  1310        JSR DISPLAY.TS.LIST
  1320 *---READ FIRST DATA SECTOR-------
  1330        LDY BUF+12
  1340        LDX BUF+13
  1350        JSR READTS
  1360 *---DISPLAY FIRST 64 BYTES-------
  1370        LDY #0
  1380        JSR DISPLAY.NEXT.16
  1390        JSR DISPLAY.NEXT.16
  1400        JSR DISPLAY.NEXT.16
  1410        JSR DISPLAY.NEXT.16
  1420        JSR CROUT
  1430        JMP .1
  1440 *--------------------------------
  1460        JSR CROUT
  1470        LDA CUR.TRACK
  1480        LDX CUR.SECTOR
  1490        JSR PR.TS
  1500        LDA #":"
  1510        JSR COUT
  1520        LDA #" "
  1530        JSR COUT
  1540        JSR COUT
  1550        LDY #0
  1560 .1     LDA BUF+13,Y      SECTOR
  1570        TAX
  1580        LDA BUF+12,Y      TRACK
  1590        BEQ .2       ...END OF LIST
  1600        JSR PR.TS
  1610        LDA #" "
  1620        JSR COUT
  1630        TYA
  1640        AND #$0F
  1650        CMP #$0E
  1660        BNE .3
  1670        JSR SEVEN.SPACES
  1680 .3     INY
  1690        INY
  1700        CPY #-12
  1710        BCC .1
  1720 .2     RTS
  1730 *--------------------------------
  1740 DISPLAY.NEXT.16
  1750        JSR SEVEN.SPACES
  1760 .1     LDA BUF,Y
  1770        JSR PRBYTE
  1780        LDA #" "
  1790        JSR COUT
  1800        INY
  1810        TYA
  1820        AND #$0F
  1830        BNE .1
  1840        TYA
  1850        SEC
  1860        SBC #16
  1870        TAY
  1880 .2     LDA BUF,Y
  1890        ORA #$80
  1900        CMP #$A0
  1910        BCS .3
  1920        LDA #"."
  1930 .3     JSR COUT
  1940        INY
  1950        TYA
  1960        AND #$0F
  1970        BNE .2
  1980        RTS
  1990 *--------------------------------
  2010        JSR CROUT
  2020        LDA #" "
  2030        LDX #7
  2040 .4     JSR COUT
  2050        DEX
  2060        BNE .4
  2070        RTS
  2080 *--------------------------------
  2090 PR.TS
  2100        JSR PRBYTE
  2110        LDA #"-"
  2120        JSR COUT
  2130        TXA
  2140        ORA #"0"
  2150        CMP #$BA
  2160        BCC .1
  2170        ADC #6
  2180 .1     JMP COUT
  2190 *--------------------------------
  2210 *--------------------------------
  2230        LDX CUR.SECTOR
  2240        LDY CUR.TRACK
  2250        DEX          NEXT SECTOR
  2260        BPL .1       ...SAME TRACK
  2270        LDX #15      ...NEXT TRACK
  2280        INY
  2290        CPY #35
  2300        BCS .2       ...END OF DISK
  2310 .1     STY CUR.TRACK
  2320        STX CUR.SECTOR
  2330        JSR READTS
  2340        CLC
  2350 .2     RTS
  2360 *--------------------------------
  2380        STY IOB.TRACK
  2390 .2     LDA /IOB
  2400        LDY #IOB
  2410        JSR ENTER.RWTS
  2420        BCS .2       ...TRY AGAIN IF ERROR
  2430        RTS
  2440 *---------------------------------
  2450 *      IOB FOR RWTS CALLS
  2460 *---------------------------------
  2470 IOB
  2480 IOB.TYPE   .HS 01   0--MUST BE $01
  2490 IOB.SLOT16 .HS 60   1--SLOT # TIMES 16
  2500 IOB.DRIVE  .HS 01   2--DRIVE # (1 OR 2)
  2520 IOB.TRACK  .BS 1    4--TRACK # (0 TO 34)
  2530 IOB.SECTOR .BS 1    5--SECTOR # (0 TO 15)
  2570 IOB.OPCODE .HS 01  12--0=SEEK, 1=READ, 2=WRITE, OR 4=FORMAT
  2580 IOB.ERROR  .BS 1   13--ERROR CODE: 0, 8, 10, 20, 40, 80
  2620 *--------------------------------
  2630 DCT    .HS 0001EFD8
  2640 *--------------------------------
  2650 BUF    .BS 256
  2660 *--------------------------------
  1010 *--------------------------------
  1020 CAT.SECTOR .EQ 0
  1030 CAT.TRACK  .EQ 1
  1040 CNTR       .EQ 2
  1050 PNTR       .EQ 3,4
  1060 TS.TRACK   .EQ 5
  1070 TS.SECTOR  .EQ 6
  1080 *---------------------------------
  1090 COUT   .EQ $FDED
  1100 CROUT  .EQ $FD8E
  1120 ENTER.RWTS .EQ $3D9
  1130 *--------------------------------
  1150        LDA #15
  1160        STA CAT.SECTOR
  1170        LDA #17
  1180        STA CAT.TRACK
  1200        BPL .2       GOT A SECTOR
  1210 .4     RTS
  1220 .2     LDA #BUF
  1230        STA IOB.BUFFER
  1240        LDA /BUF
  1250        STA IOB.BUFFER+1
  1260 *--------------------------------
  1270        LDA #CAT+11
  1280        STA PNTR
  1290        LDA /CAT+11
  1300        STA PNTR+1
  1310        LDA #7
  1320        STA CNTR
  1340        BCS .4       ...END OF CATALOG
  1350        LDA PNTR
  1360        ADC #35
  1370        STA PNTR
  1380        LDA PNTR+1
  1390        ADC #0
  1400        STA PNTR+1
  1410        DEC CNTR
  1420        BNE .3
  1430        JSR CROUT
  1440        JMP .1
  1450 *--------------------------------
  1470        LDY #0
  1480        LDA (PNTR),Y
  1490        BNE .1
  1500        SEC
  1510        RTS
  1520 .1     BPL .15
  1530        LDY #32
  1550        LDY #0
  1560 .15    STA TS.TRACK
  1570        INY
  1580        LDA (PNTR),Y
  1590        STA TS.SECTOR
  1600        INY
  1610        LDA (PNTR),Y      GET FILE TYPE
  1620        JSR PRBYTE
  1630        LDA #"-"
  1640        JSR COUT
  1650        JSR COUT
  1660 .2     INY
  1670        LDA (PNTR),Y      PRINT FILE NAME
  1680        JSR COUT
  1690        CPY #31           DON'T PRINT LAST CHAR OF NAME
  1700        BCC .2
  1710        INY
  1720        INY
  1730        LDA (PNTR),Y
  1740        JSR PRBYTE
  1750        INY
  1760        LDA (PNTR),Y
  1770        JSR PRBYTE
  1780 *---READ T/S LIST----------------
  1790        LDX TS.SECTOR
  1800        CPX #16
  1810        BCS .9
  1820        LDY TS.TRACK
  1830        CPY #35
  1840        BCS .9
  1850        JSR READTS
  1860        JSR DISPLAY.TS.LIST
  1870 *---READ FIRST DATA SECTOR-------
  1880        LDY BUF+12
  1890        CPY #35
  1900        BCS .9
  1910        LDX BUF+13
  1920        CPX #16
  1930        BCS .9
  1940        JSR READTS
  1950 *---DISPLAY FIRST 64 BYTES-------
  1960        LDY #0
  1970        JSR DISPLAY.NEXT.16
  1980        JSR DISPLAY.NEXT.16
  1990        JSR DISPLAY.NEXT.16
  2000        JSR DISPLAY.NEXT.16
  2010 .9     JSR CROUT
  2020        CLC
  2030        RTS
  2040 *--------------------------------
  2055   .LIST OFF
  2060        JSR CROUT
  2070        LDA TS.TRACK
  2080        LDX TS.SECTOR
  2090        JSR PR.TS
  2100        LDA #":"
  2110        JSR COUT
  2120        LDA #" "
  2130        JSR COUT
  2140        JSR COUT
  2150        LDY #0
  2160 .1     LDA BUF+13,Y      SECTOR
  2170        TAX
  2180        LDA BUF+12,Y      TRACK
  2190        BEQ .2       ...END OF LIST
  2200        JSR PR.TS
  2210        LDA #" "
  2220        JSR COUT
  2230        TYA
  2240        AND #$0F
  2250        CMP #$0E
  2260        BNE .3
  2270        JSR SEVEN.SPACES
  2280 .3     INY
  2290        INY
  2300        CPY #-12
  2310        BCC .1
  2320 .2     RTS
  2325    .LIST ON
  2330 *--------------------------------
  2340 DISPLAY.NEXT.16
  2345    .LIST OFF
  2350        JSR SEVEN.SPACES
  2360 .1     LDA BUF,Y
  2370        JSR PRBYTE
  2380        LDA #" "
  2390        JSR COUT
  2400        INY
  2410        TYA
  2420        AND #$0F
  2430        BNE .1
  2440        TYA
  2450        SEC
  2460        SBC #16
  2470        TAY
  2480 .2     LDA BUF,Y
  2490        ORA #$80
  2500        CMP #$A0
  2510        BCS .3
  2520        LDA #"."
  2530 .3     JSR COUT
  2540        INY
  2550        TYA
  2560        AND #$0F
  2570        BNE .2
  2580        RTS
  2585   .LIST ON
  2590 *--------------------------------
  2605   .LIST OFF
  2610        JSR CROUT
  2620        LDA #" "
  2630        LDX #7
  2640 .4     JSR COUT
  2650        DEX
  2660        BNE .4
  2670        RTS
  2675   .LIST ON
  2680 *--------------------------------
  2690 PR.TS
  2695   .LIST OFF
  2700        JSR PRBYTE
  2710        LDA #"-"
  2720        JSR COUT
  2730        TXA
  2740        ORA #"0"
  2750        CMP #$BA
  2760        BCC .1
  2770        ADC #6
  2780 .1     JMP COUT
  2785   .LIST ON
  2790 *--------------------------------
  2810 *--------------------------------
  2830        LDA #CAT
  2840        STA IOB.BUFFER
  2850        LDA /CAT
  2860        STA IOB.BUFFER+1
  2870        LDX CAT.SECTOR
  2880        LDY CAT.TRACK
  2890        JSR READTS
  2900        DEC CAT.SECTOR
  2910        RTS
  2920 *--------------------------------
  2935    .LIST OFF
  2940        STY IOB.TRACK
  2950 .2     LDA /IOB
  2960        LDY #IOB
  2970        JSR ENTER.RWTS
  2980        BCS .2       ...TRY AGAIN IF ERROR
  2990        RTS
  2995    .LIST ON
  3000 *---------------------------------
  3010 *      IOB FOR RWTS CALLS
  3020 *---------------------------------
  3030 IOB
  3035    .LIST OFF
  3040 IOB.TYPE   .HS 01   0--MUST BE $01
  3050 IOB.SLOT16 .HS 60   1--SLOT # TIMES 16
  3060 IOB.DRIVE  .HS 01   2--DRIVE # (1 OR 2)
  3080 IOB.TRACK  .BS 1    4--TRACK # (0 TO 34)
  3090 IOB.SECTOR .BS 1    5--SECTOR # (0 TO 15)
  3130 IOB.OPCODE .HS 01  12--0=SEEK, 1=READ, 2=WRITE, OR 4=FORMAT
  3140 IOB.ERROR  .BS 1   13--ERROR CODE: 0, 8, 10, 20, 40, 80
  3175    .LIST ON
  3180 *--------------------------------
  3190 DCT    .HS 0001EFD8
  3200 *--------------------------------
  3210 BUF    .BS 256
  3220 CAT    .BS 256
  3230 *--------------------------------
  3235    .LIST OFF

Writing Messages in Windows Bob Sander-Cederlof

The idea for the following program came from some similar code in the Cirtech Flipster software. Their "program manager" software displays a series of messages and menus in selected windows using a simple subroutine.

The windows are not quite as sophisticated as you may be used to if you are a Macintosh fan. This program divides the screen up vertically, with each window running the full screen width. Calls to the program specify which window to write a message into. The JSR MSG.IN.WINDOW is followed by a single byte specifying which window to use, the ASCII text of the message, and a final 00 byte signifying the end of message. MSG.IN.WINDOW first sets up the window, then clears it, then displays the message in it, and then returns to continue execution right after the 00 byte. MSG.IN.WINDOW does not make any provision for saving the previous contents of the screen inside the window and restoring it later. As I said, this is much simpler than Mac windows.

The Apple monitor has built-in window capability, with the current window being defined by four bytes in page zero. $20 is called LEFT, and defines the starting column of a screen line. This is normally 0, meaning the first column. $21 is called WIDTH, and specifies how many characters are in each line. This is usually 40 ($28), but may be 80 ($50) in a //c or enhanced //e in 80-column mode. MSG.IN.WINDOW does not make any changes to LEFT or WIDTH, although you could modify it to do so.

$22 is called WNDTOP, and specifies the top line of the working window. This is usually 0, meaning to start at the top of the screen. It could be as large as 23 ($17), meaning the bottom line of the screen. $23 is called WNDBOT, and specifies the bottom line of the working window. The number in WNDBOT is actually the number of the next line below the working window, and is usually 24 ($18) to specify a window that goes all the way to the bottom of the screen. MSG.IN.WINDOW stores new values in WNDTOP and WNDBOT, according to a table of line numbers called WINDOW.DATA.

My WINDOW.DATA table lists six different windows, but of course you could have as many as you wish. They can even overlap. The table I used contains the line numbers 0, 24, 0, 3, 9, 18, 20, and 24. This corresponds to the following windows:

     Index     WNDTOP WNDBOT Window
       0          0     24    0-23  <full screen>
       1       <better not use!!!>
       2          0      3    0-2
       3          3      9    3-8
       4          9     18    9-17
       5         18     20   18-19
       6         20     24   20-23

Lines 1080-1130 in the listing below detail the calling sequence for MSG.IN.WINDOW. The test program in lines 1500 and following shows some actual calls, with a "wait for any keystroke" between messages so you can see it happen.

Lines 1140-1180 save the caller's return address, placed on the stack by the JSR MSG.IN.WINDOW. This address will be used to pick up the calling parameters, and then used to return to the calling program. The subroutine in lines 1400-1460 increments the pointer and picks up the next byte from the calling sequence.

When we are finished displaying the message, the pointer will be pointing at the terminal 00 byte. Placing the pointer address back on the stack lets us use an RTS opcode to return to the caller. This is done in lines 1340-1390.

Lines 1200-1250 pick up the window index from the first byte following the JSR instruction. This indexes the WINDOW.DATA table, so two entries from that table are moved into WNDTOP and WNDBOT. The the monitor HOME subroutine can be called to clear the window and place the cursor in the top-left corner of the window.

Lines 1270-1330 display the message, if any. If there is no message, there still must be a terminal 00 byte. By judicious use of 8D (return) and 8A (linefeed) characters, you can display the message any way you like. If the message is too large for the window, lines will be scrolled out the top of the window and lost.

The MSG.IN.WINDOW subroutine illustrates a commonly used technique of placing messages to be printed "in-line", like PRINT "message" statements in Applesoft. I personally prefer to collect all my messages together, and use a message number in a register to select which one to print. One problem with my preferred method is that my programs are then easier to disassemble ... if that is a problem. The 6502 was not designed for easy transfer of calling parameters which follow the JSR. (The 65816 makes this kind of code easier, with its stack-relative address mode.)

  1010 *--------------------------------
  1020 HOME   .EQ $FC58
  1030 COUT   .EQ $FDED
  1040 *--------------------------------
  1050 PNTR   .EQ $00,01
  1060 WNDTOP .EQ $22
  1070 WNDBOT .EQ $23
  1080 *--------------------------------
  1090 *   CALL:   JSR MSG.IN.WINDOW
  1100 *           .DA #<window number>
  1110 *           .AS text of message
  1120 *           .HS 00    <end of msg flag>
  1130 *--------------------------------
  1150        PLA          GET RETURN ADDRESS INTO PNTR
  1160        STA PNTR     LO BYTE
  1170        PLA
  1180        STA PNTR+1   HI BYTE
  1190 *---SETUP WINDOW TOP & BOTTOM----
  1200        JSR GET.NEXT.CALL.BYTE
  1210        TAX          WINDOW INDEX
  1220        LDA WINDOW.DATA,X
  1230        STA WNDTOP
  1240        LDA WINDOW.DATA+1,X
  1250        STA WNDBOT
  1260        JSR HOME     CLEAR THE WINDOW
  1270 *---DISPLAY MESSAGE, IF ANY------
  1280        LDY #0
  1290 .1     JSR GET.NEXT.CALL.BYTE
  1300        BEQ .2       END OF MESSAGE
  1310        ORA #$80     ...JUST IN CASE
  1320        JSR COUT
  1330        JMP .1
  1340 *---RETURN TO CALLER-------------
  1350 .2     LDA PNTR+1   HI BYTE
  1360        PHA
  1370        LDA PNTR     LO BYTE
  1380        PHA
  1390        RTS
  1400 *--------------------------------
  1420        INC PNTR     LO BYTE
  1430        BNE .1
  1440        INC PNTR+1   HI BYTE
  1450 .1     LDA (PNTR),Y
  1460        RTS
  1470 *--------------------------------
  1490        .DA #0,#24,#0,#3,#9,#18,#20,#24
  1500 *--------------------------------
  1510 T
  1520        JSR MSG.IN.WINDOW
  1530        .DA #2       TOP WINDOW
  1540        .AS -/TOP LINE OF THE SCREEN/
  1550        .HS 8D
  1560        .AS -/SECOND LINE OF THE SCREEN/
  1570        .HS 8A
  1580        .AS -/...AND THE THIRD/
  1590        .HS 00       END MSG
  1600        JSR W
  1610        JSR MSG.IN.WINDOW
  1620        .DA #6       BOTTOM WINDOW
  1630        .AS -/LINE 21/
  1640        .HS 8A
  1650        .AS -/...LINE 22/
  1660        .HS 8A.8A
  1670        .AS -/...AND LINE 24/
  1680        .HS 00       END MSG
  1690        JSR W
  1700        JSR MSG.IN.WINDOW
  1710        .DA #0       FULL SCREEN
  1720        .AS -/MY MESSAGE/
  1730        .HS 00       END MSG
  1740        RTS
  1750 *--------------------------------
  1770        BPL W
  1780        STA $C010
  1790        RTS
  1800 *--------------------------------

On Dividing a BCD Value by 4 Bob Sander-Cederlof

The 6502 allows two kinds of addition and subtraction operations, depending on the state of the D-bit in the status register. After a SED (Set D) instruction, the ADC and SBC instructions will operate in decimal mode; after CLD (CLear D), ADC and SBC will operate in binary mode.

In decimal mode the range of values in a byte is from $00 to $99. The left nybble is the ten's digit, and the right nybble is the unit's digit. The decimal mode makes some programs much easier to write, and others more difficult. Having both modes is nice.

In binary mode, if you want to divide by four you just shift the value right two bit-positions. If by 8, shift 3 times. And so on. In decimal mode, you can very easily divide by powers of ten; however, dividing by four is more difficult.

I needed a quick way to tell if a number in decimal mode was divisible by four. After inspecting the binary values of the decimal-mode numbers between 00 and 99 a, I found a way. If the ten's digit is even and the unit's digit 0, 4, or 8, the number is divisible by four. Also, if the ten's digit is odd and the unit's digit is 2 or 6, the number is divisible by four. This can be tested as follows:

       LDA VALUE
       AND #$13
       BEQ ...     ...TEN'S EVEN, UNITS=0,4,8
       EOR #$12
       BEQ ...     ...TEN'S ODD, UNITS=2,6
       ...         ...NOT DIVISIBLE

Next I needed a way to actually divide by four. Again I started by inspecting the various values involved. Simply shifting right twice does not do the job, except for numbers less than ten. You cannot even divide by two by simply shifting right once, unless the ten's digit is even. Hmmm.... If the ten's digit is odd, I could subtract 6 frist and then shift right once to divide by two. Doing all that twice would result in a division by four. The subtraction must be done in binary mode, not decimal. The subroutine below in lines 1460-1590 will divide the decimal number in VALUE by four, truncating any remainder, and return the quotient in the A-register. Lines 1600-1700 show a shorter way to divide by two, provided you don't mind using the X-register.

To test my subroutines, I wrote some test programs. The first program, lines 1000-1370, runs through the values 00 to 99, printing ten values to a line. Each number that is evenly divisible by four is flagged with an asterisk. The second program, lines 1720-1990, shows the quotient after calling DIVIDE.BCD.VALUE.BY FOUR.

I am sure there must be lots of other neat tricks possible by combining binary and decimal modes in the 6502. Do you know some? Send them in, and we will publish the best!

  1010 *--------------------------------
  1020 CROUT  .EQ $FD8E
  1040 COUT   .EQ $FDED
  1050 *--------------------------------
  1060 VALUE  .EQ 0
  1070 *--------------------------------
  1080 T
  1090        LDA #0       FOR VALUE = 0 TO $FF
  1100 .1     STA VALUE
  1110        LDA #" "
  1120        JSR COUT
  1130        LDA VALUE
  1140        JSR PRBYTE
  1150 *--------------------------------
  1170        BEQ .2       ...YES
  1180        LDA #" "     ...NO
  1190        .HS 2C
  1200 .2     LDA #"*"
  1210        JSR COUT
  1220 *--------------------------------
  1230        LDA #" "     SEPARATE ITEMS IN CHART
  1240        JSR COUT
  1260        AND #$0F
  1270        CMP #9
  1280        BNE .3
  1290        JSR CROUT
  1300 *---NEXT VALUE-------------------
  1310 .3     SED          MUST DO ARITHMETIC
  1320        LDA VALUE       IN DECIMAL MODE
  1330        CLC
  1340        ADC #1
  1350        CLD          BACK TO BINARY
  1360        BCC .1       ...UNTIL WRAP-AROUND
  1370        RTS
  1380 *--------------------------------
  1410        AND #$13            .NE. STATUS IF NOT
  1420        BEQ .1
  1430        EOR #$12
  1440 .1     RTS
  1450 *--------------------------------
  1470        LDA VALUE
  1500        PHA
  1510        AND #$10
  1520        BEQ .1
  1530        PLA
  1540        SBC #6
  1550        LSR
  1560        RTS
  1570 .1     PLA
  1580        LSR
  1590        RTS
  1600 *--------------------------------
  1620        LSR
  1630        TAX
  1640        AND #8
  1650        BEQ .1
  1660        DEX
  1670        DEX
  1680        DEX
  1690 .1     TXA
  1700        RTS
  1710 *--------------------------------
  1720 D
  1730        LDA #0       FOR VALUE = 0 TO $FF
  1740 .1     STA VALUE
  1750        LDA #" "
  1760        JSR COUT
  1770        LDA VALUE
  1780        JSR PRBYTE
  1790        LDA #"."
  1800        JSR COUT
  1810 *--------------------------------
  1830        JSR PRBYTE
  1840 *--------------------------------
  1850        LDA #" "     SEPARATE ITEMS IN CHART
  1860        JSR COUT
  1880        AND #$0F
  1890        CMP #9
  1900        BNE .3
  1910        JSR CROUT
  1920 *---NEXT VALUE-------------------
  1930 .3     SED          MUST DO ARITHMETIC
  1940        LDA VALUE       IN DECIMAL MODE
  1950        CLC
  1960        ADC #1
  1970        CLD          BACK TO BINARY
  1980        BCC .1       ...UNTIL WRAP-AROUND
  1990        RTS

Booting into 80 Columns Bill Morgan

The ProDOS version of the S-C Macro Assembler is carefully written to operate in either 40 or 80 columns. When you boot the disk the assembler starts out in the 40 column mode, because we couldn't take for granted that you would have (or want) the 80 column display. Well it turns out that most people (myself included) are using 80 columns and are getting tired of typing PR#3 every time they start up the assembler.

Marc Wolfgram called up today from Wisconsin to ask how to make the assembler start up in 80 columns, and that finally got me around to finding out how. It's embarassingly easy: just a two-byte patch. Here's the procedure, assuming you're in S-C Macro Assembler ProDOS:

     $6001:00 C3

We just changed the IO.INIT call from JMP MON.HOME to JMP $C300, and that's all there is to it! Now the next time you boot up, the assembler will be in 80 column mode. RESET will return you to 40 columns. PR#3 or NEW will restore 80 columns.

Thanks, Marc, for prompting me to find out about this.

Faster Boot and More Space for DOS 3.3 Bob Sander-Cederlof

A freshly initialized DOS 3.3 disk has 496 free sectors, less whatever is used by your HELLO program. There are 16 more sectors that are either never used or which are wasted, in tracks 0 and 2. The following program modifies the code which writes the DOS image and the code which reads it back during boot, so that the entire image fits in tracks 0 and 1. A further change makes the space in track 2 available for normal files.

The new boot procedure actually is faster than the standard one, and all the new code takes less space than that which is replaced. All you give up is the ability to boot into machines with less than 48K. Does anyone still have one?

Standard DOS 3.3 stores the DOS image in two pieces. The code destined for $B600-BFFF is on track 0, sectors 0 through 9. The code for $9D00-B500 follows, from track 0/sector 10 through track 2/sector 4. Sectors 5-15 of track 2 are not used. The information stored in sectors 3 and 4 of track 2 (aimed at $B400-B5FF) is useless, because all this space is variables for DOS which do not need to be initialized. The same goes for sector 5 of track 0. The contents of sectors 10 and 11 of track 0 is not used on a "slave" disk, which is what you get with the INIT command. My disks have to stay slave disks, because we are going to reshuffle everything around so all the unused sectors end up in track 2.

My new layout stores $9D00-9DFF in track 0/sector 5, and $9E00-B3FF in track 0/sector 10 through track 1/sector 15. The following table summarizes the old and new layouts.

     Sector    Track 0    Track 1    Track 2
              Old  New   Old  New   Old  New
       0       B6  B6     A1  A4     B1   ..
       1       B7  B7     A2  A5     B2   ..
       2       B8  B8     A3  A6     B3   ..
       3       B9  B9     A4  A7     B4   ..
       4       BA  BA     A5  A8     B5   ..
       5       BB  9D     A6  A9     ..   ..
       6       BC  BC     A7  AA     ..   ..
       7       BD  BD     A8  AB     ..   ..
       8       BE  BE     A9  AC     ..   ..
       9       BF  BF     AA  AD     ..   ..
      10       ..  9E     AB  AE     ..   ..
      11       ..  9F     AC  AF     ..   ..
      12       9D  A0     AD  B0     ..   ..
      13       9E  A1     AE  B1     ..   ..
      14       9F  A2     AF  B2     ..   ..
      15       A0  A3     B0  B3     ..   ..

I published the complete commented disassembly of the code which writes the DOS image on a disk and the code for the second stage boot in AAL way back in October, 1981. The second stage boot code begins at $B700, and the DOS writer starts at $B74A. They both use a subroutine at $B793 to read/write a range of sectors. I preserved the starting points for these two routines in the program which follows, but there is a lot of new empty space. If you are interested, you could go ahead and shove all the code segments together, patch all the calls for the new locations, and get one big area of free space for adding new features.

I was able to save coding space in several ways. First, by deciding that I would not worry about running in less than 48K. Second, that I could eliminate the extra code used to clobber the language card. This is a very common patch anyway, because most of us do not want to have to keep re-loading the language card area just because we re-boot DOS. Third, by eliminating the redundant calls to $FE89 and $FE93. The first stage boot does both of these just before jumping to the second stage boot, so there is no reason to do them again. And fourth, by being more efficient. If you want to, you can save even more by doing away with the subroutine at $B7C2: part of it is redundant, and the rest can be combined with the code at $B74A.

The standard DOS boot first loads $B600-BFFF from track 0, and then skips out to track 2 to read the rest hind-end-first. The track steps are 0-1-2-1-0. My new version starts in track 0, reads it all, then reads all of track 1, and it is done. The track steps are simply 0-1. It is a lot faster. However, the overall boot time is not significantly faster, due to the time spent finding track 0 in the first place, and the time spent loading the HELLO program.

Lines 1060-1140 install the new code. The entire $B7 page is replaced, as well as a single byte at $AEB3. This byte changes the VTOC on the newly initialized disk so that track 2 is available. While I was looking at this area, I noticed that the VTOC written on the new disk is not necessarily correct. DOS does not create an entirely new VTOC for the new disk. The bitmap area is new, and several other bytes are set up. However, DOS does not store any values in the bytes which tell how many tracks, sectors per track, sector size, and T/S entries per T/S list. This means that if the last access to a disk prior to initializing a new one was to a non-standard disk, the VTOC may be incorrect on the new disk. If I load a file from a large volume on my Sider, and then INIT a floppy, the floppy's VTOC indicates 32 sectors per track and 50 tracks! Ouch! Beware!

Lines 1180-1480 are the second stage boot code. The first stage boot is located at $B601, and actually executed at $801. It loads in sectors 0-9 of track 0 into $B600-BFFF, calls $FE89 and $FE93 to set the standard 40-column input hooks, and then jumps to $B700 with the slot*16 in the X-register. My stage two begins by copying the information which came from sector 5, now found at $BB00-BBFF, to the place it belongs at $9D00-9DFF (lines 1270-1320). Next I set up a call to my RWFT subroutine.

RWFT stands for Read/Write From Table. I have a table that describes all of the segments which must be loaded from the disk during boot, or written during initialization. Stage two boot must read the same things written by initialization, but init-ing requires first writing the stuff which will be loaded by stage one boot. Stage two boot calls RWFT with A=1 (read opcode for the IOB) and Y=2 (skipping the first two entries in the table). Initialization calls RWFT with A=2 (write opcode) and Y=0 (start at the beginning of the table).

RWFT gets four items out of the table for each step. The page number and sector number indicate the end of the range to be read or written. The count tells how many pages (or sectors) need to be read or written. All of the sectors must be in the track specified by the table entry. After one range has been read, RWFT steps to the next. The table terminates when the page address of 0 is found.

For some reason the code at $AEFF looks like this:

       AEFF-  JSR $B7C2
       AF02-  JSR $B74A

Both of these subroutines are never called from any other place, so they could be combined into one. Doing so would save several bytes. Furthermore, at least with my new RWFT program, lines 2120 and 2130 could be deleted, saving six more bytes.

There are still more ways to increase the storage on standard floppies, as you probably know. You can shorten the catalog, make a few other patches, and use some sectors in track 17 ($11).

You can usually use more than 35 tracks, since most drives will handle at least 36 and many a full 40. This also only takes a few simple patches. At $AEB5 you normally find a value $8C. Add 4 to this value for each additional track. This controls the loop that builds the bitmap of available sectors in the VTOC. The byte at $BEFE controls how many tracks the formatter in RWTS lays down. It is normally $23 (decimal 35), so add one for each additional track. Just before you start the INIT command, change the byte at $B3EF. This is normally $23, the number of tracks. Add 1 for each additional track. You have to be sure to do this last patch just prior to the INIT, because reading or writing another disk will cause it to be changed back.

Incidentally, this reminds me of the potential bug I mentioned above regarding writing out an incorrect VTOC. Once today I tried to catalog a disk that had been only partially initialized. The tracks had been written, but no VTOC or catalog sectors were. Of course I got an I/O ERROR. Next I decided to INIT that same disk. It went through the formatting stage, then bombed out with an I/O error when trying to write the catalog. Looking at the VTOC on this disk, the bytes for number of tracks, et cetera, were all zero!.

Now back to extra tracks. After making a disk with the extra tracks, you really need to check them to be sure your drive handles them. Use a disk zap program and try to write on the last track. Then try to write on the previous track. If your drive will go out that far, you will be successful. If you get an error trying to find the next to the last track, keep backing up until you find a track that does work. All the ones in between were written in the same location on the disk surface as the last track. If there were any missing tracks, you need to reformat the disk with fewer tracks.

An interesting side note to this discussion is that you could format a disk with LESS than 35 tracks if you wish. Just so you at least include track 17 ($11), you can reduce the values at $BEFE, $B3EF, and $AEB5 and stop short of a full disk. Some copy protection schemes do this, along with other tricks, to frustrate the making of copies.

  1000 *SAVE S.B700-B7FF DOS 3.3
  1010 *---------------------------------
  1030 FMW.VOLUME .EQ $B5F9
  1040 RWTS       .EQ $BD00
  1050 *---------------------------------
  1060 INSTALL
  1070        LDY #0       COPY NEW CODE INTO DOS
  1080 .1     LDA NEW.B700,Y    $B700...B7FF
  1090        STA $B700,Y
  1100        INY
  1110        BNE .1
  1120        LDA #8       PATCH TO INCLUDE TRACK 2
  1130        STA $AEB3    AS FREE SPACE
  1140        RTS
  1150 *---------------------------------
  1160 NEW.B700  .PH $B700
  1170 *--------------------------------
  1180 BOOT.STAGE2
  1190        STX IOB.SLOT16
  1200        STX IOB.PRVSLT
  1210        TXA          SLOT*16
  1220        LSR          GET SLOT #
  1230        LSR
  1240        LSR
  1250        LSR
  1260        TAX          X = SLOT NUMBER
  1270 *---COPY BB00-FF TO 9D00-FF------
  1280        LDY #0
  1290 .1     LDA $BB00,Y
  1300        STA $9D00,Y
  1310        DEY
  1320        BNE .1
  1330 *---SET CURRENT TRACKS @ 0-------
  1340        TYA          A = Y = 0
  1350        STA $4F8,X
  1360        STA $478,X
  1370 *---BUILD RWFT CALL--------------
  1380        INY          Y = 1
  1390        STY IOB.PRVDRV
  1400        STY IOB.DRIVE     DRIVE = 1
  1410        TYA               A = 1 (READ OPCODE)
  1420        INY               Y = 1 (RWFT INDEX)
  1430        JSR RWFT
  1440 *---COLD START DOS---------------
  1450        LDX #$FF
  1460        TXS          EMPTY STACK
  1470        STX IOB.VOLUME
  1480        JMP $9D84    DOS HARD ENTRY
  1490 *--------------------------------
  1500        .BS $B74A-*       <<<FILLER>>>
  1510 *---------------------------------
  1520 *      WRITE DOS IMAGE ON TRACKS 0-2
  1530 *---------------------------------
  1550        LDA #2       WRITE OPCODE FOR RWTS
  1560        LDY #0       RWFT INDEX
  1570 *--------------------------------
  1590 *--------------------------------
  1600 RWFT
  1610        STA IOB.OPCODE
  1620 .1     STY RWFT.INDEX
  1630        LDA RWFT.ADDR,Y
  1640        BEQ .3       ...END OF RWFT TABLE
  1650        STA IOB.BUFFER+1
  1660        LDA RWFT.TRACK,Y
  1670        STA IOB.TRACK
  1680        LDA RWFT.SECTOR,Y
  1690        STA IOB.SECTOR
  1700        LDA RWFT.COUNT,Y
  1710        STA RWFT.N
  1720 .2     LDA /IOB
  1730        LDY #IOB
  1740        JSR ENTER.RWTS
  1750        BCS .2       ...TRY AGAIN IF ERROR
  1770        DEC IOB.BUFFER+1  NEXT PAGE
  1780        DEC RWFT.N
  1790        BNE .2
  1800        LDY RWFT.INDEX
  1810        INY
  1820        BNE .1       ...ALWAYS
  1830 .3     RTS
  1840 *--------------------------------
  1850 RWFT.N     .BS 1
  1860 RWFT.INDEX .BS 1
  1870 *--------------------------------
  1880 RWFT.ADDR   .HS BF.9D.A3.B3.00
  1890 RWFT.TRACK  .HS
  1900 RWFT.SECTOR .HS 09.05.0F.0F
  1910 RWFT.COUNT  .HS 0A.01.06.10
  1920 *--------------------------------
  1930        .BS $B7B5-*       <<<FILLER>>>
  1940 *---------------------------------
  1950 *      ENTER RWTS
  1960 *---------------------------------
  1980        PHP          SAVE STATUS ON STACK
  1990        SEI          DISABLE INTERRUPTS
  2000        JSR RWTS     CALL RWTS
  2010        BCS .1       ERROR RETURN
  2020        PLP          RESTORE STATUS
  2030        CLC          SIGNAL NO RWTS ERROR
  2040        RTS          RETURN TO CALLER
  2050 .1     PLP          RESTORE STATUS
  2060        SEC          SIGNAL RWTS ERROR
  2070        RTS          RETURN TO CALLER
  2080 *---------------------------------
  2090 *      SET UP RWTS TO WRITE DOS
  2100 *---------------------------------
  2130        STA IOB.BUFFER+1
  2140        LDA #0
  2150        STA IOB.BUFFER
  2160        LDA FMW.VOLUME  VOLUME #
  2170        EOR #$FF        UNCOMPLEMENT IT
  2180        STA IOB.VOLUME
  2190        RTS
  2200 *---------------------------------
  2210 *      CLEAR 256 BYTES STARTING AT ($42,43)
  2220 *---------------------------------
  2240        LDA #0
  2250        TAY
  2260 .1     STA ($42),Y
  2270        INY
  2280        BNE .1
  2290        RTS
  2300 *---------------------------------
  2310        .BS $B7E8-*       <<<FILLER>>>
  2320 *---------------------------------
  2330 *      IOB FOR RWTS CALLS
  2340 *---------------------------------
  2350 IOB
  2360 IOB.TYPE   .HS 01   0--MUST BE $01
  2370 IOB.SLOT16 .HS 60   1--SLOT # TIMES 16
  2380 IOB.DRIVE  .HS 01   2--DRIVE # (1 OR 2)
  2400 IOB.TRACK  .BS 1    4--TRACK # (0 TO 34)
  2410 IOB.SECTOR .BS 1    5--SECTOR # (0 TO 15)
  2450 IOB.OPCODE .BS 1   12--0=SEEK, 1=READ, 2=WRITE, OR 4=FORMAT
  2460 IOB.ERROR  .BS 1   13--ERROR CODE: 0, 8, 10, 20, 40, 80
  2500            .BS 2
  2510 DCT    .HS 0001EFD8
  2520        .BS 1
  2530 *--------------------------------

A "Gotcha!" in New //c ROMs Robert H. Bernard

Apple seems to have installed a bug in the new ROM for the Apple //c which affects DOS 3.3. I am talking about the 3.5 ROM that supports Unidisk 3.5 and AppleTalk.

The new bug manifests itself when you use the control-IxxN command to either serial port. The older //c ROMs accumulated the "xx" number in $47F; the new ones do it in $47E. Location $47E is supposed to be dedicated to slot 6, the slot where the disk drives are. DOS uses $47E to keep track of the current track position for drive 1. So, after doing the serial port command to set line length, the next time DOS tries to look at drive 1 it will have to re-calibrate.

Re-calibration is not a disaster, but it is annoying. A needless and not noiseless waste of time. To avoid it with the new ROMs, you have to save and restore the contents of $47E around any serial port command that involves scanning a numeric field.

I have looked through the entire listing of the 3.5 ROM that came with my upgrade kit, and there does not appear to be any reason why this variable was moved. Location $47F is not used for any new value that I can see.

Even though the Apple //c Technical Reference Manual reserves $47E for the firmware, and ProDOS doesn't use the cell, using a "slot 6" screen-hole for a slot 1 and 2 activity is a serious breach of the protocol for their use that dates back to the earliest Wozdays. I know Apple is dropping (or at least decreasing) their support of DOS 3.3, but this is ridiculous!

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 $18 per year in the USA, sent Bulk Mail; add $3 for First Class postage in USA, Canada, and Mexico; add $14 postage for other countries. Back issues are available for $1.80 each (other countries add $1 per back issue for postage).

All material herein is copyrighted by S-C SOFTWARE CORPORATION, all rights reserved. (Apple is a registered trademark of Apple Computer, Inc.)