If you have an unknown key matrix, the Key Matrix Whisperer can help. Hook up all the possible connections, load the key whisperer, open the REPL, and press keys one by one until you get a plausible list of row and column pins.

Download the Project Bundle

Your project will use a specific set of CircuitPython libraries and the code.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.

Continue below the full code listing for more information on using the Key Matrix Whisperer.

# SPDX-FileCopyrightText: 2022 Jeff Epler for Adafruit Industries
# SPDX-License-Identifier: MIT

# KeyMatrix Whisperer
# Interactively determine a matrix keypad's row and column pins
# Wait until the program prints "press keys now". Then, press and hold a key
# until it registers. Repeat until all rows and columns are identified. If your
# keyboard matrix does NOT have dioes, you MUST take care to only press a
# single key at a time.
# How identification is performed: When a key is pressed _some_ pair of I/Os
# will be connected. This code repeatedly scans all possible pairs, recording
# them.  The very first pass when no key is pressed is recorded as "junk" so it
# can be ignored.
# Then, the first I/O involved in the first non-junk press is arbitrarily
# recorded as a "row pin". If the matrix does not have diodes, this can
# actually vary from run to run or depending on the first key you pressed. The
# only net effect of this is that the row & column lists are exchanged.
# After enough key presses, you'll get a full list of "row" and "column" pins.
# For instance, on the Commodore 16 keyboard you'd get 8 row pins and 8 column pins.
# This doesn't help determine the LOGICAL ORDER of rows and columns or the
# physical layout of the keyboard. You still have to do that for yourself.

import board
import microcontroller
from digitalio import DigitalInOut, Pull

# List of pins to test, or None to test all pins
IO_PINS = None # [board.D0, board.D1]
# Which value(s) to set the driving pin to
values = [True] # [True, False]

def discover_io():
    return [pin_maybe for name in dir(microcontroller.pin)
            if isinstance(pin_maybe := getattr(microcontroller.pin, name), microcontroller.Pin)]

def pin_lookup(pin):
    for i in dir(board):
        if getattr(board, i) is pin:
            return i
    for i in dir(microcontroller.pin):
        if getattr(microcontroller.pin, i) is pin:
            return i
    return str(pin)

# Find all I/O pins, if IO_PINS is not explicitly set above
if IO_PINS is None:
    IO_PINS = discover_io()

# Initialize all pins as inputs, make a lookup table to get the name from the pin
ios_lookup = dict([(pin_lookup(pin), DigitalInOut(pin)) for pin in IO_PINS]) # pylint: disable=consider-using-dict-comprehension
ios = ios_lookup.values()
ios_items = ios_lookup.items()
for io in ios:

# Partial implementation of 'defaultdict' class from standard Python from
# https://github.com/micropython/micropython-lib/blob/master/python-stdlib/collections.defaultdict/collections/defaultdict.py
class defaultdict:
    def __new__(cls, default_factory=None, **kwargs): # pylint: disable=unused-argument
        # Some code (e.g. urllib.urlparse) expects that basic defaultdict
        # functionality will be available to subclasses without them
        # calling __init__().
        self = super(defaultdict, cls).__new__(cls)
        self.d = {}
        return self

    def __init__(self, default_factory=None, **kwargs):
        self.d = kwargs
        self.default_factory = default_factory

    def __getitem__(self, key):
            return self.d[key]
        except KeyError:
            v = self.__missing__(key)
            self.d[key] = v
            return v

    def __setitem__(self, key, v):
        self.d[key] = v

    def __delitem__(self, key):
        del self.d[key]

    def __contains__(self, key):
        return key in self.d

    def __missing__(self, key):
        if self.default_factory is None:
            raise KeyError(key)
        return self.default_factory()

# Track combinations that were pressed, including ones during the "junk" scan
pressed_or_junk = defaultdict(set)
# Track combinations that were pressed, excluding the "junk" scan
pressed = defaultdict(set)
# During the first run, anything scanned is "junk". Could occur for unused pins.
first_run = True
# List of pins identified as rows and columns
rows = []
cols = []
# The first pin identified is arbitrarily called a 'row' pin.
row_arbitrarily = None

while True:
    changed = False
    last_pressed = None
    for value in values:
        pull = [Pull.UP, Pull.DOWN][value]
        for io in ios:
        for name1, io1 in ios_items:
            for name2, io2 in ios_items:
                if io2 is io1:
                if io2.value == value:
                    if first_run:
                    elif name2 not in pressed_or_junk[name1]:
                        if row_arbitrarily is None:
                            row_arbitrarily = name1
                        if name2 not in pressed[name1]:
                            changed = True
                    if name2 in pressed[name1]:
                        last_pressed = (name1, name2)
                        print("Key registered. Release to continue")
                        while io2.value == value:
    if first_run:
        print("Press keys now")
        first_run = False
    elif changed:
        rows = set([row_arbitrarily])
        cols = set()
        to_check = [row_arbitrarily]
        for check in to_check:
            for other in pressed[check]:
                if other in rows or other in cols:
                if check in rows:

        rows = sorted(rows)
        cols = sorted(cols)
    if changed or last_pressed:
        print("Rows", len(rows), *rows)
        print("Cols", len(cols), *cols)
        print("Last pressed", *last_pressed)

Typical output in the REPL:

code.py output:
Press keys now
Key registered. Release to continue
Rows 1 D5
Cols 1 A0
Last pressed D5 A0

Key registered. Release to continue
Rows 1 D5
Cols 1 A0
Last pressed A2 D4
Key registered. Release to continue
Rows 8 A1 A2 CLK D3 D5 D7 D8 MISO
Cols 8 A0 A3 D10 D2 D4 D6 D9 MOSI
Last pressed D5 D10

Note how the rows and cols lists are a rearrangement (permutation) of the rows and cols from the real keyboard programs (SCK and CLK are two names for the same pin):

rows = [board.A3, board.D6, board.D10, board.D9, board.MOSI, board.D2, board.A0, board.D4]
cols = [board.A2, board.SCK, board.MISO, board.A1, board.D5, board.D7, board.D8, board.D3]

This guide was first published on Sep 14, 2022. It was last updated on Apr 15, 2024.

This page (Key Matrix Whisperer) was last updated on Apr 15, 2024.

Text editor powered by tinymce.