Kitty Paw

Build the cutest four-button mechanical keypad with the Adafruit QT Py RP2040 and a 1.54in TFT display. Using CircuitPython, you can turn this into a USB macro pad or a MIDI controller.

Kitty Paw

Designed to look like a kitty paw, this four-button keypad is 3D printed and uses mechanical key switches. Featuring key cap "Toe Beans", this little project is paired nicely with one of Adafruit's cutest dev boards.

USB Macros & MIDI

The 1.54in TFT screen displays an animated bitmap of the infamous Party Parrot and advances a frame each time a key is pressed. The buttons can be mapped to MIDI notes and keyboard shortcuts, making this fully customizable.

3D Printing & DIY Electronics

The QT Py RP2040 is perfect for projects that need a display and a few key switches. The enclosure is 3D printed with parts that snap fit together.

Prerequisite Guides

Take a moment to walk through the following guides to get familiar with the parts and CircuitPython libraries.

Parts List

What a cutie pie! Or is it... a QT Py? This diminutive dev board comes with one of our new favorite chip, the RP2040. It's been made famous in the new
$9.95
In Stock
We've been looking for a display like this for a long time - it's only 1.5" diagonal but has a high density 220 ppi, 240x240 pixel display with full-angle viewing. It...
$24.95
In Stock
For crafting your very own custom keyboard, these Kailh Black Linear mechanical key switches are deeee-luxe! With smooth actuation and Cherry MX...
$6.95
In Stock
For those who are fans of our silicone-covered wires, but are always looking to up their wiring game. We now have Silicone Cover Ribbon cables! These may look...
$3.95
In Stock
1 x USB C Cable
USB Type C Cable with Data/Charge Switch - 1 meter long

The diagram below provides a visual reference for wiring of the components. This diagram was created using the software package Fritzing.

Adafruit Library for Fritzing

Use Adafruit's Fritzing parts library to create circuit diagrams for your projects. Download the library or just grab individual parts. Get the library and parts from GitHub - Adafruit Fritzing Parts.

1.54in TFT Display to QT Py RP2040

The list of connections for the 1.54in TFT Display and QT Py RP2040. 

  • V+ from TFT to 3V on QT Py
  • GND  from TFT to GND QT Py
  • CK from TFT to SCK QT Py
  • SI from TFT to MOSI on Qt Py
  • TC from TFT to RX on QT Py
  • RT from TFT to TX on QT Py
  • DC from TFT to SCL on QT Py
  • BL from TFT to SDA on QT Py

Switches to QT Py RP2040

The list of connections for the switches and QT Py RP2040. All of the switches share common ground. The last switch in the array connects the ground pin on the QT Py. 

  • Switch 1 pin 1 to A0 on QT Py
  • Switch 2 pin 1 to A1 on QT Py
  • Switch 3 pin 1 to A2 on QT Py
  • Switch 4 pin 1 to A3 on QT Py

CAD Parts List

STL files for 3D printing are oriented to print "as-is" on FDM style machines. Parts are designed to 3D print without any support material. Original design source may be downloaded using the links below:

  • keyplate.stl
  • top.stl
  • bottom.stl
  • frame.stl
  • caps.stl
  • display-cover.stl

CAD Assembly

The four key switches snap fit onto the key plate. The TFT display is secured to the key plate with M2.5 screws and hex nuts. The key plate snap fits into the frame. The QT Py is secured to the bottom cover. The bottom cover snap fits under the frame. The TFT cover is glued to the top cover. The top cover snap fits over the top cover. The caps are fitted over the kailh mechanical key switches.

Slicing Parts

No supports are required. Slice with setting for PLA material. 

The parts were sliced using CURA using the slice settings below.

  • PLA filament 220c extruder
  • 0.2 layer height
  • 10% gyroid infill
  • 60mm/s print speed
  • 60c heated bed

Design Source Files

