Classic Computer Magazine Archive COMPUTE! ISSUE 32 / JANUARY 1983 / PAGE 187


Keith Falkner

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

1. To load and initialize the routine,
2. Now use your Apple as usual, but be sure that you do not switch to Integer BASIC!
3. To suspend use of the buffer,
CALL 2054
4. To resume use of the buffer,
CALL 2051
5. To recover memory used by the buffer, after suspending it via CALL 2054,
FP (or INT, if you choose)
6. To copy the routine from disk to disk,
Insert the disk to receive a copy.

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.

Program 1.

20 FOR I = 896 TO 935
40 Z = Z + X
50 POKE I, X
70 IF Z <  > 5155 GOTO 90
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

Program 2.

20 REM
30 FOR I = 896 TO 955
50 Z = Z + X
60 POKE I, X
80 IF Z < > 7425 THEN PRINT "OOPS. Z=";Z : STOP
90 HOME : GR
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)
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

Program 3.

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

Program 4.

110 D$  =  CHR$ (4)
120 F$  =  "GEN KEYBUF"
130 PRINT D$ "OPEN "F$
150 PRINT "FP"
190 READ Z$
200 IF Z$ = "END" GOTO 230
210 PRINT " " Z$
220 GOTO 190
230 PRINT "FP"
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
330 DATA LDA# 49, STABB, LDA# 8
350 DATA LDA# 8, STA39, LDA3EA
360 DATA CMP# 4C, BNE846, JSR3EA
370 DATA JMPE003, BITC000, BPL874
430 DATA LDAC000,"STA900,X",STAC010
480 DATA JMPFD1B,"STA(28),Y","LDA900,X"

If you have Integer BASIC in ROM or in a Language Card, substitute:


Program 5.

460 ML = 600 : FOR I = ML TO ML + 25
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