Why Pull Resistors Exist
GPIO pins in INPUT mode have very high impedance — almost no current flows through them. This means they are extremely sensitive to tiny voltages induced by nearby electrical fields. Without a defined reference voltage, the pin reads randomly: sometimes HIGH, sometimes LOW, with no button pressed at all. Pull resistors solve this by providing a weak, continuous connection to either 3.3 V (pull-up) or GND (pull-down), establishing a clear resting state that noise cannot overcome.
How a Pull-Up Resistor Works
A pull-up resistor connects the GPIO pin to the supply voltage (3.3 V on ESP32) through a resistor:
3.3V ──── 10kΩ ──── GPIO pin ──── Button ──── GND Resting state (button open): pin = HIGH (3.3V via resistor) Active state (button closed): pin = LOW (shorted to GND) → Active-LOW logic
When the button is open, the resistor holds the pin at 3.3 V. When the button is pressed, it connects the pin directly to GND, overriding the resistor (0.33 mA flows through the 10 kΩ — harmless). The GPIO now reads LOW.
How a Pull-Down Resistor Works
3.3V ──── Button ──── GPIO pin ──── 10kΩ ──── GND Resting state (button open): pin = LOW (GND via resistor) Active state (button closed): pin = HIGH (3.3V via button) → Active-HIGH logic
Pull-downs are less common in ESP32 projects because the INPUT_PULLUP mode is the default convenience option. However, some sensors actively drive their output HIGH when triggered (PIR sensors, active-HIGH reed switches), where a pull-down keeps the idle state clean.
Comparing Pull-Up vs Pull-Down
| Feature | Pull-Up | Pull-Down |
|---|---|---|
| Resting pin state | HIGH (3.3V) | LOW (GND) |
| Button wired to | GND | 3.3V |
| Button pressed = | LOW (active-LOW) | HIGH (active-HIGH) |
| ESP32 mode | INPUT_PULLUP | INPUT_PULLDOWN |
| More common? | Yes ✓ | Less common |
| Used by default I2C | Yes (SDA, SCL) | No |
Internal vs External Resistors
ESP32 has built-in pull-up and pull-down resistors accessible through software. Here is when to use each:
| Scenario | Use Internal? | Use External? |
|---|---|---|
| Button on short wire (<15 cm) | Yes ✓ | Optional |
| Button on long wire (>30 cm) | No — too weak | Yes (10 kΩ) |
| Industrial/noisy environment | No | Yes (4.7 kΩ) |
| GPIO34–GPIO39 (no internal) | N/A | Required |
| I2C bus (SDA/SCL) | Not recommended | Yes (4.7 kΩ) |
| Battery-powered, low power | Yes ✓ | Higher value (100 kΩ) |
Using Internal Pull Resistors in Code
void setup() {
Serial.begin(115200);
// Internal pull-up — pin rests HIGH, reads LOW when button pressed to GND
pinMode(16, INPUT_PULLUP);
// Internal pull-down — pin rests LOW, reads HIGH when button pressed to 3.3V
pinMode(17, INPUT_PULLDOWN);
}
void loop() {
int p16 = digitalRead(16); // LOW = button pressed (active-LOW)
int p17 = digitalRead(17); // HIGH = button pressed (active-HIGH)
Serial.printf("P16=%d (active-LOW) P17=%d (active-HIGH)n", p16, p17);
delay(200);
}
External Pull Resistor Wiring
For a 10 kΩ external pull-up:
| Connection Point | Connects To |
|---|---|
| Resistor leg 1 | ESP32 3.3V |
| Resistor leg 2 | ESP32 GPIO pin AND button terminal A |
| Button terminal B | ESP32 GND |
In code use plain INPUT mode (not INPUT_PULLUP) because the resistor is external:
// External 10kΩ pull-up wired on breadboard
void setup() {
Serial.begin(115200);
pinMode(34, INPUT); // GPIO34: input-only, needs external resistor
}
void loop() {
Serial.printf("GPIO34 = %dn", digitalRead(34));
delay(200);
}
Power Consumption Considerations
Pull resistors consume a small continuous current whenever the button is pressed (or whenever the logic is in the opposite state to the rail):
External 10kΩ pull-up, button pressed (pin shorted to GND): I = 3.3V / 10,000Ω = 0.33 mA External 10kΩ pull-up, button released (no current from GPIO): I ≈ 0 mA (tiny leakage only) Internal ~47kΩ pull-up, button pressed: I = 3.3V / 47,000Ω = 0.07 mA For battery-powered designs: use higher value external resistors (47kΩ–100kΩ) or enable internal pull-ups only when actively reading the button.
Pull Resistors and I2C
I2C buses (SDA and SCL lines) always require pull-up resistors on the bus lines. Internal pull-ups are too weak (~47 kΩ) for reliable I2C communication. Use 4.7 kΩ external pull-ups to 3.3 V on both SDA and SCL. Many I2C breakout boards (BME280, SSD1306, etc.) include these resistors on-board, so check the datasheet before adding more.
Troubleshooting Pull Resistor Problems
- Random readings with button not pressed: The pull resistor is missing or too weak. Add or lower the resistor value.
- Button press not detected: The pull-down resistor is fighting the signal. Check wiring orientation.
- Inverted logic: You used INPUT_PULLUP but code checks for HIGH. Change to check for LOW (active-LOW).
- GPIO34 always floats: No internal pull available — add a physical 10 kΩ resistor to 3.3 V.
Summary
Pull-up and pull-down resistors define the resting state of a GPIO input and prevent the floating pin problem. Use INPUT_PULLUP (active-LOW, button to GND) for most buttons. Use INPUT_PULLDOWN (active-HIGH, button to 3.3V) when the signal naturally drives HIGH. Add external 10 kΩ resistors for long wires, noisy environments, and input-only GPIO34–GPIO39 pins.