Digital I/O
A pin is a switch you control, or a question you ask — never a power supply and never a voltmeter. Plus the part nobody mentions, which is that a few pins on every ESP32 are already busy before your code starts.
Two directions, and one thing a pin is not
As an output the pin is a switch — it connects the wire to 3.3 V or to 0 V. As an input it is a comparator asking one question: is this above or below the threshold. It cannot tell you how much voltage is there; that needs the ADC.
The current limit follows from the switch idea. Roughly 20 mA is fine, 40 mA is the absolute maximum, and a motor wants 200 mA — which is why a motor never goes on a pin.
The pins that are already busy
Safe pins to start with
On a classic ESP32 dev board: 4, 5, 13, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, 33. Those do what you expect at boot and in your sketch.
Avoid GPIO 0, 2, 12 and 15 unless you know what they do at reset, avoid 6–11 entirely, and remember 34–39 are input-only. The S3, C3, C6 and P4 have their own lists — the pinout page for your chip has them.
The code
A button to ground and an LED. INPUT_PULLUP is doing the work — without it the pin floats and reads randomly, which is the first bug almost everybody writes.
const int BUTTON = 4; // pick an ordinary pin, not 0, 2, 12 or 15
const int LED = 18;
void setup() {
Serial.begin(115200);
pinMode(BUTTON, INPUT_PULLUP); // internal resistor to 3V3
pinMode(LED, OUTPUT);
}
void loop() {
bool pressed = digitalRead(BUTTON) == LOW; // LOW means pressed
digitalWrite(LED, pressed ? HIGH : LOW);
}With a pull-up, a pressed button reads LOW. That inversion feels wrong for about a week and then feels obvious.
Same circuit, same pull-up, and you can try each line at the REPL to watch the value change while you hold the button.
from machine import Pin
button = Pin(4, Pin.IN, Pin.PULL_UP)
led = Pin(18, Pin.OUT)
while True:
led.value(0 if button.value() else 1) # button reads 0 when pressedPin.PULL_UP is the same internal resistor. On an input-only pin such as GPIO 34 to 39 it does not exist, and the argument is silently ignored.
When it does not work
The pin is floating. An unconnected input is not 0 — it is an antenna. Use INPUT_PULLUP, or add a 10 kΩ resistor to 3V3 or ground yourself.
On the original ESP32, GPIO 34 to 39 are input-only. There is no output driver and no internal pull-up. The call is accepted and does nothing, which is the most confusing possible failure.
You have it on a strapping pin, or on a pin the bootloader drives. Move it to an ordinary one and add a pull-down resistor so it stays off during the first 250 ms.
A pin can pass about 20 mA comfortably and 40 mA once. An LED without a resistor asks for much more than that. Anything bigger — a motor, a strip, a relay coil — needs a transistor.
On and off is only two brightnesses. The next page is how the chip fakes everything in between.
PWM with LEDC →Edit this page — content/esp32/digital-io.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.