Apple Assembly Line
Volume 7 -- Issue 9June 1987

In This Issue...

Return of Beneath Apple ProDOS

We've just received our copies from the newest printing of Beneath Apple ProDOS, so that excellent reference is available again. Our thanks to Quality Software for keeping such valuable information at our fingertips.

However, we do not expect that Quality Software will reprint either Understanding the Apple II or Understanding the Apple //e, so once the supplies run out that's the end of those books. If you are at all interested in the innermost details of how your Apple hardware works you need to get one of these outstanding books by Jim Sather. We've never seen anything to compare to these books for the wealth of information at the chip-by-chip level. See the S-C Software ad on page three for our price.

Latest from Lancaster

Don Lancaster just sent a copy of his latest book, Ask the Guru. Computer Shopper readers will recognize the title of his monthly column in that publication; this book is a collection of the columns from March, 1985 through March, 1987. Here are all of Don's writings on the inner workings of our favorite computer, especially how to get the most out of AppleWriter and/or the LaserWriter printer.

This is undoubtedly the best-printed self-published book I've ever seen: each copy is directly printed out on Don's Laser- Writer! Even subscribers to Computer Shopper will want this book, just to have all the information in one place rather than strung out through many pages of oversized newsprint. $24.50 from Synergetics, Box 809, Thatcher, AZ 85552. (602) 428-4073.


Selective CLEAR for Applesoft VariablesBruce E. Howell

A friend of mine wrote an Applesoft program that processes classroom grades. Certain information, such as the teacher's name, would be loaded and used throughout the program, while other information, such as student grades, had to be cleared often. One solution would be having a loop to clear certain variables. This would be quick and easy with simple variables, but with multi-dimensioned arrays this can be time consuming. Another solution would be to open a temporary file and store the static information there or to POKE it in a safe place in memory, such as at $300, issue the CLEAR command to clear all Applesoft variables, then re-load the variables. My solution to the problem is Selective Clear, which allows you to select a range of real, integer or string variables to clear, or to clear an entire, specific array. Note that Selective Clear sets real and integer variables to zero, and string variables to a null string. Applesoft's CLEAR command deletes the variables, requiring arrays to be re-defined after the CLEAR.

USING SELECTIVE CLEAR

Selective Clear works with either ProDOS or DOS. To follow ProDOS convention, it is loaded at $2000, then moved up in memory to where it is used. If your Applesoft program is short, you can load Selective Clear from within your program. A longer Applesoft program would be overwritten by the initial loading of Selective Clear. To avoid this, use a short Applesoft program to load Selective Clear, then run the main program as follows:

       10 PRINT CHR$(4)"BRUN B.SEL.CLEAR"
       20 PRINT CHR$(4)"RUN MAIN.PROGRAM"

To use Selective Clear in your application, it is best to define your simple variables and group them alphabetically so that those that you want to clear often are all at one end or the other of the alphabet. The syntax for simple variables is:

   & $ > G : REM clear all string variable greater than G
   & % < M : REM clear all integer variables less that M
   & R > T : REM clear all real variables greater than T

Following the ampersand, use $, % or R to indicate string, integer or real variables to clear. Following the variable type use the greater than (>) or less than (<) sign, then a single alpha character to show the range.

Array variables are cleared individually by placing the actual variable to clear in parenthesis following the ampersand as follows:

   & (AB$) : REM  To clear the string array AB$
   & (M)   : REM  To clear the real array M
   & (TT%) : REM  To clear the integer array TT%

Note that if you try to clear an array that has not been defined, you will get an error message of ARRAY ERROR.

ENTERING THE PROGRAM

To enter Selective Clear, you can use an assembler to type in the source code as listed. I use the ProDOS version of S-C Macro Assembler. You can also enter it directly into the monitor. If you do the latter, save it with the command:

     BSAVE B.SEL.CLEAR,A$2000,L$166

HOW SELECTIVE CLEAR WORKS

The first thing done is to identify which operating system is in effect, to properly reserve memory for Selective Clear. Then the routine is moved to the proper location in memory, the ampersand vector is set and control returns back to Applesoft.

To understand how simple variables can be selected and cleared, you must understand how variables are stored in memory. Locations $69,6A point to the start of a table of simple variables, normally set to LOMEM. For real and integer variables, the actual values are stored in the table, while for string variables the table contains pointers to where the strings are stored in memory (from HIMEM down). Each variable occupies seven locations or bytes in the table, the first two locations being the variable name.

With real variables, the first byte is the positive value of the first character of the variable name. The second byte is the positive value of the second character of the variable name. For integer variables, both bytes are the negative values for the variable name, while with string variables, the first byte is positive and the second is negative. Depending on which variable type is selected, two different masks (MASK1 and MASK2) are used to determine if the current entry in the table has a variable name with the proper signs for the type you want to select.

The other choice that Selective Clear needs to make is whether the variable in the table is less than or greater that the range that you have indicated. This is done by changing the program slightly each time it is called. If greater than is selected, then a Branch on Carry Clear (BCC) is placed where the variable name is compared to the selected range. Greater than stores a Branch on Carry Set (BCS). A no match of either the variable type or range skips to the next entry in the table until the end of the table is reached.

If the variable name is within the selected range and the signs match the variable type selected, then Selective Clear clears the variable by storing zeros in the next five locations in the table. For real numbers, the first location contains a one byte exponent followed by four bytes of mantissa. Integer numbers have the high byte first followed by the low byte of the number stored in the variable. The last three bytes of the entry are not used. String variables have a one byte string length followed by the low and high bytes of the address where the string is stored. With strings, the last two bytes of the table entry are not used.

Memory locations $6B,6C point to the start of the array table. Variable names are stored in the same way as with simple variables. Since you will manually select which array to clear, the routine finds the variable's location in the table by evaluating the expression within the parenthesis using an Applesoft routine at $DEB2, then searching the table for the variable name. The first two bytes following the name are an offset pointer to the next variable in the table. Selective Clear uses their value to determine when it is done clearing. The next byte is the number of dimensions present. It uses that value to bypass that number of sets of high-low bytes which follow, each set indicating the size of each dimension. After these sets of bytes is where the actual data is stored (or pointers to data if string arrays) in the same way that simple variables are stored. To clear, the routine zeros all locations till it reaches the next array variable name.

