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.
detects parity

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
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

    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

    LD     HL, zs_minus

    LD     HL, zs_up

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,

  1. Group $FD was set, which is the same group for down and x.
  2. The key port was read while it was reacting to the group switch and so garbage was read.
  3. Group $FE was set.
  4. The key port was read before it could switch from key group $FD, so the x key was stored in A.
  5. 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 NOPs 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.

    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.

    LD     HL, $1C23
    LD     (x_pos), HL

    LD     HL, (x_pos)
    LD     (PenCol), HL
    LD     HL, string

    LD     C, 1

    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

    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

    CALL   MoveDown
    BIT    1, B
    CALL   Z, MoveLeft
    BIT    2, B
    CALL   Z, MoveRight
    JP    DispText

;There is no need to check for Down key anymore.
    CALL   MoveLeft
    BIT    3, B
    CALL   Z, MoveUp
    JP     DispText

    CALL   MoveRight
    BIT    3, B
    CALL   Z, MoveUp
    JP     DispText

    CALL   MoveUp
    JP     DispText

    LD     A, (y_pos)        ; Check if at bottom edge of screen
    CP     57
    RET    Z
    INC    A                ; Down one pixel
    LD     (y_pos), A

    LD     A, (y_pos)        ; Check if at top edge of screen
    OR     A
    RET    Z
    DEC    A                ; Up one pixel
    LD     (y_pos), A

    LD     A, (x_pos)        ; Check if at left edge of screen
    OR     A
    RET    Z

    DEC    A                ; Left one pixel
    LD     (x_pos), A

    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

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 bcalls 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

    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.

  1. 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. ↩︎

  2. 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.” ↩︎