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.
Text editor powered by tinymce.