CUSTOMIZING THE ROUTINE

I chose to clear arrays individually, since a normal program uses very few arrays. You can modify the routine to clear a range of arrays using the technique outlined for simple vari- ables. If you need to clear either simple variables only or arrays only, you can shorten the routine enough to fit at $300. You could also modify the routine to set the variable to some required value, passed to the routine, instead of clearing it.

  1000 *SAVE S.SEL.CLEAR
  1010 *--------------------------------
  1020 *
  1030 * Selective CLEAR 
  1040 *
  1050 * Copyright 1987 by Bruce E. Howell, D.D.S.
  1060 *                   2531 Cartwright Rd.
  1070 *                   Missouri City, Texas  77459
  1080 *
  1090 * Format for simple variables is:  &type>range
  1100 *                                  &type<range
  1110 *
  1120 * Where 'type' is:  $ to clear string variables
  1130 *                   % to clear integer variables 
  1140 *                   R to clear real variables
  1150 *
  1160 *       'range' is a single alpha character.
  1170 *       Variables greater (>) or less (<) than
  1180 *            this character are cleared.
  1190 *
  1200 *
  1210 * Format for array variables is:
  1220 *
  1230 *       & (C$)     to clear the string array C$
  1240 *       & (C)      to clear the real array C
  1250 *       & (C%)     to clear the integer array C%
  1260 *
  1270 *--------------------------------
  1280 CHRGET .EQ $00B1 
  1290 CHRGOT .EQ $00B7
  1300 AMPER  .EQ $03F5    Ampersand Vector
  1310 *--------------------------------
  1320 FREBUF .EQ $BEF8    PRODOS FREE BUFFERS
  1330 GETBUF .EQ $BEF5    ProDOS Obtain Buffer
  1340 *--------------------------------
  1350 ERROR  .EQ $D412    PRINT ERROR
  1360 ADDON  .EQ $D998    Add (Y) to ChrPtr
  1370 SCAN   .EQ $D9A3    SCAN TO :/EOL
  1380 PARCHK .EQ $DEB2    EVAL (EXPRES)
  1390 *--------------------------------
  1400 MOVMEM .EQ $FE2C    MOVE MEMORY  
  1410 *--------------------------------
  1420 SSA    .EQ $3C,3D   SOURCE START ADDRESS
  1430 SEA    .EQ $3E,3F   SOURCE END ADDRESS
  1440 DSA    .EQ $42,43   DESTINATION START ADDRESS
  1450 *--------------------------------
  1460 VARTAB .EQ $69,6A   START OF VARS
  1470 ARYTAB .EQ $6B,6C   START ARRAYS
  1480 STREND .EQ $6D,6E   END OF ARRAY
  1490 FRETOP .EQ $6F,70
  1500 HIMEM  .EQ $73,74
  1510 VARNAM .EQ $81,82
  1520 LOC    .EQ $E0,E1  
  1530 HOLD   .EQ $E8,E9
  1540 RANGE  .EQ $E4
  1550 MASK1  .EQ $E5
  1560 MASK2  .EQ $E6 
  1570 *--------------------------------
  1580 JMP.OP .EQ $4C      JMP OP CODE
  1590 MASK   .EQ $80
  1600 BCC.OP .EQ $90      BCC OP CODE
  1610 BCS.OP .EQ $B0      BCS OP CODE
  1620 *--------------------------------
  1630        .MA SET
  1640        LDA #]2
  1650        STA ]1
  1660        LDA /]2
  1670        STA ]1+1
  1680        .EM
  1690 *--------------------------------
  1700        .MA MOV
  1710        LDA ]2
  1720        STA ]1
  1730        LDA ]2+1
  1740        STA ]1+1
  1750        .EM
  1760 *--------------------------------
  1770        .OR $2000
  1780        .TF B.SEL.CLEAR
  1790 *--------------------------------
  1800 INIT   LDA $BF00    MLI LOCATION
  1810        CMP #JMP.OP  SEE IF ACTIVE
  1820        BEQ PRODOS   YES DO PRODOS
  1830 *---Assume in DOS 3.3------------
  1840 DOS    LDA #SELCLR
  1850        STA HIMEM
  1860        STA FRETOP
  1870        LDA /SELCLR
  1880        STA HIMEM+1
  1890        STA FRETOP+1
  1900        BNE DOMOVE   ALWAYS BRANCH
  1910 *---Assume in ProDOS-------------
  1920 PRODOS JSR FREBUF   FREE ALL BUFFERS
  1930        LDA #2       GET TWO PAGES
  1940        JSR GETBUF
  1950 *---Move routine into hole-------
  1960 DOMOVE >SET SSA,FROM   Starting address of source
  1970        >SET SEA,THRU   Ending address of source
  1980        >SET DSA,SELCLR Starting address of destination
  1990        JSR MOVMEM
  2000        LDA #JMP.OP  SET UP AMPERSAND
  2010        STA AMPER
  2020        >SET AMPER+1,SELCLR
  2030        RTS          RETURN
  2040 *--------------------------------
  2050 FROM
  2060        .PH $9800    WHERE WE END UP 
  2070 SELCLR JSR CHRGOT   Get char after "&"
  2080        LDY #MASK    If "R":  X=$80, Y=$80
  2090        LDX #MASK    If "$":  X=$80, Y=$00
  2100        CMP #'R'     If "%":  X=$00, Y=$00
  2110        BEQ .3       ...REAL VARIABLE
  2120        CMP #'$'     SEE IF STRING
  2130        BEQ .2       ...STRING VARIABLE
  2140        CMP #'%'     SEE IF INTEGER 
  2150        BEQ .1       ...INTEGER VARIABLE
  2160        JMP SEARCH.ARRAY.VARIABLES    TRY ARRAY 
  2170 .1     LDX #$00     CHECK FOR NEG
  2180 .2     LDY #$00
  2190 .3     STX MASK1    SET MASKS FOR
  2200        STY MASK2    TWO CHAR. NAME
  2210 *---Get and save relop-----------
  2220        JSR CHRGET   GET FUNCTION <>
  2230        STA FUNC
  2240 *---Get and save letter----------
  2250        JSR CHRGET   GET RANGE OF Variable Name
  2260        STA RANGE
  2270 *---Modify code for relop--------
  2280        LDA FUNC
  2290        CMP #$CF     SEE IF GT >
  2300        BNE .4       ...not >
  2310        INC RANGE    SO > RANGE NOT =
  2320        LDA #BCC.OP  SET UP GT >   
  2330        BNE .6       ...always
  2340 .4     CMP #$D1     SEE IF LT <
  2350        BEQ .5       ...it is <
  2360        LDX #$10     REPORT SYNTAX ERROR
  2370        JMP ERROR
  2380 .5     LDA #BCS.OP  SET UP LT <   
  2390 .6     STA FUNC     FUNNY TRICK
  2400 *--------------------------------
  2410 SEARCH.SIMPLE.VARIABLES
  2420        LDY #0       FOR INDEX
  2430        >MOV LOC,VARTAB   START OF VARIABLE TABLE
  2440 SIMPLE LDA LOC+1    ARE WE DONE?
  2450        CMP ARYTAB+1  
  2460        BNE .1
  2470        LDA LOC 
  2480        CMP ARYTAB  
  2490        BCC .1
  2500        JSR SCAN     YES SCAN EOL
  2510        JMP ADDON    GO APPLESOFT
  2520 *--------------------------------
  2530 .1     LDA (LOC),Y  GET VARIABLE NAME
  2540        AND #$7F     MASK HIGH BIT
  2550        CMP RANGE    COMPARE TO RANGE
  2560 *
  2570 FUNC   BCC .1       ***Opcode modified to BCC or BCS***
  2580 *
  2590        LDA (LOC),Y  GET VARIABLE TYPE
  2600        AND #MASK
  2610        EOR MASK1
  2620        BNE .3
  2630 .1     JSR INCLOC
  2640        JSR INCLOC
  2650 .2     LDA LOC      SKIP FIVE 
  2660        CLC  
  2670        ADC #$05  
  2680        STA LOC
  2690        BCC SIMPLE
  2700        INC LOC+1  
  2710        BCS SIMPLE   ALWAYS
  2720 *--------------------------------
  2730 .3     JSR INCLOC
  2740        LDA (LOC),Y
  2750        AND #MASK
  2760        EOR MASK2
  2770        BEQ .2       ...not correct type
  2780 *---Clear the value--------------
  2790        JSR INCLOC   PASS 2ND   
  2800        LDX #5
  2810        LDA #0       Store zero in 5 bytes
  2820 .4     STA (LOC),Y
  2830        JSR INCLOC
  2840        DEX
  2850        BNE .4
  2860        BEQ SIMPLE   ...always
  2870 *--------------------------------
  2880 INCLOC INC LOC  
  2890        BNE .1
  2900        INC LOC+1    ADVANCE HIGH
  2910 .1     RTS  
  2920 *--------------------------------
  2930 SEARCH.ARRAY.VARIABLES
  2940        JSR PARCHK   ERROR IF NOT
  2950        >MOV LOC,ARYTAB   START OF ARRAY TABLE
  2960 .1     LDY #0       FOR INDEX
  2970        LDA (LOC),Y  GET 1ST. CHAR.
  2980        BEQ .99      NONE PRESENT
  2990        CMP VARNAM   SEE IF SAME NAME
  3000        BNE .5       FIND NEXT ARRAY
  3010        INY
  3020        LDA (LOC),Y  GET 2ND CHAR.
  3030        CMP VARNAM+1
  3040        BNE .5       FIND NEXT ARRAY
  3050        INY          PASS 1ST NAME
  3060        LDA (LOC),Y  GET LOW OFFSET
  3070        CLC          CLEAR CARRY
  3080        ADC LOC      ADD TO CURRENT
  3090        STA HOLD     LOW END OF ARRAY
  3100        INY          
  3110        LDA (LOC),Y  GET HIGH OFFSET
  3120        ADC LOC+1    ADD TO CURRENT
  3130        STA HOLD+1   HIGH END OF ARRAY
  3140        INY
  3150 .2     JSR INCLOC
  3160        DEY
  3170        BNE .2
  3180        LDA (LOC),Y  # OF DIMS
  3190        TAX          COUNTER
  3200        JSR INCLOC   PASS # DIMS
  3210 .3     JSR INCLOC   PASS SIZE H
  3220        JSR INCLOC   PASS SIZE L
  3230        DEX
  3240        BNE .3
  3250        LDA #$00
  3260 .4     STA (LOC),Y  
  3270        JSR INCLOC
  3280        LDX LOC      SEE IF END OF
  3290        CPX HOLD     THIS ARRAY YET
  3300        BNE .4       NOT END OF ARRAY
  3310        LDX LOC+1    CHECK AGAIN
  3320        CPX HOLD+1
  3330        BNE .4
  3340        RTS          RETURN BASIC
  3350 *---Find Next Array--------------
  3360 .5     LDY #$02
  3370        LDA (LOC),Y
  3380        CLC
  3390        ADC LOC
  3400        STA HOLD     NEXT ADDRESS LOW
  3410        INY
  3420        LDA (LOC),Y
  3430        ADC LOC+1
  3440        STA LOC+1    NEXT ADDRESS HIGH
  3450        LDA HOLD
  3460        STA LOC 
  3470        CMP STREND
  3480        BNE .1
  3490        LDA LOC+1
  3500        CMP STREND+1
  3510        BNE .1
  3520 *
  3530 .99    LDX #$80     DIDN'T FIND 
  3540        JMP ERROR    say ARRAY ERROR
  3550        .EP
  3560 THRU
  3570 *--------------------------------

