EthicsComputerCourses
Join training +91 90532 10052

PWM & analogWrite

IoT & Arduino · Lesson 14 of 29 Beginner 6 min read Updated

By the end of this lesson you will understand pulse-width modulation, fade an LED smoothly up and down, set its brightness with a potentiometer, and make the fade look even to human eyes. The same technique controls motor speed and servo position later in the course.

A digital pin can only be fully on (5 V) or fully off (0 V). So how does a dimmer work? The answer is a clever trick: switch the pin on and off so fast that the LED, the motor or your eye only notices the average.

What PWM is

PWM stands for pulse-width modulation. The pin switches between HIGH and LOW many hundreds of times every second. The fraction of time it spends HIGH is called the duty cycle:

  • 0% duty: always off.
  • 25% duty: on for a quarter of each cycle.
  • 50% duty: on half the time.
  • 100% duty: always on.

Imagine a ceiling fan regulator that switches the fan fully on and off very quickly. If it is on 50% of the time, the fan settles at a medium speed. An LED switches so quickly that your eye blends the flashes into steady, dimmer light.

On the Arduino UNO, only the pins marked with a ~ can do PWM: 3, 5, 6, 9, 10 and 11.

analogWrite()

analogWrite(pin, value) starts PWM on a pin. The value goes from 0 to 255 by default (8 bits). The name is a little misleading: on most pins it does not produce a true analog voltage, it produces PWM. The average voltage is the duty cycle times 5 V.

This sketch prints the duty cycle and average voltage for some common values. It runs once, so the output is fixed:

C++
void setup() {
  Serial.begin(9600);
  while (!Serial && millis() < 3000) { }
  const int values[] = {0, 64, 128, 191, 255};
  for (int i = 0; i < 5; i++) {
    int v = values[i];
    float duty = v * 100.0 / 255.0;
    float avgVolts = v * 5.0 / 255.0;
    Serial.print("analogWrite ");
    Serial.print(v);
    Serial.print(": duty ");
    Serial.print(duty, 1);
    Serial.print(" %, average ");
    Serial.print(avgVolts);
    Serial.println(" V");
  }
}

void loop() {
}
Output
analogWrite 0: duty 0.0 %, average 0.00 V
analogWrite 64: duty 25.1 %, average 1.25 V
analogWrite 128: duty 50.2 %, average 2.51 V
analogWrite 191: duty 74.9 %, average 3.75 V
analogWrite 255: duty 100.0 %, average 5.00 V

A multimeter on DC volts would show roughly these averages, because it also smooths the fast switching.

Step 1: Fade an LED

Wire an LED with a 470 Ω resistor to pin 9, as in Digital Output. Then use two for loops: one to brighten and one to dim.

C++
const int LED_PIN = 9;          // must be a ~ PWM pin

void setup() {
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  for (int b = 0; b <= 255; b++) {     // brighten
    analogWrite(LED_PIN, b);
    delay(5);
  }
  for (int b = 255; b >= 0; b--) {     // dim
    analogWrite(LED_PIN, b);
    delay(5);
  }
}

The LED should "breathe": fading up and down about once every 2.5 seconds. Each loop has 256 steps of 5 ms, so one fade takes 256 × 5 = 1280 ms, and a full breath takes about 2.56 seconds.

Step 2: Brightness from a knob

Now combine analog input and PWM. The pot gives 0 to 1023 and analogWrite wants 0 to 255. Since 1024 ÷ 256 = 4, dividing by 4 converts one range to the other. map() does the same and is easier to read.

PartPin or legConnects to
10 kΩ potentiometerOuter legs5V and GND
10 kΩ potentiometerMiddle legA0
LED + 470 Ω resistorResistor endD9
LEDCathodeGND
C++
const int POT_PIN = A0;
const int LED_PIN = 9;
int lastLevel = -1;

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  int raw = analogRead(POT_PIN);
  int level = map(raw, 0, 1023, 0, 255);
  analogWrite(LED_PIN, level);
  if (abs(level - lastLevel) > 2) {    // print only real changes
    Serial.print("brightness:");
    Serial.println(level);
    lastLevel = level;
  }
  delay(20);
}

Turning the knob slowly from one end to the other gives lines like:

