Project 3: 3x3 Macro Pad (First Physical QMK Keyboard)
Build a handwired 3x3 macro pad using QMK, proving the full pipeline from wiring a matrix to flashing firmware and verifying HID output.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2: Beginner |
| Time Estimate | 1-2 weeks |
| Main Programming Language | C (QMK) |
| Alternative Programming Languages | None |
| Coolness Level | Level 3: Useful Gadget |
| Business Potential | Level 2: Hobby Side Hustle |
| Prerequisites | Soldering basics, QMK CLI, C arrays |
| Key Topics | Matrix wiring, diode direction, QMK build pipeline |
1. Learning Objectives
By completing this project, you will:
- Wire a 3x3 switch matrix with correct diode orientation.
- Define matrix pins and diode direction in QMK
config.h. - Build and flash a QMK firmware using the CLI.
- Debug wiring and firmware issues using QMK console.
- Produce a reliable macro pad that registers all 9 keys.
2. All Theory Needed (Per-Concept Breakdown)
2.1 Matrix Wiring and Diode Orientation in Real Hardware
Fundamentals
A physical matrix is the electrical realization of the scanning model from Project 1. Each key connects one row wire to one column wire. Diodes are placed in series with each switch so current flows in only one direction, matching the firmware scan direction. In a handwired macro pad, the main sources of failure are wiring mistakes: swapped row/col pins, reversed diodes, or broken joints. Understanding how the current actually flows through a switch and diode is the difference between a working keyboard and a confusing debugging session. A correct wiring plan maps each switch to a row and column, and that map must match the firmware pin definitions.
Deep Dive into the Concept
Handwiring forces you to translate an abstract matrix into a physical layout. Start by choosing whether rows or columns will be the “drive” lines. In QMK, this is defined by DIODE_DIRECTION (COL2ROW or ROW2COL). If you choose COL2ROW, columns are outputs driven low one at a time, and rows are inputs with pull-ups. The diode must allow current to flow from column to row. If you flip a diode, the press will either never register or will register only when a different column is active, resulting in ghost-like behavior.
The practical wiring strategy is to build bus wires for rows and columns. For a 3x3 macro pad, you can run three row wires horizontally and three column wires vertically. Each switch connects one row and one column. The diode sits between the switch and the row (or column) depending on direction, but the key is to keep orientation consistent. A simple rule: diode arrow points toward the input side (row) for COL2ROW. After wiring, continuity testing with a multimeter confirms that each switch connects the expected row/column pair.
Matrix wiring decisions also affect pin selection. MCUs have pins that are not equally friendly. Some pins are shared with USB, reset, or bootloader functions. In a Pro Micro, pins like D+ and D- are USB and must not be used for matrix scanning. QMK provides pin names (e.g., D0, D1, C6), but you must ensure those pins are physically accessible and do not conflict with hardware functions. A good practice is to select a set of pins that are contiguous and easy to wire, even if it uses a few extra wires, because debugging is easier when the mapping is simple.
Diodes are not optional for any matrix larger than a single key. Even a 3x3 matrix can ghost if you press multiple keys. The macro pad should be able to detect multiple simultaneous presses without phantom keys. This is a real-world requirement if you map macros to multiple keys or use layers. The simulator from Project 1 provides the mental model; this project gives you the tactile evidence that the model matches reality. When you see ghosting in hardware, it is almost always diode direction or missing diodes, not firmware.
Finally, real wires have resistance and capacitance. Long, unshielded wires can introduce noise that makes inputs float. Internal pull-ups can sometimes be too weak, especially if you run long row wires. If you see random keypresses, consider adding external pull-ups (e.g., 10k) or shortening the wiring. This is rare in a 3x3 pad, but the habit of thinking about electrical integrity is essential for later PCB design.
Additional wiring practice: label each row and column wire physically (masking tape tags) before soldering. This simple habit prevents hours of debugging later. When chaining diodes, keep the orientation consistent and leave a small gap between the diode body and the switch lug to avoid mechanical stress. After soldering, perform a systematic continuity test: short each switch and verify the expected row/col pair on the MCU pins, not just on the switch legs. Finally, add strain relief for the USB cable or use a connector with mechanical anchoring so that cable motion does not stress solder joints.
Extra practical note: build a simple matrix tester by temporarily connecting row and column pads to a breadboard or header so you can probe the matrix with jumper wires before final soldering. This lets you validate the physical mapping without committing to a final assembly. Also, document diode orientation with a photo and mark the cathode side on your wiring diagram; small diodes are easy to flip by accident. Finally, think about serviceability: leave enough wire slack to rework a switch, and consider using hot-swap sockets if you plan to iterate on switch choice.
How this fits on projects
This concept is the core of Project 3 and directly prepares you for Project 7 (PCB design) and Project 8 (split keyboard wiring).
Definitions & key terms
- Row/Column bus: A shared wire connecting multiple switches.
- Diode orientation: Direction of allowed current flow.
- Continuity test: Using a multimeter to verify electrical connections.
- Pull-up resistor: Biases input high when not connected.
Mental model diagram (ASCII)
COL2ROW (columns drive LOW, rows read)
C0 ---o---|>|--- R0
C0 ---o---|>|--- R1
C1 ---o---|>|--- R0
C1 ---o---|>|--- R1
Diode arrow points toward row input.
How it works (step-by-step, with invariants and failure modes)
- Choose matrix dimensions and scan direction.
- Wire rows and columns as buses.
- Install diodes in consistent orientation.
- Test continuity for each switch-row-column path.
- Verify no shorts between rows/cols.
Invariant: Each switch connects exactly one row and one column. Failure modes include swapped pins, reversed diodes, and cold solder joints.
Minimal concrete example
R0 wire: switches at (0,0) (0,1) (0,2)
C1 wire: switches at (0,1) (1,1) (2,1)
Common misconceptions
- “Diodes are optional for a small pad”: Ghosting still occurs with multiple keys.
- “Any pin can be used”: Some pins are reserved or not GPIO.
- “If one key works, wiring is correct”: A single key does not validate the full matrix.
Check-your-understanding questions
- Why does diode direction have to match scan direction?
- What happens if two columns are shorted together?
- How do you test a diode orientation with a multimeter?
Check-your-understanding answers
- The firmware expects current to flow in one direction; reversed diodes block valid presses.
- It merges columns, causing multiple keys to appear pressed when scanning.
- Use diode test mode; forward bias shows a voltage drop, reverse shows open.
Real-world applications
- Handwired macro pads and custom keyboards.
- Prototype switch matrices before PCB design.
Where you’ll apply it
- In this project: §3.2 Functional Requirements and §5.10 Phase 1.
- Also used in: Project 7, Project 8.
References
- QMK docs: handwiring guide.
- “Digital Design and Computer Architecture” Ch. 1-2.
Key insights
Correct wiring is the physical proof of the matrix model; firmware cannot fix electrical mistakes.
Summary
Handwiring a matrix teaches the real constraints of rows, columns, and diode direction. It is the fastest way to gain hardware intuition before PCB design.
Homework/Exercises to practice the concept
- Draw a wiring diagram for a 3x3 matrix with COL2ROW diodes.
- Mark which MCU pins you will use and check the pinout for conflicts.
Solutions to the homework/exercises
- Each switch connects row to column with a diode pointing toward row.
- Avoid USB pins and reset; choose GPIO-only pins like D1, D0, D4, C6, D7, E6.
2.2 QMK Build Pipeline and Keymap Structure
Fundamentals
QMK uses a structured build system where each keyboard has a directory containing config.h, rules.mk, and one or more keymaps/ folders. The keymap defines a matrix of keycodes that map physical positions to logical actions. When you run qmk compile, QMK merges core firmware, the keyboard definition, and your keymap into a single firmware image. Understanding where each configuration lives and how QMK resolves it is essential to debugging. If a key doesn’t behave as expected, the cause is often a mismatch between the physical wiring, the MATRIX_ROW_PINS/MATRIX_COL_PINS definitions, and the keymap layout.
Deep Dive into the Concept
The QMK build system is a multi-layered Makefile ecosystem. At the top, QMK defines defaults and core features. Each keyboard folder overrides these with rules.mk and config.h. Your keymap is compiled into keymap.c, which defines the matrix layout and any custom logic. When you compile, QMK runs a Python-based build orchestrator that resolves keyboard and keymap names, validates the target, and generates build artifacts in .build/. The artifact is typically a .hex or .bin file depending on MCU.
For a handwired macro pad, you will likely create a custom keyboard folder under keyboards/handwired/3x3. In config.h, you define the matrix size and pin mapping. The matrix pins are ordered arrays that map row indices to MCU pins. The ordering must match the physical wiring. In keymap.c, you define a 2D array keymaps[][MATRIX_ROWS][MATRIX_COLS] or use a layout macro. The ordering in this array must align with the physical layout. If your keymap is mirrored or rotated, the most common cause is a mismatch between matrix order and physical wiring.
QMK also provides debugging tools. qmk console connects to the keyboard and prints matrix events if you enable CONSOLE_ENABLE in rules.mk and add debug_enable=true. This allows you to see which row/column is detected when you press a key. That signal is the bridge between your wiring and your keymap. If a key in the top left prints row=2 col=0, you know the mapping is wrong. This is the fastest way to debug physical builds.
Another important concept is feature flags in rules.mk. For a small macro pad, you should disable features you do not need (like RGB or audio) to keep firmware size small. QMK firmware size constraints are real, especially on AVR chips. LTO (link-time optimization) can also help. Understanding the build pipeline gives you control over performance and memory, which will matter in later projects with more features.
Finally, the QMK build pipeline enforces a consistent workflow. You write C code, compile with qmk compile, and flash with qmk flash. Each step produces logs that can be used to debug. If a flash fails, it might not be a code problem; it may be that the MCU is not in bootloader mode. Learning to interpret build and flash output is part of becoming fluent in QMK.
Additional QMK build details: treat your keyboard folder as a minimal product. Keep rules.mk lean and explicitly disable unused features (audio, mouse keys) to avoid size bloat on AVR. Use qmk compile -j for faster iteration and check the .build directory to confirm which files are compiled. If you need to change the physical layout later, update both info.json and the layout macro so QMK Configurator remains accurate. A clean, minimal keymap and clear comments in keymap.c make future maintenance easier.
Extra build workflow tip: when you change config.h pin mappings, delete the build directory (qmk clean) to avoid stale artifacts, especially when switching between boards. For flashing, learn how your bootloader behaves: Caterina uses a short reset window, while DFU can stay in bootloader longer. If flashing intermittently fails, add a reset button to the board or a key combo mapped to QK_BOOT so you can enter bootloader reliably without physical access.
How this fits on projects
This concept is central to Project 3 and is reused in Projects 4, 5, and 6 where you customize keymaps and add features.
Definitions & key terms
- Keymap: The mapping from matrix positions to keycodes.
- rules.mk: Build configuration file for enabling/disabling features.
- config.h: Hardware configuration (matrix pins, diode direction, etc.).
- Bootloader: Firmware mode used for flashing new code.
Mental model diagram (ASCII)
keymap.c + config.h + rules.mk
│
v
QMK build system
│
v
firmware.hex -> flash -> keyboard
How it works (step-by-step, with invariants and failure modes)
- Define matrix pins in
config.h. - Write keymap in
keymap.c. - Run
qmk compileto build firmware. - Enter bootloader and run
qmk flash.
Invariant: matrix pins and keymap ordering must match physical wiring. Failure modes include wrong keyboard name, missing bootloader, or feature bloat exceeding flash size.
Minimal concrete example
#define MATRIX_ROW_PINS { D1, D0, D4 }
#define MATRIX_COL_PINS { C6, D7, E6 }
Common misconceptions
- “Keymap defines wiring”: Wiring is defined in
config.h; keymap is logical. - “QMK always auto-detects bootloader”: It only works if your board supports it.
- “More features are always fine”: AVR flash size is limited.
Check-your-understanding questions
- Where do you define matrix pin order in QMK?
- Why might a keymap appear rotated?
- What does
rules.mkcontrol?
Check-your-understanding answers
- In
config.husingMATRIX_ROW_PINSandMATRIX_COL_PINS. - The matrix order in code does not match physical wiring order.
- It enables or disables firmware features and affects size.
Real-world applications
- Custom keyboard firmware development.
- Embedded build systems for small MCUs.
Where you’ll apply it
References
- QMK docs: “Understanding QMK” and “Handwired Guide”.
- “The GNU Make Book” Ch. 2-3.
Key insights
QMK is a structured build pipeline; most bugs are mismatches between physical wiring and configuration files.
Summary
Understanding QMK’s build structure turns firmware changes from guesswork into a reliable workflow.
Homework/Exercises to practice the concept
- Create a 3x3 keymap that places numbers 1-9 in order.
- Swap two columns in
MATRIX_COL_PINSand observe the effect.
Solutions to the homework/exercises
- Use a simple layout array with KC_1..KC_9.
- The keys in those columns will appear swapped in the output.
3. Project Specification
3.1 What You Will Build
A handwired 3x3 macro pad with QMK firmware that:
- Scans a 3x3 matrix with diodes.
- Provides a custom keymap with at least one macro.
- Flashes successfully to a Pro Micro (ATmega32U4) or equivalent.
- Demonstrates correct keypresses in QMK console.
3.2 Functional Requirements
- Wiring: Each of 9 switches mapped to a unique row/column.
- Firmware config: Correct matrix pins and diode direction.
- Keymap: 9 keys mapped to known outputs (e.g., numbers).
- Macro: At least one key sends a short string.
- Verification: QMK console shows correct row/col events.
3.3 Non-Functional Requirements
- Performance: Scan rate stable under normal typing.
- Reliability: No ghosting with diodes.
- Usability: Build and flash steps documented.
3.4 Example Usage / Output
$ qmk compile -kb handwired/3x3 -km mypad
Creating hex file: .build/handwired_3x3_mypad.hex
$ qmk flash -kb handwired/3x3 -km mypad
Flashing complete
3.5 Data Formats / Schemas / Protocols
- QMK keymap arrays and config defines.
3.6 Edge Cases
- One row not responding (miswired row pin).
- Ghosting when pressing three keys (missing diode).
- Bootloader not detected.
3.7 Real World Outcome
A working 3x3 macro pad that types expected keys.
3.7.1 How to Run (Copy/Paste)
qmk compile -kb handwired/3x3 -km mypad
qmk flash -kb handwired/3x3 -km mypad
qmk console
3.7.2 Golden Path Demo (Deterministic)
- Press each key in order; console shows row/col mapping that matches wiring.
- Macro key types “QMK” exactly once.
3.7.3 If CLI: exact terminal transcript
$ qmk console
matrix scan: row=0 col=0 pressed
matrix scan: row=0 col=1 pressed
matrix scan: row=0 col=2 pressed
exit_code=0
$ qmk flash -kb handwired/3x3 -km mypad
Flashing complete
exit_code=0
$ qmk flash -kb handwired/3x3 -km badname
error: keyboard not found
exit_code=2
4. Solution Architecture
4.1 High-Level Design
Switch Matrix -> MCU GPIO -> QMK Matrix Scan -> Keymap -> USB HID
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| Matrix Wiring | Connect switches and diodes | COL2ROW vs ROW2COL |
| QMK Config | Define pins and features | Minimal feature set |
| Keymap | Map keys to outputs | Simple numeric layout |
| Debug Console | Verify row/col events | Enable console in rules.mk |
4.3 Data Structures (No Full Code)
const uint16_t keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
[0] = {{KC_1, KC_2, KC_3}, {KC_4, KC_5, KC_6}, {KC_7, KC_8, KC_9}}
};
4.4 Algorithm Overview
Key Algorithm: Matrix Scan + Keymap Lookup
- Scan matrix to detect pressed keys.
- Map (row, col) to keycode using keymap.
- Send HID report.
Complexity Analysis:
- Time: O(R * C) per scan
- Space: O(R * C)
5. Implementation Guide
5.1 Development Environment Setup
qmk setup
qmk doctor
5.2 Project Structure
keyboards/handwired/3x3/
├── config.h
├── rules.mk
└── keymaps/
└── mypad/
└── keymap.c
5.3 The Core Question You’re Answering
“How do I go from a physical switch matrix to a working QMK keyboard?”
5.4 Concepts You Must Understand First
- Matrix wiring and diode direction.
- QMK config and build pipeline.
- Bootloader flashing procedure.
5.5 Questions to Guide Your Design
- Which pins are safe for row/col use on your MCU?
- How will you verify each key’s row/col mapping?
- What macro is the most useful for your workflow?
5.6 Thinking Exercise
Map each key to a row/column pair and write the mapping on paper before wiring.
5.7 The Interview Questions They’ll Ask
- Why are diodes needed in a matrix?
- How does QMK map matrix positions to keycodes?
- What is your debugging method for a dead row?
5.8 Hints in Layers
Hint 1: Start from the handwired template
Use keyboards/handwired/ as your base.
Hint 2: Validate with continuity tests Check each switch and diode direction before flashing.
Hint 3: Use QMK console See row/col events in real time.
Hint 4: Keep firmware small
Disable unused features in rules.mk.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Embedded basics | “Making Embedded Systems” | Ch. 1-4 |
| Digital logic | “Digital Design and Computer Architecture” | Ch. 1-2 |
| Build systems | “The GNU Make Book” | Ch. 2-3 |
5.10 Implementation Phases
Phase 1: Wiring (2-3 days)
Goals: correct matrix wiring and diode orientation
Tasks:
- Wire row and column buses.
- Install diodes in consistent direction.
Checkpoint: Continuity test passes for every key.
Phase 2: Firmware (2-3 days)
Goals: QMK config and keymap
Tasks:
- Define matrix pins and diode direction.
- Build and flash firmware.
Checkpoint: QMK console shows correct row/col events.
Phase 3: Debug & Polish (1-2 days)
Goals: stable macros and documentation
Tasks:
- Add one macro and test.
- Document build + flash steps.
Checkpoint: Macro triggers reliably and documentation is clear.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Diode direction | COL2ROW vs ROW2COL | COL2ROW | matches common QMK defaults |
| Bootloader | Caterina vs DFU | Caterina | common for Pro Micro |
| Debug method | Serial vs LEDs | QMK console | fastest feedback loop |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Hardware Tests | Verify wiring | Continuity tests |
| Firmware Tests | Verify keymap | QMK console output |
| Edge Case Tests | Multi-key presses | rectangle ghost check |
6.2 Critical Test Cases
- Press each key and confirm correct row/col.
- Press three keys in rectangle; no ghosting with diodes.
- Trigger macro and confirm output.
6.3 Test Data
Keymap: 1-9 numeric layout
Macro key: sends "QMK"
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Reversed diode | Key never registers | Flip diode orientation |
| Swapped row/col pins | Keymap appears rotated | Fix pin order in config.h |
| Missing bootloader entry | Flash fails | Reset into bootloader |
7.2 Debugging Strategies
- Use a multimeter: confirm row/col continuity.
- Use QMK console: verify row/col events before keymap logic.
7.3 Performance Traps
Enabling too many QMK features can exceed flash size on AVR. Keep rules.mk minimal.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add a second layer with F-keys.
- Add a reset key in the keymap.
8.2 Intermediate Extensions
- Add an encoder for volume.
- Add per-layer RGB (if hardware allows).
8.3 Advanced Extensions
- Convert to a small PCB instead of handwired.
- Add VIA support for dynamic remapping.
9. Real-World Connections
9.1 Industry Applications
- Custom macro pads for productivity.
- Prototyping for keyboard startups.
9.2 Related Open Source Projects
- QMK handwired templates.
- VIA firmware for remapping.
9.3 Interview Relevance
- Demonstrates hardware/firmware integration.
- Shows debugging methodology for embedded systems.
10. Resources
10.1 Essential Reading
- QMK handwired guide.
- “Making Embedded Systems” Ch. 1-4.
10.2 Video Resources
- Handwired keyboard build tutorials.
10.3 Tools & Documentation
- QMK CLI, QMK Toolbox.
10.4 Related Projects in This Series
11. Self-Assessment Checklist
11.1 Understanding
- I can explain diode direction and matrix scanning.
- I can describe how QMK compiles a keymap.
11.2 Implementation
- All 9 keys register correctly.
- Macro outputs expected text.
11.3 Growth
- I can debug wiring faults with a multimeter.
12. Submission / Completion Criteria
Minimum Viable Completion:
- 3x3 matrix wired correctly.
- Firmware flashes and keys register.
Full Completion:
- Macro key works.
- No ghosting with multi-key presses.
Excellence (Going Above & Beyond):
- Documentation with wiring diagrams.
- VIA or dynamic remapping support.