More Than Just Saving the RegistersBob Sander-Cederlof

When you are writing a program it is frequently advantageous to code some parts as subroutines. Some of these subroutines will be useful enough that you may wish to use them in lots of programs. In order to make them usable in a variety of circumstances, you have to be sure that they do not have any hidden side-effects. A subroutine for general use should not change any registers, any memory, or any part of the machine state unless the change is a desired function of that subroutine.

It is getting harder and harder to live up to that ideal, as the Apple II grows. In the beginning there was only the plain vanilla 6502, and at most 48K RAM. There was plenty of memory, plenty of stack space, plenty of zero page. Every subroutine could have its own private work area, and not worry too much about RAM or register usage. Machine state was pretty much the same all day long, with the exception of the text and graphics modes. But now we have the IIgs and the 65816 with more registers, megabytes of RAM to manage, bank switching, shadowing, interrupts, variable speed processing, emulation, and so on endlessly.

It used to be enough to just save and restore the A-, X-, and Y-registers. For example, you might find the following at the beginning and end of a 6502 subroutine:

       Beginning           End
     ---------------     -------------------
       PHA    Save A       PLA     Restore Y
       TXA                 TAY
       PHA    Save X       PLA     Restore X
       TYA                 TAX
       PHA    Save Y       PLA     Restore A
                           RTS

