To build a polyphonic piano Arduino controller that registers chords without dropping notes, you need an ATmega32U4-based board (like the Arduino Micro) for native USB MIDI, paired with a diode-matrix scanned keybed. Direct-wiring 25 buttons requires 25 GPIO pins and still suffers from "ghosting" when multiple keys are pressed simultaneously. By arranging the switches in a 5x5 matrix and placing a switching diode in series with each key, you drop the pin requirement to 10 and guarantee clean polyphonic MIDI output to any DAW.
Difficulty: Intermediate (requires soldering matrix intersections and understanding I/O modes)
Time to Build: 4–6 hours (including keybed fabrication and debugging)
Estimated Cost: $18–$25 (assuming salvaged switches; $40+ if buying new mechanical keyswitches)
Target Board Variant: Arduino Micro (or any ATmega32U4 clone with native USB HID support)
Project Specifications and Parts List
The core challenge of any USB MIDI controller is translating physical switch closures into standardized MIDI Note On/Off packets over the USB bus. The Arduino Uno and Nano lack native USB HID capabilities, requiring clunky serial-to-MIDI bridges. The Arduino Micro handles this natively.
Bill of Materials (BOM)
- Microcontroller: Arduino Micro (ATmega32U4) with headers. Do not use the Uno or Nano for this build.
- Switches: 25x Tactile buttons (6x6mm) or a salvaged 25-key toy keyboard keybed.
- Diodes: 25x 1N4148 switching diodes (DO-35 glass package or SOD-123 SMD). Do not use 1N4007 rectifier diodes; their slow reverse recovery time causes matrix scanning errors at high baud rates.
- Wiring: 28 AWG solid core silicone wire for the matrix bus; 22 AWG for power rails.
- Substrate: Perfboard or custom PCB for mounting switches, or 3D-printed enclosure.
Diode Matrix Wiring and Pin Mapping
A 5x5 matrix uses 5 Row lines (driven LOW sequentially) and 5 Column lines (held HIGH via internal pull-ups). When a key is pressed, the Column reads LOW. The 1N4148 diode is placed in series with the switch. The cathode (black band) must face the Row (Output), and the anode must face the Column (Input). This prevents current from back-feeding through adjacent pressed keys, which is the root cause of phantom notes.
| Matrix Role | GPIO Pin | I/O Mode | Hardware Component | Scanning Logic Notes |
|---|---|---|---|---|
| Row 0 | 2 | OUTPUT | 1N4148 Cathode | Driven LOW during scan, HIGH otherwise |
| Row 1 | 3 | OUTPUT | 1N4148 Cathode | Sequential pull-down |
| Row 2 | 4 | OUTPUT | 1N4148 Cathode | Sequential pull-down |
| Row 3 | 5 | OUTPUT | 1N4148 Cathode | Sequential pull-down |
| Row 4 | 6 | OUTPUT | 1N4148 Cathode | Sequential pull-down |
| Col 0 | 7 | INPUT_PULLUP | 1N4148 Anode | Reads LOW when switch closed |
| Col 1 | 8 | INPUT_PULLUP | 1N4148 Anode | Reads LOW when switch closed |
| Col 2 | 9 | INPUT_PULLUP | 1N4148 Anode | Reads LOW when switch closed |
| Col 3 | 10 | INPUT_PULLUP | 1N4148 Anode | Reads LOW when switch closed |
| Col 4 | 11 | INPUT_PULLUP | 1N4148 Anode | Reads LOW when switch closed |
Compilable USB MIDI Firmware
This firmware targets the Arduino Micro. It uses the official MIDIUSB library (install via Arduino IDE Library Manager). The code includes a pre-compiler trap to prevent you from accidentally flashing this to an Uno, hardware debouncing to prevent MIDI "note stutter," and full polyphonic state tracking.
#include "MIDIUSB.h"
// Pre-compiler check: Halt compilation if board lacks native USB
#if !defined(USBCON)
#error "Target board must support native USB (e.g., Arduino Micro, Leonardo). Change board in Tools menu."
#endif
// --- PIN DEFINITIONS ---
const byte ROW_PINS[5] = {2, 3, 4, 5, 6};
const byte COL_PINS[5] = {7, 8, 9, 10, 11};
// --- MIDI CONFIGURATION ---
const byte MIDI_CHANNEL = 0; // 0 = Channel 1 in DAW
const byte BASE_NOTE = 48; // C3 (MIDI note 48)
const byte VELOCITY = 100; // Fixed velocity (switches are digital)
// --- DEBOUNCE & STATE ---
const unsigned long DEBOUNCE_MS = 5;
bool keyState[25] = {false};
bool prevState[25] = {false};
unsigned long lastDebounce[25] = {0};
void setup() {
// Initialize Rows as Outputs (High to reverse-bias diodes)
for (byte r = 0; r < 5; r++) {
pinMode(ROW_PINS[r], OUTPUT);
digitalWrite(ROW_PINS[r], HIGH);
}
// Initialize Cols as Inputs with internal pull-ups
for (byte c = 0; c < 5; c++) {
pinMode(COL_PINS[c], INPUT_PULLUP);
}
}
void loop() {
scanMatrix();
MidiUSB.flush(); // Push buffered MIDI events to host
}
void scanMatrix() {
unsigned long currentMillis = millis();
for (byte r = 0; r < 5; r++) {
// Drive current Row LOW
digitalWrite(ROW_PINS[r], LOW);
// Small delay for line capacitance to settle (critical for long wire runs)
delayMicroseconds(10);
for (byte c = 0; c < 5; c++) {
byte keyIndex = (r * 5) + c;
bool reading = (digitalRead(COL_PINS[c]) == LOW); // LOW means pressed
// Debounce logic
if (reading != keyState[keyIndex]) {
lastDebounce[keyIndex] = currentMillis;
}
if ((currentMillis - lastDebounce[keyIndex]) > DEBOUNCE_MS) {
if (reading != prevState[keyIndex]) {
keyState[keyIndex] = reading;
if (keyState[keyIndex]) {
sendMIDI(0x09, BASE_NOTE + keyIndex, VELOCITY); // Note On
} else {
sendMIDI(0x08, BASE_NOTE + keyIndex, VELOCITY); // Note Off
}
}
}
prevState[keyIndex] = reading;
}
// Return Row to HIGH
digitalWrite(ROW_PINS[r], HIGH);
}
}
// Function to construct and send raw MIDI USB packets
void sendMIDI(byte status, byte note, byte velocity) {
midiEventPacket_t event = {
(byte)(status | MIDI_CHANNEL),
(byte)(status | MIDI_CHANNEL),
note,
velocity
};
MidiUSB.sendMIDI(event);
}
Debugging: First Three Things to Check When It Fails
Matrix scanning and USB HID protocols are unforgiving of minor wiring or configuration errors. If your piano Arduino build isn't triggering notes in your DAW, follow this ranked decision path.
1. Compile Error: #error "Target board must support native USB..."
Cause: You have an Arduino Uno, Nano, or Mega selected in the IDE Tools > Board menu. These boards use an ATmega328P or ATmega2560 which lack native USB HID endpoints. They communicate via UART serial, which macOS and Windows do not recognize as a MIDI device without a third-party bridge like Hairless MIDI.
Fix: Select Arduino Micro or Arduino Leonardo in the board menu. If using a Pro Micro clone, ensure you select the correct 5V/16MHz or 3.3V/8MHz variant matching your hardware.
2. Symptom: Ghosting / Phantom Notes (Pressing C and E triggers G)
Cause: Missing diodes, or diodes installed with reversed polarity. Without the diode blocking reverse current, pressing two keys in the same row and one in a shared column creates a short circuit path that tricks the microcontroller into reading a third, unpressed intersection as LOW.
Fix: Inspect the matrix under magnification. Verify the black band (cathode) of every 1N4148 diode is pointing toward the Row wire, and the anode is pointing toward the Column switch terminal. Use a multimeter in diode-test mode: red probe on Anode, black on Cathode should read ~0.6V. Reversed will read OL (Open Loop).
3. OS-Level Error: "USB Device Not Recognized" or Missing MIDI Port in DAW
Cause: If using a cheap ATmega32U4 clone board, the USB bootloader might be corrupted, or the board is enumerating as a generic serial port rather than a MIDI device because the `MIDIUSB` library initialization failed silently.
Fix: Double-tap the reset button on the Micro to force it into bootloader mode (the onboard LED should pulse). Re-upload the sketch. Ensure you are using a data-capable USB cable, not a charge-only cable. In your DAW (Ableton, FL Studio, Logic), check the MIDI/Sync preferences to ensure the "Arduino Micro" is enabled for both Input and Track/Remote.
Extending and Simplifying the Build
A 25-key matrix is an excellent proof-of-concept, but real-world music production often demands more range or expression. Here is how you scale the architecture up or down based on your bench capabilities.
Extending: 61-Key Keybeds and Velocity Sensitivity
To scale to a full 5-octave (61-key) keyboard, you will run out of GPIO pins on the Micro. You must introduce shift registers. Use a 74HC165 (Parallel-In, Serial-Out) to read columns and a 74HC595 (Serial-In, Parallel-Out) to drive rows. This allows you to scan hundreds of keys using only 3 SPI pins (Clock, Latch, Data).
For velocity sensitivity (how hard a key is struck), standard tactile switches won't work. You need a dual-contact rubber-dome keybed (salvageable from broken Casio or Yamaha synthesizers). The firmware must measure the time delta (in microseconds) between the first contact closing and the second contact closing. A shorter delta equals a higher MIDI velocity value. The PJRC MIDI documentation provides excellent timing diagrams for dual-contact matrix scanning.
Simplifying: Capacitive Touch and Direct Wiring
If soldering 25 diodes sounds tedious, or you want a sealed, keyless enclosure, swap the mechanical matrix for an MPR121 capacitive touch sensor breakout. The MPR121 communicates via I2C (using only 2 Arduino pins) and handles the electrode scanning and threshold debouncing in hardware. You simply map the 12 available electrodes to MIDI notes. While limited to 12 keys per chip (you can daisy-chain up to 4 for 48 keys), it eliminates diode orientation issues entirely and allows you to use copper tape or even fruit as piano keys.






