ESP32 PWM using LEDC peripheral (ESP-IDF)

Introduction

This article will teach you how to produce PWM signals using the ESP32 LEDC peripheral. You will become familiar with the LEDC capabilities and APIs before creating a project in which the ESP32 controls the RGB LED’s colour. You may instruct the ESP32 to display any colour you like by adjusting the intensity of the red, green, and blue LEDs.

ESP32 LEDC Peripheral

Channels

The ESP32 LEDC peripheral is capable of producing PWM signals. It has 16 channels which are divided in two groups, each group has 8 channels. One group operates in high speed mode and the other group works in low speed mode. Depending on your application, you can choose one mode or the other.

High and Low speed mode

  • In high speed mode, the PWM duty cycle is updated automatically using hardware without CPU involvement. This is useful in some applications, for example, when changing LED intensity gradually. When operating in high speed mode, you just need to do initial setup.
  • In low speed mode, PWM duty cycle is modified by using software, i.e. using CPU resources. You explicitly call LEDC APIs to update its duty cycle.

LEDC parameters

There are several parameters that you need to consider when generating a PWM signal using LEDC peripheral:

  • Channel: which channel are you using. As discussed above, there are 16 channels, from 0 to 15.
  • GPIO pin number: which IO pin is used to drive output signal. You can choose any available IO pins which is quite flexible
  • PWM Frequency: this is the frequency of the PWM signal which is dependant on the clock source.
  • Clock source: you can configure LEDC to use one of the following clock sources: ABP_CLK (80 MHz), REF_TICK (1 MHz), RTC8M_CLK (8 MHz).
  • Duty cycle: this is the parameter that determine how long the signal is high during an interval. A duty cycle of 50% means it is high half of the interval and low during the other half. A duty cycle of 10% means the signal is high 10% of a PWM interval.
  • Duty cycle resolution: Each interval is divided in 2^(duty_resolution) - 1 equal spaces. The duty cycle resolution determines the minimum on time that can be set during 1 interval. For example, if duty cycle resolution is 13 bit, the total spaces in each interval is 8192 - 1. To achieve a duty cycle of 50%, you need to set duty value to 50% * (8192 - 1) = 4095.
  • Timer: LEDC has 3 timer modules (LEDC_TIMER_0, LEDC_TIMER_1 and LEDC_TIMER_2) that you can choose.

ESP32 LEDC APIs

Let’s take a look at the LEDC APIs in ESP-IDF in this section.

Configure LEDC Timer

To configure a LEDC timer, you use the API ledc_timer_config()

esp_err_t ledc_timer_config(const ledc_timer_config_t *timer_conf);

This function takes an argument which is a pointer to a configuration structure variable of type ledc_timer_config_t. This structure determines which mode to use (high or low speed), which timer to use, duty resolution, PWM frequency and clock source). The following example configures a LEDC_TIMER_0 in low speed mode, 13 bit duty resolution and output frequency of 1 kHz.

ledc_timer_config_t ledc_timer = {
    .speed_mode       = LEDC_LOW_SPEED_MODE,
    .timer_num        = LEDC_TIMER_0,
    .duty_resolution  = LEDC_TIMER_13_BIT,
    .freq_hz          = 1000,
    .clk_cfg          = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));

Configure LEDC channel

To configure a LEDC channel, use the API ledc_channel_config()

esp_err_t ledc_channel_config(const ledc_channel_config_t *ledc_conf);

This function accepts a pointer to a configuration structure of type ledc_channel_config_t. You can specify which channel to use, which IO pin to drive output signal, high or low speed, whether to use interrupt. For example, to use channel 0 and IO pin 32 in low speed mode:

ledc_channel_config_t ledc_channel = {
    .speed_mode     = LEDC_LOW_SPEED_MODE,
    .channel        = LEDC_CHANNEL_0,
    .timer_sel      = LEDC_TIMER_0,
    .intr_type      = LEDC_INTR_DISABLE,
    .gpio_num       = 32,
    .duty           = 0,
    .hpoint         = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));

Set and update duty cycle

To set and update PWM duty cycle, you use APIs ledc_set_duty() and ledc_update_duty(), respectively

esp_err_t ledc_set_duty(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t duty);
esp_err_t ledc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel);

Full API reference can be found in Espressif documentation.

ESP32 PWM Project Example

Now you have learnt about the LEDC peripheral and its APIs, let’s build an application to control the color of a RGB LED. Our objective is to output any color by specifying the RGB color code.

