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, the code.py file and the keymaps.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: Copyright (c) 2022 John Park & Tod Kurt for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# Ortho split keyboard
import time
import board
from adafruit_tca8418 import TCA8418
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
from keymaps import layer_keymaps # keymaps are saved in keymaps.py file
kbd = Keyboard(usb_hid.devices)
num_layers = len(layer_keymaps)
current_layer = 1
i2c_left = board.STEMMA_I2C() # uses QT Py RP2040 STEMMA QT port
i2c_right = board.I2C() # I2C channel on the QT Py RP2040 pads broken out on board
tca_left = TCA8418(i2c_left)
tca_right = TCA8418(i2c_right)
tcas = (tca_left, tca_right) # put the TCA objects in a list for easy iteration later
# set up a R0-R7 pins and C0-C4 pins as keypads
KEYPADPINS = (
TCA8418.R0, TCA8418.R1, TCA8418.R2, TCA8418.R3, TCA8418.R4,
TCA8418.C0, TCA8418.C1, TCA8418.C2, TCA8418.C3, TCA8418.C4, TCA8418.C5
)
for tca in tcas:
for pin in KEYPADPINS:
tca.keypad_mode[pin] = True
tca.enable_int[pin] = True
tca.event_mode_fifo[pin] = True
tca.key_intenable = True
print("Ortho Split Keyboard")
while True:
for i in range(len(tcas)):
tca = tcas[i] # get the TCA we're working with
keymap = layer_keymaps[current_layer][i] # get the corresponding keymap for it
if tca.key_int:
events = tca.events_count
for _ in range(events):
keyevent = tca.next_event
keymap_number = (keyevent & 0x7F)
(modifier, keycode) = keymap[keymap_number] # get keycode & modifer from keymap
# print("\tKey event: 0x%02X - key #%d " % (keyevent, keyevent & 0x7F))
if keycode is None:
pass
else:
if keyevent & 0x80: # if key is pressed
if modifier == 0: # normal keypress
kbd.press(keycode)
elif modifier == 1: # lower
current_layer = min(max((current_layer-1), 0), num_layers-1)
elif modifier == 2: # raise
current_layer = min(max((current_layer+1), 0), num_layers-1)
elif modifier == 7: # cap mod
kbd.press(Keycode.SHIFT, keycode)
else: # key released
if modifier == 7: # capped shifted key requires special handling
kbd.release(Keycode.SHIFT, keycode)
else:
kbd.release(keycode)
tca.key_int = True # clear the IRQ by writing 1 to it
time.sleep(0.01)
How It Works
The keyboard uses two TCA8418 expanders to read the matrix columns and rows of the keyboard halves. The events are queued up and sent over I2C to the QT Py RP2040 which then correlates each keypress with a keycode from the keymaps.py file. These keypresses are then sent to the computer as USB HID keys.
Libraries
You'll import libraries to provide functionality in the code:
- time
- board
- adafruit_tca8418
- usb_hid
- adafruit_hid.keyboard
- adafruit_hid.keycode
The keymaps file is imported as well so that the layer_keymaps can be accessed inside of code.py
import time import board from adafruit_tca8418 import TCA8418 import usb_hid from adafruit_hid.keyboard import Keyboard from adafruit_hid.keycode import Keycode from keymaps import layer_keymaps
kbd = Keyboard(usb_hid.devices) num_layers = len(layer_keymaps) current_layer = 1
I2C Setup
You'll use both I2C channels on the QT Py RP2040 to connect to the two TCA8418 boards. Since they share the same address which cannot be changed, they can't be on the same I2C bus.
i2c_left = board.STEMMA_I2C() # uses QT Py RP2040 STEMMA QT port i2c_right = board.I2C() # I2C channel on the QT Py RP2040 pads broken out on board tca_left = TCA8418(i2c_left) tca_right = TCA8418(i2c_right) tcas = (tca_left, tca_right) # put the TCA objects in a list for easy iteration later
Matrix Pins
The column and row pins of the TCA8418 are fixed, so we'll specify the ones we're using for the six columns and five rows of the keyboard halves.
Then, each pin is set to keypad_mode with enable and fifo (first in, first out) set.
KEYPADPINS = (
TCA8418.R0, TCA8418.R1, TCA8418.R2, TCA8418.R3, TCA8418.R4,
TCA8418.C0, TCA8418.C1, TCA8418.C2, TCA8418.C3, TCA8418.C4, TCA8418.C5
)
for tca in tcas:
for pin in KEYPADPINS:
tca.keypad_mode[pin] = True
tca.enable_int[pin] = True
tca.event_mode_fifo[pin] = True
tca.key_intenable = True
Main Loop
The main loop of the program checks each TCA8418 for events in the queue. If a key has been pressed or released it is checked against the keymap file to see which keycode to press or release. These can vary depending on modifiers and layers as well.
while True:
for i in range(len(tcas)):
tca = tcas[i] # get the TCA we're working with
keymap = layer_keymaps[current_layer][i] # get the corresponding keymap for it
if tca.key_int:
events = tca.events_count
for _ in range(events):
keyevent = tca.next_event
keymap_number = (keyevent & 0x7F)
(modifier, keycode) = keymap[keymap_number] # get keycode & modifer from keymap
# print("\tKey event: 0x%02X - key #%d " % (keyevent, keyevent & 0x7F))
if keycode is None:
pass
else:
if keyevent & 0x80: # if key is pressed
if modifier == 0: # normal keypress
kbd.press(keycode)
elif modifier == 1: # lower
current_layer = min(max((current_layer-1), 0), num_layers-1)
elif modifier == 2: # raise
current_layer = min(max((current_layer+1), 0), num_layers-1)
elif modifier == 7: # cap mod
kbd.press(Keycode.SHIFT, keycode)
else: # key released
if modifier == 7: # capped shifted key requires special handling
kbd.release(Keycode.SHIFT, keycode)
else:
kbd.release(keycode)
tca.key_int = True # clear the IRQ by writing 1 to it
time.sleep(0.01)
# SPDX-FileCopyrightText: Copyright (c) 2022 John Park & Tod Kurt for Adafruit Industries
#
# SPDX-License-Identifier: MIT
from adafruit_hid.keycode import Keycode
# https://docs.circuitpython.org/projects/hid/en/latest/api.html#adafruit-hid-keycode-keycode
# keymap is keynumber, (modifier, keycode)
# lower keymap layer
km_lf_0 = {
(1) : (0, Keycode.F11),
(2) : (0, Keycode.F1),
(3) : (0, Keycode.F2),
(4) : (0, Keycode.F3),
(5) : (0, Keycode.F4),
(6) : (0, Keycode.F5),
(11) : (0, None),
(12) : (0, None),
(13) : (0, None),
(14) : (0, None),
(15) : (0, None),
(16) : (0, None),
(21) : (0, None),
(22) : (0, None),
(23) : (0, None),
(24) : (0, None),
(25) : (0, None),
(26) : (0, None),
(31) : (0, None),
(32) : (0, None),
(33) : (0, None),
(34) : (0, None),
(35) : (0, None),
(36) : (0, None),
(41) : (0, Keycode.CONTROL),
(42) : (0, Keycode.GUI),
(43) : (0, Keycode.ALT),
(44) : (0, Keycode.GUI),
(45) : (1, Keycode.L), # lower (the keycode doesn't matter here, it's never typed)
(46) : (0, Keycode.SPACE)
}
km_rt_0 = {
(1) : (0, Keycode.F6),
(2) : (0, Keycode.F7),
(3) : (0, Keycode.F8),
(4) : (0, Keycode.F9),
(5) : (0, Keycode.F10),
(6) : (0, Keycode.F12),
(11) : (0, Keycode.HOME),
(12) : (0, Keycode.PAGE_DOWN),
(13) : (0, Keycode.PAGE_UP),
(14) : (0, Keycode.END),
(15) : (0, Keycode.INSERT),
(16) : (0, Keycode.DELETE),
(21) : (0, None),
(22) : (0, None),
(23) : (0, None),
(24) : (0, None),
(25) : (0, None),
(26) : (0, None),
(31) : (0, None),
(32) : (0, None),
(33) : (0, None),
(34) : (0, None),
(35) : (0, None),
(36) : (0, None),
(41) : (0, Keycode.SPACE),
(42) : (2, Keycode.R), # raise
(43) : (0, Keycode.LEFT_ARROW),
(44) : (0, Keycode.DOWN_ARROW),
(45) : (0, Keycode.UP_ARROW),
(46) : (0, Keycode.RIGHT_ARROW)
}
# main keymap layer
km_lf_1 = {
(1) : (0, Keycode.GRAVE_ACCENT),
(2) : (0, Keycode.ONE),
(3) : (0, Keycode.TWO),
(4) : (0, Keycode.THREE),
(5) : (0, Keycode.FOUR),
(6) : (0, Keycode.FIVE),
(11) : (0, Keycode.ESCAPE),
(12) : (0, Keycode.Q),
(13) : (0, Keycode.W),
(14) : (0, Keycode.E),
(15) : (0, Keycode.R),
(16) : (0, Keycode.T),
(21) : (0, Keycode.TAB),
(22) : (0, Keycode.A),
(23) : (0, Keycode.S),
(24) : (0, Keycode.D),
(25) : (0, Keycode.F),
(26) : (0, Keycode.G),
(31) : (0, Keycode.SHIFT),
(32) : (0, Keycode.Z),
(33) : (0, Keycode.X),
(34) : (0, Keycode.C),
(35) : (0, Keycode.V),
(36) : (0, Keycode.B),
(41) : (0, Keycode.CONTROL),
(42) : (0, Keycode.GUI),
(43) : (0, Keycode.ALT),
(44) : (0, Keycode.GUI),
(45) : (1, Keycode.L), # lower
(46) : (0, Keycode.SPACE)
}
km_rt_1 = {
(1) : (0, Keycode.SIX),
(2) : (0, Keycode.SEVEN),
(3) : (0, Keycode.EIGHT),
(4) : (0, Keycode.NINE),
(5) : (0, Keycode.ZERO),
(6) : (0, Keycode.BACKSPACE),
(11) : (0, Keycode.Y),
(12) : (0, Keycode.U),
(13) : (0, Keycode.I),
(14) : (0, Keycode.O),
(15) : (0, Keycode.P),
(16) : (0, Keycode.BACKSLASH),
(21) : (0, Keycode.H),
(22) : (0, Keycode.J),
(23) : (0, Keycode.K),
(24) : (0, Keycode.L),
(25) : (0, Keycode.SEMICOLON),
(26) : (0, Keycode.QUOTE),
(31) : (0, Keycode.N),
(32) : (0, Keycode.M),
(33) : (0, Keycode.COMMA),
(34) : (0, Keycode.PERIOD),
(35) : (0, Keycode.FORWARD_SLASH),
(36) : (0, Keycode.ENTER),
(41) : (0, Keycode.SPACE),
(42) : (2, Keycode.R), # raise
(43) : (0, Keycode.LEFT_ARROW),
(44) : (0, Keycode.DOWN_ARROW),
(45) : (0, Keycode.UP_ARROW),
(46) : (0, Keycode.RIGHT_ARROW)
}
# upper keymap layer
km_lf_2 = {
(1) : (0, None),
(2) : (0, None),
(3) : (0, None),
(4) : (0, None),
(5) : (0, None),
(6) : (0, None),
(11) : (0, Keycode.ESCAPE),
(12) : (0, None),
(13) : (0, None),
(14) : (0, None),
(15) : (0, None),
(16) : (0, None),
(21) : (0, Keycode.TAB),
(22) : (0, None),
(23) : (0, None),
(24) : (0, Keycode.MINUS),
(25) : (0, Keycode.EQUALS),
(26) : (7, Keycode.BACKSLASH), # PIPE '|'
(31) : (0, Keycode.SHIFT),
(32) : (0, None),
(33) : (0, None),
(34) : (7, Keycode.MINUS), # UNDERSCORE
(35) : (0, Keycode.KEYPAD_PLUS),
(36) : (0, Keycode.BACKSLASH),
(41) : (0, Keycode.CONTROL),
(42) : (0, Keycode.GUI),
(43) : (0, Keycode.ALT),
(44) : (0, Keycode.GUI),
(45) : (1, Keycode.L), # lower
(46) : (0, Keycode.SPACE)
}
km_rt_2 = {
(1) : (0, None),
(2) : (0, None),
(3) : (0, None),
(4) : (0, None),
(5) : (0, None),
(6) : (0, Keycode.BACKSPACE),
(11) : (0, None),
(12) : (0, None),
(13) : (0, None),
(14) : (0, None),
(15) : (0, None),
(16) : (0, Keycode.BACKSLASH),
(21) : (0, None),
(22) : (0, Keycode.LEFT_BRACKET),
(23) : (0, Keycode.RIGHT_BRACKET),
(24) : (0, None),
(25) : (0, None),
(26) : (0, Keycode.QUOTE),
(31) : (0, None),
(32) : (7, Keycode.LEFT_BRACKET),
(33) : (7, Keycode.RIGHT_BRACKET),
(34) : (0, None),
(35) : (0, None),
(36) : (0, Keycode.ENTER),
(41) : (0, Keycode.SPACE),
(42) : (2, Keycode.R), # raise
(43) : (0, Keycode.LEFT_ARROW),
(44) : (0, Keycode.DOWN_ARROW),
(45) : (0, Keycode.UP_ARROW),
(46) : (0, Keycode.RIGHT_ARROW)
}
# put the keymaps in layer lists for easy iteration later
keymaps_1 = (km_lf_0, km_rt_0)
keymaps_2 = (km_lf_1, km_rt_1)
keymaps_3 = (km_lf_2, km_rt_2)
layer_keymaps = (keymaps_1, keymaps_2, keymaps_3)
The keymaps.py file first imports the adafruit_hid.keycode library.
from adafruit_hid.keycode import Keycode
The file is organized by keymap sides (left/right), layers (0, 1, 2), and row clusters (1-6, 11-16, 21-26, etc.) These are dictionaries which have a "key" which corresponds to the key number as reported by the TCA8418, and a value tuple which tells the code two things: which modifier, and which keycode to use.
The modifier is usually 0 which means the keycode is sent as a normal keypress/release.
Modifer 1 means the "lower" key has been pressed, so a different keymap set is to be used until the "raise" key is pressed.
Modifier 2 means "raise"
Modifier 7 means a Keycode.SHIFT + the keycode are pressed at the same time. Normally you use the shift key to get this effect, but in some cases you may want to assign a key that is normally only invoked via shift. For example, a { key is usually shift + Keycode.LEFT_BRACKET, but if you want a key to press { all on its own, use modifier 7.
Page last edited January 22, 2025
Text editor powered by tinymce.