Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds nullbitsco/nibble mapping with multiplex keymatrix support #991

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions boards/nullbitsco/nibble/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Keyboard mapping for the [nullbits nibble](https://nullbits.co/nibble/).

Copy `kb.py` and `main.py` to your top level CircuitPython folder beside the kmk folder.
Edit the key mapping in `main.py` to match your keyboard layout.

The Keyboard constructor supports an optional `encoder` argument (see `kb.py`).
See the sample `main.py` for an example of how to configure the encoder.

The RGB extension in the example requires a copy of `neopixel.py` in your top level
CircuitPython folder: see https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/rgb.md.
50 changes: 50 additions & 0 deletions boards/nullbitsco/nibble/kb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import board
import digitalio

from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard
from kmk.modules.encoder import EncoderHandler
from kmk.quickpin.pro_micro.bitc_promicro import pinout as pins
from kmk.scanners import DiodeOrientation
from kmk.scanners.digitalio import MatrixScanner

# bitc pro pinout: https://nullbits.co/static/img/bitc_pro_pinout.png
# nibble/tidbit pinout: https://github.com/nullbitsco/docs/blob/main/nibble/build_guide_img/mcu_pinouts.png
# key - diode mapping https://nullbits.co/static/file/NIBBLE_diode_key.pdf
# row connects to anode end, col connects to cathode end

# also defined: board.LED_RED, board.LED_GREEN, and board.LED_BLUE == board.LED
row_pins = (pins[15], pins[14], pins[13], pins[12], pins[6]) # GPIO 22,20,23,21,4
col_mux_pins = (pins[19], pins[18], pins[17], pins[16]) # GPIO 29..26
encoder_pins = (pins[10], pins[11], None) # GPIO 8,9, button in key matrix
pixel_pin = pins[9] # GPIO 7
# LED R, G, B pins: GPIO 6, 5, 3
# extension pins GPIO 11, 12, 13, 14


class KMKKeyboard(_KMKKeyboard):
'''
Create a nullbits nibble keyboard.
optional constructor arguments:

encoder=True if encoder installed
then declare keyboard.encoders.map = [(KC.<left> , KC.<right>, None), (...)]
'''

pixel_pin = pixel_pin
i2c = board.I2C # TODO ??

def __init__(self, encoder=False):
super().__init__()

self.matrix = MatrixScanner(
col_mux_pins,
row_pins,
diode_orientation=DiodeOrientation.ROW2COL, # row is anode, col is cathode
pull=digitalio.Pull.UP,
multiplexed=True,
)

if encoder:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The encoder import belongs here, otherwise the encoder code is always loaded and wastes memory. Not an issue for this board, but a bad example to set.

Suggested change
if encoder:
if encoder:
from kmk.modules.encoder import EncoderHandler

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this entire code block should go in main.py, including the conditional import.

self.encoders = EncoderHandler()
self.encoders.pins = (encoder_pins,)
self.modules.append(self.encoders)
43 changes: 43 additions & 0 deletions boards/nullbitsco/nibble/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from kb import KMKKeyboard

from kmk.extensions.media_keys import MediaKeys
from kmk.extensions.rgb import RGB, AnimationModes
from kmk.keys import KC

keyboard = KMKKeyboard(encoder=True) # assume encoder installed
keyboard.extensions.append(MediaKeys())

XXXXX = KC.NO

# fmt: off
keyboard.keymap = [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind formatting this in the usual "looks like keymatrix" way? Prefix the keymap definition with # fmt: off and postfix with # fmt:on to selectively disable the linter.

[
XXXXX, KC.ESC, KC.N1, KC.N2, KC.N3, KC.N4, KC.N5, KC.N6, KC.N7, KC.N8, KC.N9, KC.N0, KC.MINS,KC.EQL, KC.BKSP,KC.DEL, # noqa: E231
KC.MUTE,KC.TAB, KC.Q, KC.W, KC.E, KC.R, KC.T, KC.Y, KC.U, KC.I, KC.O, KC.P, KC.LBRC,KC.RBRC,KC.BSLS,KC.GRV, # noqa: E231
KC.F1, KC.CAPS,KC.A, KC.S, KC.D, KC.F, KC.G, KC.H, KC.J, KC.K, KC.L, KC.SCLN,KC.QUOT,KC.ENT, KC.ENT, KC.PGUP, # noqa: E231
KC.F2, KC.LSFT,KC.Z, KC.X, KC.C, KC.V, KC.B, KC.N, KC.M, KC.COMM,KC.DOT, KC.SLSH,KC.RSFT,XXXXX, KC.UP, KC.PGDN, # noqa: E231
KC.F3, KC.LCTL,KC.LCMD,KC.LALT,KC.SPC, KC.SPC, KC.SPC, KC.SPC, KC.SPC, KC.RCMD,KC.RALT,KC.RCTL,KC.LEFT,XXXXX, KC.DOWN,KC.RGHT, # noqa: E231
]
]
# fmt: on

# note that encoder button is configured in the keymap (KC.MUTE above) so set to XXXXX here
keyboard.encoders.map = [
((KC.VOLD, KC.VOLU, XXXXX),), # Layer 1, encoder 1
]

rgb = RGB(
pixel_pin=keyboard.pixel_pin,
num_pixels=10,
hue_default=180,
sat_default=255,
val_default=50,
animation_mode=AnimationModes.BREATHING,
animation_speed=3,
breathe_center=2,
)

keyboard.extensions.append(rgb)

if __name__ == '__main__':
keyboard.go()
8 changes: 4 additions & 4 deletions boards/nullbitsco/tidbit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ for example the number and location of encoders and double-size keys.

The Keyboard constructor supports a couple of optional arguments (see `kb.py`).

If you're setting your tidbit up in landscape mode,
with the USB connector at top right instead of top left, pass
If you're setting your tidbit up in landscape mode,
with the USB connector at top right instead of top left, pass
`landscape_layout=True`.

You can specify the active encoder positions by passing a list like
`active_encoders=[0, 2]` which corresponds to the 1st and 3rd positions shown
in [step 6](https://github.com/nullbitsco/docs/blob/main/tidbit/build_guide_en.md#6-optional-solder-rotary-encoder-led-matrix-andor-oled-display) of the build guide.
The default is for a single encoder in either of the top two locations labeled 1
The default is for a single encoder in either of the top two locations labeled 1
in the build diagram, i.e. `active_encoders=[0]`. Pass an empty list if you skipped
adding any encoders.

Expand All @@ -27,7 +27,7 @@ from kmk.extensions.rgb import RGB, AnimationModes
keyboard = KMKKeyboard(active_encoders=[0], landscape_layout=True)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct. There is no landscape_layout parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is for the tidbit, but not for the nibble. See here: https://github.com/KMKfw/kmk_firmware/blob/main/boards/nullbitsco/tidbit/kb.py#L48


rgb = RGB(
pixel_pin=keyboard.pixel_pin,
pixel_pin=keyboard.pixel_pin,
num_pixels=8,
animation_mode=AnimationModes.BREATHING,
animation_speed=3,
Expand Down
34 changes: 17 additions & 17 deletions kmk/quickpin/pro_micro/bitc_promicro.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,27 @@
# Bit-C-Pro RP2040 pinout for reference, see https://nullbits.co/bit-c-pro/
# (unused)
pinout = [
board.D0, # Enc 3
board.D1, # Enc 3
board.D0,
board.D1,
None, # GND
None, # GND
board.D2, # Enc 2
board.D3, # Enc 2
board.D4, # Row 4 + breakout SDA
board.D5, # Row 3 + breakout SCL
board.D6, # Row 2
board.D7, # Row 1
board.D8, # Enc 1
board.D9, # Enc 1
board.D2,
board.D3,
board.D4, # breakout SDA
board.D5, # breakout SCL
board.D6,
board.D7,
board.D8,
board.D9,
# Unconnected breakout pins D11, D12, GND, D13, D14
board.D21, # WS2812 LEDs labeled D10/GP21 but only board.D21 is defined
board.D23, # MOSI - Enc 0
board.D20, # MISO - Enc 0
board.D22, # SCK - Row 0
board.D26, # A0 - Col 3
board.D27, # A1 - Col 2
board.D28, # A2 - Col 1
board.D29, # A3 - Col 0
board.D23, # MOSI
board.D20, # MISO
board.D22, # SCK
board.D26,
board.D27,
board.D28,
board.D29,
None, # 3.3v
None, # RST
None, # GND
Expand Down
35 changes: 26 additions & 9 deletions kmk/scanners/digitalio.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ def __init__(
rows,
diode_orientation=DiodeOrientation.COL2ROW,
pull=digitalio.Pull.UP,
rollover_cols_every_rows=None,
rollover_cols_every_rows=None, # with value k, treat k*r x c matrix as r x c*k
offset=0,
multiplexed=False, # 2^k outputs are multiplexed on k output pins
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of packing even more conditional functionality into the already slow digitalio scanner.
I also don't quite understand what's happening here. Is that something that could be replaced by
CircuitPythons native keypad_demux.DemuxKeyMatrix (currently beta)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at keypad_demux docs, but it didn't seem to be available on my board yet. It also didn't seem quite as flexible in terms of configuring which direction is multiplexed, and pullup v pulldown etc.

Honestly I was a bit confused why digitialio.MatrixScanner exists at all when we have keypad.KeyMatrix natively but I am pretty new to circuitpython/kmk. I thought the modification was relatively small: in the regular case we scan column n by activating the n-th column pin, but in the mutiplexed case we scan column n by writing n as a bit pattern b0 b1 .. bk and set each multiplexed column bit.

If you prefer I could clone or subclass MatrixScanner to DemuxMatrixScanner instead?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting keypad_demux build for your board shouldn't be too difficult. It is still in beta in any case.

The digitalio scanner predates the CP native matrix scanner, and there're hardware implementations that don't work with KeyMatrix -- at least for now -- that's the reason it's still around. If in doubt: always use the native scanner.
Thank you for the explanation, I understand what kind of mux we're talking about now.

Can you subclass without duplicating all of the code? I'm a bit short on time atm. and can't estimate that right now.

):
self.len_cols = len(cols)
self.len_rows = len(rows)
Expand All @@ -41,15 +42,16 @@ def __init__(
del unique_pins

self.diode_orientation = diode_orientation
self.multiplexed = multiplexed

if self.diode_orientation == DiodeOrientation.COL2ROW:
self.anodes = [ensure_DIO(x) for x in cols]
self.cathodes = [ensure_DIO(x) for x in rows]
self.translate_coords = True
self.rows_are_inputs = True
elif self.diode_orientation == DiodeOrientation.ROW2COL:
self.anodes = [ensure_DIO(x) for x in rows]
self.cathodes = [ensure_DIO(x) for x in cols]
self.translate_coords = False
self.rows_are_inputs = False
else:
raise ValueError(f'Invalid DiodeOrientation: {self.diode_orienttaion}')

Expand All @@ -59,7 +61,7 @@ def __init__(
elif self.pull == digitalio.Pull.UP:
self.outputs = self.cathodes
self.inputs = self.anodes
self.translate_coords = not self.translate_coords
self.rows_are_inputs = not self.rows_are_inputs
else:
raise ValueError(f'Invalid pull: {self.pull}')

Expand All @@ -69,6 +71,12 @@ def __init__(
for pin in self.inputs:
pin.switch_to_input(pull=self.pull)

if self.multiplexed:
if self.rows_are_inputs:
self.len_cols = 1 << self.len_cols
else:
self.len_rows = 1 << self.len_rows

self.rollover_cols_every_rows = rollover_cols_every_rows
if self.rollover_cols_every_rows is None:
self.rollover_cols_every_rows = self.len_rows
Expand All @@ -90,9 +98,17 @@ def scan_for_changes(self):
'''
ba_idx = 0
any_changed = False

for oidx, opin in enumerate(self.outputs):
opin.value = self.pull is not digitalio.Pull.UP
n = len(self.outputs)

for oidx in range(n if not self.multiplexed else (1 << n)):
if not self.multiplexed:
opin = self.outputs[oidx]
opin.value = self.pull is not digitalio.Pull.UP
else:
for bit, opin in enumerate(self.outputs):
opin.value = (oidx & (1 << bit) != 0) != (
self.pull is not digitalio.Pull.UP
)

for iidx, ipin in enumerate(self.inputs):
# cast to int to avoid
Expand All @@ -110,7 +126,7 @@ def scan_for_changes(self):
old_val = self.state[ba_idx]

if old_val != new_val:
if self.translate_coords:
if self.rows_are_inputs:
new_oidx = oidx + self.len_cols * (
iidx // self.rollover_cols_every_rows
)
Expand All @@ -135,7 +151,8 @@ def scan_for_changes(self):

ba_idx += 1

opin.value = self.pull is digitalio.Pull.UP
if not self.multiplexed:
opin.value = self.pull is digitalio.Pull.UP
if any_changed:
break

Expand Down