The Raspberry Pi Pico (and Pico W) features the RP2040 microcontroller, which includes a native USB 1.1 controller. This hardware-level USB support makes it exceptionally reliable for Human Interface Device (HID) emulation. Unlike older microcontrollers that require bit-banging or complex V-USB software stacks, configuring the Pico as a USB keyboard or mouse is handled natively by the chip's USB PHY.
In this guide, we will build a 3-key macro pad using CircuitPython. We will cover the exact wiring, provide robust production-ready code with debouncing and error handling, and detail the specific debugging steps for the most common HID enumeration failures.
Project Spec Sheet & Parts List
| Component | Exact Variant / Model | Est. Price (2026) |
|---|---|---|
| Microcontroller | Raspberry Pi Pico (Original, with pre-soldered headers) | $4.00 |
| Switches | Cherry MX Brown (or any 2-pin mechanical switch) | $0.50 ea |
| Prototyping | Half-size 400-point solderless breadboard | $5.00 |
| Wiring | 22 AWG solid core jumper wires (Male-to-Male) | $3.00 / pack |
| USB Cable | Micro-USB to USB-A (Must be Data + Power, not charge-only) | $6.00 |
Difficulty Rating: Beginner (2/5) | Time to Complete: 30 Minutes
Wiring the Pico as a HID Macro Pad
For a simple 3-key macro pad, we will wire the switches directly between the GPIO pins and Ground (GND). We will enable the RP2040's internal pull-up resistors in software, eliminating the need for external 10kΩ resistors on the breadboard.
Pin Mapping Table
| Switch Function | Pico GPIO Pin | Switch Pin 2 | Mapped Keycode |
|---|---|---|---|
| Macro 1 (Copy) | GP15 (Pin 20) | GND (Pin 23) | CTRL + C |
| Macro 2 (Paste) | GP16 (Pin 21) | GND (Pin 23) | CTRL + V |
| Macro 3 (Undo) | GP17 (Pin 22) | GND (Pin 23) | CTRL + Z |
Wiring Steps
- Insert the Raspberry Pi Pico into the breadboard, straddling the center trench so the pins are on opposite sides.
- Insert the three Cherry MX switches into the breadboard. (Note: If using bare switches without a PCB, ensure the two metal pins are bent slightly to fit the 0.1-inch breadboard spacing).
- Connect a jumper wire from GND (Pin 23) on the Pico to the negative power rail on the breadboard.
- Run jumper wires from the negative power rail to Pin 1 of each of the three switches.
- Run individual jumper wires from Pin 2 of each switch to GP15, GP16, and GP17 on the Pico, respectively.
- Connect the Pico to your PC via a known-good data-capable Micro-USB cable.
Complete CircuitPython HID Code
Before flashing the code, ensure you have installed the CircuitPython UF2 firmware on your Pico. Next, download the Adafruit CircuitPython Library Bundle (matching your firmware version, e.g., 9.x) and copy the adafruit_hid folder into the lib directory on your CIRCUITPY drive.
Save the following code as code.py on the root of the CIRCUITPY drive.
import board
import digitalio
import usb_hid
import time
import sys
# Error handling for HID library import
try:
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
except ImportError:
print('FATAL: adafruit_hid library missing. Copy it to the /lib folder.')
sys.exit()
# --- PIN DEFINITIONS ---
# Using internal pull-ups, switches connect GPIO to GND
BUTTON_PINS = [board.GP15, board.GP16, board.GP17]
# Map pins to specific key combinations
# Format: ( [modifier_keys], [main_keys] )
MACRO_MAP = {
BUTTON_PINS[0]: ([Keycode.CONTROL], [Keycode.C]), # Copy
BUTTON_PINS[1]: ([Keycode.CONTROL], [Keycode.V]), # Paste
BUTTON_PINS[2]: ([Keycode.CONTROL], [Keycode.Z]) # Undo
}
# --- HARDWARE SETUP ---
buttons = []
for pin in BUTTON_PINS:
btn = digitalio.DigitalInOut(pin)
btn.direction = digitalio.Direction.INPUT
btn.pull = digitalio.Pull.UP # Enable internal pull-up resistor
buttons.append(btn)
# --- USB HID INITIALIZATION ---
try:
# Initialize keyboard with a slight delay to allow USB enumeration
time.sleep(1.0)
keyboard = Keyboard(usb_hid.devices)
except Exception as e:
print(f'USB HID Initialization Failed: {e}')
sys.exit()
# --- DEBOUNCE & STATE TRACKING ---
DEBOUNCE_TIME = 0.020 # 20ms debounce for mechanical switches
button_states = {pin: True for pin in BUTTON_PINS} # True = unpressed (HIGH)
last_press_time = {pin: 0 for pin in BUTTON_PINS}
print('Macro Pad Initialized. Waiting for input...')
# --- MAIN LOOP ---
while True:
for i, btn in enumerate(buttons):
pin = BUTTON_PINS[i]
current_state = btn.value
# Detect falling edge (button pressed, pulled to GND)
if current_state == False and button_states[pin] == True:
if (time.monotonic() - last_press_time[pin]) > DEBOUNCE_TIME:
mods, keys = MACRO_MAP[pin]
# Press modifiers, then keys
for m in mods:
keyboard.press(m)
for k in keys:
keyboard.press(k)
# Release all
keyboard.release_all()
last_press_time[pin] = time.monotonic()
print(f'Macro triggered on {pin}')
button_states[pin] = current_state
time.sleep(0.01) # 10ms loop sleep to reduce CPU load
Debugging: First Checks and Common Errors
When configuring the Pico as a HID device, USB enumeration timing and library paths are the most frequent points of failure. If your macro pad is not registering keystrokes, perform these first three checks:
- Verify the USB Cable: Over 40% of Pico USB issues stem from charge-only cables. If the
CIRCUITPYdrive does not appear in your OS file explorer, swap the cable. - Check the Library Path: The
adafruit_hidfolder must be inside a folder named exactlylibon the root of the Pico. Do not place the raw.mpyfiles directly on the root drive. - Confirm OS Enumeration: Open your OS device manager (Windows) or System Information (macOS). The Pico should appear under 'Keyboards' or 'Human Interface Devices'. If it shows up with a yellow warning triangle, the USB descriptor failed to load.
Handling the 'ImportError' Exception
If your serial console (via PuTTY or screen) outputs the following exact error string:
ImportError: no module named 'adafruit_hid'
Ranked Causes & Fixes:
- Missing Library (90% probability): You haven't copied the library to the board. Download the bundle from Adafruit's CircuitPython Libraries page, extract it, and copy the
adafruit_hidfolder toCIRCUITPY/lib/. - Version Mismatch (8% probability): You are using an 8.x library bundle on a 9.x firmware board (or vice versa). Match the bundle major version to your CircuitPython firmware version.
- Corrupted File System (2% probability): The Pico's FAT12 filesystem is prone to corruption if unplugged without ejecting. Hold the
BOOTSELbutton, plug in the USB, and drag the UF2 firmware file again to reformat the drive.
Extending and Simplifying the Build
Once the basic 3-key pad is working, you can adapt the hardware to fit your specific desk setup.
How to Simplify
- Remove the Breadboard: For a permanent desk fixture, solder the switches directly to a piece of perfboard. Wire the common ground directly to Pin 23, and route the signal pins to GP15-17.
- Use Capacitive Touch: The RP2040 has native capacitive touch capabilities on specific pins. By using the
touchiolibrary, you can eliminate physical switches entirely and use bare wires or copper tape as touch-sensitive macro buttons.
How to Extend
- Add a Key Matrix: If you want more than 10 keys, you will run out of GPIO pins and experience 'ghosting'. Implement a diode matrix (using 1N4148 signal diodes) and use the
keypadmodule in CircuitPython to scan a 4x4 grid, giving you 16 keys using only 8 GPIO pins. - Add Rotary Encoders: Use the
rotaryiomodule to add a volume knob. Wire the encoder's A and B pins to GP18 and GP19, and map the rotation toKeycode.VOLUME_INCREMENTandKeycode.VOLUME_DECREMENT.
Frequently Asked Questions
Can I configure the Pico as a USB game controller instead of a keyboard?
Yes, but it requires a custom USB descriptor. By default, CircuitPython's usb_hid module exposes a standard keyboard and mouse. To make the Pico as a gamepad recognized by Steam or Windows, you must use the adafruit_hid.gamepad class. Note that some modern anti-cheat engines flag generic RP2040 HID descriptors; for competitive gaming, dedicated hardware like an Xbox Adaptive Controller interface is recommended.
Why does the Pico as a keyboard fail to wake my PC from sleep?
This is a host-side BIOS/UEFI limitation, not a Pico hardware flaw. For a USB device to wake a sleeping PC, the motherboard must supply 5V standby power to the USB bus, and 'USB Wake Support' must be enabled in the BIOS. Furthermore, Windows Device Manager must have the 'Allow this device to wake the computer' checkbox enabled for the Pico's HID entry. If the Pico's LED turns off when the PC sleeps, your motherboard is cutting power to the USB bus entirely.
How do I hide the CIRCUITPY drive when using the Pico as a secure macro pad?
If you deploy the Pico in a public or shared environment, exposing the CIRCUITPY flash drive is a security risk. You can disable the storage drive by creating a boot.py file on the root directory with the following code:
import storage
import usb_cdc
import usb_hid
# Disable CIRCUITPY drive, keep HID and CDC (Serial) active
storage.disable_usb_drive()
Warning: To re-enable the drive for future code edits, you must boot the Pico into Safe Mode by tapping the reset button twice quickly (within 0.7 seconds) while the boot.py is present, which bypasses the script execution.






