PWM was one of those concepts that seemed almost too simple when I first read about it — just turning a pin on and off quickly — until I actually used it to dim an LED smoothly, control a motor’s speed, and generate an analog-like output from a purely digital pin. It’s a deceptively powerful technique, and it shows up constantly in embedded systems. In this article, I’ll explain exactly how PWM works, how to configure it, and where it’s used in real designs.
What Is PWM?
Pulse Width Modulation is a technique for encoding an analog-like signal using a purely digital output pin. Instead of varying the voltage continuously (which a GPIO pin fundamentally can’t do — it’s either high or low), PWM rapidly switches the pin between high and low at a fixed frequency, varying the proportion of time spent high versus low. The “average” effect, once filtered by something with inertia (a motor, an LED combined with the human eye’s persistence of vision, an RC low-pass filter), behaves very much like a variable analog voltage.
graph LR
A[Digital GPIO Pin] -->|Switches HIGH/LOW rapidly| B[PWM Waveform]
B --> C[Perceived as Analog Voltage - by motor/LED/filter]
Duty Cycle
The key parameter in PWM is the duty cycle — the percentage of each period the signal spends HIGH.
Duty Cycle (%) = (Time HIGH / Total Period) × 100
- 0% duty cycle → always LOW → effectively “off”
- 50% duty cycle → equal HIGH/LOW time → roughly “half power”
- 100% duty cycle → always HIGH → effectively “full on”
gantt
dateFormat X
axisFormat %L
title PWM Waveforms at Different Duty Cycles (one period each, ms)
section 25% Duty Cycle
High :active, a1, 0, 25
Low :a2, 25, 100
section 50% Duty Cycle
High :active, b1, 0, 50
Low :b2, 50, 100
section 75% Duty Cycle
High :active, c1, 0, 75
Low :c2, 75, 100
PWM Frequency
The frequency determines how many complete on/off cycles happen per second. Choosing the right frequency depends heavily on the application:
- LED dimming: Needs to be fast enough (typically above ~100Hz–1kHz) that the human eye doesn’t perceive flickering — it just perceives an average brightness.
- DC motor control: Usually in the range of a few hundred Hz to tens of kHz; too low, and you’ll hear an audible whine from the motor windings; too high, and switching losses in the driver circuit increase.
- Servo motor control: A very specific standard — 50Hz (20ms period), with pulse width (not duty cycle percentage) between roughly 1ms and 2ms encoding the desired angle.
- Power supplies (buck/boost converters): Often tens of kHz to over 1MHz, chosen based on inductor/capacitor sizing and efficiency trade-offs.
How PWM Is Generated in Hardware
Microcontroller PWM is almost always generated by a hardware timer/counter peripheral, not bit-banged in software (though bit-banging is technically possible for very low frequencies). A timer counts up (or up/down) from 0 to some maximum value (the “ARR” — Auto-Reload Register, defining the period), and a compare register defines the point within that count at which the output pin toggles.
graph TD
CLK[Timer Clock] --> CNT[Counter: 0 to ARR]
CNT -->|CNT < CCR| HIGH[Output = HIGH]
CNT -->|CNT >= CCR| LOW[Output = LOW]
CNT -->|CNT = ARR, reset to 0| CNT
- ARR (Auto-Reload Register): sets the period (and therefore frequency) of the PWM signal.
- CCR (Capture/Compare Register): sets the point at which the output toggles, and therefore the duty cycle.
PWM Frequency = Timer Clock / (ARR + 1)
Duty Cycle (%) = (CCR / (ARR + 1)) × 100
Configuring PWM on an STM32 (HAL Example)
#include "stm32f4xx_hal.h"
TIM_HandleTypeDef htim3;
void PWM_Init(void) {
TIM_OC_InitTypeDef sConfigOC = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 84 - 1; // 84 MHz / 84 = 1 MHz timer clock
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 1000 - 1; // 1 MHz / 1000 = 1 kHz PWM frequency
HAL_TIM_PWM_Init(&htim3);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500; // 500/1000 = 50% duty cycle
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}
void PWM_SetDutyCycle(uint8_t percent) {
uint32_t pulse = (percent * 1000) / 100;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse);
}
Configuring PWM on Arduino
const int ledPin = 9; // Must be a PWM-capable pin (marked with ~)
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
for (int duty = 0; duty <= 255; duty++) {
analogWrite(ledPin, duty); // 0-255 maps to 0-100% duty cycle
delay(10);
}
}
Arduino’s analogWrite() abstracts away the underlying timer configuration entirely, which is convenient for simple projects but hides frequency control — on most AVR-based Arduinos, the default PWM frequency is around 490Hz or 980Hz depending on the pin.
Configuring PWM on ESP32 (LEDC Peripheral)
#define PWM_PIN 18
#define PWM_CHANNEL 0
#define PWM_FREQ 5000
#define PWM_RESOLUTION 8 // 8-bit: 0-255
void setup() {
ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
ledcAttachPin(PWM_PIN, PWM_CHANNEL);
}
void loop() {
for (int duty = 0; duty <= 255; duty++) {
ledcWrite(PWM_CHANNEL, duty);
delay(5);
}
}
Motor Control with PWM
Controlling a DC motor’s speed via PWM typically involves a motor driver IC (like the L298N or DRV8833) between the microcontroller and motor, since GPIO pins can’t source enough current to drive a motor directly.
graph LR
MCU[Microcontroller PWM Pin] --> Driver[Motor Driver IC]
Driver -->|High Current| Motor[DC Motor]
MCU -->|Direction Pins| Driver
#define MOTOR_PWM_PIN 5
#define MOTOR_DIR_PIN 6
void set_motor_speed(int speed_percent, bool forward) {
digitalWrite(MOTOR_DIR_PIN, forward ? HIGH : LOW);
int duty = map(speed_percent, 0, 100, 0, 255);
analogWrite(MOTOR_PWM_PIN, duty);
}
Servo Control with PWM
Servos interpret pulse width directly, not duty cycle percentage, at a fixed 50Hz frequency:
#include <Servo.h>
Servo myServo;
void setup() {
myServo.attach(9); // Internally generates ~50Hz, 1-2ms pulses
}
void loop() {
myServo.write(0); // ~1ms pulse -> 0 degrees
delay(1000);
myServo.write(90); // ~1.5ms pulse -> 90 degrees (center)
delay(1000);
myServo.write(180); // ~2ms pulse -> 180 degrees
delay(1000);
}
Generating an Analog Voltage from PWM (Filtering)
For applications genuinely needing an analog voltage output (not just driving something with natural inertia like a motor or LED), a simple RC low-pass filter on the PWM pin smooths the pulses into a roughly steady DC voltage proportional to the duty cycle:
graph LR
PWM[PWM Output Pin] --> R[Resistor]
R --> Node((Output Node))
Node --> C[Capacitor to GND]
Node --> Vout[Filtered ~ DC Voltage]
V_out ≈ Duty Cycle × V_high
Cutoff frequency f_c = 1 / (2π × R × C) -- should be well below PWM frequency
PWM Resolution
Resolution here refers to how many distinct duty cycle steps are available, determined by the timer’s counting range. An 8-bit PWM (0–255) gives 256 possible brightness/speed levels; a 16-bit PWM gives 65,536 levels, offering much finer control — important in applications like precise LED dimming where coarse steps can be visually noticeable at low brightness.
Resolution (bits) = log2(ARR + 1)
There’s a direct trade-off between resolution and maximum achievable frequency for a given timer clock speed: more steps per period means a longer period (lower frequency) for the same underlying clock, unless you increase the timer’s input clock frequency.
Multi-Channel PWM
Most timer peripherals support several PWM channels simultaneously (e.g., STM32 timers commonly offer 4 channels each), useful for controlling multiple LEDs (like an RGB LED, needing 3 channels) or multiple motors from a single timer:
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // Red
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); // Green
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3); // Blue
void set_rgb(uint8_t r, uint8_t g, uint8_t b) {
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, r * 4); // scale 8-bit to 10-bit ARR
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, g * 4);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, b * 4);
}
Real-World Applications
- LED dimming and RGB color mixing: Smoothly fading brightness or blending colors across three PWM-driven channels.
- DC motor speed control: Fans, robotics wheels, pumps — anywhere variable rotational speed is needed.
- Servo positioning: Robotic arms, camera gimbals, RC vehicle steering.
- Power supply regulation: Buck/boost converter switching control loops rely on PWM at their core.
- Audio generation: Simple tone/beep generation, and even crude audio playback via PWM combined with filtering.
- Haptic feedback: Vibration motor intensity control in wearables and controllers.
IoT/Practical Example: Smart Fan Speed Controller
#define FAN_PWM_PIN 25
#define TEMP_SENSOR_PIN 34
void setup() {
ledcSetup(0, 25000, 8); // 25kHz - above audible range for fans
ledcAttachPin(FAN_PWM_PIN, 0);
}
void loop() {
int raw = analogRead(TEMP_SENSOR_PIN);
float tempC = convert_to_celsius(raw);
int duty = map((int)tempC, 20, 60, 50, 255); // Ramp fan speed with temp
duty = constrain(duty, 50, 255);
ledcWrite(0, duty);
delay(2000);
}
Reliability and Practical Considerations
- Motor PWM frequency and audible noise: Frequencies below roughly 20kHz can produce an audible whine from motor coils; going above 20kHz avoids this but increases switching losses in the driver.
- EMI (electromagnetic interference): Fast PWM edges, especially at higher power, can generate EMI that interferes with nearby analog circuits (including your own ADC readings!) — physical separation and filtering help.
- Soft-start ramps: Jumping straight to a high duty cycle on a motor can cause a current spike; ramping duty cycle up gradually reduces mechanical and electrical stress.
- Dead-time in H-bridge motor drivers: In more advanced motor control (complementary PWM channels), a small “dead time” gap is needed between switching one transistor off and the other on, to avoid shoot-through current — most advanced timers (like STM32’s TIM1/TIM8) have this built in as a configurable parameter.
Frequently Asked Questions
Is PWM the same as a true analog output (DAC)? No — PWM is still fundamentally a digital signal switching between two levels; it only approximates an analog effect once averaged by something with inertia (a motor, LED+eye, or an RC filter). A true DAC (Digital-to-Analog Converter) produces an actual continuously variable voltage directly.
Why does my LED flicker at low PWM frequencies? Human persistence of vision generally can’t detect flicker above roughly 100Hz–200Hz for most people, though some can perceive flicker up to higher rates, especially in peripheral vision. Using a PWM frequency of 500Hz–1kHz or higher for LED dimming avoids this entirely.
Can I generate PWM without a hardware timer? Yes, by manually toggling a GPIO pin with precise software delays (“bit-banging”), but this consumes CPU time continuously and produces frequency/duty cycle inaccuracy from interrupt jitter — hardware timer-based PWM is almost always preferred when available.
What duty cycle is “off” and what’s “full on”? 0% duty cycle means the pin stays low the entire period (effectively off for most loads); 100% duty cycle means the pin stays high the entire period (effectively fully on) — though for active-low configurations, this can be inverted.
Summary
PWM is a simple but remarkably versatile technique: by rapidly switching a digital pin and varying the proportion of time it spends high (duty cycle), you can approximate analog control over brightness, motor speed, servo position, and even voltage output after filtering. It’s generated efficiently in hardware using timer peripherals with auto-reload and compare registers, giving precise, low-CPU-overhead control that scales from simple LED dimming to sophisticated motor control and power regulation. Once you understand the relationship between timer clock, ARR (period), and CCR (duty cycle), PWM becomes one of the most flexible tools in the embedded toolbox.
References and Further Reading
- STMicroelectronics STM32 Timer/PWM Reference Manual — st.com
- Arduino analogWrite() Reference — docs.arduino.cc/language-reference/en/functions/analog-io/analogwrite
- Espressif ESP32 LEDC (LED Control) Peripheral Documentation — docs.espressif.com
- Microchip/Atmel AVR Timer/PWM Application Notes — microchip.com
- Texas Instruments “Fundamentals of PWM” Application Report — ti.com
- RC Servo Control Standard Reference (Pulse Timing) — servocity.com