Skip to content

Instantly share code, notes, and snippets.

@jepler
Last active September 28, 2023 12:04
Show Gist options
  • Save jepler/eec312a023001b5870bfa32cc08a5aab to your computer and use it in GitHub Desktop.
Save jepler/eec312a023001b5870bfa32cc08a5aab to your computer and use it in GitHub Desktop.
Pure CircuitPython key scanner and USB HID firmware for Unicomp "MINI M" with RP2040 microcontroller
# Pure CircuitPython key scanner and USB HID firmware for Unicomp "MINI M" with RP2040 microcontroller
# Based on study of https://github.com/purdeaandrei/vial-qmk-mini-m/blob/9508763de706e50fb25111363fc1cab23ac8f95c/keyboards/unicomp/mini_m/justify_mike_smith/matrix.c#L20
import gc
import micropython
import microcontroller
from adafruit_hid.keycode import Keycode as K
from adafruit_hid.keyboard import Keyboard
import usb_hid
import supervisor
from microcontroller import watchdog as w
from watchdog import WatchDogMode
from board import *
import digitalio
def make_digitalin(p):
r = digitalio.DigitalInOut(p)
r.switch_to_input(digitalio.Pull.UP)
return r
def make_digitalout(p, initial_value=False):
r = digitalio.DigitalInOut(p)
r.switch_to_output(initial_value)
return r
LEDS = [make_digitalout(p) for p in (GP6, GP7, GP8)]
LED_COM = make_digitalout(GP5, True)
A0, A1, A2 = [make_digitalout(p) for p in (GP0, GP1, GP2)]
EN_U1, EN_U2 = [make_digitalout(p, True) for p in (GP3, GP4)]
ROW = [make_digitalin(p) for p in (
GP11, GP12, GP13, GP14,
GP15, GP16, GP17, GP18,
GP19, GP20, GP21, GP22)]
ROWS = const(12)
COLS = const(16)
N = const(ROWS*COLS)
def scanner():
def scan1(state):
for row in range(ROWS):
counts_by_row[row] = 0
for column in range(COLS):
A0.value = column & 1
A1.value = column & 2
A2.value = column & 4
if column & 8:
EN_U2.value = False
else:
EN_U1.value = False
n = 0
for row in range(len(ROW)):
pin = ROW[row]
i = row + column * ROWS
# never fill a matrix position with no associated key
# this improves rollover for instance, A+F1+W cause a ghost
# at matrix location that is otherwise unused. By not
# recording a value at the ghost location, the ghost
# suppression code below won't activate.
if kc_unused[i]: continue
value = not pin.value
n += value
counts_by_row[row] += value
state[i] = value
counts_by_column[column] = n
if column & 8:
EN_U2.value = True
else:
EN_U1.value = True
counts_by_column = [0] * COLS
counts_by_row = [0] * ROWS
state = [0] * N
latch = [0] * N
old_state = [0] * N
while True:
scan1(state)
scan1(latch)
while state != latch: # something changed
state, latch = latch, state
scan1(latch)
for column in range(COLS):
if counts_by_column[column] < 2:
continue
for row in range(ROWS):
i = row + column * ROWS
if not state[i]:
continue
if counts_by_row[row] < 2:
continue
state[i] = old_state[i]
for i in range(N):
if state[i] != old_state[i]:
yield i | (state[i] << 15)
old_state, state = state, old_state
yield None # finished a scan
kbd = Keyboard(usb_hid.devices)
keycodes = {
12: K.ESCAPE,
26: K.F1,
48: K.F2,
60: K.F3,
72: K.F4,
97: K.F5,
99: K.F6,
100: K.F7,
101: K.F8,
102: K.F9,
126: K.F10,
138: K.F11,
139: K.F12,
162: K.PRINT_SCREEN,
163: K.KEYPAD_NUMLOCK,
191: K.PAUSE,
0: K.GRAVE_ACCENT,
27: K.ONE,
30: K.TWO,
31: K.THREE,
28: K.FOUR,
24: K.FIVE,
85: K.SIX,
89: K.SEVEN,
94: K.EIGHT,
93: K.NINE,
92: K.ZERO,
150: K.MINUS,
114: K.EQUALS,
116: K.BACKSPACE,
146: K.TAB,
25: K.Q,
29: K.W,
34: K.E,
33: K.R,
32: K.T,
87: K.Y,
90: K.U,
91: K.I,
88: K.O,
84: K.P,
113: K.LEFT_BRACKET,
118: K.RIGHT_BRACKET,
117: K.BACKSLASH,
1: K.CAPS_LOCK,
134: K.A,
2: K.S,
14: K.D,
38: K.F,
50: K.G,
71: K.H,
107: K.J,
83: K.K,
119: K.L,
131: K.SEMICOLON,
155: K.QUOTE,
156: K.ENTER,
184: K.LEFT_SHIFT,
62: K.Z,
98: K.X,
74: K.C,
110: K.V,
122: K.B,
143: K.N,
11: K.M,
23: K.COMMA,
47: K.PERIOD,
59: K.FORWARD_SLASH,
181: K.RIGHT_SHIFT,
172: K.LEFT_CONTROL,
135: K.GUI,
144: K.LEFT_ALT,
10: K.SPACE,
45: K.RIGHT_ALT,
125: K.RIGHT_GUI,
58: K.APPLICATION,
169: K.RIGHT_CONTROL,
152: K.INSERT,
165: K.HOME,
154: K.PAGE_UP,
167: K.DELETE,
140: K.END,
166: K.PAGE_DOWN,
128: K.UP_ARROW,
57: K.LEFT_ARROW,
142: K.DOWN_ARROW,
130: K.RIGHT_ARROW
}
keycodes_l1 = {
89: K.KEYPAD_SEVEN,
94: K.KEYPAD_EIGHT,
93: K.KEYPAD_NINE,
150: K.KEYPAD_MINUS,
114: K.KEYPAD_PLUS,
90: K.KEYPAD_FOUR,
91: K.KEYPAD_FIVE,
88: K.KEYPAD_SIX,
107: K.KEYPAD_ONE,
83: K.KEYPAD_TWO,
119: K.KEYPAD_THREE,
131: K.KEYPAD_ASTERISK,
11: K.KEYPAD_ZERO,
#23: K.COMMA,
47: K.KEYPAD_PERIOD,
59: K.KEYPAD_FORWARD_SLASH,
}
kc_unused = [i not in keycodes for i in range(N)]
held = next(scanner())
print("held", held)
if held == 12 | 1<<15: # Escape
microcontroller.on_next_reset(microcontroller.RunMode.UF2)
microcontroller.reset()
if 1: # held == 26 | 1<<15: # F1
print("overclocking to 200MHz - 9ms scan time")
microcontroller.cpu.frequency = 200_000_000
held=None
if 0: #held is None:
print("engaging watchdog")
supervisor.set_next_code_file(__file__, reload_on_error=True)
w.timeout=2.5 # Set a timeout of 2.5 seconds
w.mode = WatchDogMode.RESET
def feed(): w.feed()
else:
print("no watchdog")
def feed(): pass
t0 = supervisor.ticks_ms()
n = 0
led = ~0
gc.disable() # So we never get GC pauses
try:
for ev in scanner():
if ev is None: # complete scan, poll LEDs
n += 1
if n == 100:
t1 = supervisor.ticks_ms()
dt = t1-t0
print(dt)
t0 = t1
n = 0
feed()
leds = kbd.led_status
if leds and leds[0] != led:
led = leds[0]
for i in range(3):
LEDS[i].value = not (led & (1 << i))
continue
key_number = ev & 0xfff
pressed = ev >> 15
keycode = keycodes.get(key_number)
if led & 1:
keycode = keycodes_l1.get(key_number, keycode)
# the normal methods aren't used so that gc allocations can be avoided
if keycode is not None:
if pressed:
kbd._add_keycode_to_report(keycode)
else:
kbd._remove_keycode_from_report(keycode)
kbd._keyboard_device.send_report(kbd.report)
finally:
kbd.release_all()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment