Adjust LED brightness with a potentiometer

Connect a 10 kΩ potentiometer and an LED, read an ESP32 analogue input and scale the reading into PWM to adjust brightness.

Getting started with ESP32 and ArduinoBeginner75 min

What you will learn

  • Connect a potentiometer as a voltage divider powered from 3V3.
  • Explain 12-bit readings, endpoint clipping and the limits of raw ADC data.
  • Scale input values from 0–4095 to the 8-bit PWM range 0–255.
  • Use the pin-based LEDC API for Arduino-ESP32 3.x.
  • Check behaviour at the endpoints and midpoint.

Before you start

Complete courses 01 and 02: sketch upload, Serial Monitor, LED polarity and a series resistor.

Reference setup

Board in Arduino IDE
ESP32 Dev Module
Arduino-ESP32
3.3.12
Serial Monitor
115200 baud

This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.

What we will build

Build a light dimmer that changes an LED's brightness when you turn a potentiometer. The Serial Monitor shows the input reading and output value, connecting a physical adjustment with a visible result.

Learning objectives

  • Connect a potentiometer as a voltage divider.
  • Read an analogue input and distinguish a raw reading from a voltage.
  • Convert the range 0–4095 into the range 0–255.
  • Control an LED using PWM.
  • Schedule sampling and reporting at different intervals.

Prerequisites

Complete courses 01 and 02 first. You should be able to upload a sketch, open the Serial Monitor and connect an LED with a series resistor. This wiring profile uses a classic ESP32 development board with an ESP32-WROOM-32 module. It does not cover ESP32-C3, S2 or S3 boards. Locate the printed GPIO labels: numbers in this course are GPIO numbers, not physical header positions.

Equipment

ComponentQuantitySpecification and notes
Development board with a classic ESP32-WROOM-32 module1Match the reference GPIO labels; check physical header positions on the actual board.
USB data and power cable1Use the connector fitted to your board; the pictured kit lists Micro-USB.
Solderless breadboard1The kit lists 830 tie points. Check whether the power rails are split.
Red LED1Anode A and cathode K; identify polarity on the actual part.
220 Ω resistor1One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors.
10 kΩ potentiometer1Two end terminals and a wiper W; identify their physical positions on the actual component.
Jumper wires6Use male-to-male or female-to-male leads to suit the board headers.

Approximate quantity.

Use a 10 kΩ potentiometer. Identify its two end terminals and its wiper before connecting it. Check the markings or documentation for the actual component. The wiper is the terminal whose resistance to the ends changes as you turn the control.

How it works

The potentiometer ends connect to 3V3 and GND. Its wiper provides a voltage between them, depending on its position. The ADC converts that voltage into a number. The sketch selects 12-bit resolution: 4096 possible values from 0 to 4095.

analogRead() returns an uncalibrated reading. For the classic ESP32, the documented measurement range with ADC_11db is approximately 0.15–3.1 V. Our potentiometer spans 0–3.3 V, so the reading can reach a limit before the control reaches its mechanical end. This is expected here; the raw number is not a precision voltage measurement. Espressif: ADC

PWM switches the LED on and off rapidly. This sketch uses 5000 Hz and an 8-bit control value from 0 to 255. Zero turns it off; 255 requests maximum on-time. A value of 128 gives approximately half the on-time, but perceived brightness is not a linear scale.

Wiring

Wiring diagram

The drawing shows electrical connections by GPIO label, not the board’s physical header layout. Power the board through USB.

Wiring diagram — electrical connections listed in the table below
Wiring diagramEnlargeSVGPNG
Connections · Wiring diagram
FromToConnection
ESP32.GPIO25R1.1Control the LED branch.
R1.2D1.AConnect the 220 Ω resistor to the LED anode.
D1.KESP32.GNDConnect the cathode to common ground.
ESP32.3V3RV1.END_AOne end of the 10 kΩ potentiometer.
ESP32.GNDRV1.END_BThe other potentiometer end.
RV1.WESP32.GPIO34Wiper to the ADC1 input.
  1. Disconnect USB before changing connections. Insert the LED and potentiometer without accidentally joining different terminals in one connected breadboard group.
  2. Connect one potentiometer end to 3V3 and the other to GND. Connect its wiper to GPIO34.
  3. Connect GPIO25 to a 220 Ω resistor, the resistor's free end to the LED anode, and the LED cathode to GND.
  4. Check the common ground and any power rails you use. Some breadboard rails have a break in the middle.
  5. Compare every connection with the diagram and connection table before reconnecting USB.

Swapping the potentiometer's two end connections reverses its direction of operation; the wiper remains on GPIO34. Use 3V3 for this circuit. Do not connect the potentiometer output to 5 V.

Arduino setup

In Arduino IDE 2.x, install esp32 by Espressif Systems, target version 3.3.12. Select ESP32 Dev Module and your board's port. Follow the official installation instructions if the board package is missing. No additional libraries are needed.

Download the Arduino example (.zip) and extract it. Open potentiometer_pwm.ino inside the matching folder. Select Verify, then Upload. Open the Serial Monitor at 115200 baud. The specified version is the course's target; compilation results and physical board checks are recorded separately.

