Jeff Epler's blog

4 September 2022, 2:11 UTC

Recent keyboard deeds


I made several USB HID keyboard conversions recently. They all use CircuitPython. The hardware setup is not documented as none are worthy of being called finished projects at this point, but the source code gives an idea what connections are needed. One or more of these may be the subject of a future Adafruit Learning System Guide.

Click a triangle to see the full code as an embedded github gist. (Note: if you're viewing this in a RSS reader, you may have to click through to get the embeds. Sorry!)

Commodore 16 Keyboard

Used an Adafruit KB2040. Gratuitous use of asyncio. The keymap needs work to be usable, but it's a good start. Direct gist link.

# SPDX-FileCopyrightText: 2022 Jeff Epler for Adafruit Industries
# SPDX-License-Identifier: MIT
# Commodore 16 to USB HID adapter with Adafruit KB2040
#
# Note that:
# * This matrix is different than the (more common) Commodore 64 matrix
# * There are no diodes, not even on modifiers, so there's only 2-key rollover.
from board import *
import keypad
import asyncio.core
from adafruit_hid.keycode import Keycode as K
from adafruit_hid.keyboard import Keyboard
import usb_hid
# True to use a more POSITIONAL mapping, False to use a more PC-style mapping
POSITIONAL = True
# Keyboard schematic https://archive.org/details/SAMS_Computerfacts_Commodore_C16_1984-12_Howard_W_Sams_Co_CC8/page/n9/mode/2up
# 1 3 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # connector pins
# R5 C7 R7 C4 R1 C5 C6 R3 R2 R4 C2 C1 R6 C3 C0 R0 # row/column in schematic
# D2 D3 D4 D5 D6 D7 D8 D9 D10 MOSI MISO SCK A0 A1 A2 A3 # conencted to kb2040 at
rows = [A3, D6, D10, D9, MOSI, D2, A0, D4] # give the following ...
cols = [A2, SCK, MISO, A1, D5, D7, D8, D3]
# ROM listing of key values from ed7.src in
# http://www.zimmers.net/anonftp/pub/cbm/src/plus4/ted_kernal_basic_src.tar.gz
# shows key matrix arrangement (it's nuts)
# del return £ f8 f1 f2 f3 @
# 3 w a 4 z s e shift
# 5 r d 6 c f t x
# 7 y g 8 b h u v
# 9 i j 0 m k o n
# down p l up . : - ,
# left * ; right escape = + /
# 1 home control 2 space c=key q stop
# Implement an FN-key for some keys not present on the default keyboard
class FnState:
def __init__(self):
self.state = False
def fn_event(self, event):
self.state = event.pressed
def fn_modify(self, keycode):
if self.state:
return self.mods.get(keycode, keycode)
return keycode
mods = {
K.ONE: K.F1,
K.TWO: K.F2,
K.THREE: K.F3,
K.FOUR: K.F4,
K.FIVE: K.F5,
K.SIX: K.F6,
K.SEVEN: K.F7,
K.EIGHT: K.F8,
K.NINE: K.F9,
K.ZERO: K.F10,
K.F1: K.F11,
K.F2: K.F12,
K.UP_ARROW: K.PAGE_UP,
K.DOWN_ARROW: K.PAGE_DOWN,
K.LEFT_ARROW: K.HOME,
K.RIGHT_ARROW: K.END,
K.BACKSPACE: K.DELETE,
K.F3: K.INSERT,
}
fn_state = FnState()
K_FN = fn_state.fn_event
# A tuple is special, it:
# * Clears shift modifiers & pressed keys
# * Presses the given sequence
# * Releases all pressed keys
# * Restores the original modifiers
# It's mostly used to send a key that requires a shift keypress on a standard
# keyboard (or which is mapped to a shifted key but requires that shift NOT
# be pressed)
#
# A consequence of this is that the key will not repeat, even if it is held
# down. So for example in the positional mapping, shift-1 will repeat "!"
# but shift-7 will not repeat "'" and shift-0 will not repeat "^".
K_AT = (K.SHIFT, K.TWO)
K_PLUS = (K.SHIFT, K.EQUALS)
K_ASTERISK = (K.SHIFT, K.EIGHT)
K_COLON = (K.SHIFT, K.SEMICOLON)
# We need these mask values for the reasons discussed above
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)
if POSITIONAL:
keycodes = [
K.BACKSPACE, K.ENTER, K.BACKSLASH, K.F8, K.F1, K.F2, K.F3, K_AT,
K.THREE, K.W, K.A, K.FOUR, K.Z, K.S, K.E, K.LEFT_SHIFT,
K.FIVE, K.R, K.D, K.SIX, K.C, K.F, K.T, K.X,
K.SEVEN, K.Y, K.G, K.EIGHT, K.B, K.H, K.U, K.V,
K.NINE, K.I, K.J, K.ZERO, K.M, K.K, K.O, K.N,
K.DOWN_ARROW, K.P, K.L, K.UP_ARROW, K.PERIOD, K_COLON, K.MINUS, K.COMMA,
K.LEFT_ARROW, K_ASTERISK, K.SEMICOLON, K.RIGHT_ARROW, K.ESCAPE, K.EQUALS, K_PLUS,
K.FORWARD_SLASH, K.ONE, K_FN, K.LEFT_CONTROL, K.TWO, K.SPACE, K.ALT, K.Q, K.GRAVE_ACCENT,
]
shifted = {
K.TWO: (K.SHIFT, K.QUOTE), # double quote
K.SIX: (K.SHIFT, K.SEVEN), # ampersand
K.SEVEN: (K.QUOTE,), # single quote
K.EIGHT: (K.SHIFT, K.NINE), # left paren
K.NINE: (K.SHIFT, K.ZERO), # right paren
K.ZERO: (K.SHIFT, K.SIX), # caret
K_AT: (K.SHIFT, K.LEFT_BRACKET),
K_PLUS: (K.SHIFT, K.RIGHT_BRACKET),
K_COLON: (K.LEFT_BRACKET,),
K.SEMICOLON: (K.RIGHT_BRACKET,),
}
else:
# TODO clear/home, up/down positional arrows
keycodes = [
K.BACKSPACE, K.ENTER, K.LEFT_ARROW, K.F8, K.F1, K.F2, K.F3, K.LEFT_BRACKET,
K.THREE, K.W, K.A, K.FOUR, K.Z, K.S, K.E, K.LEFT_SHIFT,
K.FIVE, K.R, K.D, K.SIX, K.C, K.F, K.T, K.X,
K.SEVEN, K.Y, K.G, K.EIGHT, K.B, K.H, K.U, K.V,
K.NINE, K.I, K.J, K.ZERO, K.M, K.K, K.O, K.N,
K.DOWN_ARROW, K.P, K.L, K.UP_ARROW, K.PERIOD, K.SEMICOLON, K.QUOTE, K.COMMA,
K.BACKSLASH, K_ASTERISK, K.SEMICOLON, K.EQUALS, K.ESCAPE, K.RIGHT_ARROW, K.RIGHT_BRACKET,
K.FORWARD_SLASH, K.ONE, K.HOME, K.LEFT_CONTROL, K.TWO, K.SPACE, K.ALT, K.Q, K.GRAVE_ACCENT,
]
shifted = {
}
class AsyncEventQueue:
def __init__(self, events):
self._events = events
async def __await__(self):
yield asyncio.core._io_queue.queue_read(self._events)
return self._events.get()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
pass
class XKROFilter:
"""Perform an X-key rollover algorithm, blocking ghosts if more than X keys are pressed at once
A key matrix without diodes can support 2-key rollover.
"""
def __init__(self, rollover=2):
self._count = 0
self._rollover = rollover
self._real = [0] * 64
self._ghost = [0] * 64
def __call__(self, event):
old_count = self._count
self._ghost[event.key_number] = event.pressed
if event.pressed:
if self._count < self._rollover:
self._real[event.key_number] = True
yield event
self._count += 1
else:
self._real[event.key_number] = False
yield event
self._count -= 1
twokey_filter = XKROFilter(2)
async def key_task():
# Initialize Keyboard
kbd = Keyboard(usb_hid.devices)
with keypad.KeyMatrix(rows, cols) as keys, AsyncEventQueue(keys.events) as q:
while True:
ev = await q
for ev in twokey_filter(ev):
keycode = keycodes[ev.key_number]
if callable(keycode):
keycode = keycode(ev)
keycode = fn_state.fn_modify(keycode)
if keycode is None:
continue
old_report_modifier = kbd.report_modifier[0]
shift_pressed = old_report_modifier & MASK_ANY_SHIFT
if shift_pressed:
keycode = shifted.get(keycode, keycode)
if isinstance(keycode, tuple):
if ev.pressed:
kbd.report_modifier[0] = old_report_modifier & ~MASK_ANY_SHIFT
kbd.press(*keycode)
kbd.release_all()
kbd.report_modifier[0] = old_report_modifier
elif ev.pressed:
kbd.press(keycode)
else:
kbd.release(keycode)
async def forever_task():
while True:
await asyncio.sleep(.1)
async def main():
forever = asyncio.create_task(forever_task())
key = asyncio.create_task(key_task())
await asyncio.gather( # Don't forget the await!
forever,
key,
)
asyncio.run(main())
view raw code.py hosted with ❤ by GitHub

