Text Editor

Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.

Alternatively, you can use any text editor that saves simple text files.

Download the Project Bundle

Your project will use a specific set of CircuitPython libraries and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.

Drag the contents of the uncompressed bundle directory onto your board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.

# SPDX-FileCopyrightText: 2022 Jeff Epler for Adafruit Industries
# SPDX-License-Identifier: MIT
import time
import array

import board
import digitalio
import rp2pio
import usb_hid

import adafruit_pioasm
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode as K

KBD_NRESET = board.MISO
KBD_DATA = board.RX
KBD_CLOCK = board.SCK # Note that KBD_CLOCK must be 1 GPIO# above KBD_DATA
KBD_NBUSY = board.MOSI

tandy1000_keycodes = [
    None, K.ESCAPE, K.ONE, K.TWO, K.THREE, K.FOUR, K.FIVE, K.SIX, K.SEVEN,
    K.EIGHT, K.NINE, K.ZERO, K.MINUS, K.EQUALS, K.BACKSPACE, K.TAB, K.Q, K.W,
    K.E, K.R, K.T, K.Y, K.U, K.I, K.O, K.P, K.LEFT_BRACKET, K.RIGHT_BRACKET,
    K.ENTER, K.LEFT_CONTROL, K.A, K.S, K.D, K.F, K.G, K.H, K.J, K.K, K.L,
    K.SEMICOLON, K.QUOTE, K.UP_ARROW, K.LEFT_SHIFT, K.LEFT_ARROW, K.Z, K.X,
    K.C, K.V, K.B, K.N, K.M, K.COMMA, K.PERIOD, K.FORWARD_SLASH, K.RIGHT_SHIFT,
    K.PRINT_SCREEN, K.LEFT_ALT, K.SPACE, K.CAPS_LOCK, K.F1, K.F2, K.F3, K.F4,
    K.F5, K.F6, K.F7, K.F8, K.F9, K.F10, K.KEYPAD_NUMLOCK, K.PAUSE,
    K.KEYPAD_SEVEN, K.KEYPAD_EIGHT, K.KEYPAD_NINE, K.DOWN_ARROW, K.KEYPAD_FOUR,
    K.KEYPAD_FIVE, K.KEYPAD_SIX, K.RIGHT_ARROW, K.KEYPAD_ONE, K.KEYPAD_TWO,
    K.KEYPAD_THREE, K.KEYPAD_ZERO, K.KEYPAD_MINUS, (K.LEFT_CONTROL, K.PAUSE),
    K.KEYPAD_PLUS, K.KEYPAD_PERIOD, K.KEYPAD_ENTER, K.HOME, K.F11, K.F12
]

LOCK_KEYS = (K.CAPS_LOCK, K.KEYPAD_NUMLOCK)
LOCK_STATE = {
    K.CAPS_LOCK: False,
    K.KEYPAD_NUMLOCK: False,
}
KEYPAD_NUMLOCK_LOOKUP = [
    {
        K.KEYPAD_PLUS: K.INSERT,
        K.KEYPAD_MINUS: K.DELETE,

        K.KEYPAD_SEVEN: K.BACKSLASH,
        K.KEYPAD_EIGHT: (K.LEFT_SHIFT, K.GRAVE_ACCENT),
        K.KEYPAD_NINE: K.PAGE_UP,

        K.KEYPAD_FOUR: (K.LEFT_SHIFT, K.BACKSLASH),
        #K.KEYPAD_FIVE:
        #K.KEYPAD_SIX:

        K.KEYPAD_ONE: K.END,
        K.KEYPAD_TWO: K.GRAVE_ACCENT,
        K.KEYPAD_THREE: K.PAGE_DOWN,

        K.KEYPAD_ZERO: K.ZERO,
        K.KEYPAD_PERIOD: K.PERIOD,
    },
    {
        K.KEYPAD_PLUS: (K.LEFT_SHIFT, K.EQUALS),
        K.KEYPAD_MINUS: K.MINUS,

        K.KEYPAD_SEVEN: K.SEVEN,
        K.KEYPAD_EIGHT: K.EIGHT,
        K.KEYPAD_NINE: K.NINE,

        K.KEYPAD_FOUR: K.FOUR,
        K.KEYPAD_FIVE: K.FIVE,
        K.KEYPAD_SIX: K.SIX,

        K.KEYPAD_ONE: K.ONE,
        K.KEYPAD_TWO: K.TWO,
        K.KEYPAD_THREE: K.THREE,

        K.KEYPAD_ZERO: K.ZERO,
        K.KEYPAD_PERIOD: K.PERIOD,
    }
]

