Apple Assembly Line
Volume 3 -- Issue 9 June 1983

In This Issue...

A New Look?

The cover and a few of the inside pages of this issue look a little different, don't they? [in the printed edition, the mentioned pages were printed with a dot matrix printer instead of the normal NEC Spinwriter.] Well, our offices were burglarized last night, and the Spinwriter we use for newsletter printing was damaged. The thieves also made off with three complete computer systems and a load of software. They even got the Track Ball which I used for the article in this issue! See page 9 for more details.

65C02 Note

Don Lancaster just called to report that he has gotten his hands on samples of the GTE G65SC02 processor. It does drop right into his Apple //e, and runs just fine! Note that this is the GTE version, which does not have the instructions to set, reset, and test single bits. Those are in the Rockwell chip, which still hasn't shown up. We'll keep passing on whatever we hear.


Spiral Screen Clear Roger Keating
modified and documented by Bob Sander-Cederlof

[ Roger is the author of numerous excellent war games for the Apple, some published by Strategic Simulations. Titles include Southern Command, Operation Apocalypse, Germany 1985, and Rapid Deployment Force. He is also director for Australasia of the International Apple Core. A previous version of this program was published in the newsletter of one of the Australian Apple user groups. ]

The following program demonstrates an innovative method for clearing the Apple text or lo-res graphics screen. Rather than just switching to instant blackness, it makes the process visually interesting.

The entire screen is viewed as one long coiled line. For 55 seconds this line unwinds on the screen, with blanks being fed into the center and visible characters shifting out at the bottom left corner. Here is a simplified diagram, on a 10-column 6-line screen:

      0   1   2   3   4   5   6   7   8   9
     -----------------------------------------
     | V | < | < | < | < | < | < | < | < | < | 0
     -- ----------------------------------- --
     | V | V | < | < | < | < | < | < | < | ^ | 1
     -- --- --------------------------- --- --
     | V | V | V | < | < | < | < | < | ^ | ^ | 2
     -- --- --- ------------------- --- --- --
     | V | V | V | > | > | > | > | ^ | ^ | ^ | 3
     -- --- --- ----------------------- --- --
     | V | V | > | > | > | > | > | > | ^ | ^ | 4
     -- --- ------------------------------- --
     | X | > | > | > | > | > | > | > | > | ^ | 5
     -- --------------------------------------
       0   1   2   3   4   5   6   7   8   9

There are a total of 60 characters. By calling 60 times a routine which rotates the spiral, and inserting a blank at line 3 column 3 after each rotation, I can blank out the entire 60 characters. The <, >, V, and ^ symbols show the direction each character cell will move during the rotation. The "X" in line 5 column 0 is shifted out to never-never land. (In the old days, we said it went into the "bit bucket".)

Well, that is the general idea. But I did it for the whole 24x40 screen, a little too much to show in this small space.

Here is a description of the highest level of the spiral clear program, in a language a little like BASIC:

     FOR VCNT = 1 TO 24
         FOR HCNT = 1 TO 40
             ROTATE SCREEN
             STORE BLANK AT TRAIL'S END
         NEXT HCNT
     NEXT VCNT

The use of two loops is not necessary, because the loop variables are not used at all inside the loops. We could just as well write it like this:

     FOR CNT = 1 TO 960
         ROTATE SCREEN
         STORE BLANK
     NEXT CNT

However, thinking ahead to the assembly language implementation, we know that counters are easier to manage if they have values less than 256. Therefore I like the first version better. Furthermore, counters in assembly language are frequently easier to work with if they count backwards. I ended up with the code in lines 1130-1270 of the program.

The value "$628+12" in line 1210 happens to be the memory address of the inner end of the spiral. I figured it out on paper, tried it, corrected my figuring, and tried it again.

You can add some cute wrinkles here and there. Like putting a "pause if key pressed" routine right after calls to ROTATE.SCREEN. Like storing something besides a blank. Like feeding the character from the bottom left corner into line 12 column 12, so that the original text is restored. Like varying the character stored during the 960 rotations. Like using a color value in lo-res graphics mode. I tried a lot of these, and it is amazing how much you can learn this way.

The rotation process involves four separate steps: sliding a column on the left side down, slipping an upper row to the left, shoving a column on the right side up, and scooting a lower row to the right. Appealing once again to a higher level language, it might look like this:

     XL=0 : XR=39
     YT=0 : YB=23
     WHILE YT<YB
         SLIDE COLUMN XL DOWN
         SLIP ROW YT LEFT
         SHOVE COLUMN XR UP
         SCOOT ROW YB RIGHT
         ADJUST XL, XR, YT, YB
     LOOP

It turns out it is not quite that simple, but almost. The assembly language is in lines 1300-1530.

The initial call to DOWN at line 1390 moves the leftmost column of characters down, ignoring the bottom left cell. If that cell were not ignored, it would slide to some undetermined place in memory, not necessarily even on the screen. Where it goes depends on what method is used to compute screen addresses from line number. Anyway, it would try to go to a 25th line, which does not exist.

It turns out that if BASCALC in the monitor ROM is used, the character is moved into $478, which is not part of the screen. It is usually a safe location, but some peripheral boards or DOS might use it. If you don't care about saving $478, or if you use a different method which leads to a completely safe address for the 25th line, you could omit lines 1390-1400.

I still need to show you the routines DOWN, LEFT, UP, and RIGHT. Here they are in pseudo-code:

     DOWN:  FOR Y = YB-1 TO YT STEP -1
                S(XL,Y+1) = S(XL,Y)
            NEXT Y

     LEFT:  FOR X = XL+1 TO XR
                S(X-1,YT) = S(X,YT)
            NEXT X

     UP:    FOR Y = YT+1 TO YB
                S(XR,Y-1) = S(XR,Y)
            NEXT Y

     RIGHT: FOR X = XR-1 TO XL STEP -1
                S(X+1,YB) = S(X,YB)
            NEXT X

The assembly language is in lines 1550-2240.

Each of these routines needs a function to compute the base address of a line on the screen. I called the subroutine MAKE.BASE, and implemented it in two different ways. The easiest way is to call on the subroutine BASCALC at $FBC1 in the monitor ROM.

BASCALC accepts the line number (0-23) in the A-register, computes the base address by a series of shift and masking operations, and puts the beginning address of the line into $28 and $29. No other registers are used, so it is a nice subroutine to have available. (Its only problem is that it takes a full 40 microseconds to do the job.) Since I call MAKE.BASE with the line number in the X-register, it can be written like lines 2310-2320.

