Tap Applesoft's Heartbeat
You can use machine language routines to enable Applesoft to read and rapidly process incoming data.
Imagine that your Apple is connected to some gizmo which feeds the Apple some data rapidly. The device could be, for example, a modem or some newfangled digital geiger counter monitoring an atomic reactor. In an example below, we will simulate this device with the game paddle buttons, or, if you have no paddles, with a mere piece of wire. The essential idea is that the attached device offers data to the Apple sporadically, and the data will be lost if it is not noticed and processed within a few milliseconds.
If you try to support this device with a program written in Applesoft BASIC, you will likely miss some of the data offered by the device, because Applesoft is rather slow. Assuming that such a problem does arise and must be solved, here's how.
Machine Language Patch Into CHRGET
Here is an intriguing exercise: type in and run the listing in Program 1. If you type it correctly, it will say "OK"; make sure you fix it if it says "OOPS." This program installs, but does not run, three tiny machine language routines. Now type CALL 909 and then run the program again. Inexplicably, it will make an irritating buzz for the 0.37 seconds it takes to run. Indeed, you can load and run almost any Applesoft program and listen to it run.
You may notice that difficult computations and lengthy array references are accompanied by buzzes, whereas fast-running code such as FOR/NEXT loops that do little more than count will produce brief musical tones. I do not suggest that this is a useful effect, but I hope it sparks your interest, for what is coming is a bit dull and difficult but results in a very powerful technique which you can harness to produce utterly amazing results at zero cost.
By the way, you can deactivate the noise-making routine and restore your Apple to normal by typing CALL 896. The DOS command FP is even more powerful; issue that if your Apple seems confused.
A Look Into CHRGET
Here is how the noise is caused. The Applesoft interpreter uses a tiny routine to fetch each byte of your program in turn as the program runs. The (valid) BASIC statement IF BAD THEN STOP is stored as six bytes, specifically the token for IF, the letters B, A, and D, and the tokens for THEN and STOP. The character-getting routine, which is known by the name CHRGET, will be invoked a total of seven times to execute all of this statement (the token for THEN is fetched twice, once to detect the end of the variable name BAD, and once to be executed).
Program 1 and the routine installed at location 909 introduce a detour into CHRGET so that the Apple's speaker is tweaked each time a character of the program is fetched. This of course makes the noise and accounts for the various buzzes and squeaks made by slow- and fast-running code. To see the actual machine language routine, enter the monitor via CALL -151 and enter 380L (number 380 followed by letter L) to see the routines at 896 ($0380), 909 ($038D), and 922 ($039A).
The CHRGET routine starts in location 177 ($00Bl), and can be listed by B1L (letter B, digit 1, letter L). You can verify if you wish that CALL 909 installs a JMP instruction at location 186 ($00BA), and CALL 896 restores the CMP and BCS instructions which belong there. You can return from the monitor to Applesoft by typing CTRL-C and pressing RETURN.
Now let's put this technique to use. If you have game paddles, identify PDL (1) and skip the rest of this paragraph. To simulate the button on PDL(l), you will need a piece of slender wire at least two feet long. Solid wire works better than multi-strand. Strip about one-eighth inch from each end. You shuld now turn off the Apple and open the cover carefully. Locate the GAME I/O connector at coordinates J8 on the motherboard, and stick an end of the wire into hole number three, which is third from the front on the right side.
Do be careful with this, because disaster awaits you if you pick the wrong hole, or are careless with the other end of the wire. Now close the cover of the Apple, letting the free end of the wire hang down away from the computer. Reach under the front edge of the keyboard and you will find the heads of some bolts. You will be touching the free end of that wire to one of these to simulate a press of the button. If you choose, you can loosen one of these, attach another piece of wire, tighten the bolt, and attach the two loose ends of wire to any type of switch, but this is not essential. When these preparations are complete, turn the Apple on again.
Catching Every Count
Now type in Program 2 and run it. Please note the lengthy loop in lines 130-140. This takes over half a minute to run and obviously contains none of the PEEK statements necessary to test for a press of button number one. Those tests are done by the machine language routine patched into CHRGET, at locations 922 through 965. When the program is running, press the button (or touch the wire to the bolt) as fast as you can count, and you will find that the Apple catches every single one. Actually, when you try to touch the wire to the bolt once, you almost certainly cause it to bounce and touch the bolt more than once, so the count will be higher than you expect, and never lower.
In this example the switch was tested by a few instructions in machine language. This powerful technique is possible only in machine language. Perhaps it is possible to devise a routine that would permit a few lines of BASIC to be invoked by the routine which interrupts CHRGET, but what would be the point? Our objective here is to support a rapid-fire device, and any attempt to do this in BASIC will, it is assumed, lead to missed data. At least that is where this article started.
Using The Keyboard Buffer
A totally practical application of intercepting CHRGET is a keyboard buffer, except for one troublesome detail. From time to time, in any program which handles strings, Applesoft must pause to accomplish "garbage collection" — in other words, to make available again some memory which has been used for storage of strings which were later discarded. This process usually takes from one to thirty seconds, but in an artificial and extreme case it could take over an hour!
During "garbage collect," Applesoft is totally out of touch with all external events, so the keyboard buffering routine has no way to service the keyboard. Nonetheless, the routine is of genuine help when a speedy typist is using a slow data entry program. In fact, even a moderately slow hunt-and-peck typist like me can occasionally leave Applesoft behind. With the buffer running, I never lose a key.
There are two other limitations. During processing of the LIST command, Applesoft is not using CHRGET, so the buffering routine has no chance at the keyboard. Also, when DOS is active, all BASIC functions are inactive, so again the keyboard cannot be serviced.
Program 3 shows the complete keyboard buffer program. The program occupies the first 512 points of the BASIC program area, so it destroys any Applesoft program already present.
Briefly, here is how the program works. A preliminary test verifies that Applesoft is active, for this program is inapplicable to Integer BASIC. Next, the program sees if the beginning-of-BASIC pointer has been altered to $0A01 (from the usual $0801). If so, a warm start is done, retaining the current Applesoft program; if not, the pointer is so altered, and the new routine of Applesoft is called. Then the "patch" to CHRGET is made, as in Programs 1 and 2.
The next step is a connection to the keyboard-servicing routine at the "hook" known as KSW. Whenever such a connection is what you need, you must let DOS know your intentions, or it will patiently remove your connection and restore its own hook. This is very easy — just CALL 1002 (or JSR $3EA in machine language). The program ends by entering Applesoft at the warm-start entry $E003.
By this point, the program really has not done anything except insinuate itself into the system and protect itself from harm. The actual buffer is the 256-byte area from $0900 to $09FF (2304 to 2559), and two one-byte counters look after data in the buffer. The counter BIX points to the next place where a key can be stored, and the counter BOX points to the next byte to be sent to whoever asks for a key.
For example, if BIX contains $2E and BOX contains $28, the operator has keyed six bytes ahead, and they are stored in locations $0928 through $092D. If the operator now keys exactly 250 more bytes before the running program asks for any more, the keyed bytes will be stored in $092E through $09FF, then the buffer will "wrap around" and more keys will be stored in $0900 through $0926. By this time the value in BIX will be $27, one less than that in BOX. That's 249 in addition to the six already there, and now the buffer is full, so the buffering routine will sound the "bell" when it cannot store the last byte keyed. At this point the operator must pause and wait for the program to catch up. I think this event is very unlikely.
Keys are detected and stored by the routine patched into CHRGET. A word of caution to anyone patching CHRGET: since BASIC uses this routine dozens or thousands of times a second, the patch must execute as fast as possible, else the program may be slowed to an unacceptable degree.
Does It Function?
When a key is wanted, the code at INLINK sees if one is in the buffer. If not, the standard ROM routine is called. If a key is available in the buffer, it is delivered, and the counter, BOX, is updated to account for the departed key. It is all very simple, mainly because of the eight-bit indexing automatically provided by the 6502's X-register. Indeed, if the buffer were any size but 256 bytes, the program would have been noticeably harder to write and debug.
OK, how do you key this program into your Apple? You could CALL -151 to get to the monitor, then type in all the hex stuff, 803 : 4C 09 08 4C 99 08, and so on. If you did the "homework" I assigned in last month's column, there is an easier way. Key in the pure Applesoft program in Program 4, then SAVE it, RUN it, and finally EXEC GEN KEYBUF. This final step will invoke the mini-assembler to build KEYBUF, save the result, and return control to the keyboard eventually. This process must destroy any Applesoft program in memory, so be sure you have saved Program 4 before typing the EXEC command!
To verify all this work, peer closely at the screen — the command JMP $083C should be in location 08A8. The acid test, of course, is "does it work?" Follow the instructions below to test your work, and when you actually make it work, you'll have a potent and versatile tool which makes your Apple a little bit better than it was before!
Table: How to use the Keyboard Buffer
Homework Assignment. Boot your System Master and LOAD BRIAN'S THEME. That is the program which displays pretty moire patterns in high resolution. Here is some code to add a fascinating effect! Type in the few lines in Program 5 and RUN the changed program. When the display starts acting oddly, play with the keyboard. The most recently pressed key controls the timing in a tiny machine language routine at location 600 ($258).
In my particular Apple, the keys W, K, 8, question mark, and especially CTRL-D, produce interesting effects. The machine language routine is completely relocatable, so it can be used without change in any place in memory where 26 bytes are free. So if you wish to use the routine in another program, change the variable ML to whatever suits you. The timing is so delicate that the effects change greatly when ML is just under a multiple of 256, so that a branch instruction crosses a page boundary. To stop this demonstration, you must press RESET, because the machine language routine treats CTRL-C as any other key.
10 REM 'TAP' DEMO 1 20 FOR I = 896 TO 935 30 READ X 40 Z = Z + X 50 POKE I, X 60 NEXT 70 IF Z < > 5155 GOTO 90 80 PRINT "OK" : END 90 PRINT "OOPS. Z = "; Z : END 896 DATA 169, 201, 133, 186, 169, 58 902 DATA 133, 187, 169, 176, 133, 188 908 DATA 96, 169, 76, 133, 186, 169 914 DATA 154, 133, 187, 169, 3, 133 920 DATA 188, 96, 141, 48, 192, 201 926 DATA 58, 176, 3, 76, 190, 0 932 DATA 76, 200, 0, 0
10 REM 'TAP' DEMO 2 20 REM 30 FOR I = 896 TO 955 40 READ X 50 Z = Z + X 60 POKE I, X 70 NEXT 80 IF Z < > 7425 THEN PRINT "OOPS. Z=";Z : STOP 90 HOME : GR 100 PRINT "WHILE I SCRIBBLE AIMLESSLY, " 110 PRINT "PRESS BUTTON 1 SEVERAL TIMES." 120 POKE 24, 0 : POKE 25, 0 : POKE 26, 0 : CALL 909 130 FOR I = 1 TO 1000 : COLOR= 16 * RND (I) 140 PLOT 40 * RND (I), 40 * RND (I) : NEXT 150 CALL 896 : T = PEEK (25) + 256 * PEEK (26) 160 TEXT : PRINT CHR$ (7): REM BELL! 170 HOME : PRINT "YOU PRESSED IT"; INT (T/2);" TIMES."896 DATA 169, 201, 133, 186, 169, 58 902 DATA 133, 187, 169, 176, 133, 188 908 DATA 96, 169, 76, 133, 186, 169 914 DATA 154, 133, 187, 169, 3, 133 920 DATA 188, 96, 72, 152, 72, 173 926 DATA 98, 192, 41, 128, 197, 24 932 DATA 240, 8, 133, 24, 230, 25 938 DATA 208, 2, 230, 26, 104, 168 944 DATA 104, 201, 58, 176, 3, 76 950 DATA 190, 0, 76, 200, 0, 0
0002 0000 ; THIS PROGRAM USES 512 BYTES FROM 0003 0000 ; 2048 TO 2559 TO CONTAIN AND LOOK 0004 0000 ; AFTER A 256-BYTE KEYBOARD BUFFER. 0005 0000 ; 0006 0000 ; ‘BRUN KEYBUF’ TO CREATE THE BUFFER. 0007 0000 ; ‘CALL 2054’ TO DISABLE THE BUFFER. 0008 0000 ; ‘CALL 2051’ TO RE-ENABLE THE BUFFER. 0009 0000 ; 0010 0000 ; HOW TO SAVE THE PROGRAM: 0011 0000 ; BSAVE KEYBUF, A$803, L$F8 0012 0000 ; 0013 0000 ; *= $803 ;START AT 2051. 0014 0803 ; 0015 0803 ; JUMP-TABLE OF ENTRY-POINTS: 0016 0803 ; 0017 0803 4C0908 JMP STARTS ;ENABLE BUFFER 0018 0806 4C9908 JMP CANCEL ;DISABLE BUFFER 0019 0809 ; 0020 0809 AD00E0 STARTS LDA $E000 ;WHICH LANGUAGE? 0021 080C C94C CMP #$4C ;APPLESOFT? 0022 080E D036 BNE STEXIT ;NO, SO QUIT! 0023 0810 ; 0024 0810 A90A LDA #>BASIC ;-> NEW START 0025 0812 A001 LDY # 1 ;OF BASIC (+1) 0026 0814 ; 0027 0814 C467 CPY $67 ;WARM ENTRY TO ME? 0028 0816 D004 BNE STCOLD ;NO 0029 0818 C568 CMP $68 ;WARM FOR SURE? 0030 081A F00C BEQ STLINK ;YES! 0031 081C ; 0032 081C 8467 STCOLD STY $67 ;SET UP THE NEW 0033 081E 8568 STA $68 ;START-OF-BASIC 0034 0820 A900 LDA # 0 0035 0822 8D000A STA BASIC ;TRADITION 0036 0825 204BD6 JSR $D64B ;EXECUTE ‘NEW’. 0037 0828 ; 0038 0828 A94C STLINK LDA # $4C 0039 082A 85BA STA $BA 0040 082C A949 LDA # <CHLINK ;TIE IN TO 0041 082E 85BB STA $BB ;CHRGET. 0042 0830 A908 LDA # >CHLINK 0043 0832 85BC STA $BC 0044 0834 ; 0045 0834 A97C LDA # <INLINK 0046 0836 8538 STA $38 ;TIE IN TO THE 0047 0838 A908 LDA # > INLINK ;INPUT HOOK ‘KSW’ 0048 083A 8539 STA $39 0049 083C ; 0050 083C ADEA03 STTIES LDA $3EA 0051 083F C94C CMP #$4C ;IS DOS PRESENT? 0052 0841 D003 BNE STEXIT ;NO, NO DISK HERE! 0053 0843 20EA03 JSR $3EA ;TELL DOS ABOUT TIE-IN 0054 0846 4C03E0 STEXIT JMP $E003 ;WARM START 0056 0849 ; THIS ROUTINE IS ENTERED EVERY TIME 0057 0849 ; APPLESOFT FETCHES A BYTE OF BASIC. 0058 0849 ; 0059 0849 2C00C0 CHLINK BIT $C000 ;KEY PRESSED? 0060 084C 1026 BPL CHCOLO ;NO, NOT YET 0061 084E 48 PHA ;SAVE BASIC BYTE 0062 084F 8A TXA ;SAVE X-REGISTER 0063 0850 48 PHA 0064 0851 AEAC08 LDX BIX ;GET INPUT POINTER 0065 0854 E8 INX ;PREPARE TO STEP UP 0066 0855 ECAD08 CPX BOX ;BUT IS BUFFER FULL? 0067 0858 D00A BNE CHSTOW ;NO, GO & STASH 0068 085A 98 TYA ;BUFFER FULL: 0069 085B 48 PHA ;(BELL USES Y-REG) 0070 085C 20E2FB JSR $FBE2 ;RING THE BELL! 0071 085F 68 PLA 0072 0860 A8 TAY 0073 0861 4C6E08 JMP CHRETR 0074 0864 ; 0075 0864 8EAC08 CHSTOW STX BIX ;SAVE NEW POINTER 0076 0867 CA DEX ;-> PLACE FOR THE KEY 0077 0868 AD00C0 LDA $C000 ;GET THE KEY 0078 086B 9D0009 STA BUF, X ;SAVE IN BUFFER 0079 086E ; 0080 086E 8D10C0 CHRETR STA $C010 ;RESET KEYBOARD 0081 0871 68 PLA 0082 0872 AA TAX ;RECOVER X-REG 0083 0873 68 PLA ;& BYTE OF BASIC 0084 0874 ; 0085 0874 C93A CHCOLO CMP # $3A ;(CHRGET REPLACEMENT) 0086 0876 B003 BCS CHBACK ;" 0087 0878 4CBE00 JMP $BE ;" 0088 087B 60 CHBACK RTS ;" 0089 087C ; 0090 087C ; THIS ROUTINE IS USED WHENEVER A 0091 087C ; KEY IS NEEDED FROM THE KEYBOARD. 0092 087C ; 0093 087C 8EAB08 INLINK STX SAVX ;SAVE IT 0094 087F AEAD08 LDX BOX ;GET OUTPUT POINTER 0095 0882 ECAC08 CPX BIX ;ANYTHING IN BUFFER? 0096 0885 D006 BNE INSEND ;YES, GO SEND IT! 0097 0887 AEAB08 LDX SAVX ;NO, RESTORE X-REG 0098 088A 4C1BFD JMP SFD1B ;NORMAL KEY HANDLER 0099 088D ; 0100 088D 9128 INSEND STA ($28),Y ;STOP FLASHING 0101 088F BD0009 LDA BUF, X ;GET KEY FROM BUFFER 0102 0892 EEAD08 INC BOX ;UPDATE POINTER 0103 0895 AEAB08 LDX SAVX ;RESTORE X-REG 0104 0898 60 RTS 0106 0899 ; DISABLE THE KEYBOARD BUFFER 0107 0899 ; 0108 0899 A9C9 CANCEL LDA #$C9 0109 089B 85BA STA $BA 0110 089D A93A LDA # $3A ; RESTORE CHRGET 0111 089F 85BB STA $BB ; ORIGINAL STUFF 0112 08A1 A9B0 LDA # $B0 0113 08A3 85BC STA $BC 0114 08A5 ; 0115 08A5 2089FE JSR $FE89 ;EXECUTE "IN# 0". 0116 0A8 4C3C08 JMP STTIES 0117 08AB ; 0118 08AB 00 SAVX .BYT 0 ;SAVE AREA FOR X-REG 0119 08AC 00 BIX .BYT 0 ;-> PLACE FOR NEXT BYTE 0120 08AD 00 BOX .BYT 0 ;-> NEXT ONE TO DELIVER 0121 08AE ; 0122 08AE ; (BIX = BOX) MEANS BUP IS EMPTY 0123 08AE ; (BIX + 1 = BOX) MEANS ITíS FULL! 0124 08AE ; 0125 08AE ; THE ABOVE MUST END BY $8FF 0126 08AE ; OR IT WILL BE OVERWRITTEN! 0127 08AE ; 0128 08AE BUF = $900 ;BUFFER IS $900-$9FF 0129 08AE BASIC = $A00 ;NEW START-OF-BASIC 0130 08AE ; 0131 08AE .END
BASIC 0A00 BIX 08AC BOX 08AD BUF 0900 CANCEL 0899 CHBACK 087B CHCOLO 0874 CHLINK 0849 CHRETR 086E CHSTOW 0864 INLINK 087C INSEND 088D SAVX 08AB STARTS 0809 STCOLD 081C STEXIT 0846 STLINK 0828 STTIES 083C
100 REM MAKE "GEN KEYBUF" 110 D$ = CHR$ (4) 120 F$ = "GEN KEYBUF" 130 PRINT D$ "OPEN "F$ 140 PRINT D$ "WRITE"F$ 150 PRINT "FP" 160 PRINT "MON I" 170 PRINT "BRUN MINI-ASSM" 180 PRINT "803 :"; : REM NOTICE SEMICOLON 190 READ Z$ 200 IF Z$ = "END" GOTO 230 210 PRINT " " Z$ 220 GOTO 190 230 PRINT "FP" 240 PRINT "BSAVE KEYBUF, A$803, L$F8" 250 PRINT D$ "CLOSE" 260 END 270 DATA JMP809, JMP899, LDAE000 280 DATA CMP# 4C, BNE846, LDA# A 290 DATA LDY# 1, CPY67, BNE81C 300 DATA CMP68, BEQ828, STY67 310 DATA STA68, LDA# 0, STAA00 320 DATA JSRD64B, LDA# 4C, STABA 330 DATA LDA# 49, STABB, LDA# 8 340 DATA STABC, LDA# 7C, STA38 350 DATA LDA# 8, STA39, LDA3EA 360 DATA CMP# 4C, BNE846, JSR3EA 370 DATA JMPE003, BITC000, BPL874 380 DATA PHA, TXA, PHA 390 DATA LDX8AC, INX, CPX8AD 400 DATA BNE864,TYA PHA 410 DATA JSRFBE2,PLA,TAY 420 DATA JMP86E,STX8AC,DEX 430 DATA LDAC000,"STA900,X",STAC010 440 DATA PLA,TAX,PLA 450 DATA CMP#3A,BCS87B;JMPBE 460 DATA RTS,STX8AB,LDX8AD 470 DATA CPX8AC,BNE88D,LDX8AB 480 DATA JMPFD1B,"STA(28),Y","LDA900,X" 490 DATA INC8AD,LDX8AB,RTS 500 DATA LDA#C9,STABA,LDA#3A 510 DATA STABB,LDA#B0,STABC 520 DATA JSRFE89,JMP83C,BRK 530 DATA BRK,BRK,END
If you have Integer BASIC in ROM or in a Language Card, substitute:
150 PRINT "INT" 170 PRINT "CALL - 2667" : REM MINIASSM
460 ML = 600 : FOR I = ML TO ML + 25 470 READ X : POKE I, X : NEXT 480 LIST (... OR PRINT SOME STUFF) 490 CALL ML 500 DATA 173, 80, 192, 173, 0, 192 510 DATA 41, 127, 170, 202, 208, 253 520 DATA 173 ,81, 192, 173, 0, 192 530 DATA 41, 127, 170, 202, 208, 253 540 DATA 240, 230