</>CoderDIY
← Back to list
Article

Build a DIY Macro Pad with an Arduino Pro Micro

Author: CoderDIY··Đọc bằng tiếng Việt
🎛️

If you type the same key combo dozens of times a day — copy, paste, undo, or a shortcut that opens a favorite app — a small macro pad sitting next to your keyboard saves a surprising amount of friction. The nice part of this project is that you don’t need any specialized hardware: an Arduino Pro Micro board (ATmega32U4 chip) and a few switches are enough to create a real USB HID device that your operating system recognizes exactly like a normal keyboard.

How it works

  1. The ATmega32U4 on the Pro Micro has a USB controller built directly into the chip, so it can genuinely emulate an HID (Human Interface Device) — unlike an Arduino Uno, where the USB port is just a serial bridge through a secondary chip (ATmega16U2) that can’t send real keyboard events.
  2. Each switch connects to a digital pin using INPUT_PULLUP, so no external resistor is needed — the pin reads HIGH when released and LOW when pressed.
  3. The firmware continuously polls the pin states, debounces them with a timer, and when it detects a valid press, calls the Keyboard.h library to send the matching key combo to the computer.

Parts list

  • Arduino Pro Micro (ATmega32U4, the common 5V/16MHz version)
  • 4 mechanical switches (tactile switches or full mechanical keyswitches both work)
  • Jumper wires, a breadboard, or perfboard for a permanent build
  • A 3D-printed or small plastic enclosure (optional, for a finished look)
  • A Micro-USB cable for flashing and the permanent connection
<rect x="30" y="50" width="170" height="200" rx="10" class="box" />
<text x="115" y="155" text-anchor="middle" class="label">Pro Micro</text>

<rect x="440" y="50" width="170" height="200" rx="10" class="box" />
<text x="525" y="155" text-anchor="middle" class="label">4 switches</text>

<line x1="200" y1="80" x2="440" y2="80" class="wire" />
<text x="320" y="72" text-anchor="middle" class="sublabel">D2 → Key 1 (Copy)</text>
<circle cx="200" cy="80" r="4" class="pin" />
<circle cx="440" cy="80" r="4" class="pin" />

<line x1="200" y1="120" x2="440" y2="120" class="wire" />
<text x="320" y="112" text-anchor="middle" class="sublabel">D3 → Key 2 (Paste)</text>
<circle cx="200" cy="120" r="4" class="pin" />
<circle cx="440" cy="120" r="4" class="pin" />

<line x1="200" y1="160" x2="440" y2="160" class="wire" />
<text x="320" y="152" text-anchor="middle" class="sublabel">D4 → Key 3 (Undo)</text>
<circle cx="200" cy="160" r="4" class="pin" />
<circle cx="440" cy="160" r="4" class="pin" />

<line x1="200" y1="200" x2="440" y2="200" class="wire" />
<text x="320" y="192" text-anchor="middle" class="sublabel">D5 → Key 4 (Custom)</text>
<circle cx="200" cy="200" r="4" class="pin" />
<circle cx="440" cy="200" r="4" class="pin" />

<line x1="200" y1="230" x2="440" y2="230" class="wire" />
<text x="320" y="222" text-anchor="middle" class="sublabel">GND → shared ground for all 4 switches</text>
<circle cx="200" cy="230" r="4" class="pin" />
<circle cx="440" cy="230" r="4" class="pin" />
Each switch connects to one digital pin and shares a common GND pin — no resistors needed thanks to INPUT_PULLUP.

Sample code

#include <Keyboard.h>

const int NUM_KEYS = 4;
const int keyPins[NUM_KEYS] = {2, 3, 4, 5};
bool lastState[NUM_KEYS] = {HIGH, HIGH, HIGH, HIGH};
unsigned long lastDebounceTime[NUM_KEYS] = {0, 0, 0, 0};
const unsigned long DEBOUNCE_DELAY = 25; // ms

void setup() {
  for (int i = 0; i < NUM_KEYS; i++) {
    pinMode(keyPins[i], INPUT_PULLUP);
  }
  Keyboard.begin();
}

void loop() {
  for (int i = 0; i < NUM_KEYS; i++) {
    bool reading = digitalRead(keyPins[i]);

    if (reading != lastState[i]) {
      lastDebounceTime[i] = millis();
    }

    // Only treat it as a valid press once the state has been stable past DEBOUNCE_DELAY
    if ((millis() - lastDebounceTime[i]) > DEBOUNCE_DELAY) {
      if (reading == LOW && lastState[i] == HIGH) {
        sendShortcut(i);
      }
    }

    lastState[i] = reading;
  }
}

void sendShortcut(int index) {
  switch (index) {
    case 0: // Key 1: Copy
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('c');
      break;
    case 1: // Key 2: Paste
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('v');
      break;
    case 2: // Key 3: Undo
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press('z');
      break;
    case 3: // Key 4: custom combo, e.g. open a favorite app
      Keyboard.press(KEY_LEFT_CTRL);
      Keyboard.press(KEY_LEFT_SHIFT);
      Keyboard.press('m');
      break;
  }
  delay(10);
  Keyboard.releaseAll();
}

With just 4 keys, straight-wiring like this is simple enough and doesn’t need matrix scanning. If you want to grow to 9 keys or more, switch to a row/column matrix layout so you don’t burn through digital pins — each row connects to an output pin, each column to an input pin, and the firmware scans one row at a time to figure out which key is pressed.

Common pitfalls

  • “Board not found” when re-flashing: since Keyboard.begin() takes over the USB port to act as a keyboard, a buggy sketch that hangs on boot can make the board impossible to reflash normally. The fix: double-tap the reset button quickly to force the bootloader, then flash the fix within the next few seconds.
  • Skipping debounce sends duplicate keystrokes from a single press, because the mechanical contact bounces for a few milliseconds as it opens and closes.
  • Forgetting Keyboard.releaseAll() leaves a key stuck in the “pressed” state on the OS side, causing it to repeat endlessly or block other keys.
  • Some operating systems prompt to confirm a new HID device the first time it’s plugged in — don’t panic if the macro pad doesn’t respond in the very first second.

Where to go from here

Add a dedicated “layer” key that, when held, switches to a second set of shortcuts, wire in a rotary encoder for volume control, or move to QMK/VIA firmware entirely if you want to remap keys without reflashing every time.

Tags:#arduino#firmware#productivity

Comments