The faster way to get a base address loaded is to use a table of addresses. I put the high byte of each base address in a 24-byte table, and the low byte in another. The line number in the X-register indexes this table. MAKE.BASE is in lnes 2340-2380, and the table in lines 2400-2450.

The table lookup is about twice as fast as BASCALC's 40 microseconds. Since MAKE.BASE is called once each for LEFT and RIGHT, and twice for each character moved for UP and DOWN, and the whole set is called 960 times, the overall effect is large. Using BASCALC the total time to clear the screen is 55 seconds; with the table lookup it is 40 seconds. On the other hand, the table and the code to read it take up 59 bytes, while the call to BASCALC takes only 4 bytes.

  1000 *--------------------------------
  1010 *      SPIRAL CLEAR BY ROGER KEATING
  1020 *--------------------------------
  1030 HCNT   .EQ $00
  1040 VCNT   .EQ $01
  1050 XL     .EQ $02
  1060 XR     .EQ $03
  1070 YT     .EQ $04
  1080 YB     .EQ $05
  1090 BASE   .EQ $28
  1100 *--------------------------------
  1110        .OR $800
  1120 *--------------------------------
  1130 SPIRAL.CLEAR
  1140        LDA #24      FOR 24 LINES
  1150        STA VCNT
  1160 .1     LDA #40      FOR 40 COLUMNS
  1170        STA HCNT
  1180 
  1190 .2     JSR ROTATE.SCREEN
  1200        LDA #$A0     STORE BLANK IN
  1210        STA $628+12  MIDDLE OF SCREEN
  1220 
  1230        DEC HCNT     NEXT HCNT
  1240        BNE .2
  1250        DEC VCNT     NEXT VCNT
  1260        BNE .1
  1270        RTS          FINISHED!
  1280 
  1290 *--------------------------------
  1300 ROTATE.SCREEN
  1310        LDA #0
  1320        STA XL       LEFT END
  1330        STA YT       TOP
  1340        LDA #23
  1350        STA YB       BOTTOM
  1360        LDA #39
  1370        STA XR       RIGHT END
  1380 
  1390        JSR DOWN     START LEFT SIDE
  1400        BEQ .2       ...ALWAYS
  1410 
  1420 .1     JSR DOWN     SLIDE LEFT SIDE DOWN
  1430        DEC YB       MOVE BOTTOM UP
  1440 .2     JSR LEFT     SLIP TOP LINE LEFT
  1450        INC XL       MOVE LEFT EDGE IN
  1460        JSR UP       SHOVE RIGHT SIDE UP
  1470        INC YT       MOVE TOP DOWN
  1480        JSR RIGHT    SCOOT BOTTOM LINE RIGHT
  1490        DEC XR       MOVE RIGHT EDGE IN
  1500        LDA YT
  1510        CMP YB
  1520        BCC .1       IF YT<YB, BRANCH
  1530        RTS          FINISHED
  1540 
  1550 *--------------------------------
  1560 *      MOVE LEFT SIDE DOWN
  1570 *      FOR Y=YB-1 TO YT STEP -1
  1580 *      S(XL,Y+1)=S(XL,Y) : NEXT
  1590 *--------------------------------
  1600 DOWN   LDY XL       COLUMN BEING MOVED DOWN
  1610        LDX YB       BOTTOM CELL IN COLUMN
  1620 .1     DEX
  1630        JSR MAKE.BASE
  1640        LDA (BASE),Y
  1650        PHA          SAVE CHAR IN CELL
  1660        INX
  1670        JSR MAKE.BASE
  1680        PLA
  1690        STA (BASE),Y
  1700        DEX
  1710        CPX YT       AT TOP OF COLUMN YET?
  1720        BNE .1       NO
  1730        RTS          YES
  1740 *--------------------------------
  1750 *      MOVE RIGHT SIDE UP
  1760 *      FOR Y=YT+1 TO YB
  1770 *      S(XR,Y-1)=S(XR,Y) : NEXT
  1780 *--------------------------------
  1790 UP     LDY XR       COLUMN BEING MOVED UP
  1800        LDX YT       TOP CELL IN COLUMN
  1810 .1     INX
  1820        JSR MAKE.BASE
  1830        LDA (BASE),Y
  1840        PHA          SAVE CHAR IN CELL
  1850        DEX          BACK UP
  1860        JSR MAKE.BASE
  1870        PLA
  1880        STA (BASE),Y
  1890        INX
  1900        CPX YB       AT BOTTOM OF COLUMN YET?
  1910        BNE .1       NO
  1920        RTS          YES
  1930 *--------------------------------
  1940 *      MOVE TOP LINE LEFT
  1950 *      FOR X=XL+1 TO XR
  1960 *      S(X-1,YT)=S(X,YT) : NEXT
  1970 *--------------------------------
  1980 LEFT   LDX YT       TOP LINE
  1990        JSR MAKE.BASE
  2000        LDY XL
  2010 .1     INY
  2020        LDA (BASE),Y
  2030        DEY
  2040        STA (BASE),Y
  2050        INY
  2060        CPY XR       LAST COLUMN YET?
  2070        BNE .1       NO
  2080        RTS
  2090 *--------------------------------
  2100 *      MOVE BOTTOM LINE RIGHT
  2110 *      FOR X=XR-1 TO XL STEP -1
  2120 *      S(X+1,YB)=S(X,YB) : NEXT
  2130 *--------------------------------
  2140 RIGHT  LDX YB       BOTTOM LINE
  2150        JSR MAKE.BASE
  2160        LDY XR
  2170 .1     DEY
  2180        LDA (BASE),Y
  2190        INY
  2200        STA (BASE),Y
  2210        DEY
  2220        CPY XL       FIRST COLUMN YET?
  2230        BNE .1       NO
  2240        RTS
  2250 
  2260 *--------------------------------
  2270 *      POINT BASE TO SCREEN LINE
  2280 *      (X) = LINE #
  2290 *--------------------------------
  2300 MAKE.BASE
  2310 *      TXA          ALTERNATE APPROACH
  2320 *      JMP $FBC1    USING BASCALC IN ROM
  2330 
  2340        LDA HI,X     FAST APROACH USING
  2350        STA BASE+1   TABLE LOOKUP
  2360        LDA LO,X
  2370        STA BASE
  2380        RTS
  2390 *--------------------------------
  2400 HI     .HS 0404050506060707
  2410        .HS 0404050506060707
  2420        .HS 0404050506060707
  2430 LO     .HS 0080008000800080
  2440        .HS 28A828A828A828A8
  2450        .HS 50D050D050D050D0