# Assert busy
busy_out = digitalio.DigitalInOut(KBD_NBUSY)
busy_out.switch_to_output(False, digitalio.DriveMode.OPEN_DRAIN)

# Reset the keyboard
reset_out = digitalio.DigitalInOut(KBD_NRESET)
reset_out.switch_to_output(False, digitalio.DriveMode.OPEN_DRAIN)
time.sleep(.1)
reset_out.value = True

program = adafruit_pioasm.Program("""
    wait 1 pin 1
    in pins, 1
    wait 0 pin 1
""")

sm = rp2pio.StateMachine(program.assembled,
    first_in_pin = KBD_DATA,
    in_pin_count = 2,
    pull_in_pin_up = 0b11,
    auto_push=True,
    push_threshold=8,
    in_shift_right=True,
    frequency=8_000_000,
    **program.pio_kwargs)

buf = array.array('B', [0])

MASK_LEFT_SHIFT = K.modifier_bit(K.LEFT_SHIFT)
MASK_RIGHT_SHIFT = K.modifier_bit(K.RIGHT_SHIFT)
MASK_ANY_SHIFT = (MASK_LEFT_SHIFT | MASK_RIGHT_SHIFT)

# Now ready to get keystrokes
kbd = Keyboard(usb_hid.devices)
busy_out.value = True
while True:
    sm.readinto(buf, swap=False)
    val = buf[0]
    pressed = (val & 0x80) == 0
    key_number = val & 0x7f

    if key_number > len(tandy1000_keycodes):
        # invalid keycode -- reset the keyboard
        reset_out.switch_to_output(False, digitalio.DriveMode.OPEN_DRAIN)
        time.sleep(.1)
        reset_out.value = True
        continue

    keycode = tandy1000_keycodes[key_number]
    if keycode is None:
        continue
    keycode = KEYPAD_NUMLOCK_LOOKUP[LOCK_STATE[K.KEYPAD_NUMLOCK]].get(keycode, keycode)
    if pressed:
        if keycode in LOCK_KEYS:
            LOCK_STATE[keycode] = True
        elif LOCK_STATE[K.CAPS_LOCK] and K.A <= keycode <= K.Z:
            old_report_modifier = kbd.report_modifier[0]
            kbd.report_modifier[0] = (old_report_modifier & ~MASK_RIGHT_SHIFT) ^ MASK_LEFT_SHIFT
            kbd.press(keycode)
            kbd.release_all()
            kbd.report_modifier[0] = old_report_modifier
            continue
        elif isinstance(keycode, tuple):
            old_report_modifier = kbd.report_modifier[0]
            kbd.report_modifier[0] = 0
            kbd.press(*keycode)
            kbd.release_all()
            kbd.report_modifier[0] = old_report_modifier
        else:
            kbd.press(keycode)

    else:
        if keycode in LOCK_KEYS:
            LOCK_STATE[keycode] = False
        elif isinstance(keycode, tuple):
            pass
        else:
            kbd.release(keycode)

How it works

Preliminaries

After the required import lines, there are lines to set the I/O pins used, and to map from Tandy keycodes to USB keycodes:

mport time
import array

import board
import digitalio
import rp2pio
import usb_hid

import adafruit_pioasm
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode as K

KBD_NRESET = board.MISO
KBD_DATA = board.RX
KBD_CLOCK = board.SCK # Note that KBD_CLOCK must be 1 GPIO# above KBD_DATA
KBD_NBUSY = board.MOSI

tandy1000_keycodes = [...]

Locking keys

The keyboard handles the caps lock and num lock key states internally. For instance, when caps lock is first pressed, the keyboard turns the light on and sends a "caps lock is pressed" message to CircuitPython. When caps lock is pressed again, the keyboard turns the light off and sends a "caps lock is released" message.

There's no way for CircuitPython to set the state of the caps lock or num lock LEDs. So, instead, the CircuitPython code tracks whether caps lock or num lock are active, and then modifies the events it sends back to the computer accordingly.

These lines configure which keys are locking keys, and the behavior of keypad keys depending on the numlock state:

LOCK_KEYS = (K.CAPS_LOCK, K.KEYPAD_NUMLOCK)
LOCK_STATE = {
    K.CAPS_LOCK: False,
    K.KEYPAD_NUMLOCK: False,
}
KEYPAD_NUMLOCK_LOOKUP = [...]

