Day 22: Low-level Key Input
The Key Port
It’s called low-level because we interface with the keypad hardware
itself instead of going through an API (that’s a pretty glamourous
description of GetKey
) that does it for us. That’s why it’ll be
complicated, but faaaaaaaaaaast.
OUT (n), A
: Sends a byte to port n
via the accumulator.OUT (C), reg
: Sends a byte to port C
via register reg
.IN A, (n)
: Receives a byte from port n
via the accumulator. Does not affect flags.IN reg, (C)
: Receives a byte from port C
via register reg
.- S
- affected
- Z
- affected
- P/V
- detects parity
- C
- affected
A port is a device that lets the CPU transfer bytes between other pieces of otherwise unconnected hardware. What’s that? You want an analogy? Okay, imagine you wanted to get a car from Yokohama to San Francisco; you couldn’t just drive it over because they’re separated by thousands of miles of open ocean. Instead, you’d take the car to the port of Yokohama, have a boat take it to the port in San Francisco, and drive off. Similarly, the CPU and the keypad have no real connection, so the port is used to interact.
The keyport on the TI-83 Plus is port #1, so we replace n
in the
two instructions with 1.
The first thing to do is enable the key group we want to read from. This is done by writing a value to the key port with the bit corresponding to the chosen group set to 0. Then read from the key port, and the value that comes back has a bit set to 0 for each key that is currently pressed in the selected group.
Matrix layout
So what are these group and key bits? Glad you asked! The keyboard matrix is laid out so the group and key bits are like this:1
Group bit | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
Key bit | |||||||
7 | DEL | ALPHA | X,T,θ,n | STAT | |||
6 | MODE | MATH | APPS | PRGM | VARS | CLEAR | |
5 | 2nd | x-1 | SIN | COS | TAN | ^ | |
4 | Y= | x2 | , | ) | ( | ÷ | |
3 | WINDOW | LOG | 7 | 8 | 9 | × | up |
2 | ZOOM | LN | 4 | 5 | 6 | - | right |
1 | TRACE | STO | 1 | 2 | 3 | + | left |
0 | GRAPH | 0 | . | (-) | ENTER | down |
Given the description above, if we wanted to check if the ENTER key
is pressed, we would write the value $FD
(all ones except for bit 1,
corresponding to the “1” column in the table) to select the group containing the
ENTER key, and expect the value $FE
back if the key is currently
being pressed (corresponding to the “0” row in the table).
Note that the most significant group bit is unused, and the ON key is missing! ON is separate from the rest of the keyboard because it triggers interrupts, so you need to read its state from port 4; this is explained in more detail in Day 23.
Switching Groups
Assemble and run this program. When the program is requesting input, enter x or ->, because I said so.
Program 22-1
ReadKey:
LD A, %11111101 ; Check for [-]
OUT (1), A
IN A, (1)
CP %11111011
JP Z, Minus
LD A, %11111110 ; Check for [up]
OUT (1), A
IN A, (1)
CP %11110111
JP Z, Up
JP ReadKey
Minus:
LD HL, zs_minus
bcall(_PutS)
RET
Up:
LD HL, zs_up
bcall(_PutS)
RET
zs_up: .DB "You pressed UP !", 0
zs_minus: .DB "You pressed - !", 0
Well that certainly was unexpected. You see, compared to a running program, hardware takes a very long time to react to inputs. So long in fact, that a program can easily execute several instructions before a port is ready. In the case of this program, the key port was read before it could set the correct group. In the case where x is pressed,
- Group
$FD
was set, which is the same group for down and x. - The key port was read while it was reacting to the group switch and so garbage was read.
- Group
$FE
was set. - The key port was read before it could switch from key group
$FD
, so the x key was stored in A. - A was compared with the key code for up, which just so happens to also be the key code for x, and the rest is history.
To fix this, add a delay of two NOP
s between setting the key group and
reading the port. NOP
is a do-nothing instruction that just waits for
four clock cycles. It’s so pointless it doesn’t even get its own box.
Simultaneous Keypresses
You could use either CP
or BIT
to check what keys are pressed, but we’ve only
used CP
this far. To see the difference, run Program 22-2. Press and hold
ALPHA, and hit LOG… nothing happens. Replace the CP
%11110111
with BIT 3, A
and reassemble. Do the same thing when running this
program… Done
!
Program 22-2
Pause until LOG is pressed.
Loop:
LD A, %11011111 ; Enable group
OUT (1), A
IN A, (1) ; Input a key
CP %11110111 ; Check if it's [LOG]
RET Z ; End if so
JP Loop
The reason for this behaviour lies in the way the key port reacts when multiple keys are pressed. Because the bit for a key is reset when pressed, if both key1 and key2 are pressed at the same time, you get a value back with the bits corresponding to both key1 and key2 cleared!
In Program 22-2 then, the key port was giving us %01110111
, with the bits for
both LOG and ALPHA cleared. CP didn’t work because it was
looking for the exact value %11110111
. BIT, on the other hand, will work
because bit 3 is still zero.
If you want the calculator to do something when a key is pressed, regardless of whether any other keys in the group are pressed, you should use BIT (or a shift instruction if possible). However, if you wanted a different action to be taken when two or more keys are pressed down, then you’d have to either use CP, or have a kind of a BIT chain.
Program 22-3
Demonstration of multiple keypresses.
bcall(_RunIndicOff)
LD HL, $1C23
LD (x_pos), HL
DispText:
bcall(_ClrLCDFull)
LD HL, (x_pos)
LD (PenCol), HL
LD HL, string
bcall(_VPutS)
LD C, 1
InKey:
LD A, %10111111 ; Check for [DEL] to exit
OUT (C), A
IN A, (C)
BIT 7, A
JR NZ, InArrow
LD A, $FF ; Reset key port
OUT (C), A
RET
InArrow:
LD A, $FF ; Reset key port
OUT (C), A
LD A, %11111110
OUT (C), A
IN B, (C)
BIT 0, B
JP Z, Down
BIT 1, B
JP Z, Left
BIT 2, B
JP Z, Right
BIT 3, B
JP Z, Up
JP InKey
Down:
CALL MoveDown
BIT 1, B
CALL Z, MoveLeft
BIT 2, B
CALL Z, MoveRight
JP DispText
Left:
;There is no need to check for Down key anymore.
CALL MoveLeft
BIT 3, B
CALL Z, MoveUp
JP DispText
Right:
CALL MoveRight
BIT 3, B
CALL Z, MoveUp
JP DispText
Up:
CALL MoveUp
JP DispText
MoveDown:
LD A, (y_pos) ; Check if at bottom edge of screen
CP 57
RET Z
INC A ; Down one pixel
LD (y_pos), A
RET
MoveUp:
LD A, (y_pos) ; Check if at top edge of screen
OR A
RET Z
DEC A ; Up one pixel
LD (y_pos), A
RET
MoveLeft:
LD A, (x_pos) ; Check if at left edge of screen
OR A
RET Z
DEC A ; Left one pixel
LD (x_pos), A
RET
MoveRight:
LD A, (x_pos) ; Check if at right edge of screen
CP 96-28 ; 96 - number of pixels the string takes up
RET Z
INC A ; Right one pixel
LD (x_pos), A
RET
x_pos: .DB 0
y_pos: .DB 0
string: .DB "Let\'s Go!", 0
Press any key to continue
You can also select multiple key groups at a time, by writing group values to the key port with multiple bits cleared. The value you get back is equivalent to taking the bitwise AND of the values that would be returned from each group individually. This means you might not be able to tell exactly which keys are pressed, but it’s convenient if you only care if a key is pressed and not which one.
Most usefully, you might want to check if any key (other than ON) is
pressed by writing $80
and checking if any bits in the read value are clear
(simply by comparing the result to $FF
).
Keyboard b_calls and debouncing
The calculator’s bcall
s for receiving input do some additional work that may
not be obvious when thinking about low-level input. Try running Program 22-4 and
holding down various keys (one at a time- _GetCSC
returns nothing if you’re
pressing more than one key) while comparing how fast dots are displayed.
Program 22-4
Loop:
bcall(_GetCSC)
or a ; Any key pressed?
jr z, Loop ; No, keep waiting
cp skClear
ret z ; Press CLEAR to exit
ld a, '.'
bcall(_PutC) ; Display a dot for a keypress
jr Loop
You should notice that the arrow keys and DEL will be returned multiple times after a short delay if held down, while no other keys will. This is because in most situations the calculator’s programmers expected that you only want to handle new keypresses, and not accidentally holding a key down:2 a reasonable assumption for a calculator.
On the other hand, if you want to detect a key being held down (perhaps you’re
programming a game where 2nd is a shoot button) then your program
needs to use low-level keyboard access to detect that- _GetCSC
’s helpfulness
is actually a complication in that case!
If you expect keys to be held down then you probably also need to handle
more than one being pressed at a time, which _GetCSC
will not do for you
either, so especially for games raw key input is very important.
-
In hardware, the keyboard is a matrix of switches where pressing one key connects a row to a column- writing a 0 for a group selects a row, then reading a value shows the current state of the columns. If a key is pressed, the column takes on the same value as its row. ↩︎
-
Electrical engineers refer to this as being “edge-triggered”, where the transition between two digital states is referred to as an edge. In this case, the states are “key pressed” and “key not pressed.” ↩︎