Output
brightness:0
brightness:31
brightness:97
brightness:180
brightness:255

Your numbers depend on how you turn the knob. abs() gives the size of the change, ignoring its sign, so tiny wobbles of one or two counts are not printed.

Step 3: Make it look even

Turn the knob and watch closely. Most of the visible change happens near the bottom: the step from 10 to 40 looks huge, but 200 to 230 looks almost the same. Human eyes respond to light non-linearly: we notice changes in dim light much more than in bright light.

The fix is gamma correction: map a "brightness level" to a PWM value using a curve instead of a straight line. Squaring is a simple, good-enough curve:

C++
const int LED_PIN = 9;

int gammaPwm(int level) {            // level 0..10 -> PWM 0..255
  return (long)level * level * 255 / 100;
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(9600);
  while (!Serial && millis() < 3000) { }
  for (int level = 0; level <= 10; level++) {
    Serial.print(level);
    Serial.print(" -> ");
    Serial.println(gammaPwm(level));
  }
}

void loop() {
}
Output
0 -> 0
1 -> 2
2 -> 10
3 -> 22
4 -> 40
5 -> 63
6 -> 91
7 -> 124
8 -> 163
9 -> 206
10 -> 255

The PWM values are small at first and grow quickly at the top. When you step through these levels on a real LED, each step looks like about the same change in brightness. Commercial LED lamps and phone screens use the same idea with more precise curves.

PWM beyond LEDs

UseHow PWM helpsLesson
DC motor speedThe motor's inertia averages the pulsesRelay Modules & DC Motors
Servo positionThe pulse width tells the servo which angle to holdServo Motors & Buzzers
Buzzer tonesSwitching at an audio frequency makes a soundSame lesson

Common mistakes

  • Using a non-PWM pin. On pins without ~, analogWrite only gives fully on or off.
  • Values above 255. By default the range is 0 to 255. Use map() or divide by 4 from analogRead.
  • Driving a motor straight from a PWM pin. The pin gives a signal, not power. Use a driver.
  • Integer overflow in curves. On an UNO R3, level * level * 255 can exceed the 16-bit int limit for bigger levels; casting to long keeps it safe on every board.

Key points

  • PWM switches a pin on and off fast; the duty cycle sets the average power.
  • analogWrite(pin, 0..255) works on ~ pins: 3, 5, 6, 9, 10, 11 on the UNO.
  • Average voltage = duty cycle × 5 V; 128 gives about 2.5 V on average.
  • map(analogRead(A0), 0, 1023, 0, 255) turns a knob into a brightness.
  • Eyes see brightness non-linearly; a squared (gamma) curve makes fades look even.

Exercise

Make a "sunrise" lamp: over 10 steps, raise an LED on pin 9 from level 0 to level 10 using gammaPwm(), holding each step for 1 second, then stay fully on. Print step N: pwm P for each step. What PWM value is printed at step 5?

Show solution
C++
const int LED_PIN = 9;

int gammaPwm(int level) {
  return (long)level * level * 255 / 100;
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(9600);
  while (!Serial && millis() < 3000) { }
  for (int step = 0; step <= 10; step++) {
    int pwm = gammaPwm(step);
    analogWrite(LED_PIN, pwm);
    Serial.print("step ");
    Serial.print(step);
    Serial.print(": pwm ");
    Serial.println(pwm);
    delay(1000);
  }
}

void loop() {
  // stays fully on after the sunrise
}
Output
step 0: pwm 0
step 1: pwm 2
step 2: pwm 10
step 3: pwm 22
step 4: pwm 40
step 5: pwm 63
step 6: pwm 91
step 7: pwm 124
step 8: pwm 163
step 9: pwm 206
step 10: pwm 255

At step 5 the PWM value is 63: 5 × 5 × 255 = 6375, and 6375 ÷ 100 = 63 with the fraction dropped.

Quick Quiz

1. What does a duty cycle of 50% mean?

2. Which of these UNO pins can do PWM?

3. What value range does analogWrite() use by default?

4. Why use a gamma curve when fading LEDs?

Learn IoT & Arduino with a mentor

Free lessons get you started. Our IoT & Embedded Systems training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call