IBM XT "Model F" Keyboard

Used an Adafruit QT PY RP2040. This layout is AWFUL, the "beloved" iteration of the Model F was for AT computers, with a layout a lot more like a modern keyboard (aside from the placement of the Esc and Ctrl keys anyway) Direct gist link.

import array
import board
import rp2pio
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode as K
import adafruit_pioasm
# from https://www.scs.stanford.edu/10wi-cs140/pintos/specs/kbd/scancodes-9.html
# translating from "Set 1" to USB using the adafruit_hid keycode names
# fmt: off
xt_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.RETURN, 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.GRAVE_ACCENT, K.SHIFT, K.BACKSLASH, 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.KEYPAD_ASTERISK, K.OPTION, K.SPACEBAR, 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.SCROLL_LOCK,
K.KEYPAD_SEVEN, K.KEYPAD_EIGHT, K.KEYPAD_NINE, K.KEYPAD_MINUS, K.KEYPAD_FOUR,
K.KEYPAD_FIVE, K.KEYPAD_SIX, K.KEYPAD_PLUS, K.KEYPAD_ONE, K.KEYPAD_TWO,
K.KEYPAD_THREE, K.KEYPAD_ZERO, K.KEYPAD_PERIOD, None, None, None, K.F11, K.F12
]
# fmt: on
program = adafruit_pioasm.Program("""
wait 0 pin 2
in pins, 1
wait 1 pin 2
""",
build_debuginfo=True)
sm = rp2pio.StateMachine(program.assembled,
first_in_pin = board.MISO,
in_pin_count = 3,
pull_in_pin_up = 0b111,
auto_push=True,
push_threshold=10,
in_shift_right=True,
frequency=8_000_000,
**program.pio_kwargs)
buf = array.array('H', [0])
print("Ready to type")
kbd = Keyboard(usb_hid.devices)
while True:
sm.readinto(buf, swap=False)
val = buf[0]
pressed = not (val & 0x8000)
key_number = (val >> 8) & 0x7f
keycode = xt_keycodes[key_number]
print(f"{keycode} {'PRESSED' if pressed else 'released'}")
if keycode is None:
continue
if pressed:
kbd.press(keycode)
else:
kbd.release(keycode)
view raw code.py hosted with ❤ by GitHub

Tandy 1000 Keyboard

Used an Adafruit Feather RP2040. The trickiest, as caps lock and num lock are handled in the firmware; and there are some real oddities about the layout compared to the keyboards we're used to. This keyboard also needed the most clean-up but mechanically it's 100%. Direct gist link.

import time
import digitalio
import array
import board
import rp2pio
import adafruit_pioasm
from adafruit_hid.keyboard import Keyboard
import usb_hid
from adafruit_hid.keycode import Keycode as K
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,
}
]
# D10 = blue = CN2 = RESET
# D11 = white = CN1 = DATA
# D12 = green = CN3 = CLOCK
# D13 = yellow = CN4 = /BUSY
KBD_NRESET = board.D10
KBD_DATA = board.D11
KBD_CLOCK = board.D12
KBD_NBUSY = board.D13
# 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 = not (val & 0x80)
key_number = val & 0x7f
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)
print(kbd.report)
view raw code.py hosted with ❤ by GitHub

[permalink]

All older entries
Website Copyright © 2004-2024 Jeff Epler