The 65816 continues to make news. We hear of at least two major books on 65816 Assembly Language, which should be in print soon. We also hear that sales of the chip are taking off, with some firms ordering multiplied thousands. Although we have yet to SEE one, we keep hearing reports of plug-in boards for Apples that contain a 65816 and lots of RAM: ComLog, MicroMagic, Checkmate Technology, and others.
Meanwhile, we contemplate the future advantages to just enhancing existing Apples with 65802's and big RAM boards. Applied Engineering or Checkmate will be delighted to stuff 512K additional RAM into your //c. You can add five times that much to your //e with AE's latest version of RAMWorks. Apple's forthcoming Slinky card will add up to a megabyte to any II, II Plus, or //e with a spare slot (1-7). Call APPLE's latest magazine offers the BIG BOARD for slot 0-7 use, one megabyte addressable either in Slinky fashion or with "standard" D000-FFFF mapping, for only $599. If you hurry, they have a special (even lower) price good until Sept 30th.
6800 Cross Assembler for ProDOS
The S-C 6800 Macro Cross Assembler is now also available in a ProDOS version. This is the Version 2.0 level Cross Assembler, including the additional opcodes of the Motorola 6801 and Hitachi 6301 microprocessors. Either the DOS or the ProDOS Version 2.0 Cross Assembler is $50; if you already have one you can add the other for only $20.
What has 640K of memory and is as cute as a button? My Apple //c! It didn't come with all that memory, "only" 128K of it. Before I even powered it up for the first time, I installed a 512K Z-RAM. Ready to take on Blue's 640K machine? Maybe.
I've had quite a few Apple Computers, my first had Integer ROMs and a serial number in the thirty one thousands, and my current workhorse is an Apple //e with the works. So why a //c? Well, for one it's cute, and secondly its firmware was written by Ernie Beernink and Rich Williams, the same guys that wrote the //e Enhanced ROMs and Extended Debugging Monitor. These guys write slick code. Finally, I can type control-reset with one hand.
Well, what to do after getting it home? I tried my mouse out on it, but moved it back to the //e. My paddles and joysticks all have 16-pin plugs, so I couldn't use them. I don't have an RGB interface for the //c yet, so the color monitor has to stay put. That leaves my Imagewriter printer to play with.
Having two computers and only one printer is an old problem. One usually solved with a rotary switch. I figured that I could do a little better. What I did is connect the Imagewriter to the //c's Printer port, and the //e's Super Serial Card (SSC) to the //c's Modem port. I then wrote the program that follows this article. It implements a 576K buffer for the //e, in the //c. Now I can use the printer from the //c just by typing pr#1. When I want to print from the //e, I just boot a disk on the //c, then type pr#1 on the //e. However, the printing, for the //e, goes MUCH faster. I've setup the link between the //e and the //c to transmit at 19200 baud! Assembling a listing of the buffering program takes about 7 seconds (and half of that is writing the target file)!
The SSC is in slot 1, it is configured as follows:
SW1: off off off off off on on SW2: on off off on on off off The jumper block is installed pointing towards modem
The Imagewriter's swiches are set:
SW1: open open open open closed closed open open SW2: closed closed open open.
The pieces are connected with two DIN 5-Pin(m) to DB-25(m) cables, Apple Model Number: A9C0308 (4-2, 2-3, 1-6, 3-7, and 5-20). The cable from the //e to the //c is plugged into a //c System Clock which in turn is plugged into the Modem Port.
The program should work with most any serial printer, and serial card, however, if the serial card cannot "eliminate the modem", you will need a modem-eliminator cable extension, or will have to reverse pins 2 and 3 and pins 6 and 20 of the DB-25 connector. The Apple cable I used cannot be modified.
While the listing included with this article requires a 512K Applied Engineering Z-RAM board, I have also written versions that work in a 256K Z-RAM and in a stock Apple //c. More on these versions later. The memory on a Z-RAM is implemented as additional banks of auxiliary memory. Which of the auxiliary banks is the current auxiliary bank is controlled by a new hardware location at $C073. The Z-RAM powers-up disabled, that is, with the //c's built-in auxiliary bank as the current auxiliary bank. The //c powers-up with main memory enabled and all auxiliary memory disabled. Once selected as the current auxiliary bank, a Z-RAM bank is switched around by all the normal soft switches in the same manner as the //c's built-in auxiliary bank. A 512K Z-RAM has 8 additional banks and a 256K Z-RAM has 4 more. Which additional bank is the current auxiliary bank is selected by writing an ODD number between 1 and $F (inclusive) to the bank register at $C073. The 4 most significant data bits are ignored and any even number (usually zero) selects the //c's built-in auxiliary bank. A 256K Z-RAM only has bank numbers 3, 7, $B, and $F. To ease the task of writing programs that display 80 columns of text or double hires graphics, video data is always fetched from the //c's banks, even if a Z-RAM bank is the current auxiliary bank. Because the Z-RAM plugs into the processor and MMU sockets of the //c, and since only one board may be added this way, the Z-RAM includes a Z-80 processor. The Z-RAM is also totally compatible with the RamWorks board for the //e.
The //c's serial ports are a lot like Super Serial Cards in slots 1 and 2 of a //e. The ports and the SSC both use the 6551 ACIA (Asynchronous Communications Interface Adapter) and the firmware is quite similar. There is one significant difference that I found. The SSC tells an external source of data to stop transmitting by asserting the Data Terminal Ready bit of the ACIA command register (and thus the DTR pin when the jumper block is in the terminal position), while the //c's ports control the DTR pin with the Request To Send (and transmitter control) bits. It's right there on page 254 of The Apple //c Reference Manual Volume 1. Compare this to the schematic on page 100 of the SSC Manual.
Because every //c has a 65C02 processor, I can write code using the new opcodes and it will work in other peoples' machines. Of course if the code will also work in a //e, I can not be sure that it will be executed on a 65C02. With the release of the //e enhancement kit, this situation should improve. 65802 opcodes, being new and rare, must be reserved for programs intended for use in a very few machines.
On to the program. The target file is intended to load at $2000 in main memory. The code from lines 32 to 73 is executed in the $2000 area. This section does all of the setup for what is to come. The D and I flags are cleared and set respectively, ten soft switches are thrown, the screen is cleared, the remainder of the code is copied into ALL auxiliary zero pages and stacks, a text message is written to the screen, and the two ACIAs are initialized. The code copy and message printing share a loop. Lines 66 and 70 cheat a little. The INCs are assembled and the LDA #s are treated as comments. They work because the would-be operands of the LDA #s are one greater than the values just loaded by the previous LDA #s. The 'A' in line 74 is an open-apple MouseText character. The code in aux bank 0 is then entered at label 'Scan'.
The routines 'Write' and 'Read' (lines 79 and 88), handle all access to the buffer. In 'Write', the aux bank is selected, the address within that bank is written into the operand of a store absolute instruction (the copy in the bank just selected), and then the data byte is written. That's a total of four bytes of information passed in internal registers. The data byte had to be passed in the stack pointer! It couldn't have been passed in a memory location because it would have been switched out. 'Read' is a little simpler, it returns a data byte in the Acc. Since I'm using the S-reg for data and the aux bank 0 stack page for code, the program doesn't make any use of regular stack operations. After re-selecting aux bank 0, 'Write' and 'Read' jump back to the code just after the jumps that 'called' them. Even though the $2000 code copied the entire image into every aux bank, only 'Write' and 'Read' are not used as buffer in the Z-RAM banks.
Lines 99 to 108 allocate the (zero page!) variables required to keep track of the buffer. The 'Receive' variables indicate where the next byte received will be buffered, the 'Transmit' variables indicate where the next byte to be printed is buffered, and the 'Byte.Counter' variables keep track of how full (or empty) the buffer is. If the byte counter is zero, then the 'Transmit' variables are equal to the 'Receive' variables and the buffer is empty. 'RTS.Bit' is used to keep track of the //c's 'select' state.
Lines 110 to 128 run an indicator at the top-center of the screen and check to see if you've pressed a key. If you press the space bar, and if the program hasn't asserted the Request To (NOT) Send bit (because the buffer is nearly full), the //e may be halted. This works like a printer's select button.
Lines 129 to 207 handle buffering incoming data. If the Modem ACIA detects any transmission errors, you will see an indication of this at the left end of screen line three. If no character has been received, we go check the Printer port. When a character has been received, we test if the buffer is almost full. If it is, we assert RTS' (another character may already be on the way). The byte counter is incremented. If the buffer is completely full, we tick the third position of screen line one and go check the Printer port. This means that the RTS' handshaking isn't working. You will also get overrun errors. If we have room for the character, we increment the upper left screen position, and load the character from the RxD reg into the stack pointer. We then load the 'Receive' variables, maybe juggle the address high order nibble for the overlapping language card banks, and call 'Write'. Upon return, the 'Receive' variables are advanced through the buffer memory, avoiding our program and invalid aux banks. We then fall into the Printer port code.
Lines 208 to 271 handle printing buffered data as the printer can take it. This code is similar to the code for incoming data. Fewer things can go wrong, we of course test for an empty TxD reg and an empty buffer. We check to see if the buffer is somewhat less than almost full, and may release RTS'. The byte counter is decremented here. When a character is to be printed, we increment the upper right screen position, load the 'Transmit' variables, maybe juggle, call 'Read' and stuff the character into the TxD reg. Upon return, the 'Transmit' variables are advanced (same way), and we loop to 'Scan'. Forever. Reset exits the program.
The program loops VERY quickly. It has to. At 19200 baud, a character is received from the //e every half millisecond and at 9600 baud, a character may be printed every millisecond. The pair of locations at the top center of the screen, that are changed every time around the loop, give a good indication of how fast things are happening. The locations in the upper corners (my //e is to the left of the //c and the printer is to the right) are a good representation of the values of the 'Receive' and 'Transmit' variables. When buffering, the receive indicator races ahead while the transmit indicator lags behind, but since they are both initialized to blanks and the appropriate one is incremented when a character is moved, they come to rest displaying the same character when the buffer is empty.
The symbols 'Z.RAM.Banks.Avail', 'Z.RAM.Banks.Used', 'IIc.Aux.Bank.Avail' and 'BufLen' (lines 94, 96, 273-274) determine the size of the buffer. The ADC immediate operands in lines 195 and 259 cause the buffer to advance from bank 0 to 1 to 3 to 5... to $F. The listing is setup to use a //c's aux bank and a 512K Z-RAM. The changes for a 256K Z-RAM are easy: change the SAVE and .tf filenames (320K), change the 8 in line 96 to a 4, change the 9 in line 274 to a 5, and change the ADC #1s in lines 195 and 259 to ADC #3s. The changes for operating without a Z-RAM are not as simple. I removed all the bank stuff, made the byte counter only 16 bits, and combined the code copy with the screen clear instead of the message printing. It took about 5 minutes. The resulting code just fit into the aux zero page! The source code for all three versions will be on S-C Software's next quarterly disk, and I will send a paper listing of the //c only version to anyone who sends a self addressed stamped envelope to me care of Applied Engineering. I sometimes use the //c only version even though I have a Z-RAM. With the ProDrive disk emulation software, I can lock-out bank 0, leaving it available for double hires or a 64K buffer for my //e. With a 512K Z-RAM, I get a 1024 block /RAM volume.
The program does not use any main memory for the buffer because when you have 576K of aux memory, why bother programming for "only" another 64K? The //c only version, with 64K of buffer memory, is as big or bigger than most buffer boards/boxes. If anyone writes a 128K main/aux version of the program I would appreciate a copy.
0000 .ti 81, B NApple //c w/512K Z-RAM buffering program 2.0 8/5/85 dcj 0001 ;SAVE Buf.576K 0002 ;-------------------------------- 0003 ; Dedicated to Allan B. Calhamer. 0004 ;-------------------------------- 0005 Printer.ACIA.TxD .eq $C098 (w) 0006 Printer.ACIA.Status .eq $C099 (r) 0007 Printer.ACIA.Command .eq $C09A (r/w) 0008 Printer.ACIA.Control .eq $C09B (r/w) 0009 Modem.ACIA.RxD .eq $C0A8 (r) 0010 Modem.ACIA.Status .eq $C0A9 (r) 0011 Modem.ACIA.Command .eq $C0AA (r/w) 0012 Modem.ACIA.Control .eq $C0AB (r/w) 0013 Z.RAM.Bank.Reg .eq $C073 (w) same as RamWorks 0014 Keyboard .eq $C000 (r) 0015 Store80 .eq $C001 (w) on 0016 RAMRd .eq $C003 (w) aux 0017 RAMWrt .eq $C005 (w) aux 0018 AltZP .eq $C009 (w) aux 0019 Vid40 .eq $C00C (w) 0020 SetAltChr .eq $C00F (w) w/MouseText 0021 Clear.Key.Strobe .eq $C010 (r) 0022 Text .eq $C051 (r) 0023 Page1 .eq $C054 (r) main 0024 Page2 .eq $C055 (r) aux 0025 Hires .eq $C057 (r) $2000-$3FFF too... 0026 LCRAM2 .eq $C083 (r/w; write doesn't 0027 LCRAM1 .eq $C08B change write enable) 0028 ;-------------------------------- 0029 .op 65C02 0030 .or $2000 0031 .tf Bufit576K 0032 dcj CLD rqd (now) 0033 SEI close this can of worms... 0034 LDA LCRAM2 1x...switches setup 0035 LDA Text 0036 LDA Page1 0037 LDA Hires 0038 STZ Store80 0039 STZ RAMRd 0040 STZ RAMWrt 0041 STZ AltZP 0042 STZ SetAltChr 0043 STZ Vid40 0044 LDA #" " clear 40 column screen 0045 LDX #0 0046 .1 STA $400,X 0047 STA $500,X 0048 STA $600,X 0049 STA $700,X 0050 INX 0051 BNE .1 0052 LDY #$0F install Image in aux ZPs/Stacks 0053 .2 STY Z.RAM.Bank.Reg 0054 .3 LDA Image,X 0055 STA $00,X 0056 LDA Image+$100,X 0057 STA $100,X 0058 INX 0059 BNE .3 0060 LDA Msg,Y put up a message 0061 STA $50C,Y 0062 DEY 0063 BPL .2 0064 LDA #%000.0.10.1.0 bop ACIAs 0065 STA Printer.ACIA.Command 0066 inc LDA #%000.0.10.1.1 RTS' lo 0067 STA Modem.ACIA.Command 0068 LDA #%0.00.1.1110 9600 baud 0069 STA Printer.ACIA.Control 0070 inc LDA #%0.00.1.1111 19200 baud! 0071 STA Modem.ACIA.Control 0072 LDA Modem.ACIA.RxD 0073 JMP Scan go 2 it 0074 Msg .AS 'A' as in Apple 0075 .AS -" //c buffer pgm" 0076 Image .ph $00 0077 ; aux bank specified by Acc, bank adr lo by X-reg, 0078 ; bank adr hi by Y-reg, and byte passed in S-reg! 0079 Write STA Z.RAM.Bank.Reg bank in Z-RAM 0080 STX <.1+1 modify STX operand in "this" bank 0081 STY <.1+2 0082 TSX get byte to a usable reg! 0083 .1 STX $FFFF abs adr modified for each write 0084 STZ Z.RAM.Bank.Reg revert to //c aux bank 0085 JMP W.Ret 0086 ; aux bank specified by Acc, bank adr lo by X-reg, 0087 ; bank adr hi by Y-reg, and byte returned in Acc. 0088 Read STA Z.RAM.Bank.Reg bank in Z-RAM 0089 STX <.1+1 modify LDA operand in "this" bank 0090 STY <.1+2 0091 .1 LDA $FFFF abs adr modified for each read 0092 STZ Z.RAM.Bank.Reg revert to //c aux bank 0093 JMP R.Ret 0094 Z.RAM.Banks.Avail .eq *-3 0095 ; (-3 because JMP R.Ret never executed in Z-RAM) 0096 Z.RAM.used .eq Z.RAM.Banks.Avail*8 0097 ;-------------------------------- 0098 ; buffer starts at first available location in //c aux bank 0099 Receive.Adr.Lo .da #IIc.Aux.Bank.Avail 0100 Receive.Adr.Hi .da /IIc.Aux.Bank.Avail 0101 Receive.Bank .da #$00 0102 Transmit.Adr.Lo .da #IIc.Aux.Bank.Avail 0103 Transmit.Adr.Hi .da /IIc.Aux.Bank.Avail 0104 Transmit.Bank .da #$00 0105 Byte.Counter.Lo .da #$000000 indicates empty 0106 Byte.Counter.Mid .da #$000000/256 0107 Byte.Counter.Hi .da #$000000/65536 0108 RTS.Bit .da #%000.0.10.0.0 RTS' lo 0109 ;-------------------------------- 0110 Scan LDA Page1 access main text screen 0111 INC $413 show we're alive 0112 DEC $414 0113 LDA Page2 back to aux 0114 LDA Keyboard scan keyboard 0115 BPL Scan.Modem.Port 0116 CMP #" " space toggles RTS' (DTR2B) to //e 0117 BNE .2 0118 LDA Modem.ACIA.Command 0119 AND #%000.0.10.0.0 0120 BNE .1 =>It's ok, you can turn it off... 0121 LDA RTS.Bit 0122 BNE Scan.Modem.Port =>don't do it! (yet) 0123 .1 LDA Modem.ACIA.Command 0124 EOR #%000.0.10.0.0 0125 STA Modem.ACIA.Command 0126 AND #%000.0.10.0.0 0127 STA RTS.Bit 0128 .2 BIT Clear.Key.Strobe 0129 Scan.Modem.Port 0130 LDY Modem.ACIA.Status 0131 TYA 0132 AND #%0000.0111 error bits mask 0133 BEQ .1 =>error-free operation 0134 TAX 0135 LDA Page1 access main text screen 0136 INC $4FF,X indicate error... 0137 LDA Page2 back to aux 0138 .1 TYA 0139 AND #%0000.1000 receive data reg full mask 0140 BEQ CantRx =>not full 0141 LDA Byte.Counter.Lo received a byte, 0142 LDX Byte.Counter.Mid do we assert RTS' ? 0143 LDY Byte.Counter.Hi 0144 CMP #BufLen-256 0145 BNE .2 =>buffer not @ full-256 0146 CPX /BufLen-256 0147 BNE .2 =>buffer not @ full-256 0148 CPY ^BufLen-256 0149 BNE .2 =>buffer not @ full-256 0150 LDA #%000.0.10.0.0 assert RTS' 0151 TRB Modem.ACIA.Command 0152 LDA Byte.Counter.Lo reload it 0153 .2 INC fig next byte count 0154 BNE .3 0155 INX 0156 BNE .3 0157 INY 0158 .3 CMP #BufLen do we have room for it ? 0159 BNE Room =>buffer not full 0160 CPX /BufLen 0161 BNE Room =>buffer not full 0162 CPY ^BufLen 0163 BNE Room =>buffer not full 0164 LDA Page1 access main text screen 0165 INC $402 indicate full 0166 LDA Page2 back to aux 0167 CantRx BRA Cant.Receive =>buffer is full! 0168 Room STA Byte.Counter.Lo 0169 STX Byte.Counter.Mid 0170 STY Byte.Counter.Hi 0171 LDA Page1 access main text screen 0172 INC $400 show we received a byte 0173 LDA Page2 back to aux 0174 LDX Modem.ACIA.RxD 0175 TXS pass it in S-reg 0176 LDX Receive.Adr.Lo 0177 LDY Receive.Adr.Hi 0178 BIT LCRAM2 normally use LC bank 2 0179 TYA 0180 AND #$F0 0181 CMP /$C000 if adr is in $CXXX range 0182 BNE .1 0183 BIT LCRAM1 use LC bank 1 0184 TYA 0185 ORA /$D000 0186 TAY 0187 .1 LDA Receive.Bank 0188 JMP Write 0189 W.Ret INC Receive.Adr.Lo fig next receive adr 0190 BNE Scan.Printer.Port 0191 INC Receive.Adr.Hi 0192 BNE Scan.Printer.Port 0193 LDA Receive.Bank 0194 CMP #1 0195 ADC #1 clear carry if 0, else set it 0196 CMP #$10 0197 BCC .1 =>entering/still in Z-RAM 0198 LDA #$00 wrap to //c bank 0 0199 LDX #IIc.Aux.Bank.Avail 0200 LDY /IIc.Aux.Bank.Avail 0201 BRA .2 0202 .1 LDX #Z.RAM.Banks.Avail 0203 LDY /Z.RAM.Banks.Avail 0204 .2 STA Receive.Bank 0205 STX Receive.Adr.Lo 0206 STY Receive.Adr.Hi 0207 Cant.Receive 0208 Scan.Printer.Port 0209 LDA #%0011.0000 make transmit data reg empty and 0210 AND Printer.ACIA.Status Data Carrier Detect mask 0211 CMP #%0001.0000 test empty and DCD' lo 0212 BNE Cant.Transmit =>not empty or not ready 0213 LDA Byte.Counter.Lo printer can take another byte, 0214 ORA Byte.Counter.Mid do we have one ? 0215 ORA Byte.Counter.Hi 0216 BEQ Cant.Transmit =>buffer is empty!!! 0217 LDA Byte.Counter.Lo do we release RTS' ? 0218 LDX Byte.Counter.Mid 0219 LDY Byte.Counter.Hi 0220 CMP #BufLen-2048 0221 BNE .1 =>buffer not @ full-2048 0222 CPX /BufLen-2048 0223 BNE .1 =>buffer not @ full-2048 0224 CPY ^BufLen-2048 0225 BNE .1 =>buffer not @ full-2048 0226 LDA RTS.Bit 0227 TSB Modem.ACIA.Command release RTS' (maybe) 0228 .1 STA Z.RAM.Bank.Reg+5 0229 LDA Byte.Counter.Lo fig next byte count 0230 BNE .3 0231 LDA Byte.Counter.Mid 0232 BNE .2 0233 DEC Byte.Counter.Hi 0234 .2 DEC Byte.Counter.Mid 0235 .3 DEC Byte.Counter.Lo 0236 LDA Page1 access main text page 0237 INC $427 show we printed a byte 0238 LDA Page2 back to aux 0239 LDX Transmit.Adr.Lo 0240 LDY Transmit.Adr.Hi 0241 BIT LCRAM2 normally use LC bank 2 0242 TYA 0243 AND #$F0 0244 CMP /$C000 if adr in $CXXX range 0245 BNE .4 0246 BIT LCRAM1 use LC bank 1 0247 TYA 0248 ORA /$D000 0249 TAY 0250 .4 LDA Transmit.Bank 0251 JMP Read 0252 R.Ret STA Printer.ACIA.TxD 0253 INC Transmit.Adr.Lo fig next transmit adr 0254 BNE Next 0255 INC Transmit.Adr.Hi 0256 BNE Next 0257 LDA Transmit.Bank 0258 CMP #1 clear carry if 0, else set it 0259 ADC #1 0260 CMP #$10 0261 BCC .1 =>entering/still in Z-RAM 0262 LDA #$00 wrap to //c bank 0 0263 LDX #IIc.Aux.Bank.Avail 0264 LDY /IIc.Aux.Bank.Avail 0265 BRA .2 0266 .1 LDX #Z.RAM.Banks.Avail 0267 LDY /Z.RAM.Banks.Avail 0268 .2 STA Transmit.Bank 0269 STX Transmit.Adr.Lo 0270 STY Transmit.Adr.Hi 0271 Cant.Transmit 0272 Next JMP Scan 0273 IIc.Aux.Bank.Avail .eq * 0274 BufLen .eq $90000-Z.RAM.Used-IIc.Aux.Bank.Avail 0275 .lif |
I have been thinking about a semi-automatic object code relocation scheme lately. Steve Wozniak wrote one for the 6502 back in 1976, published in various places such as Call APPLE's "Wozpak". But we are needing one for the 65C02, and maybe for the 65816.
Steve's version used his "Sweet-16" interpreter for some of the address arithmetic. That was okay, because Sweet-16 was in ROM in every Apple in those days. Not so now, although it is available to DOS 3.3 users as part of the Integer BASIC package. But we should write one that does not require Sweet-16.
Steve's relocator also used a ROM-based routine (part of the built-in disassembler) to determine how many bytes are used by each opcode. This routine has been modified in the //c monitor and the new enhanced //e monitor to include the 65C02 opcodes. That's nice, because that means Woz's program will automatically work with 65C02 programs if you run it with the new monitors. However, since I want to include all the 65816 opcodes, I need a new version.
The first step seems to be to write a program which will tell me how many bytes each opcode uses. I know that opcodes which are only one or two bytes do not need any relocation adjustments when a program is moved to a different place in memory. Most 3-byte and all 4-byte instructions contain absolute addresses; if an absolute address is inside the program being moved, it will have to be adjusted for the new location.
I haven't written the entire relocator yet, but I have written a program which will tell me all I need to know about the length of an opcode. My program returns the length in bytes and also two flags. One flag indicates the opcode is a 3-byte instruction which does include an absolute address. The other flag indicates the opcode was an immediate mode instruction. Immediate mode in 65816 code is ambiguous in length, except during execution. My program calls them two-byte instructions, but they may be three bytes each if the status bits so indicate at execution time. I am not sure how my relocator will handle this ambiguity, but for now I am content just to set a flag.
The code in the monitor which determines the length of opcodes uses a table lookup method. I figure that I could do that too, with a 64-byte table, using two bits for each opcode. I would still need a way to test for immediate mode and the special three-byte opcodes which do not have absolute addresses (MVP, MVN, PER, and BRL).
After looking at a chart which showed all the lengths, I decided to do it with bit analysis rather than table lookup. It is probably a little slower, but also a little smaller.
It turns out that almost all of the opcodes whose second hex digit is less than 8 use two bytes. There are only nine exceptions. One interesting case here is BRK, which assembles to only one byte but is considered by the microprocessor to be a two-byte opcode. I am not sure whether the relocator should considere BRK as a single byte or a two-byte opcode, but I think it should probably be one byte.
All opcodes of the with the hex values of $x8, $xA, and $xB are one byte, without exception. All opcodes with the hex values $xC, $xD, and $xE are three bytes with absolute addresses, with only one exception: $5C is a four-byte instruction. All opcodes with value $xF are four bytes each.
The column of opcodes with values $x9 are divided into two groups. Those with the first digit even ($09, 29, 49, etc.) are all three bytes each with absolute addresses. The odd ones are immediate mode opcodes, which may be either two or three bytes each depending on status bits during execution.
Here is a table of the various byte counts, which was actually computed by my program. I printed "2#" for immediate mode opcodes, and "3+" for three-byte opcodes with absolute addresses.
0 1 2 3 4 5 6 7 8 9 A B C D E F 0 2 2 2 2 2 2 2 2 1 2# 1 1 3+ 3+ 3+ 4 1 2 2 2 2 2 2 2 2 1 3+ 1 1 3+ 3+ 3+ 4 2 3+ 2 4 2 2 2 2 2 1 2# 1 1 3+ 3+ 3+ 4 3 2 2 2 2 2 2 2 2 1 3+ 1 1 3+ 3+ 3+ 4 4 1 2 2 2 3 2 2 2 1 2# 1 1 3+ 3+ 3+ 4 5 2 2 2 2 3 2 2 2 1 3+ 1 1 4 3+ 3+ 4 6 1 2 3 2 2 2 2 2 1 2# 1 1 3+ 3+ 3+ 4 7 2 2 2 2 2 2 2 2 1 3+ 1 1 3+ 3+ 3+ 4 8 2 2 3 2 2 2 2 2 1 2# 1 1 3+ 3+ 3+ 4 9 2 2 2 2 2 2 2 2 1 3+ 1 1 3+ 3+ 3+ 4 A 2 2 2 2 2 2 2 2 1 2# 1 1 3+ 3+ 3+ 4 B 2 2 2 2 2 2 2 2 1 3+ 1 1 3+ 3+ 3+ 4 C 2 2 2 2 2 2 2 2 1 2# 1 1 3+ 3+ 3+ 4 D 2 2 2 2 2 2 2 2 1 3+ 1 1 3+ 3+ 3+ 4 E 2 2 2 2 2 2 2 2 1 2# 1 1 3+ 3+ 3+ 4 F 2 2 2 2 3+ 2 2 2 1 3+ 1 1 3+ 3+ 3+ 4
The program which printed the table is in lines 1050-1320 below. The program which computes how many bytes in an opcode follows that. By inserting a "BEQ .6" between lines 1410 and 1420 I could make BRK a one-byte opcode.
My relocator should probably also be on the lookout for calls to ProDOS MLI. This is in effect a six-byte instruction. The first three bytes are $20, $00, $BF (JSR MLI). The fourth byte is the MLI function code. The last two bytes are the address of a parameter table, and so should be considered as a relocatable address.
I hope to continue to pursue this idea of a relocator, but I make no promises. Maybe one of you would like to write one and share it with the rest of us.
1000 *SAVE S.BYTE TABLE 1010 *-------------------------------- 1020 COUT .EQ $FDED 1030 CROUT .EQ $FD8E 1040 *-------------------------------- 1050 T 1060 LDX #0 1070 .1 TXA 1080 AND #$0F 1090 BNE .2 1100 JSR CROUT 1110 .2 TXA 1120 JSR GET.LENGTH.OF.OPCODE 1130 PHA 1140 AND #$07 1150 ORA #"0" 1160 JSR COUT 1170 PLA 1180 ASL POSITION XY FOR INDEX 1190 ROL 1200 ROL 1210 AND #$03 0000 00XY 1220 TAY 1230 LDA TABLE,Y 1240 JSR COUT 1250 LDA #" " 1260 JSR COUT 1270 INX 1280 BNE .1 1290 JMP CROUT 1300 *-------------------------------- 1310 TABLE .AS -/ #+/ 1320 *-------------------------------- 1330 * CALL WITH (A)= OPCODE 1340 * RETURN WITH (Y)= OPCODE 1350 * (A)= XY000LLL 1360 * LLL = # OF BYTES, 1...4 1370 * X = 1 IF ABS ADDRESS 1380 * Y = 1 IF IMMEDIATE 1390 *-------------------------------- 1400 GET.LENGTH.OF.OPCODE 1410 TAY 1420 AND #$0F 1430 CMP #$08 1440 BCC .4 XXXX 0XXX 1450 CMP #$0C 1460 BCC .3 XXXX 10XX 1470 CMP #$0F 1480 BEQ .2 XXXX 1111, L=4 1490 CPY #$5C 1500 BEQ .2 0101 1100, L=4 1510 *---L=3, ABS ADDRESS------------- 1520 .1 LDA #$83 1530 RTS 1540 *---L=4-------------------------- 1550 .2 LDA #4 1560 RTS 1570 *---XXXX 10XX-------------------- 1580 .3 CMP #$09 1590 BNE .6 X8, XA, or XB 1600 *---XXXX 1001-------------------- 1610 TYA 1620 AND #$10 1630 BNE .1 XXX1 1001, L=3 1640 *---XXX0 1001, IMMEDIATES, L=2--- 1650 LDA #$42 OR 3 IF ## MODE 1660 RTS 1670 *---XXXX 0XXX-------------------- 1680 .4 LSR CHECK ODD/EVEN 1690 BCS .5 ODD, L=2 1700 CPY #$22 1710 BEQ .2 JSL LABS, L=4 1720 CPY #$20 1730 BEQ .1 JSR ABS, L=3 1740 CPY #$40 1750 BEQ .6 RTI, L=1 1760 CPY #$60 1770 BEQ .6 RTS, L=1 1780 CPY #$62 1790 BEQ .7 PER LREL, L=3 1800 CPY #$82 1810 BEQ .7 BRL LREL, L=3 1820 CPY #$44 1830 BEQ .7 MVP, L=3 1840 CPY #$54 1850 BEQ .7 MVN, L=3 1860 CPY #$F4 1870 BEQ .1 PEA ABS, L=3 1880 *---L=2-------------------------- 1890 .5 LDA #2 L=2 1900 RTS 1910 *---L=1-------------------------- 1920 .6 LDA #1 1930 RTS 1940 *---L=3, NON-ABS ADDRESS--------- 1950 .7 LDA #3 1960 RTS 1970 *-------------------------------- |
I may have written hundreds of different versions of the elementary I/O conversion routines. The first few would have been for the IBM 704, back in college days. Then there was the G-15, the 1620, the 3100, the 3300, the 6600, the 1700, the 8090, the 960, the 980, the 990, and so on. Don't worry of those numbers don't mean anything to you. They are the "names" of computers out of the past, not micro chips.
What I am talking about is writing programs which convert input decimal characters representing decimal numbers into internal binary form, and the converse operation of converting binary numbers into decimal form. We have published several variations of both in previous newsletters, but I have some special ones to present here.
There are many variations of the basic routines, and that is one reason I have written so many. Thinking just of the output conversions (binary to decimal):
The routine I set out to write today works with unsigned integers, prints out the resulting characters rather than storing them in a string, and does not print any leading zeroes or blanks. I wrote it to work with two-byte values, between 0 adn 65535. As an added feature, I indicated in the comments how to expand it to work with larger values.
Lines 1800-2080 in the listing comprise the output conversion routine. I divide the number by ten, saving the remainder as the least significant digit; the quotient becomes the new number, so I repeat the process until the quotient is zero. Then the digits, which were all saved on the 6502 stack, are popped back off and printed.
Line 1810 starts the digit counter at 0, and line 1950 increments the counter each time a new digit is pushed onto the stack. Lines 2020-2060 pull the digits off the stack and print them in reverse order.
Lines 1970-2000 test the quotient: if it is non-zero, another division is performed; if not, we are ready to print the result. This is one place where you need to add code if your input values are larger than two bytes, as I indicated in line 1980. By the way, since we do one division before testing, an input value of zero will print as "0".
Lines 1830-1930 divide the input value by ten. It may look like I am dividing by five, but remember 5 = 10/2. I did more fiddling than analyzing in this loop, but it really does work. Line 1840 sets the loop count to 16, the number of bits in two bytes. If you want to convert three-byte values, change the 16 to 24. The loop needs to be executed once for each bit in the input value. If you are going to have values longer than two bytes, you also need to add more ROL instructions between lines 1880 and 1900, as indicated in my comment line 1890. If you were to need a three byte conversion routine, you could just remove the "*--" from the front of lines 1890 and 1980, and chane line 1840 to LDY #24.
Notice that this subroutine is very short, and fairly fast. I have an idea that some of you will think of ways to make it shorter and faster; if you do, try to keep it easily modifiable for the number of bytes in values.
Next I wrote a program to convert from a decimal string into binary, lines 1290-1720. It is also set up for unsigned two-byte integer values, with comments indicating how to modify it for longer values. I have written shorter routines before, but this one makes extension to longer values easy and tests for overflow.
The string is assumed to be in ASCII, with high bits = 1, starting at $0200, and terminated by any non-digit. It just so happens that these are just the conditions you usually find in an Apple, because almost all input routines use the buffer at $0200. Woz started it, and we all followed Woz.
Lines 1300-1330 clear the value, as well as starting the buffer index at zero. The rest of the routine scans through the digits. Each time the current value is multiplied by ten, and the next digit added. If at any point an overflow is detected (a value too large for the number of bytes) the routine rings the bell and quits. You can use some other error indication, and probably should, such as printing "NUMBER TOO LARGE".
In order to multiply by ten, I set aside another storage area equal in length to the value accumulator. At line 1380 the new digit is saved in the Y-register. The accumulated value at this point is in XH and XL. Lines 1390-1480 form the value*4 in SH and XL, leaving the original value in XH and SL. (Yes, they are criss-crossed.) Lines 1410-1420 show how you would extend this portion to longer values.
Lines 1490-1610 add value*4 to value to get value*5, and then double the result to get value*10. Again, lines 1530-1550 show how to extend the value. Lines 1630-1700 add in the new digit, and the comments show how to extend to longer values.
The top level routine in lines 1130-1270 is just a test routine. It calls the monitor line input routine. If you type an empty line, it will stop. Otherwise it calles the input conversion routine, prints the resulting value in hexadecimal, and converts it back to decimal with the output conversion routine.
1000 *SAVE S.BINDEC 1010 *-------------------------------- 1020 XL .EQ $00 1030 XH .EQ $01 1040 SL .EQ $10 1050 SH .EQ $11 1060 *-------------------------------- 1070 BELL .EQ $FBDD 1080 RDLINE .EQ $FD6A 1090 PRBYTE .EQ $FDDA 1100 COUT .EQ $FDED 1110 CROUT .EQ $FD8E 1120 *-------------------------------- 1130 T 1140 JSR RDLINE 1150 TXA 1160 BNE .1 1170 RTS 1180 .1 JSR CONVERT.DEC.TO.BIN 1190 LDA XH 1200 JSR PRBYTE 1210 LDA XL 1220 JSR PRBYTE 1230 LDA #"=" 1240 JSR COUT 1250 JSR CONVERT.BIN.TO.DEC 1260 JSR CROUT 1270 JMP T 1280 *-------------------------------- 1290 CONVERT.DEC.TO.BIN 1300 LDX #0 1310 STX XL least significant byte 1320 *-- STX XI ---ANY INTERMEDIATE BYTES--- 1330 STX XH most significant byte 1340 .1 LDA $200,X 1350 EOR #"0" 1360 CMP #10 1370 BCS .3 ...END OF NUMBER 1380 TAY SAVE CURRENT DIGIT 1390 LDA XL 1400 STA SL 1410 *-- LDA XI ---ANY INTERMEDIATE BYTES--- 1420 *-- STA SI ---FOLLOW THIS PATTERN------ 1430 LDA XH 1440 JSR SHIFT.X 1450 BCS .2 ...OVERFLOW 1460 JSR SHIFT.X 1470 BCS .2 ...OVERFLOW 1480 STA SH 1490 CLC 1500 LDA XL 1510 ADC SL 1520 STA XL 1530 *-- LDA XI ---ANY INTERMEDIATE BYTES--- 1540 *-- ADC SI ---FOLLOW THIS PATTERN------ 1550 *-- STA XI ---------------------------- 1560 LDA XH 1570 ADC SH 1580 BCS .2 ...OVERFLOW 1590 JSR SHIFT.X 1600 BCS .2 ...OVERFLOW 1610 STA XH 1620 INX SCAN TO NEXT DIGIT 1630 TYA GET DIGIT 1640 ADC XL LEAST SIGNIFICANT BYTE 1650 STA XL 1660 BCC .1 ...NO CARRY 1670 *-- INC XI ---ANY INTERMEDIATE BYTES--- 1680 *-- BNE .1 ---FOLLOW THIS PATTERN------ 1690 INC XH MOST SIGNIFICANT BYTE 1700 BNE .1 ...UNLESS OVERFLOW 1710 .2 JSR BELL SIGNAL OVERFLOW 1720 .3 RTS 1730 *-------------------------------- 1740 SHIFT.X 1750 ASL XL LEAST SIGNIFICANT BYTE 1760 *-- ROL XI ---ANY INTERMEDIATE BYTES--- 1770 ROL ...MOST SIGNIFICANT BYTE IN A 1780 RTS 1790 *-------------------------------- 1800 CONVERT.BIN.TO.DEC 1810 LDX #0 DIGIT COUNTER 1820 *---DIVIDE BY TEN---------------- 1830 .1 LDA #0 1840 LDY #16 2*(# Bytes being converted) 1850 .2 CMP #5 1860 BCC .3 1870 SBC #5 1880 .3 ROL XL 1890 *-- ROL XI ---ANY INTERMEDIATE BYTES--- 1900 ROL XH 1910 ROL 1920 DEY 1930 BNE .2 1940 PHA SAVE DIGIT ON STACK 1950 INX COUNT THE DIGIT 1960 *---NEXT DIGIT------------------- 1970 LDA XL 1980 *-- ORA XI ---ANY INTERMEDIATE BYTES--- 1990 ORA XH 2000 BNE .1 2010 *---PRINT DECIMAL---------------- 2020 .4 PLA 2030 ORA #"0" 2040 JSR COUT 2050 DEX 2060 BNE .4 2070 RTS 2080 *-------------------------------- |
Over the years I have fallen into certain habits when it comes to naming files. I find it convenient to use names starting with "S." for assembly language source files, "B." for binary object code files, and so on. Others like to use suffixes like ".SRC" and ".OBJ" for the same reasons. Some operating systems, like CP/M for example, use suffixes to indicate file type. Others, like ProDOS, let you build sub-directories to categorize your files.
Sometimes I would like to have the ability to do the same operation on a whole group of files. For example, I might want to DELETE all files starting with "B.". Or I might want to copy a whole group of files from one disk to another. If the files happen to have similar names, and if DOS allowed wildcards in filenames, it would be easier.
Some DOS 3.3 programs do have this feature: Apple's FID program, Sensible Software's Super Disk Copy, and others. They have a method for specifying a filename without spelling out the entire name.
The subroutine inside DOS 3.3 which compares a filename you have specified with the names in a catalog is found at $B1F5:
LDY #0 INX INX .1 INX LDA ($42),Y Filename you specified CMP $B4C6,X Filename in catalog sector BNE ... ...did not match INY CPY #30 BNE .1 ... matched ...
This is a very straightforward string comparison. It requires an exact match of all 30 characters of a filename. There is a similar routine at $A782 which compares a filename you specify with the filenames in the open file buffers.
I wrote a subroutine called MATCH which compares two 30-character strings, allowing wildcards. Unfortunately, it not a simple matter to plug such a subroutine into DOS 3.3, and I have not done that. It is more likely that this subroutine will find its way into some future utility programs.
I also wrote a testing program, so that I could see if my code worked. The program in lines 1110-1380 searches through a list of 30-character strings, printing those which match a key string. To simplify my test program (a good idea to keep testers simple, so they are not themselves more buggy than the testees!) I assembled in the key string and the list of strings to be searched. A slightly better test would allow me to type in the key string.
My MATCH program assumes that the address of the string to be compared with the key is stored at FN and FN+1. Characters in the filename are addressed by "(FN),Y", and in the key are addressed by "KEY,X". MATCH will return with carry set if the filename matches the key, and carry clear if not.
Both the filename and the key are stored "left-justified, blank-filled". That means there may be any number of non-significant blanks on the right end. Lines 1490-1530 scan the current filename from right-to-left, looking for the last non-blank in the name. Lines 1550-1590 do the same for the key. If there is any chance either filename or key could be completely blank, an extra line "BMI ERROR" should be inserted at 1505 and 1565.
I save the index to the right end of the key in KEY.START. Because the end of the filename and key strings is variable, I actually do the comparison from right to left. This makes the "end" actually the beginning.
Line 1610 could be "JMP .4" or "BNE .4", because the object is to get to line 1660. However, the "INX" allows me to fall through lines 1630-1640 and it takes only one byte rather than two or three.
The comparison begins at line 1660. Remember we are scanning backwards, from right to left. Lines 1660-1670 save the two string pointers. Line 1680 gets the next character from the key. If it is a wildcard, I branch back to line 1630. Note that all that happens is that the wildcard is skipped over!
If the key character is not a wildcard, it gets compared to the next character of the filename at line 1710. If it matches, lins 1730-1760 advance both pointers and the comparison continues. These lines also check to see if we have come to the left end of the key or of the filename.
If we are at the end of the filename, lines 1770-1820 check the rest of the key. If there are any characters left in the key which are not wildcards, then the current filename does not match. Otherwise, it does match. Lines 1830-1880 set the appropriate carry status and return.
If we are at the end of the key, lines 1900-1910 check whether we are also at the end of the filename. If so, the filename matched. If not, maybe it did not match. I say maybe, because if there was a wildcard, we might come out with a match if we widen the amount matched by that wildcard. Lines 1920-1990 will handle that possibility.
Two conditions bring us to line 1930. Either a character in the key did not match the current character in the filename, or there are unmatched filename characters left over after the end of the key. In either case, if there has been no wildcard in the key (so far), then the filename does not match the key. If there has been a wildcard, we can try again to match from the most recent wildcard on. We can tell whether or not there has been a wildcard so far by comparing KEY.PNTR with KEY.START. If they are the same, there has been no wildcard. Lines 1920-1990 handle all these details.
I made the wild card character itself a variable, so that you could change it by program control. Since "=" is a valid character in a filename, you might want to use something else.
With this kind of MATCH subroutine, a key of "=.OBJ" would match all names ending with ".OBJ"; "S.=" would match all names starting with "S."; "=A=B=" would match all names containing "A" followed by "B".
You can see the similarity between MATCH and a global search capability such as you might find in a word processor, or in the S-C Macro Assembler. The FIND and REPLACE commands in S-C Macro allow wildcards. However, MATCH differs in that it anchors the key to the beginning and end of the file name (unless you specify a wildcard in those positions).
If string comparisons of this type intrigue you, the book "Software Tools" develops an extremely powerful one in chapter 5. "Software Tools" is a classic book by Kernighan and Plauger, available at many bookstores. (A "classic" in computer books is one still in print after five years; this one qualifies, since it was originally published in 1976.) Their string match routine allows single- and multi-character wildcards, semi-wildcards that match subsets of characters, control of anchoring, and more. It would be a worthwhile exercise to try implementing their algorithm in 6502 language.
1000 *SAVE S.WILDCARD 1010 *-------------------------------- 1020 COUT .EQ $FDED 1030 CROUT .EQ $FD8E 1040 *-------------------------------- 1050 KEY.PNTR .EQ $00 1060 BUF.PNTR .EQ $01 1070 FN .EQ $02,03 1080 KEY.START .EQ $04 1090 CNTR .EQ $05 1100 *-------------------------------- 1110 T 1120 LDA #NAME.CNT 1130 STA CNTR 1140 LDA #FNLIST 1150 LDY /FNLIST 1160 .1 STA FN 1170 STY FN+1 1180 JSR MATCH 1190 BCC .2 ...DID NOT MATCH 1200 JSR DISPLAY 1210 .2 LDA FN 1220 CLC 1230 ADC #30 1240 LDY FN+1 1250 BCC .3 1260 INY 1270 .3 DEC CNTR 1280 BNE .1 1290 RTS 1300 *-------------------------------- 1310 DISPLAY 1320 LDY #0 1330 .1 LDA (FN),Y 1340 JSR COUT 1350 INY 1360 CPY #30 1370 BCC .1 1380 JMP CROUT 1390 *-------------------------------- 1400 * COMPARE KEY TO A FILE NAME 1410 * KEY MAY CONTAIN WILDCARDS 1420 * TRAILING BLANKS DON'T COUNT 1430 * FILE NAME ADDRESSED VIA "(FN),Y" 1440 * KEY ADDRESSED VIA "KEY,X" 1450 * KEY AND FILE NAME ARE UP TO 30 CHARS LONG 1460 * (STORED LEFT-JUSTIFIED, BLANK-FILLED) 1470 *-------------------------------- 1480 MATCH 1490 LDY #30 FIND LAST NON-BLANK CHAR 1500 .1 DEY IN FILE NAME 1510 LDA (FN),Y 1520 CMP #" " 1530 BEQ .1 1540 *-------------------------------- 1550 LDX #30 FIND LAST NON-BLANK CHAR 1560 .2 DEX IN KEY 1570 LDA KEY,X 1580 CMP #" " 1590 BEQ .2 1600 STX KEY.START 1610 INX 1620 *---WILD CARD-------------------- 1630 .3 DEX ADVANCE KEY POINTER 1640 BMI .8 ...END OF KEY IS WILD, SO MATCHES 1650 *-------------------------------- 1660 .4 STX KEY.PNTR 1670 .5 STY BUF.PNTR 1680 .6 LDA KEY,X 1690 CMP WILD.CARD 1700 BEQ .3 ...WILD CARD CHARACTER 1710 CMP (FN),Y 1720 BNE .11 ...NO MATCH 1730 DEX 1740 BMI .10 ...END OF KEY 1750 DEY 1760 BPL .6 ...STILL MORE TO COMPARE 1770 *---END OF FILE NAME, MORE KEY--- 1780 .7 LDA KEY,X 1790 CMP WILD.CARD 1800 BNE .9 ...REST OF KEY NOT WILD, NO MATCH 1810 DEX 1820 BPL .7 1830 *---VALID MATCH------------------ 1840 .8 SEC SIGNAL MATCH 1850 RTS 1860 *---NOT A MATCH------------------ 1870 .9 CLC 1880 RTS 1890 *---END OF KEY------------------- 1900 .10 DEY MATCH IF END OF NAME 1910 BMI .8 ...END OF NAME 1920 *---IF AFTER WILDCARD, SLIP------ 1930 .11 LDX KEY.PNTR START KEY OVER AGAIN 1940 CPX KEY.START 1950 BEQ .9 ...NOT AFTER A WILDCARD 1960 LDY BUF.PNTR SLIP TO LEFT IN BUFFER 1970 DEY 1980 BPL .5 TRY AGAIN 1990 BMI .7 REST OF KEY BETTER BE WILD 2000 *-------------------------------- 2010 WILD.CARD .AS -/=/ 2020 *-------------------------------- 2030 KEY .AS -/A= / 2040 *-------------------------------- 2050 FNLIST .AS -/A SIMPLE KEY / 2060 .AS -/NOT SUCH A SIMPLE KEY / 2070 .AS -/NOT A SIMPLE KEY AT ALL / 2080 .AS -/A SIMPLE KEY AFTER ALL / 2090 NAME.CNT .EQ *-FNLIST/30 2100 *-------------------------------- |
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.)