Potentiometer and PWM: the ESP32 ADC Controls LED Brightness
We read a potentiometer voltage with the 12-bit ADC and turn it into PWM with LEDC. Why to use ADC1 rather than ADC2, and how pulse-width modulation works.
About the project
Two basic "translators" between the analog world and a digital program: the ADC turns a voltage into a number, and PWM turns a number into an "average" voltage. Turn the potentiometer knob and the LED brightness follows smoothly.
What you need
- ESP32 DevKit
- 10 kΩ potentiometer
- LED and 220 Ω resistor
Wiring
| Part | Connection |
|---|---|
| Potentiometer, outer pins | 3V3 and GND |
| Potentiometer, middle (wiper) | GPIO34 |
| LED | GPIO25 → 220 Ω → anode; cathode → GND |
The potentiometer works as a voltage divider: the wiper gives 0 to 3.3 V depending on the knob position.
ADC: from voltage to a number
analogRead() on the ESP32 returns 0…4095 (12 bits). A few details:
- Use the ADC1 pins (GPIO32–39). ADC2 (GPIO0, 2, 4, 12–15, 25–27) does not work while Wi-Fi is on.
- The scale is non-linear near the ends: below ~0.1 V and above 3.1 V accuracy drops. For accurate measurements use
analogReadMilliVolts(): it applies the factory calibration. - Never feed more than 3.3 V into the pin.
PWM: from a number to brightness
An LED cannot shine "at half", but it can be switched on and off very quickly. Keep it on 30 % of the time, 5000 times a second, and the eye sees 30 % brightness. The share of "on" time is the duty cycle.
On the ESP32, PWM comes from the hardware LEDC unit. In the arduino-esp32 3.x core:
ledcAttach(pin, freq, bits)binds the pin and sets the frequency and resolution;ledcWrite(pin, duty)sets the duty from 0 to 2^bits − 1.
Code
// Potentiometer (ADC) → LED brightness (LEDC PWM)
const int POT_PIN = 34; // ADC1: keeps working with Wi-Fi on
const int LED_PIN = 25;
void setup() {
Serial.begin(115200);
ledcAttach(LED_PIN, 5000, 8); // 5 kHz, 8-bit (0..255)
}
void loop() {
int raw = analogRead(POT_PIN); // 0..4095
int duty = map(raw, 0, 4095, 0, 255);
ledcWrite(LED_PIN, duty);
Serial.printf("ADC=%4d duty=%3d\n", raw, duty);
delay(200);
}
Good to know
- Perceived brightness is non-linear: 0 → 10 % looks like a much bigger step than 80 → 100 %. For a smooth dimmer, square the value:
duty = raw * raw / 65793(gives 0…255). analogWrite()also works in core 3.x, but LEDC lets you choose the frequency and resolution yourself.- Above ~20 kHz PWM is inaudible (it matters for motors); for LEDs 1–5 kHz is enough.
What next
The same potentiometer can drive a servo, or a motor's speed through a MOSFET: the simulator has examples for both.