Hardware used

QTYComponent NameBuy on amazon.com
1ESP32 DevKit CAmazon
1RGB LEDAmazon
1Resistor KitAmazon
1BreadboardAmazon
1Jumper Wire KitAmazon

Affiliate Disclosure: When you click on links in this section and make a purchase, this may result in this site earning a commission at no extra cost to you.

Schematic

You will connect 3 IO pins of ESP32 development kit to control 3 pins of RGB LED following this table:

ESP32 IO PinRGB Pin
32Red
33Green
25Blue
GNDGround

Code

#include <stdio.h>
#include "driver/ledc.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

typedef struct {
    uint8_t r;
    uint8_t g;
    uint8_t b;
} color_t;

#define LED_RED_PIN         33
#define LED_GREEN_PIN       32
#define LED_BLUE_PIN        25

ledc_timer_config_t ledc_timer = {
    .speed_mode = LEDC_LOW_SPEED_MODE,
    .timer_num  = LEDC_TIMER_0,
    .duty_resolution = LEDC_TIMER_13_BIT,
    .freq_hz = 1000,
    .clk_cfg = LEDC_AUTO_CLK
};

ledc_channel_config_t ledc_channel[3];

static void init(void)
{
    ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));

    ledc_channel[0].channel = LEDC_CHANNEL_0;
    ledc_channel[0].gpio_num = LED_RED_PIN;

    ledc_channel[1].channel = LEDC_CHANNEL_1;
    ledc_channel[1].gpio_num = LED_GREEN_PIN;
    
    ledc_channel[2].channel = LEDC_CHANNEL_2;
    ledc_channel[2].gpio_num = LED_BLUE_PIN;

    for (int i = 0; i < 3; i++)
    {   
        ledc_channel[i].speed_mode = LEDC_LOW_SPEED_MODE;
        ledc_channel[i].timer_sel = LEDC_TIMER_0;
        ledc_channel[i].intr_type = LEDC_INTR_DISABLE;
        ledc_channel[i].duty = 0;
        ledc_channel[i].hpoint = 0;
        
        ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel[i]));
    }
}

static void setColor(color_t color)
{
    uint32_t red_duty = 8191 * color.r / 255;
    uint32_t green_duty = 8191 * color.g / 255;
    uint32_t blue_duty = 8191 * color.b / 255; 

    ESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, red_duty));
    ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0));

    ESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1, green_duty));
    ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1));

    ESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_2, blue_duty));
    ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_2));
}

void app_main(void)
{
    const color_t red = { .r = 255, .g = 0, .b = 0 };
    const color_t green = { .r = 0, .g = 255, .b = 0 };
    const color_t blue = { .r = 0, .g = 0, .b = 255 };
    const color_t yellow = { .r = 255, .g = 255, .b = 0 };
    const color_t purple = { .r = 128, .g = 0, .b = 128 };
    const color_t silver = { .r = 192, .g = 192, .b = 192 };

    color_t sample_colors[6] = { red, green, blue, yellow, purple, silver };

    init();

    while (1) {
        for (int i = 0; i < 6; i++)
        {
            printf("Color %d\r\n", i);
            setColor(sample_colors[i]);
            vTaskDelay(pdMS_TO_TICKS(2000));
        }
    }
}

Full code project can be seen on Github.

Code explanation

  • In the above code, you have configured to use LEDC_TIMER_0 using the API ledc_timer_config(). You pass the address of a configuration variable ledc_timer which tells to use 13 bit duty resolution, 1 kHz output frequency and in low speed mode.
  • You use 3 LEDC channels, LEDC_CHANNEL_0, LEDC_CHANNEL_1 and LEDC_CHANNEL_2 to drive Red, Green and Blue LEDs respectively. Channel configurations are stored in an array ledc_channel[3] and passed in API ledc_channel_config() in a loop, each loop configures a channel.
  • You define a structure color_t which specifies the red, green and blue component of a color.
  • To show a color, you call setColor() function. Inside this function, you set and update duty cycle for each LEDC channel based on the value of the red, green or blue component, for example, the red_duty value is calculated by 8191 * color.r / 255. This is because duty resolution is set to 13 bit.
  • In the main while loop, you iterate through an array of pre-defined colors and displays them, then idle for 2 seconds.

Wrapping Up

In this article, you have learnt about PWM generation using ESP32 LEDC module and ESP-IDF. In the next article, you will further develop from this project to build smart lighting application.

Leave a Comment