In This Issue...
Macro Assembler in EPROM
A number of you have asked about the possiblility of getting the S-C Macro Assembler in EPROM to put on an Apple Firmware card, or a CCS 12K Eprom card. If you want to do it, and know how to modify the firmware card to accept EPROMs, I will send a set of 5 EPROMs containing the assembler for $64. If you also need the monitor in EPROM, add another $16. The assumption will be that you have Applesoft on the mother board, and that you already own the S-C Macro Assembler on disk.
Advertising in AAL
Due to the increased costs of printing more than 1600 copies per month, and with the desire to limit the percentage of advertising pages to less than 30% each month, I have decided to raise the page rate again.
For the June 1982 issue, the price will be $50 for a full page, $30 for a half page. So-called "classified" ads, of up to forty words, will be $5.
The Best Book So Far for Beginners
Roger Wagner's book for beginners wanting to learn assembly language programming is now out, at $19.95. (My price is only $18.) Called "Assembly Lines: The Book", it began as simply a reprint of the series Roger writes for Softalk Magazine. But there is a lot more material in the book, and 100 pages of Appendices. Appendix B, 70 pages, is a very lucid description of every 6502 opcode. If you rank yourself as a beginning assembly language programmer, this book will be a tremendous help.
Good Price on the NEC printers
I can ship you an NEC PC-8023A-C dot matrix printer for only $595. I believe the normal list price is $795, but mail order prices are generally less. I also have the Grappler interface card and cable, configured for the NEC printer, for only $150 (normally $175, I think). And if you want both printer and interface at the same time, the combined price is only $695.
I have two of these printers, and like them better than my Epson MX-80. Why? Faster: 100cps instead of 80cps. Fully equipped: standard features include graphics, tractor feed, and friction feed. Handier: the friction feed is just like a typewriter, platen and all; and option switches, should you wish to change them, are accessible without removing any screws. I run one of them with an Epson parallel interface, and the other with the Grappler.
If you would rather have a Spinwriter (that is what this newsletter is printed on), call me for prices.
Vinyl Diskette Pages for your S-C Assembler Binder
I am having 1000 special pages manufactured. They will fit the binder that comes with the Macro Assembler, and will hold one diskette each and a 3x5 index card. For $6 I'll send you ten of them. For $12 I'll send them in a binder. For $36 you can have a binder with ten blank diskettes in vinyl pages. The binder is also just right for storing back issues of AAL.
I found a portion of code tucked away in DOS that will perform an RWTS call for you, doing away with the necessity of finding a place to put a controlling subroutine, an IOB, etc.
As you know, RWTS (Read/Write Track and Sector) gives the programmer the ability to read a sector from any specified track and put it in a buffer in RAM. It also allows the programmer to manipulate the buffer and write it back out to any specified track and sector on the disk.
In this 48K DOS routine, which happens to be the same for 3.3 as well as 3.2, all the programmer has to do is to plug in the track and sector desired and whether he wants to read it from disk to the buffer, or write it from the buffer to the disk. (The buffer is a fixed 256-byte location beginning at $B4BB (46267).) A simple CALL 45111 or a JSR $B037 will then perform the transfer. (You must remember to restore the original Read/Write code back to "2" when you are finished, so that the system can write to the directory when it needs to).
Here is a disassembled and commented version of the routine, which (for lack of a better term) I have named "WRTDIR". This should aid in the development of programs that need to examine or alter the contents of a disk.
This routine, which normally writes a directory sector to the disk from the buffer at $B4BB.B5BA (46267-46522), can be used as a general RWTS utility by plugging in:
Value Name $Loc Loc Read/Write (1/2) RW $B041 45121 Track No. ($0-$22) TK $B397 45975 Sector No. ($0-$F) SC $B398 45976
Then call 45111 or JSR $B037 and set RW to 2 when done.
1000 .OR $B037 1010 .TF B.WRTDIR 1020 *-------------------------------- 1030 BUFSTHI .EQ $AAC6 1040 BUFSTLO .EQ $AAC5 1050 CALLRWTS .EQ $B052 1060 IOBBUF .EQ $B7F0 1070 RW .EQ 2 1080 SC .EQ $B398 1090 TK .EQ $B397 1100 *-------------------------------- 1110 WRTDIR JSR SETBUFAD 1120 LDX TK 1130 LDY SC 1140 LDA #RW 1150 JMP CALLRWTS 1160 *-------------------------------- 1170 SETBUFAD LDA BUFSTLO PUT BUFFER'S 1180 STA IOBBUF STARTING ADDRESS IN 1190 LDA BUFSTHI INPUT OUTPUT BLOCK 1200 STA IOBBUF+1 1210 RTS |
1. Loading the Language Card version: When you type "EXEC LOAD LCASM", the language card is loaded with a copy of the monitor from the Apple mother board, and with the file S-C.ASM.MACRO.LC. The EXEC file also makes a small modification to the memory image depending on which language you have on the mother board.
If you have serial number M-5275 or earlier, the EXEC file does not do the final step of turning on the Assembler. I expected you to type the DOS command "INT" (if Applesoft is on the mother board) or "FP" (if Integer BASIC is on the mother board) to enter the Assembler.
You can make a minor change to the EXEC file to make it automatically turn on the assembler after loading. The next to the last line of the EXEC file is now "C082"; change it to "C080" for automatic turn-on.
Here is a step-by-step procedure for the change. Try it on a COPY of the master disk, in case you make a mistake.
By the way, Bob Potts (from the Bank Of Louisville) was here last week. He brought along a Corvus 5-meg drive, so we put the Language Card version of the Assembler on it. For some reason which we can't explain, the EXEC file hangs up after the BLOAD (but only if the language card has not been loaded since power up). We changed the EXEC file slightly, and it always worked. Instead of doing the BLOAD while in the monitor, we did it from Applesoft. Here is the new version of the file:
REM LOAD S-C MACRO ASSEMBLER REM INTO THE LANGUAGE CARD CALL-151 C081 C081 F800<F800.FFFFM BLOAD S-C.ASM.MACRO.LC 300:A9 4C CD 00 E0 F0 12 8D 00 E0 A9 00 8D 01 E0 A9 D0 8D 02 E0 A9 CB 8D D1 03 60 300G C080 3D3G
You may also want to change the HELLO file to include an option to EXEC LOAD LCASM automatically.
2. If you have a Language Card, and your Assembler has a serial number of about M-5030 or earlier, the memory limits are not set up properly in all cases.
Check your copy of S-C.ASM.MACRO.LC by loading it into the language card and typing "$D2C6" from inside the assembler. If you don't have $A0 there, then you need to install this patch:
1. Type in these monitor commands: :$D2C6:A0 0C :$D2C8:20 1E D3 A9 00 91 58 85 :$D2D0:D9 AD 00 E0 C9 4C F0 08 2. Type "BSAVE S-C.ASM.MACRO.LC,A$D000,L$231F".
3. If you have serial number M-5287 or earlier, a more difficult to apply patch is needed to correct a problem in printing the symbol table. If you are using the .TI directive, and if you have several lines of local labels in the symbol table listing, and if the page break comes between two such lines, the listing is messed up in a disastrous way.
To fix the S-C.ASM.MACRO, do the following (very carefully):
1. Get into the assembler by typing "BRUN S-C.ASM.MACRO". 2. Type the following monitor commands: :$2AF<26AF.26D5M :$26B1<2AF.2D5M :$26AF:84 2F :$26C1:CA 3. Type "BSAVE S-C.ASM.MACRO,A$1000,L$21D3"
To repair the language card version, do the following:
1. EXEC LOAD LCASM, and get into the assembler by typing INT (unless you already made the change to the EXEC file noted above). 2. Type the following monitor commands: :$C083 C083 :$2AF<E7FB.E821M :$E7FD<2AF.2D5M :$E7FB:84 2F :$E80D:16 3. Type "BSAVE S-C.ASM.MACRO.LC,A$D000,L$231F"
If these patches and my instructions seem too difficult, you can send me $2.50 and your S-C Macro Assembler diskette; I will update it with the new HELLO program, the new LOAD LCASM file, and the patched copies of the assembler.
While working on my new soon-to-be-released data file system, I came to see the importance of a speedy 6502 block move routine. I have resisted "moves" like the coming of winter. I have a super two pass sort routine that is very fast. Providing a person has less than 2000 files there is no need to do much file moving, since only pointers need move. (My system has a 64K card, giving me a 112K system.)
Unfortunately, the real world needs some 10,000 or more files, and these of course must be sorted too. By physically moving the files as directed by the sorted pointers, and then moving all this to disk, it is possible to use a merge sort to get the whole job done in the least amount of time. With this preamble behind us, let's get on the move!
I have benchmarked three approaches to moving blocks: the monitor move down (located at $FE2C) which I'm sure you all have used, and its variation, a similar move up routine. Next is the Applesoft block move, and third is a self modifying move which I call "Quick Move".
To ease such a tedious undertaking, I have included a BASIC connection to pass variables and determine the benchmark precision. To help further, I have added a hex converter, a memory dump routine, and an automatic 3D0G vector using the ctrl-Y command from the monitor. To help with the problem of what block of memory was moved where, I wrote a memory fill routine. This acts to place the memory address back into memory, on two-byte boundaries. You can easily read memory to see where it came from.
My first benchmark was a block move of 10,000 bytes made 100 times. The next was a move of 10,240 bytes, again made 100 times. Here are the conditions and resulting times:
mon up mon dn AS QM Case I Lo=18674=$48F0 47 53 17.2 15.3 seconds Case II Lo=18432=$4800 48.7 54.7 16.7 14.7
Note: 0.5 seconds of these values due to BASIC overhead.
As you can see, the old monitor move is not made for high speed moves. For one thing, a two-byte subtraction is carried out for each byte that is moved. It is much more efficient to do the subtraction only once, before you start. A closer look shows that it is faster to move more data, providing you move a whole number of memory pages! The time needed to move the "extra" 240 bytes was negative 0.5 seconds for the Applesoft block move and negative 0.6 seconds for the "Quick Move". There was no sensitivity to start and destination boundaries. "Quick Move" was 3.7 times faster than the monitor move!
I tried putting the first half of Quick Move on page zero at $A0, but the speed improvement was only 0.7 seconds (about 5%) over the time it took when located at $3000.
As a further note, each move routine requires its own parameter organization. If files are to be moved and not lost, attention must be paid to exact specification of end points and lengths.
100 GOSUB 300: REM INITIALIZE BINARY PROGRAM 105 A = 0: B = 0: C = 0: J = 0 106 TEXT : HOME : PRINT : PRINT " BLOCK MOVE TESTING"; SPC( 8)"LOOP = ";L + 1: PRINT : PRINT "(0) END" SPC( 13)"(1) TEST OVERHEAD": PRINT : PRINT "(2) M.MOVE UP" SPC( 7)"(3) M.MOVE DOWN" 107 PRINT : PRINT "(4) A.MOVE UP" SPC( 7)"(5) QUICK MOVE DOWN": PRINT : PRINT "(6) HEX DUMP" SPC( 8)"(7) ADDRESS FILL" 108 PRINT : PRINT "(8) HEX CONVERT" SPC( 5)"(9) EDIT": PRINT : PRINT "(10) MONITOR WITH CTRL-Y RETURN": PRINT 109 PRINT "(11) SET CALL LOOP = 100": PRINT 110 PRINT "PICK TEST NUMBER (0 TO 11) ";: INPUT I: IF I < 0 OR I > 11 THEN 110 112 HI = 28672: REM $7000 HI BLOCK 114 LO = 18672: REM $48F0 LO BLOCK 115 LO = 18432: REM $4800 LO BLOCK 118 LE = 2056: REM $808 LENGTH OF MOVE 130 ON I GOSUB 150,160,170,180,190,200,210,220,230,240,250 134 IF I = 0 THEN END 136 IF I > 5 THEN PRINT : PRINT "HIT ANY KEY ";: GET A$: GOTO 105 140 PRINT CHR$ (7);I;" DONE ";J;" TIMES": GET A$: GOTO 105 150 A = LO: B = LO: C = LO + 1: FOR J = 0 TO L: CALL BASE,A,B,C: NEXT : RETURN : REM TIME BASIC 160 A = LO: B = HI: C = HI + LE: FOR J = 0 TO L: CALL BASE,A,B,C: NEXT : RETURN : REM M.MOVE UP 170 A = LO: B = HI: C = LO - LE - 1: FOR J = 0 TO L: CALL BASE,A,B,C: NEXT : RETURN : REM M.MOVE DOWN 180 A = HI: B = LO: C = HI + LE: FOR J = 0 TO L: CALL BASE + 3,A,B,C: NEXT : RETURN : REM A.MOVE UP 190 A = LO: B = LO - LE: C = HI - LO: FOR J = 0 TO L: CALL BASE + 6,A,B,C: NEXT : RETURN : REM QUICK MOVE DOWN 200 CALL BASE + 9,LO - LE - 32,0,HI + LE + 32: RETURN 210 CALL BASE + 12,LO - LE - 16,HI + LE + 16,0: RETURN 220 HOME : VTAB 8: PRINT "INPUT DECIMAL NUMBER ";: INPUT N: CALL BASE + 15,N: RETURN : REM HEX CONVERTER 230 TEXT : HOME : POKE 33,33: LIST : END 240 HOME : FOR I = 1017 TO 1018: A = PEEK (I - 40): POKE I,A: NEXT : POKE 1016,76: POKE 72,0: CALL - 151 250 L = 99: GOTO 105 300 HIMEM: 12287: REM $2FFF 305 BASE = 12288: IF PEEK (BASE) = 76 THEN RETURN : REM .OR $3000 310 D$ = CHR$ (13) + CHR$ (4) 320 PRINT D$"BLOAD B.BLOCK MOVE BENCHMARKS" 330 PRINT : RETURN 1000 ******************************* 1010 * * 1020 * BENCHMARKING BLOCK MOVES * 1030 * * 1040 * BY WILLIAM R. SAVOIE 3/82 * 1050 * LIMERICK TRAINING CENTER * 1060 * C/O GENERAL PHYSICS CORP. * 1070 * 341 LONGVIEW RD., LINFIELD * 1080 * PENNSYLVANIA ZIP 19468 * 1090 ******************************* 1100 1110 1120 *-------------------------------* 1130 * APPLE II PAGE ZERO MEMORY USE * 1140 *-------------------------------* 1150 1160 A1L .EQ $3C MONITOR 1170 A1H .EQ $3D USE 1180 A2L .EQ $3E FOR 1190 A2H .EQ $3F BLOCK 1200 A3L .EQ $40 MOVE 1210 A3H .EQ $41 1220 A4L .EQ $42 1230 A4H .EQ $43 1240 FACMO .EQ $A0 FP REGISTER 1250 FACLO .EQ $A1 FP REGISTER 1260 1270 *-------------------------------* 1280 * OTHER APPLE II MEMORY MAPPING * 1290 *-------------------------------* 1300 1310 BLTU .EQ $D39B BLOCK TRANSFER 1320 FRMNUM .EQ $DD67 FORMULA=>NUM 1330 COMA .EQ $DEBE CHECK COMA 1340 AYINT .EQ $E10C MAKE INTEGER 1350 PRNTX .EQ $F944 PRINT X 1360 NXTA1 .EQ $FCBA INCR POINTER 1370 PRBYTE .EQ $FDDA PRINT A 1380 MOVE .EQ $FE2C MONITOR MOVE 1390 1400 1410 *-------------------------------* 1430 .OR $3000 1440 .TF B.BLOCK MOVE BENCHMARKS 1450 *-------------------------------* 1460 1470 * THIS CODE ALLOWS SIMPLE ENTRY WITHOUT THE & COMMAND 1480 BEGIN JMP MONITOR.MOVE 1490 JMP APPLESOFT.MOVE 1500 JMP QUICK.MOVE 1510 JMP DUMP HEX OUTPUT 1520 JMP FILL LABLE MEMORY 1530 1540 * TO HELP A HEX CONVERTER 1550 CONVERT 1560 JSR GETVAR GET VARIABLE 1570 JSR PRNTX HI BYTE OUT 1580 LDA FACLO GET LOW BYTE 1590 JMP PRBYTE HEX OUTPUT 1600 .PG 1610 * THIS CODE FILLS MEMORY WITH IT'S OWN ADDRESS 1620 * WHICH IS VERY USEFULL FOR CHECKING BLOCK MOVES 1630 FILL JSR GM GET VARIABLES 1640 .01 LDY #$01 TWO BYTE OFFSET 1650 LDA A1L GET LOW BYTE 1660 STA (A1L),Y WRITE ADDRESS LO 1670 DEY MOVE LEFT 1680 LDA A1H GET HI ADDRES 1690 STA (A1L),Y WRITE TO MEMORY 1700 JSR NXTA1 INCREMENT PTR 1710 JSR NXTA1 TWICE 1720 BCC .01 GO TELL DONE 1730 RTS 1740 1750 * A UTILITY DUMP T0 SEE MEMORY FROM BASIC 1760 DUMP JSR GM GET VARS 1770 LDA $C010 CLEAR STROBE 1780 .01 LDA $C000 GET KEY IF ONE 1790 BPL .03 GO DUMP HEX 1800 LDA $C010 CLEAR STROBE 1810 .02 LDA $C000 READ KEY AGAIN 1820 BPL .02 WAIT FOR KEY 1830 CMP #$8D 'RETURN' KEY? 1840 BEQ .04 EXIT 1850 LDA $C010 CLEAR STROBE 1860 .03 JSR $FDA3 8 HEX OUT 1870 LDA A2L END LOW 1880 CMP $A1 DESIRED LOW 1890 LDA A2H HI TOO 1900 SBC $A0 ENOUGH? 1910 BCC .01 GO TELL DONE 1920 .04 RTS 1930 1940 * GET VARIABLE FROM BASIC 1950 * MUST BE <65357 OR SYNTAX ERR 1960 * PLACE IN REGISTERS X AND A 1970 1980 GETVAR JSR COMA MUST SEE COMA 1990 JSR FRMNUM GET NUMBER 2000 JSR AYINT MAKE INTEGER 2010 LDX FACMO HI BYTE 2020 LDA FACLO LOW BYTE 2030 RTS 2040 2050 2060 * GET BASIC VARIABLES INTO THE 2070 * MONITORS WORK REGISTERS USED BY 2080 * COMMANDS M,V,G,L,S,T,-,+,..ETC 2090 2100 GM JSR GETVAR BLOCK START 2110 STA A1L LOW BYTE 2120 STX A1H HI BYTE 2130 JSR GETVAR BLOCK END 2140 STA A2L LOW 2150 STX A2H HI BYTE 2160 JSR GETVAR DEST. START 2170 STA A4L LOW 2180 STX A4H HI 2190 RTS 2200 .PG 2210 *-------------------------------* 2220 * THE OLD MONITOR MOVE * 2230 * MOVE BLOCK OF MEMORY UP/DOWN * 2240 *-------------------------------* 2250 2260 MONITOR.MOVE 2270 JSR GM SET UP MOVE 2280 LDA A1L START LOW 2290 CMP A4L END LOW 2300 LDA A1H START HI 2310 SBC A4H WHICH BIGGER? 2320 BCS MOVEDN GO DOWN IN MEM 2330 2340 MOVEUP LDY #$00 CLEAR INDEX 2350 .01 LDA (A2L),Y GET DATA 2360 STA (A4L),Y PUT DATA 2370 LDA A4L GET INDEX 2380 BNE .02 PAGE CROSS? 2390 DEC A4H HI BYTE 2400 .02 DEC A4L LOW BYTE 2410 LDA A2L 2420 CMP A1L END YET? 2430 LDA A2H 2440 SBC A1H 2450 LDA A2L 2460 BNE .03 PAGE CROSS? 2470 DEC A2H HI BYTE 2480 .03 DEC A2L LOW BYTE 2490 BCS .01 GO TELL DONE 2500 RTS 2510 2520 MOVEDN LDY #$00 CLEAR INDEX 2530 JMP MOVE MONITOR MOVE 2540 2550 2560 *------------------------------* 2570 * AND ALONG CAME THE PEOPLE AT * 2580 * MICROSOFT WITH THEIR MOVE * 2590 *------------------------------* 2600 2610 APPLESOFT.MOVE 2620 JSR GETVAR HI ADDRESS OF BLOCK TO MOVE 2630 STA $96 LOW 2640 STX $97 HI BYTE 2650 JSR GETVAR BLOCK END 2660 PHA SAVE TELL LAST 2670 TXA NEED 9B,9C 2680 PHA TO GETVARS 2690 JSR GETVAR HI ADDRESS OF DISTINATION 2700 STA $94 LO&HI BYTES 2710 STX $95 2720 2730 * WE USED $9B,9C TO GET THE TWO BYTE FP VALUE 2740 PLA END OF BLOCK 2750 STA $9C HI BYTE 2760 PLA 2770 STA $9B LOW BYTE TOO 2780 SEC SUBTRACT COMMING 2790 JMP BLTU A.MOVE 2800 .PG 2810 *-------------------* 2820 * MOVING IN RAM CAN * 2830 * BE EVEN FASTER * 2840 *-------------------* 2850 2860 QUICK.MOVE 2870 JSR GETVAR GET START 2880 STA .01+1 LOW BYTE 2890 STA .06+1 COPY HERE TOO 2900 STX .01+2 HI 2910 JSR GETVAR DESTINATION 2920 STA .02+1 LO 2930 STA .07+1 COPY HERE TOO 2940 STX .02+2 HI BYTE 2950 JSR GETVAR END ADDRESS 2960 TXA SET TEST FOR 2970 BEQ .05 MOVE<256? 2980 2990 * X=PAGE NUMBERS TO MOVE 3000 LDY #$00 INDEX=0 3010 .01 LDA $4800,Y SOURCE 3020 .02 STA $4000,Y DESTINATION 3030 INY NEXT BYTE 3040 BNE .01 SMALL MOVE 3050 .03 INC .01+2 HI SOURCE 3060 .04 INC .02+2 HI DESTINATION 3070 DEX DONE? 3080 BNE .01 Y=0 SO MOVE PAGE 3090 3100 * SET UP REMAINING MOVE 3110 .05 LDY $A1 LOW BYTE LENGTH 3120 BEQ .08 GO IF NONE 3130 LDA .01+2 COPY HI BYTE 3140 STA .06+2 FOR SOURCE 3150 LDA .02+2 AND 3160 STA .07+2 DESTINATION 3170 3180 * NOW WITH X=0 START MOVING LOW BYTE OF LENGTH 3185 * (Y) = REMAINING BYTES TO MOVE 3190 .06 LDA $4800,X SOURCE 3200 .07 STA $4000,X DESTINATION 3210 INX NEXT 3220 DEY MOVE ENOUGH? 3230 BNE .06 GO TELL DONE 3240 .08 RTS 3250 .LIST OFF |
Bill Linn, author of AED, stopped by the other day. Bill is a Vice President at Cullinane Corporation, and was in Dallas for a user convention. Since it was Sunday, and we are both earnest Christians, he spent the morning with Becky and me and the kids at church. Later we all went out for an excellent lunch at the local Harvey House.
He brought the latest version of AED along, and showed me all the new features. AED now has keyboard macros! They are user definable, but he has predefined quite a few for you. If you type two escapes in a row, the top 18 lines of the screen are used to display a menu of all the currently defined escape macros. Escape followed by some other character inserts the corresponding text string at the current cursor position.
There is a utility program on the disk for use in defining your own macro strings, and it is very easy to use. In fact, you use AED editing to modify simple DATA statements within the utility itself. When you are through with your changes, the utility modifies the macro table within AED and on the disk.
Again I say, if you are not fully satisfied with your current stable of Applesoft programming aids, you owe it to yourself to buy AED. It will save you countless hours of frustrating retyping as you create and edit and restructure and debug and modify Applesoft programs.
Last month I sent Bob a recursive macro definition that he put in the AAL for everyone to see. In case you have forgotten what recursive means, let me explain it somewhat. If you have a macro that calls itself under certain conditions, that macro is called 'recursive'. It's kind of like the plastic cup I had when I was little. There was a picture on the cup of a little bear sitting in a high-chair. The bear was holding a plastic cup and on the cup was a picture of a little bear in a high-chair holding a plastic cup with a picture of a bear in a high-chair holding a cup with a picture of a bear......
I always used to wonder how many bears there were and how big the littlest one was. Now, when we are using recursion in our macros we want to be sure we know that there is a last little bear -- a last call -- a way of leaving the lowest level of recursion. Otherwise (since each recursive call uses up some memory/stack space) we will soon run out of room to store the return information and BOOM goes the assembly.
My macro uses the principle of 'divide and conquer' to allocate a chosen number of bytes all of which hold the value we want them to have. We might use this macro with a table of 128 bytes. All non-alphabetic characters (codes $0 to $40, plus assorted others) will have the value $FF in their corresponding bytes in the table. All alphabetics will have a number indicating their relative frequency in English text. We could set up the table with lines and lines of hex strings for all the non-alphabetics. Or we could let the program filter out the alphabetics and use a shorter table. But for the sake of the example let us assume we need the program to be as fast as possible and memory space is no object.
Here is the macro definition I came up with:
.MA DB Macro name is "DB" .DO ]1<2 If only one left, .DA ]2 generate a data byte .ELSE If more than one left, >DB ]1/2,]2 call DB for half of them, >DB ]1+1/2,]2 and call DB again for the other half .FIN .EM |
Here is the table I talked about:
>DB $41,#$FF 65 bytes filled with $FF .HS 00000000 upper case (fill in your own frequencies) ...... >DB 5,#$FF 5 bytes filled with $FF .HS 00000000 lower case ...... >DB 5,#$FF 5 bytes filled with $FF |
Here is a sample program to use such a table:
LDA CHARACTER.BYTE get character TAY LDA TABLE,Y get frequency ...... and then you have to use it |
The macro calls itself to set up half of the area desired and then calls itself again to set up the other half. The adding of one to the second call makes sure that both odd and even values for the first parameter will work. If the call only needs one byte to be set up then a .DA is used to take care of it. That provides the end of the little bears -- when the first parameter is one.
When I needed a macro like this my first idea was to have each recursive call take care of one byte and then call itself to take care of the rest. If the macro was called with zero repetitions then nothing would be done except end the macro. The problem with that method is the amount of stack space used as the recursion goes to very deep levels. The method used in the example will only recurse, for example, 8 levels to generate 127 bytes of data.
By the way, notice that you must put the pound sign (#) on the second parameter if you want to generate single bytes. Leaving it off will generate two-byte values of data. I chose that method to make the macro more flexible. You might want to put the pound sign (#) inside the macro to make it safer in case you always want to generate single bytes of data. Also, you can use calculated values like #'F+$80 to generate tables of some character value.
The assembly of recursive macros produces quite a few extra lines in the listing, so after checking it out you will probably want to turn off the listing of the macro expansion with ".LIST MOFF". Here is a sample listing with the macro expansion listing on:
1000 *-------------------------------- 1010 * LEE MEADOR'S SECOND RECURSIVE MACRO 1020 *-------------------------------- 1030 .MA DB 1040 .DO ]1<2 1050 .DA ]2 1060 .ELSE 1070 >DB ]1/2,]2 1080 >DB ]1+1/2,]2 1090 .FIN 1100 .EM 1110 *-------------------------------- 0800- 1120 >DB 3,#0 0000> .DO 3<2 0000> .ELSE 0800- 0000> >DB 3/2,#0 0000> .DO 3/2<2 0800- 00 0000>> .DA #0 0000>> .ELSE 0000>> .FIN 0801- 0000> >DB 3+1/2,#0 0000>> .DO 3+1/2<2 0000>> .ELSE 0801- 0000>> >DB 3+1/2/2,#0 0000>>> .DO 3/2/2<2 0801- 00 0000>>> .DA #0 0000>>> .ELSE 0000>>> .FIN 0802- 0000>> >DB 3+1/2+1/2,#0 0000>>> .DO 3/2+1/2<2 0802- 00 0000>>> .DA #0 0000>>> .ELSE 0000>>> .FIN 0000>> .FIN 0000> .FIN |
Here is a routine to directly call RWTS (Read/Write Track/Sector), the subroutine in DOS that actually reads for or writes to the disk. Many programs use a routine like this to handle disk I/O, without all the time-consuming overhead of the DOS file manager.
All you need to do to use RWTS directly is to place certain information into an Input/Output control Block (IOB), and tell RWTS where the IOB is. Following is an explanation of the IOB (the addresses are those of DOS's own IOB; you can use it yourself, or build your own wherever is convenient):
Address Description $B7E8 Table type, always $01 $B7E9 Slot number times 16, usually $60 $B7EA Drive number, $01 or $02 $B7EB Volume number expected, $00 matches anything $B7EC Track number, $00 through $22 $B7ED Sector number, $00 through $0F $B7EE-F Address of Device Characteristics Table, $B7FB for DOS's own DCT $B7F0-1 Address of buffer, wherever you want $B7F2 Not used $B7F3 Byte count if partial sector, $00 normally $B7F4 Command $00 = SEEK $01 = READ $02 = WRITE $04 = FORMAT $B7F5 Error Code $00 = No errors $08 = Error in initialization $10 = Write protect error $20 = Volume mismatch $40 = Drive error $B7F6 Last volume number $B7F7 Last slot number $B7F8 Last drive number
The Device Characteristics Table (whose address is at $B7EE,EF) is a four-byte block containing information about the disk drive. For a standard Apple Disk II this block always contains $00 01 EF D8.
For our purposes, the most important items in the IOB are track, sector, buffer address, and command. By manipulating these, you can read any sector of the disk into any area of memory. All you need to do is set up the IOB, load the A- and Y-registers with the address of the IOB, and JSR $3D9. And if you decide to use the file manager's IOB, you can even set up the A- and Y-registers by a simple JSR $3E3.
RWTS will read the track and sector you choose into your 256-byte buffer. If there was a disk error, RWTS will return with the carry bit set and an error code stored in the IOB. It is then up to the user's program to decide what to do about the error. If there was no error, carry will be clear.
This month we'll set up a short program using the RWTS Caller to read an entire track of the disk into 16 consecutive pages of memory. DOS stores information on a track starting at sector $0F and working back to sector $00, so we must read a sector into the buffer, decrement the sector in the IOB, and increment the buffer pointer.
RWTS.CALLER:
Lines 1680-1850 set up the IOB, transferring values from the program variables.
Lines 1870-1890 load the address of the IOB and call RWTS.
Lines 1900-1910 are necessary to avoid confusing the system monitor. (RWTS and the monitor both use location $48.)
Lines 1960-2030 ring a warning if a disk error occurred, and display the error code.
TRACK READ:
Lines 1260-1350 initialize the variables and call input routines.
Lines 1370-1440 read the sectors from $0f through $00 into the buffer. Line 1410 will end the program if an error occurred.
Line 1460 will become a display routine. (Or, whatever processing you want to do on the buffer.)
Lines 1500-1650 will become input routines; right now they just set the track, buffer and command variables.
CAUTIONS:
1) These routines have very little error-checking. It is very easy to make a trivial error and lose information from a diskette. Always test an RWTS-calling program on a diskette yhou don't care about.
2) If you store information on a blank area of a diskette using these techniques, DOS doesn't know you have taken some space. Unless you modify the VTOC to show that sectors are used, DOS can overwrite your data. (What's a VTOC?, you say. Volume Table of Contents. We'll go into that another time.)
More:
There is more about RWTS on pages 94-98 of Apple DOS Manual, and a goldmine of information in Beneath Apple DOS, by Don Worth and Pieter Lechner. (Quality Software, 1981.)
1000 *SAVE TRACK READ 1010 *-------------------------------- 1020 SLOT .EQ $00 $60 OR $70 1030 DRIVE .EQ $01 1 OR 2 1040 VOLUME .EQ $02 0 = DON'T CARE 1050 TRACK .EQ $03 $00 TO $22 1060 SECTOR .EQ $04 $00 TO $0F 1070 BUFFER .EQ $05,06 1080 COMMAND .EQ $07 1 = READ, 2 = WRITE 1090 PREG .EQ $48 1100 * 1110 RWTS .EQ $3D9 1120 * 1130 IOB .EQ $B7E8 DOS'S OWN IOB 1140 IOB.SLOT .EQ $B7E9 1150 IOB.DRIVE .EQ $B7EA 1160 IOB.VOLUME .EQ $B7EB 1170 IOB.TRACK .EQ $B7EC 1180 IOB.SECTOR .EQ $B7ED 1190 IOB.BUFFER .EQ $B7F0,F1 1200 IOB.COMMAND .EQ $B7F4 1210 IOB.ERROR .EQ $B7F5 1220 * 1230 PRBYTE .EQ $FDDA 1240 COUT .EQ $FDED 1250 *-------------------------------- 1260 SETUP 1270 LDA #$60 1280 STA SLOT SLOT 6 1290 LDA #$01 1300 STA DRIVE DRIVE 1 1310 LDA #$00 1320 STA VOLUME ANY VOLUME 1330 JSR GET.TRACK 1340 JSR GET.BUFFER 1350 JSR GET.COMMAND 1360 *-------------------------------- 1370 READ.TRACK 1380 LDA #$0F START AT SECTOR $0F 1390 STA SECTOR 1400 .1 JSR RWTS.CALLER READ ONE SECTOR 1410 BCS EXIT EXIT IF ERROR 1420 INC BUFFER+1 NEXT BUFFER PAGE 1430 DEC SECTOR NEXT SECTOR 1440 BPL .1 NOT DONE, READ NEXT SECTOR 1450 *-------------------------------- 1460 DISPLAY 1470 *-------------------------------- 1480 EXIT RTS 1490 *-------------------------------- 1500 GET.TRACK 1510 LDA #$11 1520 STA TRACK TRACK $11 (DIRECTORY) 1530 RTS 1540 *-------------------------------- 1550 GET.BUFFER 1560 LDA #0 1570 STA BUFFER BUFFER AT $4000 1580 LDA #$40 1590 STA BUFFER+1 1600 RTS 1605 .PG 1610 *-------------------------------- 1620 GET.COMMAND 1630 LDA #1 1640 STA COMMAND READ 1650 RTS 1660 *-------------------------------- 1670 RWTS.CALLER 1680 LDA SLOT TRANSFER 1690 STA IOB.SLOT VALUES 1700 LDA DRIVE INTO 1710 STA IOB.DRIVE IOB 1720 LDA VOLUME 1730 STA IOB.VOLUME 1740 LDA TRACK 1750 STA IOB.TRACK 1760 LDA SECTOR 1770 STA IOB.SECTOR 1780 LDA COMMAND 1790 STA IOB.COMMAND 1800 LDA BUFFER 1810 STA IOB.BUFFER 1820 LDA BUFFER+1 1830 STA IOB.BUFFER+1 1840 LDA #$00 1850 STA IOB.ERROR 1860 *-------------------------------- 1870 LDY #IOB LOAD IOB 1880 LDA /IOB ADDRESS 1890 JSR RWTS CALL RWTS 1900 LDA #$00 1910 STA PREG SOOTHE MONITOR 1920 BCS ERROR.HANDLER 1930 RTS 1940 *-------------------------------- 1950 ERROR.HANDLER 1960 LDA #$87 BELL 1970 JSR COUT RING 1980 JSR COUT ING 1990 JSR COUT ING 2000 LDA IOB.ERROR 2010 JSR PRBYTE DISPLAY ERROR CODE 2020 SEC EXIT WITH CARRY SET 2030 RTS |
Recently I was asked to come up with a machine language subroutine that involved using the Apple game buttons. Fortunately for me at the time, I forgot that my paddles were not plugged in. I was in for a rude awakening because when I tested the program, it said that all of the buttons were constantly being pushed!
I needed some additional programming to check whether the buttons were even plugged in. The problem occurs because the Apple button logic returns the same value for a pushed button as for a missing button. To get technical: in TTL logic, when an input pin of an IC chip is left unconnected, the chip thinks the pin is at a logic "1". The Apple buttons supply a logic "1" when they are plugged in and pushed. Hence, the hardware cannot tell the difference between a plugged-in-pushed-down button and a missing button.
What the hardware does know for sure is when a button is plugged in and not pushed. This is the only case in which a logic "0" is developed. I had to use this knowledge to write a program which could tell what a logic "1" really means. Since an installed unpushed button does unambiguously announce its presence by a "0" in bit 7 of the input byte, I could make a mask indicating which buttons appear to be installed.
I started by writing the GET.BUTTON.STATUS subroutine. It reads each of the three buttons, and packs the three bit-7's into one byte. The way I wrote it, bit 7 of the returned byte represents button 1, bit 6 is button 2, and bit 5 is button 3. If a button is installed and not pushed, the corresponding bit will be "1"; otherwise it will be "0".
Look at the listing, lines 1250-1350, and I'll describe how GET.BUTTON.STATUS works. I used an indexed loop, where X goes from 2 to 0, step -1, addressing all three of the buttons. Only bit 7 of a button byte is significant. I invert this bit (line 1280), and shift it into the carry status bit (line 1290). Then line 1300 rolls the bit into GB.PUSH. After all three have been read and rolled, I pick up GB.PUSH and zero out the lower five bits at line 1340.
Now lets look at the other three subroutines. GAME.BUTTON.INITIALIZE simply clears out GB.STAT. We have to start with GB.STAT = 0, so that as we discover each installed button we can set its bit. Call this subroutine once at the beginning of your overall run.
GAME.BUTTON.INSTALLED reads the current button status; remember that a "1" here indicates an installed but unpushed button. So line 1140 merges all "1" bits into GB.STAT. You need to call this subroutine several times, at time intervals of several seconds at least, to be sure that every installed button is noticed at least once. (The first few times you call it, you might be pushing down an installed button; then finally you let go, so this subroutine sees the button.)
GAME.BUTTON.PUSHED reads the current button status, and with a little boolean logic comes out with the final result: a "1" indicating an installed and pushed button, and a "0" meaning either a missing button or an unpushed button. The result is in the A-register, and also in GB.PUSH.
Here is a truth table of the logic involved:
GB.STAT CURRENT READING EOR AND STAT STAT 0 (no button) 0 (none or pushed) 0 0 0 (no button) 1 (unpushed) 1 0 1 (button) 0 (none or pushed) 1 1 1 (button) 1 (unpushed) 0 0
There are other possible complications in reading buttons, which I have not handled here. You might want to "debounce" the buttons, so that you don't get false indications of multiple pushes when the button begins to make or break contact. And, you might want to guarantee that any action which happens from a pushed button only happens once per push.
Lines 1400-1910 demonstrate the usage of the subroutines. After clearing the screen, the buttons are continuously monitored until you press any key on the keyboard. The status of each button will be displayed on the screen: button 1 on the to line, button 2 on the second line, and button 3 on the third line. If you hold a button pushed and then start the program, it will say "not installed" until you release the button; from then on it will track the button properly.
If you have the shift key mod installed in button 3, it will say "not installed" until you press the shift key; from then on it will say "pushed" when you are not pushing, and "not pushed" when you are. This is because the sense of the shift key is the opposite of the normal game paddle buttons.
1000 *-------------------------------- 1010 * GAME BUTTON SUBROUTINES 1020 *-------------------------------- 1030 GAME.BUTTON .EQ $C061 BASE ADDRESS 1040 *-------------------------------- 1050 .OR $800 1060 *-------------------------------- 1070 GAME.BUTTON.INITIALIZE 1080 LDA #0 1090 STA GB.STAT 1100 RTS 1110 *-------------------------------- 1120 GAME.BUTTON.INSTALLED 1130 JSR GET.BUTTON.STATUS 1140 ORA GB.STAT SET BITS OF ANY BUTTONS 1150 STA GB.STAT PLUGGED IN AND NOT PUSHED 1160 RTS 1170 *-------------------------------- 1180 GAME.BUTTON.PUSHED 1190 JSR GET.BUTTON.STATUS 1200 EOR GB.STAT MASK OUT BUTTONS WHICH 1210 AND GB.STAT ARE NOT PLUGGED IN 1220 STA GB.PUSH 1230 RTS 1240 *-------------------------------- 1250 GET.BUTTON.STATUS 1260 LDX #2 1270 .1 LDA GAME.BUTTON,X 1280 EOR #$80 INVERT SENSE 1290 ASL INTO CARRY BIT 1300 .2 ROR GB.PUSH 1310 DEX NEXT BUTTON 1320 BPL .1 1330 LDA GB.PUSH 1340 AND #$E0 CLEAR EXTRANEOUS BITS 1350 RTS 1360 *-------------------------------- 1370 GB.STAT .BS 1 1380 GB.PUSH .BS 1 1390 *-------------------------------- 1400 MON.CV .EQ $25 1410 MON.HOME .EQ $FC58 1420 MON.VTAB .EQ $FC22 1430 MON.CLREOL .EQ $FC9C 1440 MON.COUT .EQ $FDED 1450 TEST.MASK .EQ $00 1460 *-------------------------------- 1470 TEST JSR MON.HOME 1480 JSR GAME.BUTTON.INITIALIZE 1490 .1 LDA #0 1500 STA MON.CV 1510 JSR MON.VTAB 1520 JSR GAME.BUTTON.INSTALLED 1530 JSR GAME.BUTTON.PUSHED 1540 LDA #$84 1550 STA TEST.MASK 1560 .2 LDA TEST.MASK 1570 AND GB.PUSH 1580 BNE .3 PUSHED 1590 LDA TEST.MASK 1600 AND GB.STAT 1610 BNE .4 NOT PUSHED 1620 LDY #QTGONE-QTS NOT INSTALLED 1630 .HS 2C 1640 .3 LDY #QTPUSHED-QTS 1650 .HS 2C 1660 .4 LDY #QTNOTPSH-QTS 1670 JSR MSGOUT 1680 LSR TEST.MASK 1690 BCC .2 1700 LDA $C000 1710 BPL .1 1720 STA $C010 1730 RTS 1740 BCS .1 ...ALWAYS 1750 *-------------------------------- 1760 MSGOUT LDA QTS,Y 1770 PHA 1780 ORA #$80 1790 JSR MON.COUT 1800 INY 1810 PLA 1820 BPL MSGOUT 1830 JSR MON.CLREOL 1840 LDA #$8D 1850 JMP MON.COUT 1860 *-------------------------------- 1870 QTS 1880 QTPUSHED .AT /PUSHED/ 1890 QTNOTPSH .AT /NOT PUSHED/ 1900 QTGONE .AT /NOT INSTALLED/ 1910 *-------------------------------- |
When I received my copy of the S-C Macro Assembler, my first task was to make up a set of branch macro definitions to use in all my programs. This set will finally eliminate the need to check usage of BCC, BCS, etc., and generally make the programs more readable.
There are six branch-on-tests: >BLT, >BLE, >BGE, >BGT, >BEQ, AND >BNE. All of these would normally be used with two parameters, e.g. >BGT P1,P2...reads: if contents of accumulator is greater than P1 then branch to P2. The first four of these can be gainfully used with only one parameter, after a comparison. Sample program:
LDA #$8D CMP #$8E >BLT THERE ... results in a branch to THERE |
While >BEQ and >BNE are defined so as to work with only one parameter, there is no reason to so use them - it is easier to just use BEQ and BNE.
The macro >BRA (BRanch Always) is used with one parameter - any others are ignored. >BRA LABEL causes a jump to LABEL, up to +- 127 bytes away. To overcome this limitation I decided to put the macro facility to good use to provide for easy branching to any part of a program - this is necessary for writing relocatable code. I settled for a two-paramter code >JMP:
>JMP P1,P2 ... where P1 is the intermediate or final label you wish to branch to and P2 is the label for this instruction.
Instructions such as the foregoing are inserted anywhere you wish in the program (within 127 bytes of each other) to allow for unlimited branching whilst retaining relocatable code. The following is an example of how you might use the >JMP code:
1000 A etc. (more code....) 2000 >JMP A,B (more code....) 3000 >BRA B |
When a program designed as the above is run it will simulate an absolute jump to A. The >BRA B will branch to label B, which contains a >BRA A. This sequence of instructions is transparent to the rest of the program, as the first instruction in the >JMP is to skip around the the >BRA within the definition.
This use of macro definitions can easily be extended to the X and Y registers; simply substitute CPX's or CPY's for the CMP's.
Following are some examples of these macros at work:
>BLT #3,THERE....if (A) is less than 3 then go THERE >BGT $40,THIS....if (A) is greater than ($40) then go THIS >BEQ #'A,THAT....if (A) is equal to $41 then go THAT |
To use these macros in all your programs, place the command .IN MACRO.BRANCH.LIBRARY at the beginning of your source program.
1000 * MACRO BRANCH LIBRARY 1010 * BY R.F. 0'BRIEN 1020 *-------------------------------- 1030 * >BLT P1 (,P2) BRANCH IF (A) < P1...TO P2 1040 .MA BLT 1050 .DO ]#>1 1060 CMP ]1 1070 BCC ]2 1080 .ELSE 1090 BCC ]1 1100 .FIN 1110 .EM 1120 *-------------------------------- 1130 * >BLE P1 (,P2) BRANCH IF (A)<=P1...TO P2 1140 .MA BLE 1150 .DO ]#>1 1160 CMP ]1 1170 BEQ ]2 1180 BCC ]2 1190 .ELSE 1200 BEQ ]1 1210 BCC ]1 1220 .FIN 1230 .EM 1240 *-------------------------------- 1250 * >BGE P1 (,P2) BRANCH IF (A)>=P1...TO P2 1260 .MA BGE 1270 .DO ]#>1 1280 CMP ]1 1290 BCS ]2 1300 .ELSE 1310 BCS ]1 1320 .FIN 1330 .EM 1340 *-------------------------------- 1350 * >BGT P1 (,P2) BRANCH IF (A)>P1...TO P2 1360 .MA BGT 1370 .DO ]#>1 1380 CMP ]1 1390 BEQ :1 1400 BCS ]2 1410 :1 1420 .ELSE 1430 BEQ :1 1440 BCS ]1 1450 :1 1460 .FIN 1470 .EM 1480 *-------------------------------- 1490 * >BRA P1 BRANCH ALWAYS...TO P1 1500 .MA BRA 1510 CLV 1520 BVC ]1 1530 .EM 1540 *-------------------------------- 1550 * >BEQ P1 (,P2) BRANCH IF (A)=P1...TO P2 1560 .MA BEQ 1570 .DO ]#>1 1580 CMP ]1 1590 BEQ ]2 1600 .ELSE 1610 BEQ ]1 1620 .FIN 1630 .EM 1640 *-------------------------------- 1650 * >BNE P1 (,P2) BRANCH IF (A)<>P1...TO P2 1660 .MA BNE 1670 .DO ]#>1 1680 CMP ]1 1690 BNE ]2 1700 .ELSE 1710 BNE ]1 1720 .FIN 1730 .EM 1740 *-------------------------------- 1750 * >JMP P1,P2 BRANCH ALWAYS TO P1 BY 1760 * BRANCHING TO P2 (SEE ARTICLE) 1770 .MA JMP 1780 CLV 1790 BVC :1 1800 ]2 CLV 1810 BVC ]1 1820 :1 1830 .EM |