Complete sketch

potentiometer_pwm.ino Arduino / C++
Download .ino

#include <Arduino.h>

constexpr uint8_t LED_PIN = 25;
constexpr uint8_t POT_PIN = 34;
constexpr uint32_t PWM_FREQUENCY_HZ = 5000;
constexpr uint8_t PWM_RESOLUTION_BITS = 8;
constexpr uint32_t SAMPLE_INTERVAL_MS = 20;
constexpr uint32_t REPORT_INTERVAL_MS = 200;

bool pwmReady = false;
uint32_t lastSampleMs = 0;
uint32_t lastReportMs = 0;
uint16_t rawValue = 0;
uint8_t duty = 0;

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  pinMode(POT_PIN, INPUT);
  analogReadResolution(12);
  analogSetPinAttenuation(POT_PIN, ADC_11db);

  // Arduino-ESP32 3.x uses the GPIO pin in ledcAttach() and ledcWrite().
  pwmReady = ledcAttach(LED_PIN, PWM_FREQUENCY_HZ, PWM_RESOLUTION_BITS);
  if (!pwmReady) {
    Serial.println("ERROR: PWM setup failed. Check the board and ESP32 core.");
    return;
  }

  ledcWrite(LED_PIN, 0);
  lastSampleMs = millis();
  lastReportMs = lastSampleMs;
  Serial.println("WB Maker Bridge - Turn the potentiometer to dim the LED.");
}

void loop() {
  if (!pwmReady) {
    delay(100);
    return;
  }

  const uint32_t now = millis();

  if (now - lastSampleMs >= SAMPLE_INTERVAL_MS) {
    lastSampleMs = now;
    rawValue = analogRead(POT_PIN);
    duty = (static_cast<uint32_t>(rawValue) * 255U) / 4095U;
    ledcWrite(LED_PIN, duty);
  }

  if (now - lastReportMs >= REPORT_INTERVAL_MS) {
    lastReportMs = now;
    Serial.print("ADC: ");
    Serial.print(rawValue);
    Serial.print(" | PWM: ");
    Serial.println(duty);
  }
}

The displayed sketch and its download use the same Arduino file. Save your own copy before experimenting.

Understanding the sketch

Find the constants for input GPIO34, output GPIO25, frequency 5000 Hz and 8-bit resolution. The ADC and PWM are configured once in setup().

ledcAttach() configures PWM on a pin, and ledcWrite() sets its duty value. This example uses the Arduino-ESP32 3.x API, where these calls take a pin. Older examples using ledcSetup() and ledcAttachPin() need adaptation. Espressif: LEDC · Migration to 3.x

Approximately every 20 ms, the sketch samples the input and calculates raw reading × 255 / 4095. Integer arithmetic discards the fractional part. A raw input of 2048 therefore produces 127. This is a calculation example: the physical midpoint need not produce exactly 2048.

Serial reporting runs approximately every 200 ms. These separate intervals provide responsive adjustment and readable output. Scheduling uses millis(), so there is no long blocking pause between readings.

Run and observe

Turn the potentiometer slowly across its range. The LED should move from off or very dim to the greatest brightness allowed by this circuit. Compare the change with the Serial Monitor values. Hold the control still for about ten seconds and note any small variations.

CheckRecordWhat you examine
Minimum settingRaw input and PWMThe lower limit
Approximate midpointInput, PWM and apparent brightnessReading-to-output scaling
Maximum settingWhere 4095 first appearsClipping near the upper end
Stationary controlSmallest and largest readingsReading stability

Troubleshooting

ProblemCheck and correction
Values change but the LED stays darkCheck GPIO25, LED polarity, its resistor and the common ground.
Reading is always 0 or 4095Check the wiper, GPIO34 and separate potentiometer terminals. A short flat section near an endpoint is expected.
Readings jump widelyCheck loose connections and whether the wiper actually reaches the input.
ledcAttach is not definedCheck the ESP32 board selection and package version 3.3.12.
Serial output is missing or unreadableCheck the port and 115200 baud; press EN/RESET if necessary.

Independent challenge

Make two separate versions. In the first, brightness decreases as the reading increases. In the second, the maximum PWM value is 128. Change only the output calculation, then check both endpoints.

Check your understanding

  1. Why does the wiper connect to an analogue input?
  2. Does a PWM value of 128 request a constant 1.65 V output?
  3. Why can the reading reach 4095 before the mechanical end?

Answer guidance and worked solution

The wiper provides a changing voltage, which the ADC converts into a number. PWM changes the on-time of a digital output; 128 does not request a constant output voltage. The ADC can reach its limit early because its documented measurement range is narrower than the potentiometer's full travel to 3.3 V.

For the reversed version, subtract the calculated duty value from 255: raw input 0 gives 255, and input 4095 gives 0. For the limited version, calculate raw reading × 128 / 4095. The endpoints become 0 and 128. This limits on-time to approximately half the maximum, without implying exactly half the perceived brightness.

Downloads

Download the sketch and diagram. Record your board model, package version and observations.

Primary references