Initialize the keyboard

To initialize the keyboard, first assert the "busy" and "reset" signals. Then, release the "reset" signal to allow the keyboard to initialize itself. The "busy" signal will remain asserted until CircuitPython is ready to receive key events.

These signals are "active low" and "open drain", which means that to assert the signal, the correct value to send to the pin is False.

# Assert busy
busy_out = digitalio.DigitalInOut(KBD_NBUSY)
busy_out.switch_to_output(False, digitalio.DriveMode.OPEN_DRAIN)

# Reset the keyboard
reset_out = digitalio.DigitalInOut(KBD_NRESET)
reset_out.switch_to_output(False, digitalio.DriveMode.OPEN_DRAIN)
time.sleep(.1)
reset_out.value = True

Configure the PIO peripheral

These lines configure a program within the PIO peripheral. The program waits for the clock input to become true, then copies the data value, and waits for the clock input to become false.

The peripheral is configured to wait for 8 bits of data before making all 8 bits available to CircuitPython in a buffer.

program = adafruit_pioasm.Program("""
    wait 1 pin 1
    in pins, 1
    wait 0 pin 1
""")

sm = rp2pio.StateMachine(program.assembled,
    first_in_pin = KBD_DATA,
    in_pin_count = 2,
    pull_in_pin_up = 0b11,
    auto_push=True,
    push_threshold=8,
    in_shift_right=True,
    frequency=8_000_000,
    **program.pio_kwargs)

Main loop

Finally, the program is ready to receive key events from the keyboard and translate them to USB. First, read one value from the PIO peripheral and turn it into a boolean 'pressed' value and a Tandy key number value.

If the key number is not valid, something has gone wrong. Reset the keyboard and try again. Otherwise, event processing continues below.

# Now ready to get keystrokes
kbd = Keyboard(usb_hid.devices)
busy_out.value = True
while True:
    sm.readinto(buf, swap=False)
    val = buf[0]
    pressed = (val & 0x80) == 0
    key_number = val & 0x7f
    
    if key_number > len(tandy1000_keycodes):
        # invalid keycode -- reset the keyboard
        reset_out.switch_to_output(False, digitalio.DriveMode.OPEN_DRAIN)
        time.sleep(.1)
        reset_out.value = True
        continue
        
    keycode = tandy1000_keycodes[key_number]
    if keycode is None:
        continue

Next, translate the keycode based on the caps lock state. Then, if the event is a press, process it one of several ways:

  • If it's a lock key, just update the state of the lock key.
  • If caps lock is on, and the keycode is alphabetic, then send it shifted.
  • If the key code is a tuple, send it as multiple events. This is used, for instance, to send the backslash key when num lock is off and numpad "7" is pressed.
  • Otherwise send a simple USB key press event.

Otherwise, if the event is a release, it's processed in one of the following ways:

  • If it's a lock key, just update the state of the lock key.
  • If it's a tuple, there's nothing to do.
  • Otherwise send a simple USB key release event.
...
    keycode = KEYPAD_NUMLOCK_LOOKUP[LOCK_STATE[K.KEYPAD_NUMLOCK]].get(keycode, keycode)
    if pressed:
        if keycode in LOCK_KEYS:
            LOCK_STATE[keycode] = True
        elif LOCK_STATE[K.CAPS_LOCK] and K.A <= keycode <= K.Z:
            old_report_modifier = kbd.report_modifier[0]
            kbd.report_modifier[0] = (old_report_modifier & ~MASK_RIGHT_SHIFT) ^ MASK_LEFT_SHIFT
            kbd.press(keycode)
            kbd.release_all()
            kbd.report_modifier[0] = old_report_modifier
            continue
        elif isinstance(keycode, tuple):
            old_report_modifier = kbd.report_modifier[0]
            kbd.report_modifier[0] = 0
            kbd.press(*keycode)
            kbd.release_all()
            kbd.report_modifier[0] = old_report_modifier
        else:
            kbd.press(keycode)

    else:
        if keycode in LOCK_KEYS:
            LOCK_STATE[keycode] = False
        elif isinstance(keycode, tuple):
            pass
        else:
            kbd.release(keycode)

This guide was first published on Sep 27, 2022. It was last updated on Apr 09, 2024.

This page (Coding the Keyboard) was last updated on Apr 09, 2024.

Text editor powered by tinymce.