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.
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 *-------------------------------- |
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.
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"---
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 *-------------------------------- |
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.
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.)