Community project
3D Printed Macro Pad
Lucian Raileanu
Published July 29, 2026

This project builds a 6-button macro pad using a Raspberry Pi Pico microcontroller and 3D-printed enclosure. The pad supports hot-swap mechanical switches and can be reconfigured to send any F13-F24 key combination, making it ideal for triggering macros in creative software, games, or productivity applications.
The guide includes a complete wiring diagram showing how to connect each button to the Pico's GPIO pins with a common ground configuration, a parts list with recommended components, and step-by-step assembly instructions for printing and preparing the case. The included firmware handles debouncing, stores custom key mappings in the Pico's flash memory, and provides a serial interface for reprogramming the pad without reflashing.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| Push ButtonMX hot-swap key 1 | 1 | Momentary push button switch |
| Push ButtonMX hot-swap key 2 | 1 | Momentary push button switch |
| Push ButtonMX hot-swap key 3 | 1 | Momentary push button switch |
| Push ButtonMX hot-swap key 4 | 1 | Momentary push button switch |
| Push ButtonMX hot-swap key 5 | 1 | Momentary push button switch |
| Push ButtonMX hot-swap key 6 | 1 | Momentary push button switch |
Assembly
5 stepsPrint and prepare the enclosure
Print the 2 × 3 top plate, bottom shell, six keycaps, and a USB opening sized for the Raspberry Pi Pico. Test-fit the Pico and USB cable before installing electronics.
- Tip: Use a 0.2 mm layer height for keycaps and plate.
- Tip: Leave clearance around the USB connector so the cable is not stressed.
- ⚠ Do not force the Pico or USB plug into a tight printed opening.
Install hot-swap sockets and switches
Press each MX hot-swap socket into the underside of the printed plate, then insert the six MX switches from above. Label physical positions 1–6 from left to right, top row then bottom row.
- Tip: Test that every switch clicks and sits flat before attaching wires.
- Tip: Hot-swap sockets let switches be removed later without desoldering.
- ⚠ Each switch has two electrical contacts; do not connect both contacts directly together.
Create the common ground wire
Daisy-chain one contact of every switch using a single insulated wire, then connect that common wire to a GND pin on the Pico.
- Tip: Pre-crimped Dupont leads or a small terminal block can reduce soldering at the Pico.
- Tip: Keep the ground wire routed around the perimeter of the plate.
- ⚠ Unplug the Pico from USB while fitting or soldering wires.
Wire the six signal contacts
Connect the unused contact of each switch to the Pico: key 1→GP2, key 2→GP3, key 3→GP4, key 4→GP5, key 5→GP6, key 6→GP7. These direct connections eliminate the need for a matrix or diodes.
- Tip: Use different wire colors or small labels for GP2 through GP7.
- Tip: Tug-test each connection gently before closing the case.
- ⚠ Never connect a switch signal wire to 3V3, 5V, or VSYS; it must connect only to its named GPIO.
Close the case and configure the pad
Mount the Pico in the shell, fit the keycaps, and connect USB. Deploy the firmware with Schematik’s Deploy button. Then run data/macro_pad_remapper.py on a computer with Python/Tkinter and pyserial, select the Pico serial port, choose F13–F24 for each displayed key, and click Save mapping to pad.
- Tip: Default assignments are F13, F14, F15, F16, F17, and F18.
- Tip: The saved mapping remains in the Pico after USB power is removed; games can bind those uncommon function keys directly.
- ⚠ The remapper must be closed before another application can use the Pico serial port.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| GPIO 2 | key_1 SIGNAL | digital |
| GND | key_1 GND | ground |
| GPIO 3 | key_2 SIGNAL | digital |
| GND | key_2 GND | ground |
| GPIO 4 | key_3 SIGNAL | digital |
| GND | key_3 GND | ground |
| GPIO 5 | key_4 SIGNAL | digital |
| GND | key_4 GND | ground |
| GPIO 6 | key_5 SIGNAL | digital |
| GND | key_5 GND | ground |
| GPIO 7 | key_6 SIGNAL | digital |
| GND | key_6 GND | ground |
Firmware
Raspberry Pi Pico#include <Arduino.h>
#include <Keyboard.h>
#include <EEPROM.h>
// Hoisted type definitions
struct PadConfig {
uint32_t magic;
uint8_t functionNumber[KEY_COUNT];
};
// Forward declarations
uint8_t defaultFunctionNumber(uint8_t index);
bool validFunctionNumber(int number);
uint8_t hidKeyForFunction(uint8_t number);
void saveConfig();
void loadConfig();
void sendMap();
void handleCommand(char *command);
void serviceSerial();
void sendMappedKey(uint8_t index);
const uint8_t keyPins[] = {2, 3, 4, 5, 6, 7};
const uint8_t KEY_COUNT = 6;
const unsigned long DEBOUNCE_MS = 20;
// Python GUI protocol: GET, SET <1-6> <13-24>, RESET.
// The map is retained in the Pico's flash-backed EEPROM emulation.
const uint32_t CONFIG_MAGIC = 0x4D504144;
PadConfig config;
bool stablePressed[KEY_COUNT] = {};
bool lastSample[KEY_COUNT] = {};
unsigned long lastChangeMs[KEY_COUNT] = {};
char commandBuffer[48];
uint8_t commandLength = 0;
uint8_t defaultFunctionNumber(uint8_t index) { return 13 + index; }
bool validFunctionNumber(int number) { return number >= 13 && number <= 24; }
uint8_t hidKeyForFunction(uint8_t number) {
switch (number) {
case 13: return KEY_F13; case 14: return KEY_F14; case 15: return KEY_F15;
case 16: return KEY_F16; case 17: return KEY_F17; case 18: return KEY_F18;
case 19: return KEY_F19; case 20: return KEY_F20; case 21: return KEY_F21;
case 22: return KEY_F22; case 23: return KEY_F23; case 24: return KEY_F24;
default: return KEY_F13;
}
}
void saveConfig() {
EEPROM.put(0, config);
EEPROM.commit();
}
void loadConfig() {
EEPROM.begin(sizeof(PadConfig));
EEPROM.get(0, config);
if (config.magic != CONFIG_MAGIC) {
config.magic = CONFIG_MAGIC;
for (uint8_t i = 0; i < KEY_COUNT; ++i) config.functionNumber[i] = defaultFunctionNumber(i);
saveConfig();
}
for (uint8_t i = 0; i < KEY_COUNT; ++i) {
if (!validFunctionNumber(config.functionNumber[i])) config.functionNumber[i] = defaultFunctionNumber(i);
}
}
void sendMap() {
Serial.print("MAP ");
for (uint8_t i = 0; i < KEY_COUNT; ++i) {
if (i) Serial.print(',');
Serial.print(config.functionNumber[i]);
}
Serial.println();
}
void handleCommand(char *command) {
if (!strcmp(command, "GET")) { sendMap(); return; }
if (!strcmp(command, "RESET")) {
for (uint8_t i = 0; i < KEY_COUNT; ++i) config.functionNumber[i] = defaultFunctionNumber(i);
saveConfig();
Serial.println("OK RESET");
return;
}
int keyNumber, functionNumber;
if (sscanf(command, "SET %d %d", &keyNumber, &functionNumber) == 2 &&
keyNumber >= 1 && keyNumber <= KEY_COUNT && validFunctionNumber(functionNumber)) {
config.functionNumber[keyNumber - 1] = functionNumber;
saveConfig();
Serial.println("OK SET");
return;
}
Serial.println("ERR Use GET, SET <1-6> <13-24>, or RESET");
}
void serviceSerial() {
while (Serial.available()) {
char c = (char)Serial.read();
if (c == '\r') continue;
if (c == '\n') {
commandBuffer[commandLength] = '\0';
if (commandLength) handleCommand(commandBuffer);
commandLength = 0;
} else if (commandLength < sizeof(commandBuffer) - 1) {
commandBuffer[commandLength++] = c;
} else {
commandLength = 0;
Serial.println("ERR Command too long");
}
}
}
void sendMappedKey(uint8_t index) {
Keyboard.press(hidKeyForFunction(config.functionNumber[index]));
delay(8);
Keyboard.releaseAll();
}
void setup() {
loadConfig();
for (uint8_t i = 0; i < KEY_COUNT; ++i) {
pinMode(keyPins[i], INPUT_PULLUP);
stablePressed[i] = lastSample[i] = (digitalRead(keyPins[i]) == LOW);
}
Serial.begin(115200);
Keyboard.begin();
}
void loop() {
serviceSerial();
unsigned long now = millis();
for (uint8_t i = 0; i < KEY_COUNT; ++i) {
bool samplePressed = (digitalRead(keyPins[i]) == LOW);
if (samplePressed != lastSample[i]) {
lastSample[i] = samplePressed;
lastChangeMs[i] = now;
}
if (now - lastChangeMs[i] >= DEBOUNCE_MS && samplePressed != stablePressed[i]) {
stablePressed[i] = samplePressed;
if (stablePressed[i]) sendMappedKey(i);
}
}
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.