Breaking and Entering Bill Morgan

We had a burglary here last night (today is May 26). Thieves got into the building somehow, and broke through the doors of several businesses, including your favorite software house and newsletter publisher.

They got three complete computer systems, including a two day old Apple //e that belonged to Judy Preston (she handles your orders), and an Apple /// system on loan from Apple Computer!

Just in case somebody tries to sell any of you some used Apple equipment, here's the list of what we lost, including serial numbers where known:

     Apple ][ Plus Computer         1498251
     STB 16K RAM Card
     Epson Printer Card
     Apple High-Speed Serial Card
     Wico Track Ball w/Interface
     Apple Disk Controller
     2 Apple Disk Drives            414611 & 
     Epson MX-80 Printer
     NEC Green Monitor              1315572
     R-H Super Fan II

     Apple //e Computer             152413
     Extended 80-Column Card
     Apple Disk Controller
     Apple Disk Drive
     Leedex B/W Monitor

     Apple /// Computer
     Apple /// Monitor
     Apple Silentype Printer

     TI Programmer Calculator (LCD Display)

If you do see any of the above items, call your local police and/or S-C Software. Try to find out the name and address of the person selling the goods.

The thieves also took who-knows-how-many disks (at least three Flip-Files, two library cases, and many loose ones), containing the text and code from about the last three newsletters, all the disks from the Apple /// project (including the source code for the new assembler), several projects-in-progress, and whatever was handy. It will probably be months until we know what all is gone.

As I mentioned on the front page, the Spinwriter was damaged. They apparently got about half way down the hall carrying the printer, and then dropped it onto the concrete floor! We don't yet know what will be necessary to repair it.

Anyway, we're all alive and well, and we're going to carry on. See you next month!


Binary to Decimal Conversion Bob Sander-Cederlof

Dear Assembly Line,

I hope you can answer a question I have concerning assembly language. I am just a beginner.

How can I convert hex numbers to decimal from within a running machine language program? That is, take bytes from locations A and A+1, convert the bytes to their decimal equivalent, and store the resulting ASCII bytes in other memory locations.

As a new member of A.P.P.L.E., I called their technical assistance line. They advised me to call Don Williams, who in turn directed me to Val Golding, editor of Call APPLE magazine. Val referred me to old issues of Call APPLE and Apple Orchard which I don't have available.

This appears to be a sticky question that nobody wants to deal with. There are a million ways to do this from BASIC, but I have yet to see one that will do it strictly within machine language.

(signed) I. M. Perplexed Anaheim, California

------

Dear Mr. Perplexed,

Your odyssey in search of a conversion program sounds as frustrating as it probably was! Let me assure you no one is trying to avoid dealing with these kinds of programs.

It is just that there are hundreds of variations. And many of them have been printed during the past five or six years in Micro, Nibble, Call APPLE, Apple Orchard, Kilobaud, Byte, and hundreds of Apple user group newsletters. All or most back issues of the major magazines are available through anthology volumes such as The Nibble Express, The Best of Micro, and Peeking at Call APPLE.

Also, almost all of the books written to teach 6502 assembly language programming include as examples subroutines to convert from binary to decimal and decimal to binary. We especially recommend Roger Wagner's "Assembly Lines--The Book" and Lance Leventhal's "6502 Subroutines". In fact, we even sell both of them at a slight discount.

I don't want to send you away looking for yet another source of information, so here is a routine I have used.

The subroutine assumes that you have placed the binary value into a variable called BINARY.VALUE, and places the converted result into DECIMAL.VALUE as a series of five ASCII characters. After conversion, the value in BINARY.VALUE will be zeroed.

This particular version of conversion produces leading zeroes for values below 10000. Variations I have used substitute leading blanks, or left justify with trailing blanks, or return a left justified value with a digit count.

The general method involves subtracting 10000 as many times as possible, and counting the times to get the first digit; subtracting 1000 from the remainder as many times as possible and counting the times to get the next digit; and so on. To simplify indexing, the constants 1, 10, 100, 1000, and 10000 are stored with the low-order bytes in one table, and the high-order bytes in another.

Line 1060 sets the Y-register to zero, for use as a pointer to the byte positions in DECIMAL.VALUE. If you were storing into a line buffer, you might enter th routine with the Y-register pointing to the starting place in the line and skip this initialization step.

Line 1070 sets X=4, which is one less than the number of digits you want to convert. X=4 gives five digits; if you are sure the value of the decimal number will be smaller, you can set X for fewer digits and enter past this point.

The loop from line 1090 through 1290 develops each digit in turn. Line 1090 starts a new digit by loading an ASCII zero. This value is pushed onto the stack during the subtraction that follows. Each time a subtraction is successful, it will be pulled off, incremented, and then pushed back on.

Lines 1120-1170 subtract the current divisor without storing the difference back into BINARY.VALUE. If the difference is positive, it is stored and the digit incremented. If the result is negative, then that was one subtraction too many, so the difference is discarded and the digit is retrieved from the stack at line 1250.

Line 1260 stores the converted digit into DECIMAL.VALUE, and line 1270 advances the digit pointer. Line 1280 advances the divisor pointer. If there are more divisors, line 1290 branches back to convert the next lower digit.

I have used a form of this subroutine inside the S-C Macro Assembler. That version includes options to also print the decimal value as it is being converted. Different entry conditions control whether the number will be printed, stored in a buffer, or both. I also allow control over the leading zeroes/blanks option and the number of digits.

This is only one method out of many, but it is fairly compact and easy to understand, without being too slow.

  1000 *SAVE S.CONVERT BIN TO DEC
  1010 *---------------------------------
  1020 *      CONVERT VALUE TO DECIMAL CHARACTERS
  1030 *      BY BOB SANDER-CEDERLOF
  1040 *---------------------------------
  1050 CONVERT
  1060        LDY #0       POINT AT FIRST CHARACTER
  1070        LDX #4       5 DIGITS
  1080 *--------------------------------
  1090 .1     LDA #$B0     SET DIGIT TO ASCII ZERO
  1100 .2     PHA          PUSH DIGIT ON STACK
  1110        SEC          SUBTRACT CURRENT DIVISOR
  1120        LDA BINARY.VALUE
  1130        SBC PLNTBL,X
  1140        PHA          SAVE BYTE ON STACK
  1150        LDA BINARY.VALUE+1
  1160        SBC PLNTBH,X
  1170        BCC .3       LESS THAN DIVISOR
  1180        STA BINARY.VALUE+1
  1190        PLA          GET LOW BYTE OFF STACK
  1200        STA BINARY.VALUE
  1210        PLA          GET DIGIT FROM STACK
  1220        ADC #0       INCREMENT DIGIT (CARRY WAS SET)
  1230        BNE .2       ...ALWAYS
  1240 .3     PLA          DISCARD BYTE FROM STACK
  1250        PLA          GET DIGIT FROM STACK
  1260        STA DECIMAL.VALUE,Y
  1270        INY          POINT TO NEXT DIGIT
  1280 .4     DEX          POINT TO NEXT DIVISOR
  1290        BPL .1
  1300        RTS          RETURN
  1310 *---------------------------------
  1320 PLNTBL .DA #1
  1330        .DA #10
  1340        .DA #100
  1350        .DA #1000
  1360        .DA #10000
  1370 PLNTBH .DA /1
  1380        .DA /10
  1390        .DA /100
  1400        .DA /1000
  1410        .DA /10000
  1420 *--------------------------------
  1430 BINARY.VALUE  .BS 2
  1440 DECIMAL.VALUE .BS 5
  1450 *--------------------------------

The Tiniest Motherboard Bob Sander-Cederlof

When I was at the Boston Applefest last week, Chad Pennebaker of Douglas Electronics showed me what he called the world's smallest Apple Mother Board. It is a bus board really, with ten Apple-Compatible 50-pin sockets labeled A, B, C, and 1 thru 7. The seven numbered slots accept almost any board made for Apples. Slot A is designed to hold a 6502 card, slot B a RAM card, and slot C a ROM card.

Some fantastic prices too: mother board, $95; CPU card, $90; 64K RAM card which maps with 48K from 0 to $BFFF, and 12K from $D000 to $FFFF, and another 4K from $D000 to $DFFF (sound familiar?), $190; 12K EPROM card (without EPROMS), $70.

Chad showed me a beautiful little cabinet it all fits in, Apple-beige metal with walnut sides. I didn't catch the price. There is also a power supply available, which looks just like the Apple unit. A card which provides keyboard and screen functions is on the way.

Here's how to reach them: Douglas Electronics, Inc., 718 Marina Blvd., San Leandro, CA 94577. Or call at (415) 483-8770.


Replacing INIT Can Be Dangerous Bill Morgan

I have recently spent several days beating my head against an impossible bug, and I'll bet that some of you have run into, or will hit, the same thing. Here's my tale....

One of the most common enhancements to Apple's DOS is the addition of new commands. You do this by replacing one of the existing commands, usually INIT, VERIFY, or MAXFILES, with new code and a new name. I got into trouble replacing INIT, and had a great time figuring out why!

The SHOW Command ...

I was working with the SHOW command, as published in the July, 1982 Apple Assembly Line. Typing SHOW <filename> displays any sequential text file on the screen; it's a really handy command to have.

SHOW is installed in place of INIT. Making the change involves placing the new code over the File Manager's INIT handler at $AE8E and at $A54F, changing the command name at $A884, and altering the table of permissible and required keywords at $A909. These are the usual steps to replace a command. See the July '82 issue for more details about SHOW.

... and File Manager Calls

The S-C Word Processor uses one of the popular high-speed methods to handle text files. It calls the File Manager to OPEN the file, then uses its own code to LOAD or SAVE text, by directly calling RWTS. So far, so good, this is basically normal. However, after I installed the SHOW command in DOS, I couldn't SAVE new files from the Word Processor! SAVEing to an existing file worked just fine, but trying to create a new file returned a FILE NOT FOUND error. What???

A little bit of digging convinced me that this was impossible. What does changing INIT have to do with OPENing a text file? There is that table which tells what a command can or cannot do, but SHOW is nowhere near OPEN, and besides, the table is used by the command handler, not by the File Manager. You tell the File Manager that it can create a file, if necessary, by setting the X register to zero before doing the JSR $3D6. The Word Processor does that just right.

A lot more digging brought the solution to light. When the File Manager (FM) OPENs a file, it does refer to that table at $A909 to see if it can create a new file. It loads the X register with a command index kept at $AA5F, then checks the corresponding table entry. But what does that have to do with INIT? Read on...

When you enter FM through $3D6 it jumps to a special entry at $AAFD. The first thing it does there is check the X register to see if it will be allowed to create a new file. If X is nonzero (no new file), FM stores a 2 in the command index ($AA5F). That is the index for the LOAD command, which cannot create a file. If X is zero (new file allowed), FM stores that zero in the command index. And zero is the index to the INIT command, which does create a new file (usually HELLO).

So there it is. If we replace INIT with a command that is not allowed to create a new file, we will mess up programs that call the File Manager directly to OPEN new files. Ouch!!

Fixing the Problem

There are several possible ways to avoid such trouble:

  1. Don't mess with INIT. If you leave it alone, it won't bite you. But, INIT is so useless in a running program and offers so much space for new code. It's sure hard to resist.
  2. Only replace INIT with other commands that are allowed to create new files. That's OK, but limiting.
  3. Add these additional patches to whatever new command you're playing with:
    1. Put JMP FM.ENTRY.PATCH at $AAFD.
    2. And put FM.ENTRY.PATCH wherever is convenient (in SHOW I put it at $AEA5, just after the PAUSE.CHECK code):
          FM.ENTRY.PATCH
                 CPX #0          Create new file?
                 BEQ .1          0 means yes
                 LDX #2          No, use LOAD index
                 BNE .2          ... Always
          .1     LDX #4          Yes, use SAVE index
          .2     JMP $AB03       Return to File Manager

Conclusion

This kind of problem is a great example of why you need to be very careful about patching an operating system: it's hard to tell what kind of "impossible" interactions will turn up. On the other hand, how else can we learn about what's really going on inside our Apples? And what else can replace the "AHA!!! Ahhh..." sensation you get when you unravel a really cute bug? Keep on patchin'.


Reformatting a Lot of Text Bob Sander-Cederlof

At the Boston AppleFest I picked up a copy of David Durkee's SoftGraph program. You probably read the series of four articles in Softalk (Jan thru Apr, 1983) in which he developed this fine little system for editing data and creating pie, bar, and line charts. If you didn't, let me recommend them.

(If you don't get Softalk, why not? It's free! The best magazine there is for Apple owners! Send your serial number to Al Tommervik at Softalk, Box 60, North Hollywood, CA 91603 today!)

Anyway, back to SoftGraph. On the disk is a 107 sector binary file loaded with ASCII characters. A small program on the disk will display or print the text from this file. Never satisfied with things as they come, I wanted to load the file into my word processor. The problem itself may be irrelevant to you, but the steps to solving it can be quite instructive. Follow along now....

My word processor will read binary or text files, but it expects the data to be ASCII with the high bits all set to 1. Naturally, Durkee's file had all high bits set to 0. "That's OK, I'll just run the handy little code from AAL Dec 82, 'Add Bit-Control to Apple Monitor' "

So I did, but setting all the high bits wasn't quite enough. A second feature of the file came to light: no carriage returns anywhere. Each line was padded with trailing blanks to fill exactly 40 bytes. Even the blank lines contained 40 blanks. I fidgeted in my chair, and pulled a little hair.

After several false starts I finally resorted to a trick I learned back in the dark ages before structured programming, with its scientific rules and esoteric vocabulary, was invented. I constructed a flow chart! I do this every once and a while, as a thinking tool. But you probably won't find any in my documentation, because they are just a tool. I usually modify my thinking as I code, which obseletes the chart. Most charts are done on odd bits of scrap paper, and don't last overnight.

Here was my plan. First, BLOAD the file somewhere in memory and find its end by looking at $AA60 and $AA61. This pair of bytes contains the length of the last file loaded. Second, run a conversion program to change the data IN PLACE. Third, BSAVE the modified data on a new file. I decided to save time by manually typing the BLOAD and BSAVE commands, rather than writing code to do these steps inside my conversion program.

It so happened that the file would fit between $2000 and $8977. With the S-C Assembler in the language card at $D000, this space was available. The source code for my conversion program fit above $9000 running up to $95FF. The object code started at $800, and didn't make it out of that page.

Without drawing the flow chart for you, here is the general idea of the conversion process:

  1. Set up two pointers, one for retrieving characters out of the text and the other for storing converted characters into the text.
  2. Get 40 characters from the text into a little buffer. If at the end (marked by a 00 byte), quit.
  3. Scan backwards in the little buffer to the first non-blank character.
  4. If the whole line is blank, store a carriage return into the text.
  5. Otherwise, copy the little buffer back into the text, with the high bit set on each byte. Then add one blank, because we need to maintain a blank between words. (They may not print on separate lines after re-formatting with my word processor.)
  6. Back to 2.

(I'm sorry, but except for the lines and boxes, that does look a lot like a flow chart.)

One little wrinkle I thought about but didn't implement until later had to do with double spacing between paragraphs. I handled it by checking the length of the previous line after stuffing the carriage return for a blank line. If the previous line was non-blank, I sent an extra carriage return back into the text.

My routine counts on the assumed condition that the resulting text will be shorter than the original text. I knew the file began with several lines of 40 blanks, each being replaced with a single carriage return, so I felt confident that all would work. If I was wrong, I would be storing modified data on top of yet-unprocessed data, with wild results. Don't worry, it worked out OK.

Lines 1050-1090 define some variables. The data will begin at $2000, because I put it there with a "BLOAD DOCFILE,A$2000" command. GET.PNTR and PUT.PNTR will start out pointing at $2000. Each time I pull 40 characters out of the data I will add 40 to GET.PNTR. Each time I put one character back into the data I will add one to PUT.PNTR. LAST.LINE.SZ keeps track of previous line length so I can get double spacing between paragraphs.

BUFFER is for the forty characters pulled out of the data each cycle through the program. I frequently use $200 for buffers like this, because it is a nice handy area. And most Apple software uses $200 for a buffer. But...since the monitor does use $200, it can be difficult to see what is put there by my program. Hence, this time I put the buffer at $280 instead.

Lines 1110-1390 implement the logic flow described above. Previous line length starts out zero when there were no previous lines (1120-1130). The two pointers get initialized at 1140-1190.

Calling GET.40.CHARS pulls the next 40 bytes out of the data into my buffer at $280. If a 00 byte is hit, the subroutine returns with carry set; if not, carry is clear. Line 1210 acts on the carry info to end the program if we are done.

TRUNCATE.BLANKS starts at the end of the buffer looking for non-blanks. If the whole buffer is blank, the subroutine sets carry. If not, it clears carry. Line 1230 acts on carry. Non-blank lines are copied back into the data by PUT.CHARS, and then PUT.CHAR is used to add one trailing blank. Blank lines cause a return ($8D) to be put into the data, and if the previous line was non-blank a second return is added.

PUT.CHAR (lines 1410-1470) stores the byte in the A-register into the data where PUT.PNTR points. Then PUT.PNTR is incremented. PUT.CHARS (lines 1810-1880) calls PUT.CHAR once for each character in the buffer, omitting all the trailing blanks.

With trepidation I typed $800G to execute it, after being sure I had saved the source code and then opened both disk drive doors. To my surprise, the program ran without failure the very first time! Not to say it was perfect.

After execution I looked at PUT.PNTR to see where the new data ended. It was $70C7, if I remember correctly. Then I BSAVE'd with "BSAVE DOCFILE 2,A$2000,L$50C7", and loaded my word processor.

In the word processor I loaded the new DOCFILE 2, and it was all there. Somehow two small sections were missing all the carriage returns, but all the rest was perfect. I still don't know what caused those two sections (total of about ten lines) to fail, but it isn't all that important. I used the word processor to fix them, and the job was done.

  1000 *SAVE S.CONVERT DURKEE
  1010 *--------------------------------
  1020 *      CONVERT DURKEE'S DOCFILE
  1030 *      TO S-C WORD ASCII FORMAT
  1040 *--------------------------------
  1050 DATA         .EQ $2000
  1060 GET.PNTR     .EQ $00,01
  1070 PUT.PNTR     .EQ $02,03
  1080 LAST.LINE.SZ .EQ $04
  1090 BUFFER       .EQ $280
  1100 *--------------------------------
  1110 CONVERT
  1120        LDA #0
  1130        STA LAST.LINE.SZ
  1140        LDA #DATA
  1150        STA GET.PNTR
  1160        STA PUT.PNTR
  1170        LDA /DATA
  1180        STA GET.PNTR+1
  1190        STA PUT.PNTR+1
  1200 .1     JSR GET.40.CHARS
  1210        BCS .3
  1220        JSR TRUNCATE.BLANKS
  1230        BCS .2       EMPTY LINE
  1240        JSR PUT.CHARS
  1250        LDA #$A0     BLANK
  1260        JSR PUT.CHAR
  1270        BNE .1       ...ALWAYS
  1280 .2     LDA #$8D     <RETURN>
  1290        JSR PUT.CHAR
  1300        LDA LAST.LINE.SZ
  1310        BEQ .1
  1320        LDA #0
  1330        STA LAST.LINE.SZ
  1340        LDA #$8D
  1350        JSR PUT.CHAR
  1360        BNE .1       ...ALWAYS
  1370 .3     LDA #0       <EOL>
  1380        JSR PUT.CHAR 
  1390        RTS
  1400 *--------------------------------
  1410 PUT.CHAR
  1420        LDY #0
  1430        STA (PUT.PNTR),Y
  1440        INC PUT.PNTR
  1450        BNE .1
  1460        INC PUT.PNTR+1
  1470 .1     RTS
  1480 *--------------------------------
  1490 GET.40.CHARS
  1500        LDY #0
  1510 .1     LDA (GET.PNTR),Y
  1520        CMP #0
  1530        BEQ .3       END OF DATA
  1540        ORA #$80
  1550        STA BUFFER,Y
  1560        INY
  1570        CPY #40
  1580        BCC .1
  1590        LDA #39      CARRY SET
  1600        ADC GET.PNTR
  1610        STA GET.PNTR
  1620        BCC .2
  1630        INC GET.PNTR+1
  1640 .2     CLC
  1650        RTS
  1660 .3     SEC          END OF DATA
  1670        RTS
  1680 *--------------------------------
  1690 TRUNCATE.BLANKS
  1700 .1     LDA BUFFER-1,Y
  1710        CMP #$A0     BLANK?
  1720        BNE .2       NO
  1730        DEY          YES
  1740        BNE .1
  1750        SEC          EMPTY LINE
  1760        RTS
  1770 .2     STY LAST.LINE.SZ
  1780        CLC
  1790        RTS
  1800 *--------------------------------
  1810 PUT.CHARS
  1820        LDX #0
  1830 .1     LDA BUFFER,X
  1840        JSR PUT.CHAR
  1850        INX
  1860        CPX LAST.LINE.SZ
  1870        BCC .1
  1880        RTS

Track Balls Bill Morgan

If you have ever played Centipede or Missile Command in the arcade, then you know that a track ball is about the best control device made. A joystick usually can move across the whole screen a little faster, but a track ball gives much finer, smoother control. A "mouse" is said to be even better, but the only mouse I have seen advertised for the Apple ][ sells for about $300-400. Besides, I never have 2-3 square feet of free space on my desk, for the mouse to run around on.

Several track balls for the Apple have appeared recently, all in the $60-80 price range. I have tried out two of them so far, from TG Products and from Wico Corp. Here's what I think:

The TG track ball is an Apple-colored box about 5 x 6 x 2 1/2 inches, with a red ball that is a little over two inches in diameter. There are two pushbuttons on the left edge of the box. It plugs into the game port, just like a joystick. It contains two potentiometers and can be used with existing paddle-reading software, also just like a joystick. This ball feels stiff and jerky, and requires a constant downward pressure on the ball to keep the pots properly tracking. The range of values is 0-255, with no wraparound.

$64.95 TG Products, 1104 Summit Ave., #110, Plano, TX, 75074

The Wico track ball is a cream-colored ball in a black and red box, just about the same size as the TG. There are two buttons in the upper left corner of the top side, convenient to the left thumb. The larger button is about .8" in diameter, the other is about .3" across. This unit uses its own interface card (which is supplied), so it leaves the game port free, but requires a motherboard slot.

Wico's ball is based on the same design as the arcade controls: the track ball rolls on ball bearings, and is read by optically counting the revolutions of the rollers supporting the ball. This design gives a much better, smoother feel to the ball's motion, and gives the programmer more flexible ways to read and control the ball. However, it also means that no existing programs can use the Wico ball.

$79.95 Wico Corp., 6400 Gross Point Rd., Niles, IL, 60648

In summary, the TG track ball fits right into the "standard" Apple environment. It plugs into the game port and works with all software that reads paddles 0 and 1, but it feels awkward to use. I consider it a poor substitute for a joystick or paddles, where they are appropriate ... and a poor substitute for a real trackball, if that's what you need.

On the other hand, the Wico track ball is much more responsive and comfortable to use, but it requires an interface slot and special programming. I think it's well worth the effort, and I intend to try using it in as many different applications as I can think of. Wico's trackball comes with a booklet containing a couple of pages about programming with their interface. The following is a summary of that information, plus whatever I've been able to figure out. The program given here reads the ball and displays the X and Y values on the screen in hex notation. It also checks the keyboard for the keys "1" through "4", and sets the ball's speed to match.

The interface card contains no ROM. It does have eight registers which you read and/or write to control the trackball. Here is a table of the registers' addresses and functions ("N" is slot number + 8, i.e., $9-$F):

Address       Read         Write

 $C0N0     X Position   X Position
 $C0N1     Y Position   Y Position
 $C0N2     Bounded      Bounded
 $C0N3     Wraparound   Wraparound
 $C0N4         -        Speed 1
 $C0N5         -        Speed 2
 $C0N6     Buttons      Speed 3
 $C0N7         -        Speed 4

The first two registers, $C0N0 and $C0N1, contain the X and Y readings from the trackball. You can write to these locations to set starting values, or to force particular values at any time. Lines 1850-2040 of the program show how to read the registers, limit their values, and keep track of current value, last value and change since last reading.

Another approach is to read only the change in value from the trackball, and keep track of the values separately. To do that, first turn on the wraparound feature, as described below. Then set $C0N0 and $C0N1 to 0. That takes care of initial- ization. Now, whenever you want to read the changes in the ball's position, just call this routine:

       LDA XREG
       STA DX
       LDA YREG
       STA DY
       LDA #0
       STA XREG
       STA YREG
       RTS

Since wraparound is permitted, DX and DY will be positive when the ball was moved down or right, and negative when it was moved up or left. Reset the registers to 0 after reading them, and next time you call this routine they will again contain only the change in value.

$C0N2 (BOUNDRY) and $C0N3 (WRAPS) control whether the readouts will stop at 0 and 255, or wrap around. Reading or writing to either address will set the corresponding condition.

You can read the state of the pushbuttons from $C0N6; each button turns on one bit. Bit 7 (sign bit) is the large button and bit 6 (overflow bit) is the small one. These are very easy to test from assembly language; just BIT $C0N6 and use BMI & BPL for the large button, or BVC & BVS for the small one. Lines 2060-2140 of the program show a good way to translate these bits into bytes, so Applesoft can easily test them.

The speed or scale of the readout can be controlled by writing to two of the locations from $C0N4-$C0N7. The values written do not matter, you're throwing soft switches. These addresses select a divider to apply to the X and Y readings. Here's a table of addresses and effects:

  Addresses       Speed    Divide by

$C0N6 & $C0N4    Fastest      1
$C0N6 & $C0N5    Med Fast     2
$C0N7 & $C0N4    Med Slow     4
$C0N7 & $C0N5    Slowest      8

At the fastest setting, a quarter-turn of the ball produces about a sixteen-point difference in the X or Y reading. At the slowest setting, the same motion changes the readout by two points. Lines 2160-2260 are just a quick way to read the keyboard and produce a value of 0-3. Lines 2280-2390 are how I translate that number into writing the correct pair of addresses.

The track ball has also proved to be an excellent cursor control for graphics work, and a lot of fun to use for controlling menu selection. I'm looking forward to trying it out as a cursor control with the S-C Word Processor.

We don't plan to stock the Track Balls, but if you want one, we can get the Wico unit for you for $75 plus shipping.

  1000 *--------------------------------
  1010 *  READ AND WRITE WICO TRACKBALL INTERFACE
  1020 *--------------------------------
  1030 CH       .EQ $24
  1040 KEYBOARD .EQ $C000
  1050 STROBE   .EQ $C010
  1060 VTABZ    .EQ $FC24
  1070 HOME     .EQ $FC58
  1080 COUT     .EQ $FDED
  1090 PRBYTE   .EQ $FDDA
  1100 *--------------------------------
  1110 *    WICO INTERFACE REGISTERS
  1120 
  1130 SLOT    .EQ 4     INTERFACE LOCATION
  1140 
  1150 BASE    .EQ SLOT*$10+$C080
  1160 REGS    .EQ BASE+0
  1170 XREG    .EQ BASE+0
  1180 YREG    .EQ BASE+1
  1190 BOUNDRY .EQ BASE+2
  1200 WRAP    .EQ BASE+3
  1210 SPEED1  .EQ BASE+4
  1220 SPEED2  .EQ BASE+5
  1230 SPEED3  .EQ BASE+6
  1240 SPEED4  .EQ BASE+7
  1250 BUTTONS .EQ BASE+6
  1260 *--------------------------------
  1270 SETUP  JSR HOME
  1280        LDA #0
  1290        STA SPEED         START AT TOP SPEED
  1300        STA BOUNDRY       NO WRAPAROUND
  1310        LDY #1            DO THIS TWICE
  1320 .1     LDA HIGH.LIMITS,Y
  1330        SBC LOW.LIMITS,Y  SET INITIAL
  1340        LSR               VALUE TO
  1350        ADC LOW.LIMITS,Y  CENTER OF
  1360        STA LOCATIONS,Y   LIMITS
  1370        STA REGS,Y
  1380        STA LAST.VALUES,Y
  1390        DEY
  1400        BPL .1            DONE?
  1410 
  1420 LOOP   LDA #10           CENTER DISPLAY
  1430        JSR VTABZ
  1440        LDA #16           ON SCREEN
  1450        STA CH
  1460        JSR READ.BALL     GO READ BALL
  1470        LDA X
  1480        JSR PRBYTE        SHOW X READING
  1490        LDA #$A0
  1500        JSR COUT
  1510        LDA Y             AND Y READING
  1520        JSR PRBYTE
  1530        JMP LOOP          DO IT AGAIN
  1540 *--------------------------------
  1550 *   VARIABLES
  1560 *--------------------------------
  1570 LOW.LIMITS
  1580 X.LOW  .DA #0       MINIMUM VALUES
  1590 Y.LOW  .DA #0
  1600 
  1610 HIGH.LIMITS
  1620 X.HIGH .DA #$FF     MAXIMUM VALUES
  1630 Y.HIGH .DA #$FF
  1640 
  1650 LOCATIONS
  1660 X      .DA #0       POINTS TO PLOT
  1670 Y      .DA #0
  1680  
  1690 LAST.VALUES
  1700 LAST.X .DA #0       FROM LAST CALL
  1710 LAST.Y .DA #0
  1720  
  1730 DELTAS
  1740 DX     .DA #0       CHANGE IN VALUES
  1750 DY     .DA #0
  1760  
  1770 S1     .DA #0       LARGE BUTTON
  1780 S2     .DA #0       SMALL BUTTON
  1790 
  1800 SPEED  .DA #0       0=FASTEST, 3=SLOWEST
  1810  
  1820 *--------------------------------
  1830 READ.BALL
  1840  
  1850 SET.X.AND.Y
  1860        LDY #1            DO THIS TWICE
  1870 .1     LDA REGS,Y        READ BALL
  1880        CMP LOW.LIMITS,Y  TOO LOW?
  1890        BCS .2            NO, GO ON
  1900        LDA LOW.LIMITS,Y  YES, FORCE LOW LIMIT
  1910        STA REGS,Y
  1920 .2     CMP HIGH.LIMITS,Y TOO HIGH?
  1930        BCC .3            NO, GO ON
  1940        LDA HIGH.LIMITS,Y YES, FORCE HIGH LIMIT
  1950        STA REGS,Y
  1960 .3     STA LOCATIONS,Y   USE THIS POINT
  1970        PHA               SAVE IT
  1980        SEC
  1990        SBC LAST.VALUES,Y CALCULATE CHANGE
  2000        STA DELTAS,Y
  2010        PLA               RESTORE POINT USED
  2020        STA LAST.VALUES,Y AND SAVE IT FOR NEXT READING
  2030        DEY
  2040        BPL .1            DONE?
  2050  
  2060 SET.SWITCHES
  2070        LDA #0
  2080        STA S1       ZERO
  2090        STA S2       READOUTS
  2100        LDA BUTTONS  READ PUSHBUTTONS
  2110        ASL          BIT 7 TO CARRY
  2120        ROL S1         TO S1.
  2130        ASL          BIT 6 TO CARRY
  2140        ROL S2         TO S2.
  2150  
  2160 CHECK.KEYBOARD
  2170        LDA KEYBOARD   KEYPRESS?
  2180        BPL EXIT       NO, GO ON
  2190        STA STROBE     YES
  2200        CMP #$B5       >4?
  2210        BCS EXIT       YES, GO ON
  2220        CMP #$B0       <1?
  2230        BCC EXIT       YES, GO ON
  2240        AND #$0F       LOSE HIGH NYBBLE
  2250        SBC #1         MAKE 0-3
  2260        STA SPEED      AND SAVE IT
  2270 
  2280 SET.SPEED
  2290        LDA SPEED    GET SPEED
  2300        PHA
  2310        AND #2       USE BIT 1
  2320        LSR          NOW 0 OR 1
  2330        TAY          INDEX
  2340        STA SPEED3,Y HIT SPEED3 OR SPEED4
  2350        PLA          GET SPEED AGAIN
  2360        AND #1       USE BIT 0
  2370        TAY
  2380        STA SPEED1,Y HIT SPEED1 OR SPEED2
  2390 EXIT   RTS
  2400 *--------------------------------
  2410        .LIF

Ampersand Monitor Caller Bob Sander-Cederlof

A recent issue of the Maple Orchard, magazine published by the Loyal Ontario Group Interested in Computers (LOGIC, P. O. Box 696 station B, Willowdale, Ontario, Canada M2K 2P9) was entirely devoted to assembly language. Bob Stitt, also one of our readers, authored an article called "A New Utility for Applesoft".

Bob's new utility provided a way to execute monitor commands from inside a running Applesoft program. He implemented the S.H.Lam method in machine language as an ampersand routine.

A long long time ago someone named S. H. Lam showed the world a neat way to execute monitor commands from Applesoft. His method was published by Call APPLE back about 1978, I think. Lam POKEd the characters of a string containing a monitor command into the monitor's input buffer at $200. He included " N D9C6G" at the end of each command string, to return control to Applesoft. Then POKE 72,0:CALL-144 executes the command.

     100 C$="300:A9 3A 20 C0 DE 60 N D9C6G"
     110 FOR I=1 TO LEN(C$)
     120 POKE 511+I, ASC ( MID$ (C$,I,1)) + 128
     130 NEXT
     140 POKE 72,0 : CALL-144

Bob Stitt's utility replaces lines 110-140 above with a simple ampersand statement:

     110 & C$

After reading the article, I decided to try writing my own. I came up with a different technique; it is probably no better, but it is bigger. Mine works like a similar routine coded by Steve Wozniak inside the mini-assembler in the Integer BASIC ROMs. The only advantage I find is that there is no need to append " N D9C6G" to each command string. As a result, you can execute "3D0G" (if you like) and stop execution of your Applesoft program.

Lines 1220-1280 set up the ampersand vector. You can BLOAD the program and CALL 768 to run these lines, or simply BRUN it.

Line 1310 clears the monitor MODE, so that it realizes it is at the beginning of a command.

Lines 1320 and 1330 set up the string which follows the ampersand. The length will be in the A-register, and the address of the first character in INDEX ($5E,$5F). Lines 1340-1420 copy the string data into the monitor's buffer at $200. The characters are moved in backwards order, after first storing a carriage return at the end. So far the code is very similar to that of Bob Stitt.

Lines 1440-1590 are very similar to code found in the mini-assembler (at $F538-$F559 in the Integer BASIC ROMs). MON.GETNUM parses one hexadecimal number, if present, and then returns with a modified form of the first non-hex character in the A-register. Lines 1480-1520 search the monitor's command table for a matching character. If none is found, you will hear a bell. If found, the carriage return command is a special case. Lines 1540-1590 handle the carriage return command, and lines 1440-1450 handle all the others.

When the command has been fully parsed and executed, control will return to your Applesoft program. That is, unless your command had the effect of aborting Applesoft. Here is a sample program:

     10 PRINT CHR$(4)"BLOAD B.MONITOR" : CALL 768
     20 INPUT C$
     30 & C$
     40 PRINT : GOTO 20

Note that to type in a command which contains a ":" you will have to type a leading quotation mark. Otherwise Applesoft will issue its "EXTRA IGNORED" message and truncate your input at the colon.

Inside your Applesoft program you can build the command string using any sort of string functions and concatenation you wish.

  1000 *SAVE S.AMPER MONITOR
  1010 *--------------------------------
  1020 *      &MONITOR
  1030 *--------------------------------
  1040 AMPERSAND.VECTOR .EQ $3F5
  1050 AS.FRMEVL  .EQ $DD7B
  1060 AS.FRESTR  .EQ $E5FD
  1070 *--------------------------------
  1080 MON.MODE   .EQ $31
  1090 MON.YSAV   .EQ $34
  1100 INDEX      .EQ $5E,5F
  1110 *--------------------------------
  1120 BUFFER     .EQ $200
  1130 MON.TOSUB  .EQ $FFBE
  1140 MON.GETNUM .EQ $FFA7
  1150 MON.CHRTBL .EQ $FFCC
  1160 MON.BELL   .EQ $FF3A
  1170 MON.BL1    .EQ $FE00
  1180 MON.ZMODE  .EQ $FFC7
  1190 *--------------------------------
  1200        .OR $300
  1210 *--------------------------------
  1220 SETUP  LDA #$4C     JMP OPCODE
  1230        STA AMPERSAND.VECTOR
  1240        LDA #FAKE.MONITOR
  1250        STA AMPERSAND.VECTOR+1
  1260        LDA /FAKE.MONITOR
  1270        STA AMPERSAND.VECTOR+2
  1280        RTS
  1290 *--------------------------------
  1300 FAKE.MONITOR
  1310        JSR MON.ZMODE
  1320        JSR AS.FRMEVL
  1330        JSR AS.FRESTR
  1340        TAY
  1350        LDA #$8D     <RETURN>
  1360 .1     STA BUFFER,Y
  1370        TYA
  1380        BEQ FMN2     END OF STRING
  1390        DEY
  1400        LDA (INDEX),Y
  1410        ORA #$80
  1420        BNE .1       ...ALWAYS
  1430 *--------------------------------
  1440 FMN1   JSR MON.TOSUB
  1450        LDY MON.YSAV
  1460 FMN2   JSR MON.GETNUM
  1470        STY MON.YSAV
  1480        LDY #22
  1490 .1     CMP MON.CHRTBL,Y
  1500        BEQ .2
  1510        DEY
  1520        BPL .1
  1530        JMP MON.BELL
  1540 .2     CPY #21
  1550        BNE FMN1
  1560        LDA MON.MODE
  1570        LDY #0
  1580        DEC MON.YSAV
  1590        JMP MON.BL1

Apple Assembly Line is published monthly by S-C SOFTWARE CORPORATION, P.O. Box 280300, Dallas, Texas 75228. Phone (214) 324-2050. Subscription rate is $15 per year in the USA, sent Bulk Mail; add $3 for First Class postage in USA, Canada, and Mexico; add $13 postage for other countries. Back issues are available for $1.50 each (other countries add $1 per back issue for postage).

All material herein is copyrighted by S-C SOFTWARE, all rights reserved. Unless otherwise indicated, all material herein is authored by Bob Sander-Cederlof. (Apple is a registered trademark of Apple Computer, Inc.)