4 September 2022, 2:11 UTC
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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()) |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
All older entries
Website Copyright © 2004-2024 Jeff Epler