The project assembly was designed in Fusion 360. This can be downloaded in different formats like STEP, STL and more. Electronic components like Adafruit's board, displays, connectors and more can be downloaded from the Adafruit CAD parts GitHub Repo.

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.

CircuitPython Quickstart

Follow this step-by-step to quickly get CircuitPython running on your board.

Click the link above to download the latest CircuitPython UF2 file.

Save it wherever is convenient for you.

To enter the bootloader, hold down the BOOT/BOOTSEL button (highlighted in red above), and while continuing to hold it (don't let go!), press and release the reset button (highlighted in blue above). Continue to hold the BOOT/BOOTSEL button until the RPI-RP2 drive appears!

If the drive does not appear, release all the buttons, and then repeat the process above.

You can also start with your board unplugged from USB, press and hold the BOOTSEL button (highlighted in red above), continue to hold it while plugging it into USB, and wait for the drive to appear before releasing the button.

A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.

You will see a new disk drive appear called RPI-RP2.

 

Drag the adafruit_circuitpython_etc.uf2 file to RPI-RP2.

The RPI-RP2 drive will disappear and a new disk drive called CIRCUITPY will appear.

That's it, you're done! :)

Safe Mode

You want to edit your code.py or modify the files on your CIRCUITPY drive, but find that you can't. Perhaps your board has gotten into a state where CIRCUITPY is read-only. You may have turned off the CIRCUITPY drive altogether. Whatever the reason, safe mode can help.

Safe mode in CircuitPython does not run any user code on startup, and disables auto-reload. This means a few things. First, safe mode bypasses any code in boot.py (where you can set CIRCUITPY read-only or turn it off completely). Second, it does not run the code in code.py. And finally, it does not automatically soft-reload when data is written to the CIRCUITPY drive.

Therefore, whatever you may have done to put your board in a non-interactive state, safe mode gives you the opportunity to correct it without losing all of the data on the CIRCUITPY drive.

Entering Safe Mode in CircuitPython 6.x

This section explains entering safe mode on CircuitPython 6.x.

To enter safe mode when using CircuitPython 6.x, plug in your board or hit reset (highlighted in red above). Immediately after the board starts up or resets, it waits 700ms. On some boards, the onboard status LED (highlighted in green above) will turn solid yellow during this time. If you press reset during that 700ms, the board will start up in safe mode. It can be difficult to react to the yellow LED, so you may want to think of it simply as a slow double click of the reset button. (Remember, a fast double click of reset enters the bootloader.)

Entering Safe Mode in CircuitPython 7.x

This section explains entering safe mode on CircuitPython 7.x.

To enter safe mode when using CircuitPython 7.x, plug in your board or hit reset (highlighted in red above). Immediately after the board starts up or resets, it waits 1000ms. On some boards, the onboard status LED (highlighted in green above) will blink yellow during that time. If you press reset during that 1000ms, the board will start up in safe mode. It can be difficult to react to the yellow LED, so you may want to think of it simply as a slow double click of the reset button. (Remember, a fast double click of reset enters the bootloader.)

In Safe Mode

Once you've entered safe mode successfully in CircuitPython 6.x, the LED will pulse yellow.

If you successfully enter safe mode on CircuitPython 7.x, the LED will intermittently blink yellow three times.

If you connect to the serial console, you'll find the following message.

Auto-reload is off.
Running in safe mode! Not running saved code.

CircuitPython is in safe mode because you pressed the reset button during boot. Press again to exit safe mode.

Press any key to enter the REPL. Use CTRL-D to reload.

You can now edit the contents of the CIRCUITPY drive. Remember, your code will not run until you press the reset button, or unplug and plug in your board, to get out of safe mode.

Flash Resetting UF2

If your board ever gets into a really weird state and doesn't even show up as a disk drive when installing CircuitPython, try loading this 'nuke' UF2 which will do a 'deep clean' on your Flash Memory. You will lose all the files on the board, but at least you'll be able to revive it! After loading this UF2, follow the steps above to re-install CircuitPython.

Once you've finished setting up your QT Py RP2040 with CircuitPython, you can access the code, bitmap and necessary libraries by downloading the Project Bundle.

To do this, click on the Download Project Bundle button in the window below. It will download as a zipped folder.

import board
import displayio
import digitalio
from adafruit_st7789 import ST7789
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
import usb_midi
import adafruit_midi
from adafruit_midi.note_on          import NoteOn
from adafruit_midi.note_off         import NoteOff

#  if you want to use this as an HID keyboard, set keyboard_mode to True
#  otherwise, set it to False
keyboard_mode = True
#  if you want to use this as a MIDI keyboard, set midi_mode to True
#  otherwise, set it to False
midi_mode = False

#  change keyboard shortcuts here
#  defaults are shortcuts for save, cut, copy & paste
#  comment out ctrl depending on windows or macOS
if keyboard_mode:
    keyboard = Keyboard(usb_hid.devices)
    #  modifier for windows
    ctrl = Keycode.CONTROL
    #  modifier for macOS
    #  ctrl = Keycode.COMMAND
    key0 = Keycode.S
    key1 = Keycode.X
    key2 = Keycode.C
    key3 = Keycode.V
    shortcuts = [key0, key1, key2, key3]

#  change MIDI note numbers here
if midi_mode:
    midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
    midi_notes = [60, 61, 62, 63]

# Release any resources currently in use for the displays
displayio.release_displays()

#  spi display setup
spi = board.SPI()
tft_cs = board.D7
tft_dc = board.D5

display_bus = displayio.FourWire(
    spi, command=tft_dc, chip_select=tft_cs, reset=board.D6
)

#  display setup
display = ST7789(display_bus, width=240, height=240, rowstart=80)

# CircuitPython 6 & 7 compatible
#  bitmap setup
bitmap = displayio.OnDiskBitmap(open("/parrot-240-sheet.bmp", "rb"))

# Create a TileGrid to hold the bitmap
parrot0_grid = displayio.TileGrid(bitmap, pixel_shader=getattr(bitmap, 'pixel_shader', displayio.ColorConverter()),
                                 tile_height=240, tile_width=240)

# # CircuitPython 7+ compatible
# bitmap = displayio.OnDiskBitmap("/parrot-240-sheet.bmp")

# Create a TileGrid to hold the bitmap
# parrot0_grid = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader,
#                                tile_height=240, tile_width=240)

# Create a Group to hold the TileGrid
group = displayio.Group()

# Add the TileGrid to the Group
group.append(parrot0_grid)

# Add the Group to the Display
display.show(group)

#  digital pins for the buttons
key_pins = [board.A0, board.A1, board.A2, board.A3]

#  array for buttons
keys = []

#  setup buttons as inputs
for key in key_pins:
    key_pin = digitalio.DigitalInOut(key)
    key_pin.direction = digitalio.Direction.INPUT
    key_pin.pull = digitalio.Pull.UP
    keys.append(key_pin)

p = 0 #  variable for tilegrid index
a = 0 #  variable for tile position

#  states for buttons
key0_pressed = False
key1_pressed = False
key2_pressed = False
key3_pressed = False

#  array for button states
key_states = [key0_pressed, key1_pressed, key2_pressed, key3_pressed]

while True:
    #  default tile grid position
    parrot0_grid[a] = p

    #  iterate through 4 buttons
    for i in range(4):
        inputs = keys[i]
        #  if button is pressed...
        if not inputs.value and key_states[i] is False:
            #  tile grid advances by 1 frame
            p += 1
            #  update button state
            key_states[i] = True
            #  if a midi keyboard...
            if midi_mode:
                #  send NoteOn for corresponding MIDI note
                midi.send(NoteOn(midi_notes[i], 120))
            #  if an HID keyboard...
            if keyboard_mode:
                #  send keyboard output for corresponding keycode
                #  the default includes a modifier along with the keycode
                keyboard.send(ctrl, shortcuts[i])
            #  if the tile grid's index is at 9...
            if p > 9:
                #  reset the index to 0
                p = 0
        #  if the button is released...
        if inputs.value and key_states[i] is True:
            #  update button state
            key_states[i] = False
            #  if a midi keyboard...
            if midi_mode:
                #  send NoteOff for corresponding MIDI note
                midi.send(NoteOff(midi_notes[i], 120))

Upload the Code, Graphic and Libraries to the QT Py RP2040

After downloading the Project Bundle, plug your QT Py RP2040 into the computer USB port. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the QT Py RP2040's CIRCUITPY drive. 

  • lib folder
  • code.py
  • parrot-240-sheet.bmp

Your QT Py RP2040 CIRCUITPY drive should look like this after copying the lib folder, parrot-240-sheet.bmp file and code.py file.

HID Keyboard or MIDI Keyboard?

The code is written so that you can use the keypad as either an HID keyboard or a MIDI controller.

After importing the libraries, you can set either keyboard_mode or midi_mode to True to choose your device type.

import board
import displayio
import digitalio
from adafruit_st7789 import ST7789
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
import usb_midi
import adafruit_midi
from adafruit_midi.note_on          import NoteOn
from adafruit_midi.note_off         import NoteOff

#  if you want to use this as an HID keyboard, set keyboard_mode to True
#  otherwise, set it to False
keyboard_mode = True
#  if you want to use this as a MIDI keyboard, set midi_mode to True
#  otherwise, set it to False
midi_mode = False

Setup for HID Keyboard Shortcuts

You can also customize what each key does in each mode. The default settings for keyboard_mode are shortcuts for save, cut, copy and paste. You can modify these in the if statement, if keyboard_mode:. The variables assigned to the keycodes for each key are stored in the shortcuts array.

#  change keyboard shortcuts here
#  defaults are shortcuts for save, cut, copy & paste
#  comment out ctrl depending on windows or macOS
if keyboard_mode:
    keyboard = Keyboard(usb_hid.devices)
    #  modifier for windows
    ctrl = Keycode.CONTROL
    #  modifier for macOS
    #  ctrl = Keycode.COMMAND
    key0 = Keycode.S
    key1 = Keycode.X
    key2 = Keycode.C
    key3 = Keycode.V
    shortcuts = [key0, key1, key2, key3]

Setup for MIDI Notes

The default MIDI notes for midi_mode are setup in the if statement if midi_mode: and are stored in the midi_notes array.

#  change MIDI note numbers here
if midi_mode:
    midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
    midi_notes = [60, 61, 62, 63]

SPI TFT Screen Setup

The keypad uses a TFT screen that uses SPI to communicate with the QT Py RP2040. This requires defining pins for tft_cs, tft_dc and reset in the code.

#  spi display setup
spi = board.SPI()
tft_cs = board.D7
tft_dc = board.D5

display_bus = displayio.FourWire(
    spi, command=tft_dc, chip_select=tft_cs, reset=board.D6
)

#  display setup
display = ST7789(display_bus, width=240, height=240, rowstart=80)

Party Parrot Bitmap Setup

