Nineteen Pins, Sixty Keys, and a Spacebar That Finally Quit

Mechanical Keyboard Building
🎮 Play: Thock Tuner

The spacebar on my daily driver went mushy last week. Fifteen years of service, probably forty million actuations if I average my typing, and the stabilizers finally gave out. Three hours later I’d ordered a hot-swap PCB, a sampler pack of sixty switches, and had seventeen browser tabs open about something called Krytox 205g0.

Hobby number seventy-three is Mechanical Keyboard Building.

What I want to document here isn’t the full build—that’s still in progress, parts en route—but the firmware architecture, because it surprised me. QMK (Quantum Mechanical Keyboard) is open-source firmware that runs on most custom keyboard PCBs, and it’s one of the most elegant pieces of embedded systems work I’ve encountered.

The Matrix Problem

A keyboard with 60 keys doesn’t have 60 individual wires running to the microcontroller. That would require 60 GPIO pins. Instead, switches are wired in a matrix—rows and columns. A typical 60% layout uses a 5×14 matrix: five row pins and fourteen column pins, for nineteen total GPIOs handling up to 70 switch positions.

The controller scans one row at a time: drive row 0 low, read columns 0-13, drive row 1 low, read columns again, repeat. A full scan takes microseconds. The debounce algorithm (default: Sym Defer with 5ms settling time) handles the electrical noise when contacts close.

Here’s the layout definition from a typical keymap.c:

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [0] = LAYOUT_60_ansi(
        KC_ESC,  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_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,
        MO(1),   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_LSFT,          KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,    KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH,          KC_RSFT,
        KC_LCTL, KC_LGUI, KC_LALT,                            KC_SPC,                            KC_RALT, MO(2),   KC_APP,  KC_RCTL
    ),
    [1] = LAYOUT_60_ansi(
        KC_GRV,  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_DEL,
        _______, _______, KC_UP,   _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,
        _______, KC_LEFT, KC_DOWN, KC_RGHT, _______, _______, _______, _______, _______, _______, _______, _______,          _______,
        _______,          _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,          _______,
        _______, _______, _______,                            _______,                            _______, _______, _______, _______
    )
};

MO(1) is a momentary layer switch—hold it and layer 1 becomes active, where the same physical keys now produce F1-F12 and arrow keys. _______ means “transparent,” falling through to the layer below. Layer 2 could hold macros, mouse keys, media controls. The architecture supports 32 layers.

Tap-Dance and Timing

The feature that made me genuinely impressed: tap-dance. A single key can perform different actions depending on how many times you tap it within a configurable window (default 200ms).

void dance_cln_finished(tap_dance_state_t *state, void *user_data) {
    if (state->count == 1) {
        register_code16(KC_COLN);  // one tap: colon
    } else {
        register_code16(KC_SCLN);  // two taps: semicolon
    }
}

You can also distinguish between tap and hold on the same key. Tap for one character, hold for a modifier. The timing detection uses a state machine that tracks press/release intervals across the debounce window.

This is the same kind of temporal pattern recognition I used when building the pitch-tracking conductor baton—sampling input, detecting patterns, triggering actions based on timing signatures. Different domain, identical structure.

What The Contacts Are Made Of

I expected copper. The crosspoint contacts inside Cherry MX switches are actually a gold-silver alloy: AuAg10. The precious metals reduce oxidation and contact resistance over the rated 100-million-cycle lifespan. Every keypress is happening on tiny traces of treasure.

The force to actuate these switches is measured in centiNewtons, not grams—though the community uses grams colloquially. A “45g” linear like Cherry MX Red actuates at 45cN. Heavy tactiles like MX Greens require 80cN. The difference spans roughly the weight of eight nickels stacked on your fingertip.

The Stabilizer Problem

Longer keys—spacebar, shift, enter—need stabilizers to prevent rocking. These are the weakest link in most keyboards. Poorly tuned stabilizers introduce rattle, mush, and inconsistent feel. The fix involves three steps:

  1. Clipping: Factory stabilizers have small feet that add travel noise. Clip them flush.
  2. Lubing: Dielectric grease (not the same lubricant as switches) on the wire-to-housing contact points.
  3. Band-aid mod: A small fabric pad under each stabilizer stem dampens bottom-out impact.

The tolerance here is smaller than I expected. Too much grease and the key feels sluggish. Too little and the wire rattles against the housing. The tuning process reminded me of adjusting the tension wrench pressure in lockpicking—there’s a correct feel, and your fingers have to learn it.


Parts arrive Thursday. Sixty-two switches to test, one PCB to flash, and a spacebar that needs to stop sounding like a broken promise. I’ll document the physical build when I have switches to photograph, but for now: the firmware is already compiling.