PWM & analogWrite
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:
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() {
}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.
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.
| Part | Pin or leg | Connects to |
|---|---|---|
| 10 kΩ potentiometer | Outer legs | 5V and GND |
| 10 kΩ potentiometer | Middle leg | A0 |
| LED + 470 Ω resistor | Resistor end | D9 |
| LED | Cathode | GND |
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:
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:
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() {
}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
| Use | How PWM helps | Lesson |
|---|---|---|
| DC motor speed | The motor's inertia averages the pulses | Relay Modules & DC Motors |
| Servo position | The pulse width tells the servo which angle to hold | Servo Motors & Buzzers |
| Buzzer tones | Switching at an audio frequency makes a sound | Same lesson |
Common mistakes
- Using a non-PWM pin. On pins without ~,
analogWriteonly gives fully on or off. - Values above 255. By default the range is 0 to 255. Use
map()or divide by 4 fromanalogRead. - 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 * 255can exceed the 16-bitintlimit for bigger levels; casting tolongkeeps 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
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
}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?
Duty cycle is the fraction of time the signal is HIGH.
2. Which of these UNO pins can do PWM?
The PWM pins are D3, D5, D6, D9, D10 and D11, marked with ~.
3. What value range does analogWrite() use by default?
The default PWM resolution is 8 bits, so 0 to 255.
4. Why use a gamma curve when fading LEDs?
Eyes are much more sensitive to changes in dim light, so the PWM steps must grow towards the top.
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
