In this ESP32 PWM tutorial with ESP-IDF, we will learn how to generate PWM or pulse width modulation module with ESP32 using ESP-IDF. We will look at how to produce the PWM signals with LEDC hardware of ESP32 and attach those signals to any of the GPIO pins of the ESP board. For demonstration, we will control the brightness of an LED by varying the duty cycle of output signal through a potentiometer. With the help of a potentiometer, we will map ADC value into the duty cycle.
Before we move ahead, make sure you have the latest version of VS Code installed on your system with the ESP-IDF extension configured.
PWM Introduction
Pulse width modulation or PWM is a signal which we usually obtain from analog or digital ICs such as ESP32 boards. It is a square waveform, which provides the output signal, that can be either high or low at a time. If using a 3.3V power source, the PWM signals would be either high (3.3V) or low (0V). The signal’s “on time” is the amount of time it is high, while its “off-time” is the amount of time it is low. We need to be familiar with the following two terminologies, which are closely related to the digital signal, in order to better comprehend the ideas of PWM in our ESP32 board:
- Duty Cycle
- Frequency
Duty Cycle
The duty cycle is the percentage of time for which the PWM signal stays HIGH or is ‘on time.’ For instance, a signal that is always OFF has a 0% duty cycle, whereas a signal that is constantly ON has a 100% duty cycle. The unique aspect of this is that the user may regulate the duty cycle to manage the “on time.” The formula is used to find the duty cycle:
Duty Cycle = ON Time of Signal / Time Period of Signal
Frequency
The frequency of a PWM signal is the number of cycles per second. It also defines how quickly a PWM signal completes one cycle which is known as the time period. Therefore, the ON and OFF times of a PWM signal are added to determine the signal’s Period. For instance, if a signal has a 20ms time period, then it will have a frequency of 50Hz. The following formulae show the relationships between frequency and time period:
Frequency = 1/Time Period
Time Period = ON time + OFF time
ESP32 PWM Pins
In ESP32, PWM is supported through all output pins only. The ESP32 LED PWM controller, therefore, possesses 16 separate channels that can be configured to generate PWM signals.
PWM Supported Pins | All output pins: GPIO0-GPIO5 GPIO12-GPIO33 |
If you are using Arduino IDE instead of ESP-IDF, you can refer to this article:
ESP32 PWM ESP-IDF APIs
Before we move ahead, let us discuss some important functions that are required to access the PWM channels of ESP32.
ESP-IDF provides driver/ledc.h library that is required for PWM to control LEDs. We will be required to set the timer configuration, channel configuration and change the PWM signal. Let’s see how it will be accomplished.
The first step is to include the header file:
#include "driver/ledc.h"
This library is used to control the brightness of LEDs as well as produce PWM signals. The 16 LEDC channels are divided into two groups of 8 channels that give rise to separate waveforms. Out of the two groups of LEDC channels, one works in high speed and the other works in low-speed mode. Moreover, both groups of channels are can work on contrary clock sources as well.
Now let us see how to set up the LEDC channel in either of the two modes (high or low speed). We will start with the timer configuration.
ESP32 Timer Configuration
In this step, we configure the timer by defining the PWM signal’s duty cycle resolution, frequency, speed mode, timer number, and source clock. To set the timer, we will use the ledc_timer_config() function. This function takes in the ledc_timer_config_t data structure. Inside, this data structure, we will define the following parameters:
- Speed mode (ledc_mode_t)
- PWM signal frequency
- Resolution of duty cycle
- Timer number (ledc_timer_t)
- Source Clock (ledc_clk_cfg_t)
Note: The duty cycle resolution and source clock are related to the frequency of the PWM signal. The duty cycle resolution has an inverse relationship with the PWM frequency. This means that the higher the PWM frequency, the lower is the duty cycle resolution and vice versa. Similarly, the source clock frequency has a direct relationship with the PWM signal frequency. This means that the higher the source clock frequency, the higher the maximum PWM frequency can be set and vice versa.
In the following function ledc_timer_config(), we are configuring LEDC timer with the specified timer/frequency(Hz)/duty_resolution. It takes in a parameter timer_conf which is a pointer of the LEDC timer configure structure.
ledc_timer_config(const ledc_timer_config_t *timer_conf)
The following table demonstrates the three different clock sources that can be configured, with their respective clock frequency and speed modes available.
Clock Type | Clock Frequency | Speed Mode |
---|---|---|
APB_CLK | 80MHz | High or Low |
REF_TICK | 1MHz | High or Low |
RTC8M_CLK | 8MHz | Low |
The REF_TICK clock is capable of dynamic frequency scaling whereas the RTC8M_CLK clock is capable of both dynamic frequency scaling and light sleep.
ESP32 PWM Channel Configuration
Next, to set the channel we will use the function ledc_channel_config(). This function takes in the ledc_channel_config_t structure that defines the channel configuration parameters inside it. This will enable the PWM signal to be generated at the specified GPIO along with the frequency and duty cycle resolution already set in the timer configuration. If you want to stop the PWM signal generation, call the ledc_stop() function.
In the following function ledc_channel_config(), we are configuring LEDC Channel with the specified channel/output gpio_num/interrupt/source timer/frequency(Hz)/LEDC duty resolution. It takes in a parameter timer_conf which is a pointer of the LEDC timer configure structure.
ledc_channel_config(const ledc_channel_config_t *ledc_conf)
Set ESP32 PWM Duty Cycle
Now let us learn how to change the PWM signal. In the case of LEDs, changing the PWM signal means varying the duty cycle so the brightness of the LED changes accordingly. There are several different ways of varying the PWM signal including software and hardware. We will focus on the APIs that we will use in our project.
To set the duty cycle of the PWM signal using Software we will use the function ledc_set_duty(). It takes in three parameters. The first parameter is the speed mode. The second parameter is the LEDC channel. The third parameter is the LEDC duty that you want to set. The range of this duty is [0, (2**duty_resolution) – 1]. This means that if the selected duty resolution is 10, then the duty cycle values can range from 0 to 1023. This provides a resolution of approximately 0.1%.
ledc_set_duty(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t duty)
To initiate the LEDC duty, we will use the function, ledc_update_duty(). It is used to update the LEDC channel parameters. It takes in two parameters. The first parameter is the speed mode and the second parameter is the LEDC channel.
ledc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel)
Note: After calling ledc_set_duty(), it is necessary to call the ledc_update_duty() function as it is responsible for activating the LEDC duty changes that were set.
Moreover, the ledc_get_duty() function gives the LEDC duty value that is currently set. It also takes in two parameters which are the speed mode and the LEDC channel respectively. It will return the duty at the present PWM cycle.
ledc_get_duty(ledc_mode_t speed_mode, ledc_channel_t channel)
There are several more ways to change the PWM. Refer to the official Espressif ESP32 API for LED Control for more details.
PWM Control LED Brightness with ESP32 ESP-IDF
Now let us demonstrate a project in VS code with ESP-IDF extension related to ESP32 PWM signal generation. We will set up a LED brightness control project, where a potentiometer connected with ESP32 will be used to vary the brightness of the LED. We will connect the potentiometer with an ADC pin of ESP32. When the potentiometer knob is rotated, the varying ADC values will be used to set the duty cycle of the LED, thus changing the brightness.
ADC means analog to digital conversion where an analog signal is converted to a digital value with a resolution. The ESP32 has two 12-bit ADCs (ADC 1 & ADC 2) and supports a maximum of 18 analog channels. Analog values in ESP32 are read using variable voltages between 0-3.3V. The resulting voltage is then given a value between 0 and 4095 for 12 bit resolution. Accordingly, a value of 0 equals to 0V, while the maximum value 4095 equals to 3.3V. Any intermediate related values will likewise be assigned in the appropriate manner. If using ADC with 10 bits resolution, then the ADC values will range from 0-1023.
PWM is pulse-width modulation which we will use to fade the LED. Although it is not a genuine digital-to-analog conversion (DAC), the output power is decreased by the duty cycle (change in on-off ratio) when a high-frequency digital signal is generated. This results, in the LED fading.
ESP32 PWM Control Brightness of LED Circuit
The following components are required for our ESP32 PWM project:
- ESP32 board
- 5mm LED
- 220 ohm current limiting resistor
- Breadboard
- Potentiometer
- Connecting Wire
The diagram below shows how to setup the circuit for this project.
The potentiometer is powered by 3.3V from ESP32. Its center terminal is connected to GPIO32 which is ADC1_CH4. The last terminal is connected with the GND pin of ESP32.
We have used GPIO27 to connect with the anode pin of the LED through a 220 ohm current limiting resistor. The cathode pin of the LED is connected with the GND pin of ESP32.
Feel free to use any other appropriate ESP32 PWM pin to connect with the LED and ESP32 analog pin to connect with the potentiometer.
This is how our circuit looks like after connecting all the components.
ESP32 Control LED Brightness (PWM) with ESP-IDF
Open your VS Code and create a new ESP-IDF project. Now head over to the main.c file. We will define the functions and the program code here.
EPS32 Control LED Brightness (PWM) Code
We will use this script to control the brightness of the LED connected at GPIO27 through an average of the raw ADC values acquired from ADC1 Channel 4.
#include <stdio.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/adc.h"
#include "driver/ledc.h"
#define SAMPLE_CNT 32
static const adc1_channel_t adc_channel = ADC_CHANNEL_4;
#define LEDC_GPIO 27
static ledc_channel_config_t ledc_channel;
static void init_hw(void)
{
adc1_config_width(ADC_WIDTH_BIT_10);
adc1_config_channel_atten(adc_channel, ADC_ATTEN_DB_11);
ledc_timer_config_t ledc_timer = {
.duty_resolution = LEDC_TIMER_10_BIT,
.freq_hz = 1000,
.speed_mode = LEDC_HIGH_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&ledc_timer);
ledc_channel.channel = LEDC_CHANNEL_0;
ledc_channel.duty = 0;
ledc_channel.gpio_num = LEDC_GPIO;
ledc_channel.speed_mode = LEDC_HIGH_SPEED_MODE;
ledc_channel.hpoint = 0;
ledc_channel.timer_sel = LEDC_TIMER_0;
ledc_channel_config(&ledc_channel);
}
void app_main()
{
init_hw();
while (1)
{
uint32_t adc_val = 0;
for (int i = 0; i < SAMPLE_CNT; ++i)
{
adc_val += adc1_get_raw(adc_channel);
}
adc_val /= SAMPLE_CNT;
ledc_set_duty(ledc_channel.speed_mode, ledc_channel.channel, adc_val);
ledc_update_duty(ledc_channel.speed_mode, ledc_channel.channel);
vTaskDelay(500 / portTICK_RATE_MS);
}
}
How the Code Works?
Firstly, we will start by including the necessary libraries for this project. This includes the ledc driver, ADC driver, ADC calibration and FreeRTOS libraries. The driver/adc.h and esp_adc_cal.h libraries are required for the ADC driver and ADC calibration respectively. The driver/ledc.h library is required for PWM to control the LED.
#include <stdio.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/adc.h"
#include "driver/ledc.h"
Here we are defining a variable that is the sample count. It is set to 32. This will be used for better approximating the ADC value. We will be reading the ADC value 32 times, average it and set it as the duty cycle in every loop.
#define SAMPLE_CNT 32
We are using ADC1 Channel 4 to connect our potentiometer.
static const adc1_channel_t adc_channel = ADC_CHANNEL_4;
We have connected the LED at GPIO27. Therefore, we will define a variable called ‘LEDC_GPIO’ that will hold the GPIO pin 27. Moreover, we will define the LED control channel, ledc_channel, and the GPIO pin to use with
it, as follows:
#define LEDC_GPIO 27
static ledc_channel_config_t ledc_channel;
Initialize Hardware
The init_hw() function, is responsible for initializing the ADC and the LED control channel.
For initializing ADC, first we have set the ADC bit width for ADC1 at 10 bit width using the function adc1_config_width(). This means that the ADC values will vary between 0-1023. Then, we will configure the attenuation parameter of ADC1 channel 4 to 11db. This sets the ADC input voltage range to approximately 150- 2450mV.
For initializing the LED control channel, we will configure the timer first. We are using LEDC_TIMER_0 with 1000Hz frequency. For the PWM duty cycle. we have also set its resolution to 10 bits. High speed mode is selected. After configuring the timer, we will configure the LED control channel. We are using LEDC_CHANNEL_0 on our LEDC_GPIO which we previously defined as GPIO27 (connected with the LED). You can avail up to 8 channels as they are available. This LEDC_GPIO which we have configured uses the LEDC_TIMER_0 as previously set up.
static void init_hw(void)
{
adc1_config_width(ADC_WIDTH_BIT_10);
adc1_config_channel_atten(adc_channel, ADC_ATTEN_DB_11);
ledc_timer_config_t ledc_timer = {
.duty_resolution = LEDC_TIMER_10_BIT,
.freq_hz = 1000,
.speed_mode = LEDC_HIGH_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&ledc_timer);
ledc_channel.channel = LEDC_CHANNEL_0;
ledc_channel.duty = 0;
ledc_channel.gpio_num = LEDC_GPIO;
ledc_channel.speed_mode = LEDC_HIGH_SPEED_MODE;
ledc_channel.hpoint = 0;
ledc_channel.timer_sel = LEDC_TIMER_0;
ledc_channel_config(&ledc_channel);
}
Control LED Brightness
Inside the loop() function, we will acquire the raw ADC value using ad1_get_raw() function and specify the ADC channel number as a parameter inside it. This gets saved in the integer variable called ‘adc_value’. The value is read 32 times and then its average value gets saved in the variable ‘adc_value’. This is done to achieve a good approximated value.
Next, to set the duty cycle of the LED, we use the function ledc_set_duty() and specify the speed mode, ledc channel, and the adc_val as parameters inside it. Here the duty cycle is being set to the value held in the variable ‘adc_value’.Then to activate the duty cycle value we use the function, ledc_update_duty() and specify the speed mode and the ledc channel inside it. Moreover, we add a delay of 500ms by using the vTaskDekay() function before it loops again.
void app_main()
{
init_hw();
while (1)
{
uint32_t adc_val = 0;
for (int i = 0; i < SAMPLE_CNT; ++i)
{
adc_val += adc1_get_raw(adc_channel);
}
adc_val /= SAMPLE_CNT;
ledc_set_duty(ledc_channel.speed_mode, ledc_channel.channel, adc_val);
ledc_update_duty(ledc_channel.speed_mode, ledc_channel.channel);
vTaskDelay(500 / portTICK_RATE_MS);
}
}
Compiling the Sketch
To flash your chip, type the following command in the serial terminal. Remember to replace the COM port with the one through which your board is connected.
idf.py -p COMX flash monitor
After the code flashes successfully, rotate the potentiometer knob to vary the duty cycle of the PWM signal fed to GPIO27. As the ADC readings increase from 0-1023, the brightness of the LED increases. Similarly, rotating the knob in the opposite direction causes the ADC readings to decrease from 1023-0, hence the LED fades.
Video demo:
You may also like to read:
- ESP32 GPIO with ESP-IDF with LED Blinking example
- ESP32 Push Button with ESP-IDF (Digital Input)
- ESP32 ADC with ESP-IDF Measure Analog Inputs
We are a team of experienced Embedded Software Developers with skills in developing Embedded Linux, RTOS, and IoT products from scratch to deployment with a demonstrated history of working in the embedded software industry. Contact us for your projects: admin@esp32tutorials.com