Apple Assembly Line
Volume 5 -- Issue 11 August 1985

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.

//c + Z-RAM = 576K Printer Buffer
Another Auxiliary Memory Program
David C. Johnson
Applied Engineering

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 #%         bop ACIAs
  0065        STA Printer.ACIA.Command
  0066   inc  LDA #%         RTS' lo
  0067        STA Modem.ACIA.Command
  0068        LDA #%          9600 baud
  0069        STA Printer.ACIA.Control
  0070   inc  LDA #%          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 #%  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 #%
  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 #%
  0125        STA Modem.ACIA.Command
  0126        AND #%
  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 #%   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

How Many Bytes for each Opcode? Bob Sander-Cederlof

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.

  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
  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 *--------------------------------
  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 *--------------------------------

Generic Conversion Routines Bob Sander-Cederlof

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.

  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
  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 *--------------------------------
  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
  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
  1760 *--    ROL XI    ---ANY INTERMEDIATE BYTES---
  1770        ROL          ...MOST SIGNIFICANT BYTE IN A
  1780        RTS
  1790 *--------------------------------
  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 *--------------------------------

A Wildcard Filename Search Bob Sander-Cederlof

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
 .1    INX
       LDA ($42),Y  Filename you specified
       CMP $B4C6,X  Filename in catalog sector
       BNE ...      ...did not match
       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.

  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 *--------------------------------
  1430 *      FILE NAME ADDRESSED VIA "(FN),Y"
  1440 *      KEY ADDRESSED VIA "KEY,X"
  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------
  1940        CPX KEY.START
  1950        BEQ .9       ...NOT AFTER A WILDCARD
  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.)