This code saves and restores all three machine registers, so that they may all be used for any other purpose inside the subroutine.

On the other hand, you might not find this code, because most subroutines written in the years gone by used one or more of the three registers to pass parameters. To use such a subroutine, you would first place the values used by the subroutine into the registers; then you would call the subroutine. If the subroutine was supposed to return an answer, it might be returned in the same registers. If the subroutine needed to use page zero at all, it might have its own private locations.

When page zero became over-crowded, we started saving and restoring chunks of page zero. If only a few bytes were needed, they could be saved on the stack. If a larger block was needed, the subroutine might save the block in a private area of RAM outside of page zero.

The IIgs and 65816 are so complicated now that I almost despair of ever writing a completely safe subroutine. Each subroutine must be careful analyzed to see what information needs to be saved and restored and what internal states, modes, and values need to be used.

Rather than try to give you a complete entry-setup-exit template, I am going to show you the components and let you construct your own as you require them.

The first item that occurs to me has to do with the native and emulation modes. If you are writing a subroutine that can be called from both modes yet internally must use one particular mode, you need to save the caller's mode. The standard way of doing this is:

       Beginning                  End
     -------------------------  ------------------
       PHP   Save m and x         PLP  Get caller's mode
       SEC   (or CLC)             XCE
       XCE  Get into my mode      PLP  Get caller's m,x
       PHP  Save caller's mode    RTS

Seeing that RTS at the end reminds me of another difficulty. A subroutine cannot possibly know whether it was called with a JSR or a JSL instruction. JSR pushes a two-byte return address on the stack, while JSL pushes a three byte return address. If a subroutine was called with a JSR, it must end with an RTS. Likewise, if the call was made with JSL, the return must be an RTL. You have to decide what you are going to use and write the subroutine accordingly. If you need both, you will have to write two different subroutines (although they might share some code internally).

If you need to save and restore the three main registers (A, X, and Y) without knowing ahead of time which mode the caller was in, you need to be sure and save all 16-bits of each of them. Here is how I do that:

       Beginning                  End
     -------------------------  ------------------
       PHP   Save m and x         CLC  (Unless still in
       CLC   Native mode          XCE   the native mode)
       XCE                        REP #$30
       PHP   Save caller's mode   PLY  Restore Y
       REP #$30  Full 16-bit      PLX  Restore X
       PHA   Save A               PLA  Restore A
       PHX   Save X               PLP
       PHY   Save Y               XCE  Restore caller's mode
                                  PLP  Caller's m and x
                                  RTS

If the subroutine is being called with a JSL, it is possibly being called from a different bank. That means I may need to worry about the Data Bank Register. In the 65816 there is an 8-bit B-register which contains the bank number to use with all 16-bit addressing modes used to read or write data. If my subroutine needs to use local data or variables which are in the same bank as my subroutine, and the caller was in a different bank, I will most likely need to save his B-register and set up my own. There is also a Program Bank Register, called the K-register, which is changed automatically by the JSL and RTL instructions. To save the B-register and make it the same as my K-register, so I can refer to local data and variables, I need the following lines near the beginning of my subroutine:

       PHB     Save the caller's B-register
       PHK     Push Program Bank
       PLB     Use it as Data Bank also

Then near the end of the subroutine I need the corresponding line:

       PLB     Restore the caller's B-register

It is conceivable that the caller may have the Stack Pointer set to some weird value. In the 6502 the S-register is only 8-bits wide, but in the 65816 it has 16 bits. The stack can be made to start anywhere in bank 0 when you are in native mode. If you have programs that use more than one stack area, you may need to save and restore the caller's stack pointer and use your own inside your subroutine. In particular, if you are going to use Emulation Mode, or call monitor subroutines which do so, you will need to use the standard stack in page 1 inside your subroutine. The following four lines will save the current stack pointer and set up your own, assuming you are already in Native mode with both m- and x-bits cleared for full 16-bit operation:

       TSC     current stack pointer to A (16-bits)
       LDX ##xxxx   new value of stack pointer
       TXS
       PHA     save previous value on new stack

You may think of other ways to do this, but something of the sort must be done. If you use the above, then near the end of your subroutine you need to restore the original stack pointer as follows:

       PLX
       TXS

In a 6502 there is only one page zero, at addresses $0000 through $00FF. In the 65816 page zero may start anywhere in bank 0, because there is a 16-bit D-register which defines the starting address of the so-called "direct" page. If your programs fiddle with the D-register, then your subroutine might need to save and restore it. If your are calling any of the old monitor subroutines, be aware that they require a "standard" page zero. In 16-bit mode you can do it this way:

       Save               Restore
     ----------------   ---------------------
       PHD  Old D-reg     PLD   Get old D-reg
       PEA $xxxx
       PLD  New D-reg

So far I have only been worrying about the 65816 itself. What about all the Apple soft switches? The //e and //c were enough of a challenge, with all the memory banking and 80-column options. Now along comes the IIgs, an order of magnitude more complex!

In fact it is so complex that I have not mastered it yet. For now all I am going to point out is the rules for calling the old monitor subroutines and the new "tools". To use the old monitor subroutines you must be in Emulation mode, and running in bank 0 with normal page zero and stack pointers. The monitor itself must "be" in bank 0. Since the ROM is really in bank FF, you have the option of copying it to RAM in bank 0 or using the soft switch at $C081, $C089, $C082, or $C08A to map ROM into bank 0.

To call the new "tools" you must be in full 16-bit native mode. I think that is just about the only requirement. Then you use JSL to call the tools via the vectors in bank $E1.

