Wednesday, March 7, 2012

Direct Keyboard Input From BASIC


The BASIC INPUT command reads a line of text from the keyboard and assigns it to one or more variables. Sometimes you want to get a single key without requiring the user to press <Enter>. You may also not want to echo the character to the screen. Many versions of BASIC provide a command called GET that reads a character from the keyboard immediately but Apple 1 BASIC does not. I describe here a way to do this using a small machine language routine that is called from BASIC.

Section III on page 8 of the Apple-1 Operation Manual describes how to read a key from the keyboard in assembly language. The keyboard interfaces through a 6821 PIA (Peripheral Interface Adaptor) chip. The high order bit of the control register at address $D011 will be set when a key has been pressed. The key code can then be read from the data register at address $D010. The high order bit of the key code is always 1 and the low order 7 bits are the ASCII code for the key that was pressed.

We can take the sample code from the Apple manual and turn it into a routine that we can call by adding an RTS (return from subroutine) at the end. We also need to store the character we read in a memory location for later use. Below is assembler code in a form that is accepted by the Krusader assembler if you want to try typing it in and assembling it.

CR .= $D011 ; CONTROL REGISTER
DATA .= $D010 ; DATA REGISTER
POLL LDA CR ; READ CONTROL REG
 BPL POLL ; BRANCH UNTIL BIT 7 SET
 LDA DATA ; GET CHARACTER
 STA KEY ; STORE IT
 RTS  ; RETURN
KEY .B $00 ; KEY CODE

The listing from Krusader is a little more readable:

000 CR     .=  $D011         ; CONTROL 
001 DATA   .=  $D010         ; DATA REG
002 POLL   LDA CR            ; READ CON
003        BPL POLL          ; BRANCH U
004        LDA DATA          ; GET CHAR
005        STA KEY           ; STORE IT
006        RTS               ; RETURN
007 KEY    .B  $00           ; KEY CODE

Below is the disassembly from Krusader:

0300   AD 11 D0    LDA   $D011
0303   10 FB       BPL   $0300
0305   AD 10 D0    LDA   $D010
0308   8D 0C 03    STA   $030C
030B   60          RTS   
030C   00          BRK  

The zero value we stored in location KEY gets disassembled as a BRK instruction but it's really just data. We can call this routine from BASIC using the CALL command and then reading location KEY.

First lets convert the start address of the routine and of KEY to decimal:

$0300 = 767
$030C = 780

Our BASIC code to call it looks like this. We call the routine, and then when it returns to BASIC we get the key from location 780. Since the high bit of the character is normally set, we can subtract 128 to get the more conventional ASCII code for the key.

10 CALL 767
20 K=PEEK(780)
30 IF K>=128 THEN K=K-128
40 PRINT "KEY=";K
50 END 

And running it, we see the output:

>RUN
KEY=65

In the example above I hit the key "A".

This works but we need to arrange to make sure that the machine language program resides in memory. We could save it as part of the binary dump of the program (as we discussed in a previous blog posting) but it would be ideal if the BASIC program set this all up itself so it was self-contained. Let's POKE the program into memory from our BASIC program.

We need to convert the hex bytes in the assembly listing to decimal needed by BASIC. Here is the conversion:

Hex:  AD 11  D0  10  FB  AD  10  D0  8D  0C  03  60
Dec: 173 17 208  16 251 173  16 208 141  12   3  96

We can POKE these values into memory starting at address $0300 or 768 decimal. It's a little verbose because we don't have a DATA statement in Apple 1 BASIC but for a short program like this it is fine.

The complete program is shown below. I made the code to initialize the machine language program a subroutine we can call at line 1000. We need to do this once. The routine to get a key is a subroutine at line 1100.  The main program displays the key code. We then loop back unless the the key was code 81 (the letter "Q") in which case we quit.

10 GOSUB 1000
20 GOSUB 1100
30 PRINT "GOT KEY: ";K
40 IF K <> 81 THEN 20
50 END
1000 REM INIT ML CODE
1010 POKE 768,173:POKE 769,17:POKE 770,208:POKE 771,16:POKE 772,251:POKE 773,173
1020 POKE 774,16:POKE 775,208:POKE 776, 141:POKE 777,12:POKE 778,3:POKE 779,96
1030 RETURN
1100 REM GET KEYBOARD KEY
1110 CALL 768
1120 K = PEEK(780)
1130 IF K > 128 THEN K = K - 128
1140 RETURN

If you've been paying close attention and followed all of this, you may be wondering why we can't avoid the machine language routine entirely and just read the keyboard registers directly from BASIC using PEEKs. The problem is that BASIC polls the keyboard after every statement and if a key is pressed, it stops execution. We need to read the PIA data register after a key is pressed to clear the control register bit that indicates that there was a keypress. By doing this in machine language we do it all outside of BASIC and avoid the problem of BASIC stopping.

If Apple 1 BASIC was like most other implementations, only stopping when a Control-C was pressed, we could get by with the approach of just PEEKing the hardware registers directly.

Some BASIC versions of GET wait for a key to be pressed and return it, like here. Some return an empty string if a key was not pressed. You could easily modify this program to return a suitable value (e.g. 0) if no key has been pressed, if this was the behaviour you needed.

I hope you can see from this example that it is quite easy to call machine language from BASIC and you could extend this to more complex machine language code that might return more values such as numbers or strings.

No comments: