RP2040 Custom Keyboard Guide

Mini-Tac RP2040 Keyboard — Flashing, KMK, and QMK Guide

Flashing, first bring-up, KMK setup, and a production-style QMK starter pack for a custom 67-key RP2040 keyboard. The examples are specific to this Mini-Tac board, but the process is written as a reusable instructional for similar RP2040 keyboard PCBs.

MCU RP2040 External QSPI flash on board
Matrix 5 × 14 67 populated switch positions
Boot mode USB_BOOT Short USB_BOOT to GND while plugging in
Firmware paths KMK / QMK Fast bring-up or long-term keyboard firmware

Overview

This board is a custom RP2040 keyboard with onboard USB, external QSPI flash, a crystal, a BOOTSEL-style USB boot header, and a 5-row / 14-column key matrix. The examples below use the exact GPIO assignments shown in the schematic and the exact 67 populated key positions shown on the PCB.

Observed board details RP2040 MCU, W25Q128JV flash, USB connector, 12 MHz crystal, 3.3V regulator, and a USB_BOOT header.
Best first step Enter RP2040 USB mass-storage mode first. If the board enumerates as RPI-RP2, the boot ROM, USB path, and flash access path are likely healthy enough to proceed.
Recommended workflow: use KMK for the fastest proof that the matrix works, then move to QMK for a more permanent keyboard firmware build.
Topic This board How to adapt for a different RP2040 board
Rows GP0, GP1, GP2, GP3, GP4 Replace with the actual row GPIOs used on the target PCB
Columns GP5 through GP18 Replace with the actual column GPIOs used on the target PCB
Diode direction ROW2COL Verify from the schematic; do not assume the same direction on every board
Boot entry Short USB_BOOT to GND while plugging in USB Use the board's BOOTSEL button, reset button, or boot jumper
Physical matrix shape 67 used positions inside a 5x14 electrical grid Update the QMK layout macro or KMK coord_mapping
Mini-67 full schematic

Full circuit schematic — RP2040, flash, crystal, USB, 5×14 key matrix

Entering Flash Mode

The RP2040 contains a ROM bootloader. On this board, the schematic shows a USB_BOOT header that can be pulled low to enter that mode.

  1. Unplug the keyboard.
  2. Short USB_BOOT to GND on the programming header.
  3. While keeping that short in place, plug the keyboard into USB.
  4. The computer should mount a mass-storage device named RPI-RP2.
  5. Copy a valid .uf2 firmware file to that drive.
  6. The board should reboot automatically after the copy finishes.
Important: this board does not show a dedicated reset button in the schematic. If firmware needs to be reflashed, the simplest reset method is usually unplug / replug while using the USB_BOOT header.
If no RPI-RP2 drive appears: check USB power, D+ / D- routing, cable quality, regulator output, QSPI flash soldering, and shorts around the RP2040 or crystal.

Exact Board Matrix Map

Electrically, the board is a full 5x14 matrix. Physically, only 67 of the 70 possible intersections are populated. That matters for both QMK layout macros and KMK coordinate mapping.

Rows : GP0 GP1 GP2 GP3 GP4 Cols : GP5 GP6 GP7 GP8 GP9 GP10 GP11 GP12 GP13 GP14 GP15 GP16 GP17 GP18 Matrix occupancy by switch designator Row 0 : SW1 SW2 SW3 SW4 SW5 SW6 SW7 SW8 SW9 SW10 SW11 SW12 SW13 SW14 Row 1 : SW15 SW16 SW17 SW18 SW19 SW20 SW21 SW22 SW23 SW24 SW25 SW26 SW27 SW28 Row 2 : SW29 SW30 SW31 SW32 SW33 SW34 SW35 SW36 SW37 SW38 SW39 SW40 SW41 SW67 Row 3 : SW42 SW43 SW44 SW45 SW46 SW47 SW48 SW49 SW50 SW51 SW52 ---- SW53 ---- Row 4 : SW54 SW55 SW56 SW57 SW58 SW59 SW60 SW61 SW62 SW63 ---- SW64 SW65 SW66
Diode direction: the schematic shows current flowing from row to switch to diode to column, so firmware should be configured as ROW2COL.
Mini-67 PCB layout

PCB layout — Mini-Tac Keyboard v1.0, 67 switches across 5×14 matrix

Key Layout

Physical layout from the keycap artwork — 67 keys across a 5×14 electrical matrix. Empty matrix positions (row 3 col 11, row 4 col 10) are shown as gaps.

` 1 2 3 4 5 6 7 8 9 0 - = Bspc Esc Q W E R T Y U I O P [ ] \ Tab A S D F G H J K L ; ' Ins Ent Caps Z X C V B N M , . / Shift Ctrl Alt Del Fn Spc Home End PgUp PgDn

Dark keys = modifier/function  ·  Green border = Fn layer toggle  ·  Gaps at row 3 col 11 and row 4 col 10 match unpopulated matrix positions

KMK Path — Fastest Bring-Up

KMK is the quickest way to verify that the board works as a keyboard. It runs on CircuitPython and stores configuration as editable files on the board.

When KMK makes sense

  • Fast hardware validation
  • Easy pin and keymap changes without recompiling
  • Good early-stage development path for a brand new custom PCB
Tradeoff: KMK is convenient for bring-up, but QMK is the stronger long-term choice for a custom production keyboard firmware build.

KMK install flow

  1. Put the board into RPI-RP2 mode.
  2. Copy a compatible CircuitPython RP2040 .uf2 to the drive.
  3. After reboot, a CIRCUITPY drive should appear.
  4. Copy the kmk/ folder onto that drive.
  5. Create or replace code.py with a board-specific keymap.

Exact KMK example for this board

This example uses the exact row and column pins from the schematic and an exact coord_mapping for the 67 populated keys.

import board

from kmk.kmk_keyboard import KMKKeyboard
from kmk.keys import KC
from kmk.modules.layers import Layers
from kmk.scanners import DiodeOrientation

keyboard = KMKKeyboard()
keyboard.modules.append(Layers())

# Exact matrix pins from this RP2040 board
keyboard.row_pins = (
    board.GP0,
    board.GP1,
    board.GP2,
    board.GP3,
    board.GP4,
)

keyboard.col_pins = (
    board.GP5,
    board.GP6,
    board.GP7,
    board.GP8,
    board.GP9,
    board.GP10,
    board.GP11,
    board.GP12,
    board.GP13,
    board.GP14,
    board.GP15,
    board.GP16,
    board.GP17,
    board.GP18,
)

# The schematic shows current flowing from row -> switch -> diode -> column.
keyboard.diode_orientation = DiodeOrientation.ROW2COL

# This PCB is not a full 5x14 rectangle. There are 70 possible matrix positions,
# but only 67 switches are populated. coord_mapping lets the keymap stay in
# physical order instead of forcing three fake entries.
keyboard.coord_mapping = [
     0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13,
    14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
    28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
    42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 54,
    56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 67, 68, 69,
]

FN = KC.MO(1)

keyboard.keymap = [
    [
        # Row 1 — number row
        KC.GRV,  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.BSPC,

        # Row 2
        KC.ESC,  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,

        # Row 3
        KC.TAB,  KC.A,    KC.S,    KC.D,    KC.F,    KC.G,    KC.H,
        KC.J,    KC.K,    KC.L,    KC.SCLN, KC.QUOT, KC.INS,  KC.ENT,

        # Row 4 — 12 keys (col 11 unpopulated)
        KC.CAPS, KC.Z,    KC.X,    KC.C,    KC.V,    KC.B,
        KC.N,    KC.M,    KC.COMM, KC.DOT,  KC.SLSH, KC.UP,

        # Row 5 — 13 keys (col 10 unpopulated)
        KC.LSFT, KC.LCTL, KC.LALT, KC.DEL,  FN,      KC.SPC,  KC.HOME,
        KC.END,  KC.PGUP, KC.PGDN, KC.LEFT, KC.DOWN, KC.RGHT,
    ],
    [
        # Row 1 → F-keys
        KC.TRNS, KC.F1,   KC.F2,   KC.F3,   KC.F4,   KC.F5,   KC.F6,
        KC.F7,   KC.F8,   KC.F9,   KC.F10,  KC.F11,  KC.F12,  KC.RESET,

        # Row 2
        KC.TRNS, KC.TRNS, KC.UP,   KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS,
        KC.P7,   KC.P8,   KC.P9,   KC.PMNS, KC.TRNS, KC.TRNS, KC.TRNS,

        # Row 3
        KC.TRNS, KC.LEFT, KC.DOWN, KC.RGHT, KC.TRNS, KC.TRNS, KC.HOME,
        KC.P4,   KC.P5,   KC.P6,   KC.PPLS, KC.TRNS, KC.TRNS, KC.END,

        # Row 4 — 12 keys
        KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS,
        KC.PGUP, KC.P1,   KC.P2,   KC.P3,   KC.PSLS, KC.VOLU,

        # Row 5 — 13 keys
        KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS, KC.TRNS,
        KC.TRNS, KC.MPRV, KC.VOLD, KC.MPLY, KC.MNXT, KC.MUTE,
    ],
]

if __name__ == '__main__':
    keyboard.go()
Why this KMK example is useful: the keymap is already arranged in physical key order, and the three unused matrix intersections are removed by coord_mapping.

QMK Path — Production-Style Starter Pack

QMK is the better choice for a custom keyboard that is expected to stay in service. It provides a more conventional keyboard firmware workflow, strong layer support, and a large ecosystem.

Suggested folder structure

qmk_firmware/
└── keyboards/
    └── handwired/
        └── mini_tac_67/
            ├── config.h
            ├── keyboard.h
            ├── rules.mk
            └── keymaps/
                └── default/
                    └── keymap.c

rules.mk

MCU = RP2040
BOARD = GENERIC_RP_RP2040
BOOTLOADER = rp2040

BOOTMAGIC_ENABLE = yes
EXTRAKEY_ENABLE = yes
NKRO_ENABLE = yes
LTO_ENABLE = yes

config.h

#pragma once

#define VENDOR_ID 0x4A43
#define PRODUCT_ID 0x6701
#define DEVICE_VER 0x0001
#define MANUFACTURER "Jaysons Workshop"
#define PRODUCT "Mini-Tac 67"

#define MATRIX_ROWS 5
#define MATRIX_COLS 14

#define MATRIX_ROW_PINS { GP0, GP1, GP2, GP3, GP4 }
#define MATRIX_COL_PINS { GP5, GP6, GP7, GP8, GP9, GP10, GP11, GP12, GP13, GP14, GP15, GP16, GP17, GP18 }

#define DIODE_DIRECTION ROW2COL

#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET
#define RP2040_BOOTLOADER_DOUBLE_TAP_RESET_TIMEOUT 200U

#define USB_SUSPEND_WAKEUP_DELAY 0
#define SERIAL_NUMBER "mini-tac-67"

/*
 * The board uses external QSPI flash (W25Q128JV in the BOM).
 * If compiled UF2 files copy successfully but never boot, verify the
 * RP2040 second-stage flash setting against the actual flash part used
 * on the PCB and then add the correct RP2040_FLASH_* define here.
 */

keyboard.h

This layout macro is specific to the Mini-Tac board. It keeps the 67 physical keys while inserting KC_NO into the three matrix locations that are not populated.

#pragma once

#include "quantum.h"

#define LAYOUT_67( \
    K00, K01, K02, K03, K04, K05, K06, K07, K08, K09, K0A, K0B, K0C, K0D, \
    K10, K11, K12, K13, K14, K15, K16, K17, K18, K19, K1A, K1B, K1C, K1D, \
    K20, K21, K22, K23, K24, K25, K26, K27, K28, K29, K2A, K2B, K2C, K2D, \
    K30, K31, K32, K33, K34, K35, K36, K37, K38, K39, K3A, K3C, \
    K40, K41, K42, K43, K44, K45, K46, K47, K48, K49, K4B, K4C, K4D  \
) { \
    { K00, K01, K02, K03, K04, K05, K06, K07, K08, K09, K0A, K0B, K0C, K0D }, \
    { K10, K11, K12, K13, K14, K15, K16, K17, K18, K19, K1A, K1B, K1C, K1D }, \
    { K20, K21, K22, K23, K24, K25, K26, K27, K28, K29, K2A, K2B, K2C, K2D }, \
    { K30, K31, K32, K33, K34, K35, K36, K37, K38, K39, K3A, KC_NO, K3C, KC_NO }, \
    { K40, K41, K42, K43, K44, K45, K46, K47, K48, K49, KC_NO, K4B, K4C, K4D }  \
}

keymaps/default/keymap.c

This is a starting keymap, not a statement about the final legends or cap sizes used on the board. Replace keys as needed once the hardware is typing reliably.

#include QMK_KEYBOARD_H

enum layer_names {
    _BASE,
    _FN,
};

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [_BASE] = LAYOUT_67(
        KC_GRV,  KC_1,    KC_2,    KC_3,    KC_4,    KC_5,    KC_6,    KC_7,    KC_8,    KC_9,    KC_0,    KC_MINS, KC_EQL,  KC_BSPC,
        KC_ESC,  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_TAB,  KC_A,    KC_S,    KC_D,    KC_F,    KC_G,    KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN, KC_QUOT, KC_INS,  KC_ENT,
        KC_CAPS, KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,    KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH, KC_UP,
        KC_LSFT, KC_LCTL, KC_LALT, KC_DEL,  MO(_FN), KC_SPC,  KC_HOME, KC_END,  KC_PGUP, KC_PGDN, KC_LEFT, KC_DOWN, KC_RGHT
    ),

    [_FN] = LAYOUT_67(
        KC_TRNS, KC_F1,   KC_F2,   KC_F3,   KC_F4,   KC_F5,   KC_F6,   KC_F7,   KC_F8,   KC_F9,   KC_F10,  KC_F11,  KC_F12,  QK_BOOT,
        KC_TRNS, KC_TRNS, KC_UP,   KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_P7,   KC_P8,   KC_P9,   KC_PMNS, KC_TRNS, KC_TRNS, KC_TRNS,
        KC_TRNS, KC_LEFT, KC_DOWN, KC_RGHT, KC_TRNS, KC_TRNS, KC_HOME, KC_P4,   KC_P5,   KC_P6,   KC_PPLS, KC_TRNS, KC_TRNS, KC_END,
        KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_PGUP, KC_P1,   KC_P2,   KC_P3,   KC_PSLS, KC_VOLU,
        KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_TRNS, KC_MPRV, KC_VOLD, KC_MPLY, KC_MNXT, KC_MUTE
    )
};

Compile and flash

# From the root of qmk_firmware
qmk compile -kb handwired/mini_tac_67 -km default

# After the .uf2 is built, put the board in boot mode and either:
# 1) drag the .uf2 onto the RPI-RP2 drive
# or
# 2) use qmk flash if the board is already in RP2040 UF2 boot mode
qmk flash -kb handwired/mini_tac_67 -km default
Why BOARD = GENERIC_RP_RP2040 is used: this board is a custom RP2040 PCB, not a pre-defined RP2040 dev board with a baked-in pinout. The generic RP2040 board option is the clean base when all pins are defined manually.
Flash chip note: the PCB BOM shows a Winbond W25Q128JV. If QMK compiles and copies correctly but the board will not boot the image, verify the RP2040 second-stage flash setting against the exact flash part fitted on the board.

Troubleshooting

Board will not enter boot mode

  • Check that USB_BOOT actually pulls the RP2040 boot-select path low.
  • Verify USB cable quality; many failures are charge-only cables.
  • Measure 3.3V at the regulator output and at the RP2040 supply pins.

RPI-RP2 appears, but flashed firmware never runs

  • Check the crystal and its loading network.
  • Inspect the external QSPI flash orientation and soldering.
  • For QMK, re-check the second-stage flash compatibility.

Some keys do not work or entire rows / columns are dead

  • Confirm row and column GPIO assignments match the schematic.
  • Confirm diode direction in firmware is ROW2COL.
  • Check for solder bridges or open circuits on the dead row or column trace.
  • Use a continuity tester from MCU pin to switch pad.

Recommended bring-up order

  1. Confirm the board reaches RPI-RP2.
  2. Flash CircuitPython and verify KMK sees keypresses.
  3. Once the matrix is proven, move to the QMK starter pack.
  4. Add a dedicated boot key or reset method in the final QMK layout.