The party parrot bitmap is sized so that each tile is 240x240 to fill the TFT screen. This makes for a larger than normal bitmap file since it has 10 tiles (that's 240x2400!). The code is using OnDiskBitmap() to load the bitmap so that you don't run into any memory allocation issues.

# CircuitPython 6 & 7 compatible
#  bitmap setup
bitmap = displayio.OnDiskBitmap(open("/parrot-240-sheet.bmp", "rb"))

# Create a TileGrid to hold the bitmap
parrot0_grid = displayio.TileGrid(bitmap, pixel_shader=getattr(bitmap, 'pixel_shader', displayio.ColorConverter()),
                                 tile_height=240, tile_width=240)

# # CircuitPython 7+ compatible
# bitmap = displayio.OnDiskBitmap("/parrot-240-sheet.bmp")

# Create a TileGrid to hold the bitmap
# parrot0_grid = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader,
#                                tile_height=240, tile_width=240)

# Create a Group to hold the TileGrid
group = displayio.Group()

# Add the TileGrid to the Group
group.append(parrot0_grid)

# Add the Group to the Display
display.show(group)

Key Input Setup

The keys are setup as digital inputs using a for statement that iterates through the key_pins array. This array has the QT Py RP2040's pins that the keys are connected to.

#  digital pins for the buttons
key_pins = [board.A0, board.A1, board.A2, board.A3]

#  array for buttons
keys = []

#  setup buttons as inputs
for key in key_pins:
    key_pin = digitalio.DigitalInOut(key)
    key_pin.direction = digitalio.Direction.INPUT
    key_pin.pull = digitalio.Pull.UP
    keys.append(key_pin)

Variables and States

p and a are variables that are used in the loop to track the index position of the tile grid. This advances the party parrot animation on the screen.

The key_states array is used to track the states of the four keys for debouncing. When a key is not pressed, its state is False and when it is pressed, its state is True.

p = 0 #  variable for tilegrid index
a = 0 #  variable for tile position

#  states for buttons
key0_pressed = False
key1_pressed = False
key2_pressed = False
key3_pressed = False

#  array for button states
key_states = [key0_pressed, key1_pressed, key2_pressed, key3_pressed]

The Loop

At the beginning of the loop, the tile grid's index is set to p. This will allow for the tile grid to advance as p's value changes.

while True:
    #  default tile grid position
    parrot0_grid[a] = p

Pressing a Key

If one of the keys is pressed and its corresponding state is False, then p increases in value by 1 and its state is updated to True.

#  iterate through 4 buttons
    for i in range(4):
        inputs = keys[i]
        #  if button is pressed...
        if not inputs.value and key_states[i] is False:
            #  tile grid advances by 1 frame
            p += 1
            #  update button state
            key_states[i] = True

MIDI Mode

If the keypad is setup to be in midi_mode, then a NoteOn message is sent for the corresponding MIDI note number from the midi_notes array.

#  if a midi keyboard...
            if midi_mode:
                #  send NoteOn for corresponding MIDI note
                midi.send(NoteOn(midi_notes[i], 120))

HID Keyboard Mode

If the keypad is setup to be in keyboard_mode, then keyboard.send() is used to send the keycode(s) that correspond to the pressed key.

#  if an HID keyboard...
            if keyboard_mode:
                #  send keyboard output for corresponding keycode
                #  the default includes a modifier along with the keycode
                keyboard.send(ctrl, shortcuts[i])

Reset the Party

Since the party parrot's tile grid has 10 tiles, it needs to have its index reset to 0 when it goes above 9.

#  if the tile grid's index is at 9...
            if p > 9:
                #  reset the index to 0
                p = 0

Releasing a Key

When you release a key and the key's state is True, the state is updated to False to reset its state.

Additionally, if you're in midi_mode, a NoteOff message is sent for the corresponding MIDI note number. An equivalent action is not needed for the keyboard_mode since keyboard.send() includes a release message.

#  if the button is released...
        if inputs.value and key_states[i] is True:
            #  update button state
            key_states[i] = False
            #  if a midi keyboard...
            if midi_mode:
                #  send NoteOff for corresponding MIDI note
                midi.send(NoteOff(midi_notes[i], 120))

Wiring TFT Display

Use a 10-wire silicone ribbon cable to connect the TFT display to the QT Py. Measure and cut the cable so it's about 11cm (4.3in) in length. Peel wiring so there's only 8 of the 10 wires. The TFT display needs 8 wired connections to the QT Py.

Solder TFT Display

Using wire strippers, remove a bit of insulation from each wire. Tin the tips by applying a bit of solder. This helps prevent the strands of wire from fraying. Solder all eight wires to the following pins on the TFT.

  • VIN
  • GND
  • SCK
  • MOSI
  • TFTCS
  • RST
  • DC
  • Lite

Wired TFT Display

Double check all of wires are properly soldered to the pins on the TFT display.

Key Plate and Switches

Four mechanical key switches are press fitted into the 3D printed key plate. Choose which style of cherry mx compatible key switch to use. 

Install Key Switches

The mechanical key switches are press fitted into the square holes. The key plate is symmetrical so the switches can be installed on either side.

Key Switch Wires

Use a 10-wire silicone ribbon cable to connect the four key switches to the QT Py. Use the following wire lengths.

  • 3x 1-wire ribbon cable - 25mm (1in)
  • 1x 5-wire ribbon cable - 127mm (5in)

Solder Key Switches

Start by soldering the ground pins together using the short single wires. To daisy chain together, ground pins will share two wires. Solder one wire from the 5-wire ribbon cable to one of the ground pins. Solder the remaining wires to the signal pins on the switches.

QT Py Wiring

Double check the wiring from the key switches and TFT display. Get the QT Py RP2040 ready to wire.

Wiring TFT to QT Py

Solder the wires from the TFT display to the pins on the QT Py. Make the following connections.

  • V+ from TFT to 3V on QT Py
  • GND  from TFT to GND QT Py
  • CK from TFT to SCK QT Py
  • SI from TFT to MOSI on Qt Py
  • TC from TFT to RX on QT Py
  • RT from TFT to TX on QT Py
  • DC from TFT to SCL on QT Py
  • BL from TFT to SDA on QT Py

Wired TFT Display and QT Py

Double check the wiring is correct and the solder joints are solid.

Wiring Switches to QT Py

Get the key plate switches ready to solder to the QT Py.

Solder Switches to QT Py

Solder the four signal wires from the keys to the QT Py. Solder the ground wire to the ground pin on the QT Py.

Wired Key Switches

Double check all of the wires are correctly soldered. Check the solder joints are all solid.

Installing TFT Display

Fit the TFT Display through the large hole from the bottom of the key plate and pull it out to the top. The display should be facing up with the stems of the key switches.

TFT Display Hardware

Use the following hardware to secure the TFT display to the key plate.

  • 4x M2.5 x 12mm
  • 4x M2.5 hex nuts

Secure TFT Display

Line up the mounting holes from the TFT display to the holes on the key plate. While holding the display in place, insert a M2.5 screw through the mounting hole. Use the hex nut to secure the PCB to the key plate. Repeat this process for the other mounting holes.

Installed TFT Display

Double check the TFT display is correctly installed and secured. The screws and hex nuts should be finger tightened.

Install Key Plate to Frame

The Key Plate is fitted into the frame. Orient the frame so the key plate can drop into the cavity. The key plate should rest on the internal ledge. Press the key plate to make it flush with the ledge inside the frame.

Installed Key Plate

Double check the key plate is correctly installed into the frame.

Installing QT Py

The QT Py RP2040 is press fitted into the holder built into the bottom cover. The USB Port should be facing out. The holder is designed to fit the PCB with it elevated. Trim any excess solder bits from the bottom of the PCB to make it sit flush.

Install Bottom Cover

The bottom cover is snap fitted under the frame. Carefully arrange the cables so they're fitted inside the frame. Start by fitting one side first and then firmly pressing the parts together.

Install Display Cover

Fit the display cover into the center paw shaped hole in the top cover. This can optionally be glued to the top cover if you prefer. 

Install Top Cover

With the display cover fitted into the top, begin to install it over the frame. Similar to the bottom cover installation, start with one side and firmly press to other side together.

Install Key Caps

Get the key caps ready to install to the key switches.

Installing Key Caps

The key caps are press fitted over the stems of the key switches. Orient the caps so they're matching the toe beans.

Installed Key Caps

Firmly boop (press down on) each of the toe beans to fully install.

Final Build

And we're ready for toe beans! Congratulations on completing your build.

This guide was first published on Jun 29, 2021. It was last updated on 2021-06-29 17:17:58 -0400.