The Direct Answer: Wiring and Reading a Pushbutton in Arduino
To reliably read a pushbutton in Arduino, wire one leg of the switch to a digital pin (e.g., Pin 2) and the opposite leg to GND. Configure the pin using pinMode(2, INPUT_PULLUP); to activate the microcontroller's internal 20kΩ–50kΩ pull-up resistor. This eliminates the need for external resistors on your breadboard. When you read the pin with digitalRead(2), it will return HIGH when the button is released and LOW when pressed.
This guide targets the Arduino Uno R3 (ATmega328P) and Arduino Nano v3, though the INPUT_PULLUP logic applies universally across AVR-based boards and most ESP32/ESP8266 variants (note: ESP32 pins 34-39 are input-only and lack internal pull-ups). The inverted logic (pressed = LOW) trips up many beginners, but it is the industry standard for switch reading because it saves components and reduces wiring faults.
Pushbutton Hardware Specs and Switch Bounce Data
Not all switches behave identically on a workbench. When mechanical contacts close, they do not make a clean electrical connection instantly. The metal reeds physically chatter, causing rapid voltage spikes known as switch bounce. If your code reads the pin during this chatter window, a single physical press will register as 5 to 20 digital triggers.
Below is a data-dense breakdown of common pushbutton types you will encounter in embedded projects, including their typical bounce times and electrical ratings.
| Switch Type & Example Part | Typical Bounce Time | Contact Resistance | Max Rating | Best Use Case |
|---|---|---|---|---|
| 6x6mm Tactile (C&K PTS645 / Omron B3F) |
5 – 15 ms | ≤ 100 mΩ | 50mA @ 12VDC | Breadboards, PCB user interfaces, reset buttons. |
| 30mm Arcade Button (Sanwa OBSC-30) |
1 – 4 ms | ≤ 50 mΩ | 5A @ 250VAC | Human interface devices, heavy-duty panel mounts. |
| Roller Lever Limit Switch (Omron D4N Series) |
< 1 ms | ≤ 25 mΩ | 10A @ 120VAC | CNC endstops, 3D printer homing, industrial interlocks. |
| Piezo Solid-State (E-Switch PS Series) |
0 ms (No bounce) | N/A (Semiconductor) | 100mA @ 24VDC | Wet environments, medical devices, high-vibration zones. |
Because standard tactile switches exhibit 5–15 ms of bounce, your software must enforce a "dead time" after detecting an edge transition. Relying on hardware debouncing (RC low-pass filters) adds bulk; software debouncing via millis() is the preferred modern approach.
Internal vs. External Pull Resistors: Which to Choose?
A microcontroller GPIO pin configured as an input has high impedance. If left unconnected to a definitive voltage rail, it acts as an antenna, picking up electromagnetic noise and returning random 0s and 1s. A pull resistor ties the pin to a known state when the switch is open.
| Configuration | Wiring Required | Logic State (Open) | Logic State (Closed) | When to Use |
|---|---|---|---|---|
Internal Pull-Up (INPUT_PULLUP) |
Switch between Pin and GND | HIGH | LOW | 95% of hobby and prototyping projects. Saves components. |
| External Pull-Up (10kΩ to 5V) | Switch to GND + Resistor to 5V | HIGH | LOW | Long wire runs (>1 meter) where internal 30kΩ is too weak to overcome capacitance. |
| External Pull-Down (10kΩ to GND) | Switch to 5V + Resistor to GND | LOW | HIGH | When interfacing with legacy logic that strictly expects Active-High signals. |
Parts List and Pin Mapping
For this build, we are using the internal pull-up configuration. Gather the following components:
- Microcontroller: Arduino Uno R3 (ATmega328P) or compatible clone.
- Switch: 6x6mm Through-Hole Tactile Pushbutton (e.g., C&K PTS645SM43SMTR92).
- Wiring: 22 AWG solid-core hookup wire (3 strands).
- Indicator: 5mm LED with a 220Ω current-limiting resistor (or use the onboard Pin 13 LED).
| Component | Arduino Pin | Function |
|---|---|---|
| Pushbutton Leg 1 | Digital Pin 2 | Input (with internal pull-up enabled) |
| Pushbutton Leg 2 | GND | Reference ground to complete the circuit |
| LED Anode (+) | Digital Pin 13 | Output (drives the indicator LED) |
| LED Cathode (-) | GND (via 220Ω) | Current return path |
Complete Debounced Pushbutton Code
Below is the complete, compilable C++ code for the Arduino IDE. It uses a non-blocking state machine to handle switch bounce without using the delay() function, ensuring your main loop remains free to handle other tasks like sensor polling or motor control.
/*
* Non-Blocking Debounced Pushbutton Reader
* Target Board: Arduino Uno R3 / Nano v3 (ATmega328P)
* Author: ElectricalFlux
*/
// --- Pin Definitions ---
const int BUTTON_PIN = 2; // Pushbutton connected to D2 and GND
const int LED_PIN = 13; // Onboard LED or external LED on D13
// --- Debounce State Variables ---
bool lastButtonState = HIGH; // The previous reading from the input pin
bool currentButtonState = HIGH; // The current debounced state
unsigned long lastDebounceTime = 0; // The last time the output pin was toggled
const unsigned long debounceDelay = 50; // Debounce window in milliseconds
// --- Application State ---
bool ledState = LOW;
void setup() {
// Initialize serial for debugging
Serial.begin(115200);
while (!Serial) { ; } // Wait for serial port to connect (needed for native USB boards)
// Configure pins
pinMode(BUTTON_PIN, INPUT_PULLUP); // Enables internal 20k-50k pull-up resistor
pinMode(LED_PIN, OUTPUT);
// Set initial LED state
digitalWrite(LED_PIN, ledState);
Serial.println("System Ready. Awaiting button press...");
}
void loop() {
// 1. Read the raw state of the switch
bool reading = digitalRead(BUTTON_PIN);
// 2. Check if the raw state changed (could be bounce or a real press)
if (reading != lastButtonState) {
// Reset the debouncing timer
lastDebounceTime = millis();
}
// 3. If the state has been stable longer than the debounce delay, accept it
if ((millis() - lastDebounceTime) > debounceDelay) {
// If the accepted state is different from our current tracked state, update it
if (reading != currentButtonState) {
currentButtonState = reading;
// 4. Trigger action only on the 'pressed' transition (Active-Low = LOW)
if (currentButtonState == LOW) {
ledState = !ledState; // Toggle LED
digitalWrite(LED_PIN, ledState);
Serial.println("Button Pressed & Debounced. LED Toggled.");
}
}
}
// 5. Save the raw reading for the next loop iteration
lastButtonState = reading;
}
Debugging: First Three Things to Check When It Fails
When your pushbutton circuit misbehaves, do not immediately rewrite your code. Hardware and logic faults follow predictable patterns. Here are the first three things to check, ranked by frequency.
1. The Pin is Floating (Random Serial Output)
Symptom: The Serial Monitor prints "Button Pressed" continuously, or the LED flickers randomly when you wave your hand near the board.
Cause: You forgot INPUT_PULLUP in your pinMode() declaration, or you are using an ESP32 input-only pin (GPIO 34-39) which lacks internal pull-ups.
Fix: Verify your setup block contains pinMode(BUTTON_PIN, INPUT_PULLUP);. If using an ESP32 input-only pin, you must solder an external 10kΩ pull-up resistor to 3.3V.
2. The Compilation Error: lvalue required as left operand of assignment
Symptom: The Arduino IDE throws this exact error string during compilation, highlighting your if statement.
Cause: You used a single equals sign = (assignment) instead of a double equals sign == (comparison) in your conditional check. For example: if (currentButtonState = LOW).
Fix: Change the single = to ==. To prevent this permanently, adopt the "Yoda condition" habit: write if (LOW == currentButtonState). If you accidentally type =, the compiler will catch it immediately because you cannot assign a value to a literal constant.
3. Multiple Triggers per Single Physical Press
Symptom: You press the button once, but the LED toggles twice, or your menu scrolls past your target selection.
Cause: Switch bounce is bypassing your logic. This usually happens if you rely on a simple delay(50) but place it in the wrong part of the loop, or if your physical switch is heavily oxidized and bouncing for >50ms.
Fix: Implement the millis() state-machine provided in the code block above. If the issue persists, measure the switch with an oscilloscope; if bounce exceeds 50ms, clean the contacts with isopropyl alcohol or increase the debounceDelay constant to 100ms.
Extending and Simplifying the Build
Once you have a single pushbutton working reliably, you will inevitably need more. Here is how to scale your design without running out of GPIO pins or duplicating code.
Simplifying with the Bounce2 Library
For production firmware or complex projects, writing custom millis() timers for every button bloats your codebase. The Bounce2 library abstracts the state machine into a clean object-oriented API. It handles the debounce math and provides helper methods like fell() (triggered exactly once when the button is pressed) and rose() (triggered on release).
Scaling with Resistor Ladders and Matrices
If you need to add 4 to 12 buttons (like a numeric keypad), do not use 12 GPIO pins. Instead, use a diode matrix or an R-2R resistor ladder feeding into a single analog pin. For 16+ buttons, integrate an I2C GPIO expander like the MCP23017. The MCP23017 gives you 16 additional interrupt-capable pins using only the two I2C wires (SDA/SCL), and it includes built-in pull-up configuration registers, completely eliminating the need for external resistor networks on large control panels.






