If you�ve played around with Arduino before, you�re familiar with how simple it is to generate a PWM signal using the analogWrite()
function�just specify the pin to use and the duty cycle, and you�re good to go.
But with the ESP32, it�s like playing a game on a slightly harder level. We get more controls (yay!), but we also have to manage them wisely (which is a little tricky). The ESP32 asks us to be specific about a few more things, such as the PWM frequency, PWM resolution, the channel to be used, and, of course, the duty cycle and pin number. Phew, that sounds like a lot, but don�t worry!
This guide will teach you everything you need to know about PWM on the ESP32, from core concepts to practical examples.
ESP32 PWM Pins
On the ESP32, PWM output is possible on all GPIO pins except for four input-only GPIO pins. The GPIOs highlighted below support PWM.
PWM on ESP32
The ESP32 has two PWM peripherals: the LED Control Peripheral (LEDC) and the Motor Control Pulse Width Modulator Peripheral (MCPWM).
The MCPWM peripheral is intended for motor control and includes additional features such as a dead zone and auto-braking. On the other hand, the LEDC peripheral is specifically designed for driving LEDs and includes features such as auto-dimming as well as more advanced features. It can, however, be used to generate PWM signals for a variety of other purposes.
In this tutorial, we will be focusing primarily on the LEDC peripheral.
PWM Frequency
The LEDC peripheral, like most PWM controllers, uses a timer to generate PWM signals.
Think of a timer as �ticking along,� counting up until it reaches a maximum number, at which point it resets itself to zero and the next counting cycle begins again. The time between these resets (i.e., how long it takes to reach the maximum value) represents the PWM Frequency and is measured in Hertz (Hz). For example, if we specify a frequency of 1 Hz, the timer will take 1 second to count from 0 to the maximum value before starting the next cycle. If we specify a frequency of 1000 Hz , the timer will only take 1 millisecond to count from 0 to the maximum value.
The ESP32 can generate a PWM signal with a frequency of up to 40 MHz.
PWM Resolution
So, what exactly is this �maximum� value? The �maximum� value is determined by the PWM Resolution. If the PWM resolution is �n� bits, the timer counts from 0 to 2n-1 before it resets. For example, if we configure the timer with a frequency of 1 Hz and a resolution of 8 bits, the timer will take 1 second to count from 0 to 255 (28). In the case of a frequency of 1 Hz and a resolution of 16 bits, the timer will still take 1 second, but it will count from 0 to 65,535 (216).
It is important to understand that when we have higher resolution, we essentially have more �timer increments� within the same specified period of time. What we thus have is more �granularity� in the timings.
The ESP32�s PWM resolution can be adjusted from 1 to 16 bits. This means that the duty cycle can be set at up to 65,536 (216) different levels. This gives you fine control over things like LEDs, enabling them to glow with subtle variations in brightness, or motors, allowing them to run at very precise speeds.
Duty Cycle
Next, we define the Duty Cycle of the PWM output. The duty cycle indicates how many timer ticks the PWM output will remain high before it goes low. This value is stored in the timer�s capture/compare register (CCR).
When the timer resets, the PWM output goes high. When the timer reaches the value stored in the capture/compare register, the PWM output goes low. The timer, however, continues counting. Once the timer reaches its maximum value, the PWM output goes high again, and the timer resets to start counting for the next period.
For example, imagine we wish to generate a PWM signal with a frequency of 1000 Hz, an 8-bit resolution, and a 75% duty cycle. Given the 8-bit resolution, the timer�s maximum value will be 255 (28-1). With a frequency of 1000 Hz, the timer will take 1 ms (0.001 s) to count from 0 to 255. The duty cycle of the PWM is set at 75%, meaning that the value 256 * 75% = 192 will be stored in the capture/compare register. In this case, when the timer resets, the PWM output will be set high. The PWM output will remain high until the counter reaches 192, at which point it will toggle to low. Once the timer hits 255, the PWM output will toggle back to high, and the timer will reset to commence counting for the next period.
PWM Channels
Now let us turn our attention to the concept of a channel. A channel represents a unique PWM output signal.
The ESP32 has 16 channels, which means it can generate 16 unique PWM waveforms. These channels are divided into two groups, each containing 8 channels: 8 high-speed channels and 8 low-speed channels.
The high-speed channels are implemented in hardware and are therefore able to provide automatic and glitch-free changes to the PWM duty cycle. Low-speed channels, on the other hand, lack these features and rely on software to change their duty cycle.
Within each group, there are 4 timers shared among 8 channels, which means that every two channels share the same timer. Since the timer determines the frequency, it�s important to understand that we cannot adjust the frequency of each channel independently within a pair. However, we can control the PWM duty cycle of each channel independently.
To summarize, an ESP32 has 16 PWM channels that can operate at eight distinct frequencies, and each of these channels can operate with a different duty cycle.
To generate a PWM signal on a specific pin, you �attach� that pin to a channel. This linkage tells the ESP32 to output the PWM waveform generated by the channel on the specified pin. Multiple pins can be attached to the same channel, which means they can all output the same PWM signal. Despite the fact that all GPIO pins support PWM output, the ESP32 has only 16 channels, so only 16 different PWM waveforms can be produced at the same time. This does not limit the number of pins that can output PWM signals, but it does limit the variety of signals that can be output simultaneously.
Practically speaking, if you have a set of LEDs that you wish to blink in perfect sync, you can set up a single channel with specific frequency and duty cycle, and then attach all the relevant pins (which are connected to the LEDs) to this channel. However, when working with servos, especially in situations like a robotic arm where each joint (servo) must be controlled independently, it becomes advantageous to assign different pins to different channels.
Choosing PWM Frequency and Resolution
The ESP32 can generate a PWM signal with a frequency of up to 40 MHz, and the PWM resolution can be adjusted from 1 to 16 bits. But this doesn�t mean you can set a frequency of 40 MHz and a resolution of 16 bits at the same time. This is due to the fact that the maximum PWM frequency and resolution are both bound by the clock source.
To illustrate this, consider a clock (whether it�s a CPU clock or a timer doesn�t matter) running at a frequency of 40 MHz. In this case, the maximum achievable PWM frequency is also 40 MHz. We cannot generate a PWM wave faster than our clock allows.
And what about the resolution? Well, resolution is really about how finely we can slice up one period of the PWM wave into different duty cycles. And here is the insight: slicing up the PWM wave requires a CPU clock running at PWM_freq * 2PWM_resolution. Why? Because to create those duty cycles, you need to be able to create those time slices.
From this, two important points become clear:
- PWM_freq * 2PWM_resolution cannot exceed the clock speed.
- PWM frequency and resolution are interdependent. The higher the PWM frequency, the lower the duty cycle resolution (and vice versa).
According to Espressif documentation, the LEDC low-speed timer clock source is an 80 MHz APB clock. As a general guideline, you should aim to keep PWM_freq * 2PWM_resolution below 80 MHz.
Additionally, the Espressif documentation includes examples to back this up:
- A PWM frequency of 5 kHz can have a maximum duty resolution of 13 bits, which results in a resolution of ~0.012%, or 213=8192 discrete levels.
- A PWM frequency of 20 MHz can have a maximum duty resolution of 2 bits, which results in a resolution of 25%, or 22=4 discrete levels.
- A PWM frequency of 40 MHz can have a duty resolution of just 1 bit, meaning the duty cycle remains fixed at 50% and cannot be adjusted.
If none of this makes sense to you, consider this: The Arduino Uno provides a ~490 Hz PWM waveform at 8 bits. This is more than enough to fade an LED smoothly. So, you can always start there (500 Hz frequency, 8-bit resolution) and then play around.
Generating PWM signal with the LEDC Library
Let�s get right to it! The ESP32 Arduino core includes the LEDC Library, which makes it easier to manage Pulse Width Modulation (PWM) on the ESP32. While the LEDC library was designed to control LEDs, it can also be used for other applications where PWM waveforms are useful, such as emitting �music� through piezo speakers and driving motors.
The steps below show how to use the LEDC library to generate a PWM signal with the ESP32 using Arduino IDE.
- Select a PWM Channel: There are 16 channels to choose from, numbered from 0 to 15.
- Determine the PWM Frequency: It can go up to 40 MHz, but for our LED fading example, a frequency of 500 Hz should suffice.
- Determine the PWM Resolution: It ranges from 1 to 16 bits. The number of discrete duty cycle levels is determined by 2resolution. For example, setting the resolution to 8 bits results in 256 discrete duty cycle levels [0�255]. On the other hand, a resolution of 16 bits provides 65,536 discrete duty cycle levels [0�65535].
- Choose the GPIO Pin(s): Choose one or more GPIO pins on the ESP32 to output the PWM signal.
- Configure the PWM Channel: Configure the selected PWM channel with the selected frequency and resolution using the ledcSetup(channel, freq, resolution) function.
- Attach the Pin(s) to the Channel: Attach the selected pin(s) to the selected channel using the ledcAttachPin(pin, channel) function.
- Set the Duty Cycle: Finally, set the actual duty cycle value for a given channel using the ledcWrite(channel, dutycycle) function.
Example 1 � Fading an LED
Here�s a quick example sketch that shows how to fade an LED�perfect for demonstrating the PWM generation on the ESP32.
Wiring
The wiring is quite simple. Take an LED and a 330 ? current-limiting resistor, and put them in the breadboard as shown in the figure below. Wire the longer leg of the LED, the anode, to pin GP18 via the 330 ? resistor, and wire the shorter leg to the ground pin of your ESP32.
Code
Copy the code below to your Arduino IDE.
const int PWM_CHANNEL = 0; // ESP32 has 16 channels which can generate 16 independent waveforms
const int PWM_FREQ = 500; // Recall that Arduino Uno is ~490 Hz. Official ESP32 example uses 5,000Hz
const int PWM_RESOLUTION = 8; // We'll use same resolution as Uno (8 bits, 0-255) but ESP32 can go up to 16 bits
// The max duty cycle value based on PWM resolution (will be 255 if resolution is 8 bits)
const int MAX_DUTY_CYCLE = (int)(pow(2, PWM_RESOLUTION) - 1);
const int LED_OUTPUT_PIN = 18;
const int DELAY_MS = 4; // delay between fade increments
void setup() {
// Sets up a channel (0-15), a PWM duty cycle frequency, and a PWM resolution (1 - 16 bits)
// ledcSetup(uint8_t channel, double freq, uint8_t resolution_bits);
ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
// ledcAttachPin(uint8_t pin, uint8_t channel);
ledcAttachPin(LED_OUTPUT_PIN, PWM_CHANNEL);
}
void loop() {
// fade up PWM on given channel
for(int dutyCycle = 0; dutyCycle <= MAX_DUTY_CYCLE; dutyCycle++){
ledcWrite(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
// fade down PWM on given channel
for(int dutyCycle = MAX_DUTY_CYCLE; dutyCycle >= 0; dutyCycle--){
ledcWrite(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
}
Testing the Example
Now, upload the code to your ESP32. You will observe the LED�s brightness change smoothly from completely off to fully lit and back again.
Code Explanation:
Several constants are defined at the beginning of the sketch to configure the PWM characteristics. First, the constant PWM_CHANNEL
is defined and set to 0. ESP32 has 16 channels (0 to 15), and each can generate independent waveforms.
Then, PWM_FREQ
is defined and set to 500. This is the frequency of our PWM signal. Recall that the Arduino Uno uses ~490 Hz. This is sufficient to fade an LED smoothly.
Next, PWM_RESOLUTION
is set to 8. This is the resolution (in bits) of the PWM signal. While we�re using 8 bits (same as Arduino Uno), ESP32 can go up to 16 bits.
const int PWM_CHANNEL = 0;
const int PWM_FREQ = 500;
const int PWM_RESOLUTION = 8;
After that, MAX_DUTY_CYCLE
is calculated using the formula 2PWM_RESOLUTION?1. This value determines the maximum achievable duty cycle based on the chosen resolution.
const int MAX_DUTY_CYCLE = (int)(pow(2, PWM_RESOLUTION) - 1);
Following this, LED_OUTPUT_PIN
is set to 18. This is the ESP32 GPIO pin to which the LED is connected.
And finally, DELAY_MS
is defined and set to 4. This is the delay (in milliseconds) between increments to control the speed of the LED�s fading.
const int LED_OUTPUT_PIN = 18;
const int DELAY_MS = 4;
During setup, the ledcSetup()
function is called to configure the PWM properties using the previously defined constants. This function takes three arguments: the PWM channel, the PWM frequency, and the PWM resolution.
ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
Next, the ledcAttachPin()
function is used to attach the GPIO pin to the PWM channel responsible for generating the PWM signal. In this case, the PWM signal generated by PWM_CHANNEL
, which corresponds to channel 0, will appear upon LED_OUTPUT_PIN
, which corresponds to GPIO 16.
ledcAttachPin(LED_OUTPUT_PIN, PWM_CHANNEL);
In the loop, the first for
loop iteratively increases the duty cycle from 0 to its maximum possible value (MAX_DUTY_CYCLE
). This gradually brightens the LED.
for(int dutyCycle = 0; dutyCycle <= MAX_DUTY_CYCLE; dutyCycle++){
ledcWrite(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
The second for
loop decrements the duty cycle from MAX_DUTY_CYCLE
to 0: This gradually dims the LED.
for(int dutyCycle = MAX_DUTY_CYCLE; dutyCycle >= 0; dutyCycle--){
ledcWrite(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
In both for loops, the ledcWrite()
function is used to set the brightness of the LED. This function accepts as arguments the channel that is generating the signal and the duty cycle.
ledcWrite(ledChannel, dutyCycle);
Example 2 � Same PWM Signal on Multiple GPIOs
You can get the same PWM signal on multiple GPIOs at the same time. To do so, you simply need to attach those GPIOs to the same channel.
Wiring
Add two more LEDs to your circuit in the same way you did the first. Connect them to GPIO 19 and 21.
The image below shows how to connect everything.
Code
Now, let�s modify the previous example to fade three LEDs using the same PWM signal from the same channel.
const int PWM_CHANNEL = 0; // ESP32 has 16 channels which can generate 16 independent waveforms
const int PWM_FREQ = 500; // Recall that Arduino Uno is ~490 Hz. Official ESP32 example uses 5,000Hz
const int PWM_RESOLUTION = 8; // We'll use same resolution as Uno (8 bits, 0-255) but ESP32 can go up to 16 bits
// The max duty cycle value based on PWM resolution (will be 255 if resolution is 8 bits)
const int MAX_DUTY_CYCLE = (int)(pow(2, PWM_RESOLUTION) - 1);
const int LED_1_OUTPUT_PIN = 18;
const int LED_2_OUTPUT_PIN = 19;
const int LED_3_OUTPUT_PIN = 21;
const int DELAY_MS = 4; // delay between fade increments
void setup() {
// Sets up a channel (0-15), a PWM duty cycle frequency, and a PWM resolution (1 - 16 bits)
// ledcSetup(uint8_t channel, double freq, uint8_t resolution_bits);
ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
// ledcAttachPin(uint8_t pin, uint8_t channel);
ledcAttachPin(LED_1_OUTPUT_PIN, PWM_CHANNEL);
ledcAttachPin(LED_2_OUTPUT_PIN, PWM_CHANNEL);
ledcAttachPin(LED_3_OUTPUT_PIN, PWM_CHANNEL);
}
void loop() {
// fade up PWM on given channel
for(int dutyCycle = 0; dutyCycle <= MAX_DUTY_CYCLE; dutyCycle++){
ledcWrite(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
// fade down PWM on given channel
for(int dutyCycle = MAX_DUTY_CYCLE; dutyCycle >= 0; dutyCycle--){
ledcWrite(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
}
Testing the Example
Now, upload the code to your ESP32. You will observe all three LEDs fade simultaneously, because all GPIOs are outputting the same PWM signal.
Code Explanation:
If you compare this sketch to the first one, you�ll notice that they are very similar, with just a few differences. Let�s take a look at these differences.
In the global area, three additional constants named LED_1_OUTPUT_PIN
, LED_2_OUTPUT_PIN
, and LED_3_OUTPUT_PIN
are defined and set to 18, 19, and 21 respectively. This indicates that we are dealing with three separate LEDs, each connected to its own GPIO pin on the ESP32.
const int LED_1_OUTPUT_PIN = 18;
const int LED_2_OUTPUT_PIN = 19;
const int LED_3_OUTPUT_PIN = 21;
Then, in the setup, the ledcAttachPin()
function is invoked three times instead of just once, as in the previous code. Each function call attaches a different GPIO pin (LED_1_OUTPUT_PIN
, LED_2_OUTPUT_PIN
, LED_3_OUTPUT_PIN
) with the same PWM channel (PWM_CHANNEL
), meaning the same PWM signal will be output to all three LEDs.
ledcAttachPin(LED_1_OUTPUT_PIN, PWM_CHANNEL);
ledcAttachPin(LED_2_OUTPUT_PIN, PWM_CHANNEL);
ledcAttachPin(LED_3_OUTPUT_PIN, PWM_CHANNEL);
Note that despite the addition of more LEDs, there is no change to the loop()
function. This is because the same PWM channel controls all LEDs.
Example 3 � Fading an LED using the Potentiometer
This example sketch shows you how to fade an LED using the potentiometer.
Wiring
Remove the two extra LEDs you added to your circuit and add a potentiometer. Attach one outer pin of the potentiometer to 3.3V, the opposite outer pin to GND, and its middle pin (wiper) to GPIO 34.
The image below shows how to connect everything.
Code
const int PWM_CHANNEL = 0; // ESP32 has 16 channels which can generate 16 independent waveforms
const int PWM_FREQ = 500; // Recall that Arduino Uno is ~490 Hz. Official ESP32 example uses 5,000Hz
const int PWM_RESOLUTION = 8; // We'll use same resolution as Uno (8 bits, 0-255) but ESP32 can go up to 16 bits
// The max duty cycle value based on PWM resolution (will be 255 if resolution is 8 bits)
const int MAX_DUTY_CYCLE = (int)(pow(2, PWM_RESOLUTION) - 1);
const int LED_OUTPUT_PIN = 18;
const int POT_PIN = 34;
const int DELAY_MS = 100; // delay between fade increments
void setup() {
// Sets up a channel (0-15), a PWM duty cycle frequency, and a PWM resolution (1 - 16 bits)
// ledcSetup(uint8_t channel, double freq, uint8_t resolution_bits);
ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
// ledcAttachPin(uint8_t pin, uint8_t channel);
ledcAttachPin(LED_OUTPUT_PIN, PWM_CHANNEL);
}
void loop() {
int dutyCycle = analogRead(POT_PIN);
dutyCycle = map(dutyCycle, 0, 4095, 0, MAX_DUTY_CYCLE);
ledcWrite(PWM_CHANNEL, dutyCycle);
delay(DELAY_MS);
}
Testing the Example
Now try turning the potentiometer all the way one way, then all the way the other. Watch the LED; this time, you�ll see the LED�s brightness change smoothly from completely off at one end of the potentiometer knob�s limit to fully lit at the other.
Code Explanation:
Again! There are only a few differences between this sketch and the first one. Let�s take a look at these differences.
An additional constant named POT_PIN
is defined in the global area. It is set to 34, indicating that the potentiometer is connected to GPIO 34 on the ESP32 and will be used to dynamically determine the duty cycle and, therefore, the brightness of the LED.
const int POT_PIN = 34;
Then, in the loop, instead of using for loops to gradually increase and decrease the LED�s brightness, a function analogRead(POT_PIN)
is called, which takes a raw reading from the potentiometer.
int dutyCycle = analogRead(POT_PIN);
The reading from the potentiometer, which ranges from 0 to 4095, is then mapped to a new range that spans from 0 to MAX_DUTY_CYCLE
using the map()
function. This mapping aligns the potentiometer values to the PWM signal�s allowed duty cycle values. This makes sure that the LED brightness can be changed across its whole range.
dutyCycle = map(dutyCycle, 0, 4095, 0, MAX_DUTY_CYCLE);
Finally, the ledcWrite()
function takes this mapped value and applies it directly to the PWM signal, adjusting the LED brightness in real-time based on the potentiometer position.
ledcWrite(PWM_CHANNEL, dutyCycle);