Beyond this elementary level, it is easy to get lost. It reminds me of the "twisty little passages" in the Original Adventure Game. My advice: be careful; keep close track of everything you change; put everything back the way you found it. If your subroutine works, you may have done it correctly. Maybe not. You might not find out what you left out till next year when you try to use it from a new environment.

Recent articles in Call APPLE have discussed some of the same ideas, and you may profit by reading them. Look at David Sparks' "The Ethical Subroutine" in the June 1987 issue, and Bob Bishop's "Modular Assembly Language Programming" series in earlier issues this year.


Correction to ProDOS PatcherBob Sander-Cederlof

Mike McConnell called with a correction to my Applesoft ProDOS Patcher that affected its ability to find and fix versions 1.1.1 and 1.2. Lines 430 and 440 PEEKed at A, A+1, A+2, and A+3; in fact they should be PEEKing at A, A+3, A+6, and A+9. Change those two lines to:

     430 IF PEEK(A)<>157 OR PEEK(A+3)<>157 THEN 500
     440 IF PEEK(A+6)<>157 OR PEEK(A+9<>157 THEN 500

Then I noticed an error in the REMark at line 450. What it should say is:

     450 REM ---FOUND VERSION 1.1.1 OR 1.2,
                        SO CHANGE "STA" TO "LDA"---

Using the S-C Macro Assembler
with an Echo Speech Synthesizer
Rick Hayner

In 1977, I went to college to train to become a computer programmer. I was programming an IBM 360-30 main frame, with output which was not usable by me, since I am totally blind, and couldn't read the print output. My first programming language was IBM 360 Assembly Language. Depending on sighted readers who knew nothing about computers to read the listings, and having them help me find errors on IBM cards which sometimes had been punched on a keypunch which didn't print on the tops of the cards, was enough to drive a sane person crazy. Ever since then, I have had the desire to become a good Assembly Language programmer.

After getting an Apple //e, in 1985, I thought that using the ECHO Speech Synthesizer with an editor/assembler would be great. The problem was finding one which would talk.

While talking with Larry Skutchan, a friend of mine who is also a blind programmer, I asked him if he knew about an assembler that would talk, and he recommended the S-C Macro Assembler. I work part time for a company on designing software to help physically handicapped people. My boss, Scott Atwood, asked me if I would be willing to learn 6502 Assembly Language, and if I knew of an assembler which would talk, and I told him about the S-C Macro Assembler. About a week later, Scott brought the assembler to me, and using the instructions in the manual, which I was able to obtain as textfiles on diskette, I had the assembler talking within 10 minutes of getting it.

The ECHO Speech Synthesizer uses a program called Textalker, which resides in the language card area. This means I must use the "main memory" version of the S-C Macro Assembler. The instructions for patching the assembler gave the address of the instructions which had to be patched out in the main memory version, so there were no problems encountered in patching the assembler.

The thing which stops most Machine Language programs from talking is a "JSR $FE89" followed by a "JSR $FE93". These instructions are like doing an "IN#0" and a "PR#0". The problem is that Textalker changes the I/O hooks, and these routines disconnect Textalker. Changing these two instructions to six "NOP" instructions, allows the S-C Macro Assembler to talk. Another thing which stops programs from talking, is people sometimes store data directly into screen memory locations, instead of using "COUT". Fortunately, the S-C Macro Assembler doesn't do this, but you do have to patch out the JSR's mentioned above.

I really like the S-C Macro Assembler. I haven't figured out how to use all of its features yet, but I use it almost every day, and always learn something new about it every time I use it.

I have written some macros for controlling the Echo, and I have included them here for other Echo users.

I want to thank all the people at S-C Software for their help in learning to use the S-C Macro Assembler. They have always been willing to answer all questions, whether they have been about the assembler or about programming. I would recommend the S-C Macro Assembler to anyone who is interested in Assembly Language, whether they are blind or sighted.

  1000 *SAVE ECHO MACROS
  1010 *--------------------------------
  1020 *
  1030 *   Macros for ECHO Speech Synthesizer, by Rick Hayner
  1040 *
  1050 *   Requires Textalker Version 3.1.1 or later.
  1060 *
  1070 *--------------------------------
  1080 *   Generic ECHO Command:  >ECHO "X"  (X is command letter)
  1090 *--------------------------------
  1100        .MA ECHO
  1110        LDA #$85     Control-E
  1120        JSR MON.COUT
  1130        LDA #"]1"    Command letter
  1140        JSR MON.COUT
  1150        .EM
  1160 *--------------------------------
  1170 * TURN ECHO OFF:  >OFF
  1180        .MA OFF
  1190        >ECHO "O"
  1200        .EM
  1210 *--------------------------------
  1220 * TURN ECHO ON:  >ON
  1230        .MA ON
  1240        >ECHO "B"
  1250        .EM
  1260 *--------------------------------
  1270 * PUT ECHO IN TALK MODE:  >TALK
  1280        .MA TALK
  1290        >ECHO "T"
  1300        .EM
  1310 *--------------------------------
  1320 * PUT ECHO IN LETTER MODE:  >LETTER
  1330        .MA LETTER
  1340        >ECHO "L"
  1350        .EM
  1360 *--------------------------------
  1370 * PUT ECHO IN WORD MODE:  >WORD
  1380        .MA WORD
  1390        >ECHO "W"
  1400        .EM
  1410 *--------------------------------
  1420 *   TURN UPPERCASE SPELL ON:  >USPELL
  1430        .MA USPELL
  1440        >ECHO "K"
  1450        .EM
  1460 *--------------------------------
  1470 * TURN UPPERCASE SPELL OFF:  >USPELLOFF
  1480        .MA USPELLOFF
  1490        >ECHO "N"
  1500        .EM
  1510 *--------------------------------
  1520 * PUT ECHO IN SOME PUNCTUATION:  >SOME
  1530        .MA SOME
  1540        >ECHO "S"
  1550        .EM
  1560 *--------------------------------
  1570 * PUT ECHO IN MOST PUNCTUATION:  >MOST
  1580        .MA MOST
  1590        >ECHO "M"
  1600        .EM
  1610 *--------------------------------
  1620 * PUT ECHO IN ALL PUNCTUATION:  >ALL
  1630        .MA ALL
  1640        >ECHO "A"
  1650        .EM
  1660 *--------------------------------

Assembly Listings on ProDOS Text FilesJohn L. Hanna

From the ProDOS version of the S-C Macro Assembler, how does one direct the assembly listing to a text file for later inclusion in a word processor file? Articles in AAL some years ago showed how to do th is for the DOS 3.3 version, but the techniques given do not work with the ProDOS version.

After some head scratching and some snooping, I did find a way that works. By patching the PBITS entries for the OPEN and WRITE commands I can make them into DIRECT commands. (Without the patches, OPEN and WRITE are not legal as DIRECT commands.) I'll show how to make these two patches in a moment.

Once the patches are installed, by merely typing "OPEN LISTING" and "WRITE LISTING" an output textfile starts absorbing all screen-directed information. I make sure I am ready to assemble, and then type the OPEN and WRITE commands. At this point the screen output disappears, because it is all going to the text file instead. Then I type the ASM command, and the listing is written on the text file. At the end of the assembly the text file is automatically closed, and the prompt re-appears.

There are some "gotchas". First, the S-C Macro Assembler has a fixed number of file buffers. If I use one of them to send the listing to a text file, it is not possible to also have a target file (.TF file) at the same time. This could restrict your possibilities when working with large programs, because the object code does have to go somewhere. Judicious use of the .TA, .DU, or .PH directives could enable you to a assemble even large programs while directing the listing to a text file.

Second, the text file will not be truncated if a short listing is written on a previously exisitng file which was longer. The solution is to always DELETE such a file before you OPEN it.

As a test, I assembled the S-C DisAssembler (an excellent product, by the way) and sent the listing to a text file. I first removed the .TI line so that the resulting file would not have page breaks. Working with two floppy drives, the source code on one and the output listing file on the other, it produced a 177 block text file in just under five minutes. I moved the file into Appleworks with no problems. After adjusting the margins to 0 inches it was 123K, 2298 lines, and saved onto a 95K AWP file.

Now, how do you make those two patches? First you have to find the two bytes to change. Bob has produced several slightly different versions so this requires a little snooping. With the S-C Macro Assembler loaded and running, enter the monitor command "$B900.B99F". This will display the area containing the two bytes that need changing. In some of the monitor versions it will display in both hex and ASCII, but in most just in hex. There are two strings of bytes you need to find:

       4F 50 45 CE xx xx 2D
       57 52 49 54 C5 xx xx 21

The first string starts somewhere near $B918, and the second somewhere near $B958. The "xx xx" indicates two bytes which could have various values. The "2D" and "21" are the two bytes to patch. In my copy of the S-C Macro Assembler, these were at $B91F and $B95F, respectively. Be sure you find and use the correct addresses for yours. Change the 2D to 0D, and the 21 to 01, as follows:

       :$B91F:0D N B95F:01

That is all there is to it! Note that the change is temporary, because it does not modify the SCASM.SYSTEM file. You could find the appropriate place and do that if you wish, but it is just as effective to create a small EXEC file to install the changes whenever you need them.


Printing a Tiny Address BookletBob Sander-Cederlof

Do you have trouble remembering all the phone numbers and addresses of your family and friends? I sure do. At work I keep a Rolodex file for the most frequently needed ones, but I can hardly carry it around in my pocket. I could buy a little blank address book at any store, but my handwriting is too poor and they all take up too much space.

I had a vision of a tiny address booklet, no bigger than a credit card, which could hold about 100 names, addresses, and phone numbers. I could carry it in my credit card wallet, so it would almost always be with me when I need it.

It is easy enough to use a word processor to type all the information into my Apple. Last year I did just that, and printed it all out in the condensed mode on my NEC 8023 dot matrix printer. I then cut the printout into little pages and pasted them up in booklet format. Using a Minolta copying machine, I "xeroxed" the paste-up onto both sides of the same sheet of paper. Then using scissors and staples I assembled a little booklet the same size as a credit card, with 12 pages counting the front and back covers. I left those covers plain white, and covered them with clear packing tape, so it could last a little longer.

Indeed it did last, for about a year. Now there are so many changes that it is getting hard to read. I need to bring it up to date. But can the computer do a little more of the work?

Of course it can. Especially with the help of the new Epson EX-800 dot matrix printer I just bought, for only $399 plus tax at the local Soft Warehouse. It prints a lot faster than my other printers, which is why I bought it. But it also has a lot of new features. One of them is the ability to print subscripts and and superscripts. I found out that in this mode, with Condensed and Elite also selected, the printout is not only very small, but very clear and easy to read. The characters are shorter, allowing me to adjust the vertical spacing to 5/72 inch per line. This gets over 150 lines on each sheet of paper! It also lets me get 50 lines (including top and bottom margin) in the 3.5 inches of my booklet pages. And I can print 33 characters per line on each one of the pages, leaving a nice left and right margin. The Epson EX-800 makes two passes over each line in this mode, so that the miniature print is "near letter quality".

The printer helps, but I still wanted some automatic way to layout the individual pages and print them in booklet order, on BOTH SIDES of the same sheet of paper. I started to do it in Applesoft, but suddenly it seemed easier in Assembly Language. The resulting program prints all of the "front sides" and then waits while I take the paper out of the printer, turn it over, and put it back in. Then it prints the "back sides". All I have to do is fold the sheet down the middle, slice it into three 4-page folders, stack them up, and staple them together. It worked beautifully, and the result is nestling nicely in my shirt pocket right now.

To understand my program a little more easily, it might help to take three small pieces of paper, stack them up, fold the stack in half, and call it a booklet. Then label the pages from front to back. Label the front cover "FC", the inside pages 0 through 9, and the back cover "BC". Then take the booklet apart and look at each sheet. I call the outer sheet "FC/0--9/BC", the next sheet "1/2--7/8", and the inner sheet "3/4--5/6". My program prints on pages 0 through 9, and leaves FC and BC blank.

My program works under the assumption that the text file containing all the names and addresses is already in memory. It just happens in my case to begin at $2121. Each character is in ASCII with the high bit on, and the file image ends with a $00 byte. You could make obvious adjustments to the program to work with text in some other position or a different format.

My program also assumes that there are blank lines between each entry, and no blank lines within any entry. This helps me to easily be sure that an entry is not printed partly on one page and partly on another. I counted the number of lines in my entire directory, finding 341 lines. Since I am printing on ten pages, that is about 34 lines per page. I decided to print at least 32 lines on each page, and then finish printing whatever entry was in progress. This resulted in the last page being a little shorter than the rest, and left enough bottom margin on most pages for adding extra names later.

My program begins by setting up the Epson printer. You will need to modify this setup to correspond with the capabilities of your own printer and interface card. Lines 1220-1250 install the printer hook. As you can see, my printer interface card was in slot 5, which is highly unusual. Yours is probably in slot 1, so you will want to change the "$C500" to "$C100". The setup string is shown in lines 1640-1720. The first two lines are control-I commands to the interface card, and the rest are ESCAPE commands to the Epson printer.

The next major activity in my program involves building a table of addresses which point to the beginning of each booklet page in the text buffer. The subroutine which does this is shown in lines 2450-2700. It simply starts with the first page at the given address of $2121 (probably different in your case), scans to the beginning of the next page and saves the address, and so on for 11 pages. The extra page does not really exist, but it helps me set up another table which is used to mark the END of each page. I store the addresses in two different tables: PAGES and SPAGES. PAGES is used for the beginning and current address in each page, and is modified during printout. SPAGES is used to find the end of each page, and is not modified during printout.

Another value you may want to change is the number 32 in line 2780. This is the minimum number of lines to be printed on each page. It turns out my longes page had 35 lines printed on it. If you use a different booklet size or a different ver- tical spacing, you will probably want a different value here.

Looking back at the main program, lines 1360-1450 print the front side of the sheet. Line 1470 pauses the program so that I can turn the paper over and re-insert it into the printer, and then lines 1480-1540 print the back side. My subroutine PRINT.TWO.PAGES does the real work here. The page number (0-9) of the left page is in the A-register and that of the right page in the X-register.

PRINT.TWO.PAGES assumes there are a total of 50 lines per page. You should change the number on line 1830 for a different size page. This count includes the top and bottom margin. I forgot to include a little extra for trimming, but it worked out all right for me. The number 42 on line 1860 controls the starting column for the right-hand page. I had a 33-character line, a 9-character center page gap, and the right page starts in column 43. You may need to change this number also.

The number 35 on line 1880 was arrived at experimentally. Between adjusting the tractors left and right to position the paper, and changing this number 35 up or down, you can center the blank space between the left and right pages exactly over the center of the printer paper. This is important, because when we turn the paper over and print on the back side we want the pages to line up correctly.

Lines 1900-1960 print a line for the left-hand page and tab to the starting column for the right-hand page. Then lines 1980-2040 print a corresponding line for the right-hand page. Of course, either or both of these may be blank lines.

I wish I could show you what the finished product looks like, but that is not practical. If you stop by some time, I will show it to you. Meanwhile, you could adapt this to your own printer and needs and make your own booklet.

A project comes to mind, which one of you might like to tackle. How about parameterizing the variables so that it could make booklets of various dimensions and page counts, and integrate it into a word processor? The S-C Word Processor naturally comes to mind, since so many of you have purchased it with the source code.

  1000 *SAVE PAGINATOR
  1010 *--------------------------------
  1020        .DUMMY
  1030        .OR 0        DEFINE PAGE ZERO VARIABLES
  1040 PAGES  .BS 24       Room for 12 pointers
  1050 PAGE   .BS 1
  1060 PNTR   .BS 2
  1070 LPAGE  .BS 1
  1080 RPAGE  .BS 1
  1090 LINES  .BS 1
  1100 TABBER .BS 1
  1110        .ED          END OF DUMMY SECTION
  1120 *--------------------------------
  1130 MON.CH .EQ $24
  1140 *--------------------------------
  1150 BEGIN  .EQ $2121    My text buffer starts here
  1160 *                   and ends with a 00 byte.
  1170 *--------------------------------
  1180 CROUT  .EQ $FD8E
  1190 COUT   .EQ $FDED
  1200 *--------------------------------
  1210 T
  1220        LDA #$C500   Hook into my printer card
  1230        STA $36          in slot 5
  1240        LDA /$C500   (Yours is probably in slot 1)
  1250        STA $37
  1260 *---Setup Printer----------------
  1270        LDY #0       Send setup characters to my 
  1280 .1     LDA STRING,Y      printer interface card
  1290        JSR COUT          and to my Epson EX-800
  1300        INY               Printer.
  1310        CPY #STR.LEN
  1320        BCC .1
  1330 *---Find the pages---------------
  1340        JSR BUILD.POINTER.TABLE
  1350 *---Print the front side---------
  1360        JSR CROUT
  1370        LDA #2       Print front side of 1/2--7/8
  1380        LDX #7
  1390        JSR PRINT.TWO.PAGES
  1400        LDA #4       Print front side of 3/4--5/6
  1410        LDX #5
  1420        JSR PRINT.TWO.PAGES
  1430        LDA #0       Print front side of FC/0--9/BC
  1440        LDX #9
  1450        JSR PRINT.TWO.PAGES
  1460 *---Print the back side----------
  1470        JSR FLIP.PAPER.OVER
  1480        JSR CROUT
  1490        LDA #8       Print back side of 1/2--7/8
  1500        LDX #1
  1510        JSR PRINT.TWO.PAGES
  1520        LDA #6       Print back side of 3/4--5/6
  1530        LDX #3
  1540        JSR PRINT.TWO.PAGES
  1550 *---Disconnect Printer-----------
  1560        LDA #$C300   Rehook the 80-column screen
  1570        STA $36
  1580        LDA /$C300
  1590        STA $37
  1600        RTS
  1610 *--------------------------------
  1620 *   Printer Setup String
  1630 *--------------------------------
  1640 STRING .HS 09.4E    Turn off Video
  1650        .HS 09.58    Bit 7 = 0
  1660        .HS 1B.53.01 Subscript
  1670        .HS 1B.41.05 5/72 inch vertical
  1680        .HS 1B.23    Cancel Bit 7
  1690        .HS 1B.3D    Bit 7 = 0
  1700        .HS 1B.4D    Elite
  1710        .HS 1B.0F    Condensed
  1720 STR.LEN .EQ *-STRING
  1730 *--------------------------------
  1740 FLIP.PAPER.OVER
  1750 .1     LDA $C000    Really just wait until I
  1760        BPL .1       manually turn it over.
  1770        STA $C010    Signal by typing any key.
  1780        RTS
  1790 *--------------------------------
  1800 PRINT.TWO.PAGES
  1810        STA LPAGE    Left Page Number
  1820        STX RPAGE    Right Page Number
  1830        LDA #50      Print 50 Lines per Page
  1840        STA LINES
  1850 *---Print left side--------------
  1860 .1     LDA #42      Right Page begins in col. 43
  1870        STA TABBER
  1880        LDA #35      Left Margin to center on page
  1890        STA MON.CH
  1900        LDA LPAGE
  1910        JSR SETUP.PAGE.PNTR
  1920        BCS .2       ...end of this page
  1930        JSR PRINT.LINE
  1940 .2     JSR TAB.TO.MIDDLE
  1950        LDA LPAGE
  1960        JSR UPDATE.PAGE.PNTR
  1970 *---Print right side-------------
  1980        LDA RPAGE
  1990        JSR SETUP.PAGE.PNTR
  2000        BCS .3       ...end of this page
  2010        JSR PRINT.LINE
  2020 .3     JSR CROUT
  2030        LDA RPAGE
  2040        JSR UPDATE.PAGE.PNTR
  2050 *--------------------------------
  2060        DEC LINES    Count down for the page
  2070        BNE .1       More lines on this page
  2080        RTS          Finished
  2090 *--------------------------------
  2100 SETUP.PAGE.PNTR
  2110        ASL          Double page number for index
  2120        TAX
  2130        LDA PAGES,X  Get pointer to current
  2140        STA PNTR          to current place on page
  2150        CMP SPAGES+2,X    ...compare to end of page
  2160        LDA PAGES+1,X
  2170        STA PNTR+1
  2180        SBC SPAGES+3,X    Leave .CS. if end of page.
  2190        RTS               ...or .CC. if not end yet.
  2200 *--------------------------------
  2210 UPDATE.PAGE.PNTR
  2220        ASL          Double page number for index
  2230        TAX
  2240        LDA PNTR     Save current place on page
  2250        STA PAGES,X       for next line
  2260        LDA PNTR+1
  2270        STA PAGES+1,X
  2280        RTS
  2290 *--------------------------------
  2300 PRINT.LINE
  2310 .1     JSR NEXT.CHAR     Print up to <RETURN>
  2320        BCS .2            ...or end of text
  2330        DEC TABBER        Count the character
  2340        JSR COUT          and print it.
  2350        JMP .1            ...next character
  2360 .2     RTS
  2370 *--------------------------------
  2380 TAB.TO.MIDDLE
  2390 .1     LDA #" "          TABBER has remaining
  2400        JSR COUT          blank count to reach
  2410        DEC TABBER        the right side.
  2420        BNE .1
  2430        RTS
  2440 *--------------------------------
  2450 BUILD.POINTER.TABLE
  2460        LDA #BEGIN        Start at the beginning 
  2470        STA PNTR              of my text buffer.
  2480        STA PAGES         First page starts here.
  2490        LDA /BEGIN        Hi byte of same...
  2500        STA PNTR+1
  2510        STA PAGES+1
  2520        LDA #1            for PAGE = 1 to 11
  2530        STA PAGE
  2540 *--------------------------------
  2550 .1     JSR SCAN.TO.END.OF.PAGE
  2560        LDA PAGE     Save pointer to beginning of page
  2570        ASL               double for index
  2580        TAX
  2590        LDA PNTR          It is also END+1 for
  2600        STA PAGES,X           previous page.
  2610        STA SPAGES,X
  2620        LDA PNTR+1        Hi-byte of same.
  2630        STA PAGES+1,X
  2640        STA SPAGES+1,X
  2650 *--------------------------------
  2660        INC PAGE          next PAGE
  2670        LDA PAGE
  2680        CMP #12           (up to 12)
  2690        BCC .1            ...do another page
  2700        RTS               finished
  2710 *--------------------------------
  2720 SCAN.TO.END.OF.PAGE
  2730        JSR SCAN.32.LINES
  2740        JSR SCAN.TO.END.OF.CURRENT.ENTRY
  2750        RTS
  2760 *--------------------------------
  2770 SCAN.32.LINES
  2780        LDX #32
  2790 .1     JSR SCAN.TO.NEXT.LINE
  2800        DEX
  2810        BNE .1
  2820        RTS
  2830 *--------------------------------
  2840 SCAN.TO.END.OF.CURRENT.ENTRY
  2850 .1     JSR SCAN.TO.NEXT.LINE
  2860        JSR NEXT.CHAR
  2870        BCC .1       ...not <RETURN> or end-of-text
  2880        BEQ .4       ...found end-of-text
  2890 .2     JSR NEXT.CHAR
  2900        BEQ .4       ...found end-of-text
  2910        BCS .2       ...found another <RETURN>
  2920        LDA PNTR     Backup the text pointer
  2930        BNE .3            to the <RETURN>
  2940        DEC PNTR+1
  2950 .3     DEC PNTR
  2960 .4     RTS
  2970 *--------------------------------
  2980 SCAN.TO.NEXT.LINE
  2990 .1     JSR NEXT.CHAR
  3000        BCC .1       ...not end-of-text or <RETURN>
  3010        RTS
  3020 *--------------------------------
  3030 *   Get next character from text buffer
  3040 *      Return .CC. if not <RETURN> or end-of-text
  3050 *             .CS. and .EQ. if end-of-text
  3060 *             .CS. and .NE. if <RETURN>
  3070 *--------------------------------
  3080 NEXT.CHAR
  3090        SEC          Return .CS. if <eot> or <return>
  3100        LDY #0
  3110        LDA (PNTR),Y
  3120        BEQ .2       ...it is end-of-text
  3130        INC PNTR
  3140        BNE .1
  3150        INC PNTR+1
  3160 .1     CMP #$8D
  3170        BEQ .3
  3180        CLC
  3190 .2     RTS
  3200 .3     INY          Return .CS. and .NE. if <RETURN>
  3210        RTS
  3220 *--------------------------------
  3230 SPAGES .BS 24
  3240 *--------------------------------
  9999   .LIF

Apple Assembly Line (ISSN 0889-4302) 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.)