ESP32-IDF — LEDC Get Started
The LED control (LEDC) peripheral — ESP_IDF_Series Episode #04
This post will give you enough understanding of applied C programming language from where you can take yourself to a higher level of expertise.
The ESP32 is a dual-core system with two Harvard Architecture Xtensa LX6 CPUs.
It has 41 Peripherals, such as SPI, I2C, UART, WiFi, and BT, among others, but in this post, we will focus only on the LEDC peripheral (or LED_PWM controller).
So we will be dealing with LEDC (LED Controller) fade example code provided by Expressif in its ESP-IDF — PERIPHERALS directory, offered to the public in this Github Repo link:
https://github.com/espressif/esp-idf/tree/master/examples/peripherals/ledc
Here is the official Expressif LED Control API page:
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html
So what is the LED Control Module?
Is a module, among others provided for free by Expressif, built inside of ESP32’s driver/ledc.h library to provide services such as generating pwm signals to control the LED intensity or other purposes, like driving DC motors, valves, pumps, hydraulics, and other mechanical parts, RGB strips control, signal generator, etc.You can read about this from page 300 of ESP32 Technical Reference Manual (13. LED_PWM)
The LED_PWM controller (LEDC) is primarily designed to control the intensity of LEDs, although it can be used to generate PWM signals for other purposes as well. It has 16 channels that can generate independent waveforms that can be used to drive RGB LED devices.
For maximum flexibility, the high-speed as well as the low-speed channels can be driven from one of four high-speed/low-speed timers. The PWM controller also has the ability to automatically increase or decrease the duty cycle gradually, allowing for fades without any processor interference. To increase resolution, the LED_PWM controller is also able to dither between two values, when a fractional PWM value is configured. (from ESP32 Technical Reference Manual)
The process to use this peripheral is divided into three steps:
1. Configure Timer by specifying the PWM signal’s frequency and duty cycle resolution;2. Configure Channel by associating it with the timer and GPIO to output the PWM signal; 3. Change PWM Signal that drives the output in order to change LED’s intensity. This can be done under the full control of software or with hardware fading functions.
To run it on you win10:
cd to:
C:\Users\%userprofile%\Desktop\esp-idf\examples\ledc
type:
idf.py build
or
idf.py menuconfig
to run:
idf.py -p COM3 flash
Let’s see line-by-line of this code:
Don’t get put off by the size of the code… I’ll make a sort of reverse-engineering to the coding technique applied by Expressif programmers, and analyze it thoroughly.
At first glance, it looks like a boilerplate code, when in fact Expressif offers us a fantastic way how to code professionally for readability and consistency:)
Let’s get it on!
Of course, at the header, it declares all the libraries needed:
#include <stdio.h>#include "freertos/FreeRTOS.h"
#include "freertos/task.h"#include "driver/ledc.h"
#include "esp_err.h"
As you can see, these 3 groups are for a standard lib for C, libs for FreeRTOS, and the last one, the theme of this post, for LEDC (LED Controller peripheral) using .h file… and error management :)
Just fine!
Now Expressif declares a bunch of defines for CONSTANTS:
#ifdef CONFIG_IDF_TARGET_ESP32
APP NAME API NAME#define LEDC_HS_TIMER LEDC_TIMER_0
#define LEDC_HS_MODE LEDC_HIGH_SPEED_MODE
#define LEDC_HS_CH0_GPIO (18)
#define LEDC_HS_CH0_CHANNEL LEDC_CHANNEL_0
#define LEDC_HS_CH1_GPIO (19)
#define LEDC_HS_CH1_CHANNEL LEDC_CHANNEL_1#endif#define LEDC_LS_TIMER LEDC_TIMER_1
#define LEDC_LS_MODE LEDC_LOW_SPEED_MODE#ifdef CONFIG_IDF_TARGET_ESP32S2#define LEDC_LS_CH0_GPIO (18)
#define LEDC_LS_CH0_CHANNEL LEDC_CHANNEL_0
#define LEDC_LS_CH1_GPIO (19)
#define LEDC_LS_CH1_CHANNEL LEDC_CHANNEL_1#endif#define LEDC_LS_CH2_GPIO (4)
#define LEDC_LS_CH2_CHANNEL LEDC_CHANNEL_2
#define LEDC_LS_CH3_GPIO (5)
#define LEDC_LS_CH3_CHANNEL LEDC_CHANNEL_3
#define LEDC_TEST_CH_NUM (4)
#define LEDC_TEST_DUTY (4000)
#define LEDC_TEST_FADE_TIME (3000)
(*name convention: MODULE_SPEEDMODE_CHX_VARIABLE)
It is known as a from-to constant; from APP to API CONSTANTS ;)
Most statements #define are related to the declaration of channels. Hold on, you will figure it out soon…keep on going…
To declare a channel and match each name with the LEDC API, it is important to set the following variable information per channel:
. channel
. duty
. gpio_num
. mode
. hpoint
. timer_sel
Most of the variables declared above are obvious, unless hpoint;
What follows below is my try to explain these terms:)
But first, let’s prepare and set the configuration of timers that will be used by LED Controller:
01#Step — Configuring the Timer:
TIMER
ledc_timer_config_t ledc_timer = {
.duty_resolution = LEDC_TIMER_13_BIT,
.freq_hz = 5000,
.speed_mode = LEDC_LS_MODE,
.timer_num = LEDC_LS_TIMER,
.clk_cfg = LEDC_AUTO_CLK,
};
The ESP32 has a 20_bit configurable duty timer resolution.
The frequency and the duty resolution are interdependent. The higher the PWM frequency, the lower duty resolution is available, and vice versa. This relationship might be important if you are planning to use this API for purposes other than changing the intensity of LEDs. For more details, see Section Supported Range of Frequency and Duty Resolutions. (from ESP32 Technical Reference Manual)
The 13-bit duty resolution was chosen ie the counter goes up to 8192 before overflow, 5k frequency, and low-speed mode.
So far, so good!
02#Step — Configuring the Channels:
CHANNEL
It is good to know that this peripheral (LEDC) has 2 groups of 8 channels each. 4 high-speed (HS) mode and 4 low-speed (LS) mode channels.
For testing, four channels will be raised: channels CH[0], CH[1], CH[2], and CH[3];
#define LEDC_HS_CH0_CHANNEL LEDC_CHANNEL_0
#define LEDC_HS_CH1_CHANNEL LEDC_CHANNEL_1
#define LEDC_LS_CH2_CHANNEL LEDC_CHANNEL_2
#define LEDC_LS_CH3_CHANNEL LEDC_CHANNEL_3
(*name convention: MODULE_SPEEDMODE_CHX_CHANNEL)
As well as four GPIOs will be initialized: GPIOs 18,19, 4, and 5; Pins 18 and 19 for HS and pins 4 and 5 for LS mode. In each pin, I will attach 4 LEDs 3mm type (2 red for HS and 2 green for LS)
#define LEDC_HS_CH0_GPIO (18)
#define LEDC_HS_CH1_GPIO (19)
#define LEDC_LS_CH2_GPIO (4)
#DEFINE LEDC_LS_CH3_GPIO (5)
(*name convention: MODULE_SPEEDMODE_CHX_GPIO)
Just fine!
If the target is ESP32 — for this experiment, I’ll be using WiFi LoRa 32 (v2) — the GPIOs 18, and 19 will be set to HS mode, and GPIOs 4 and 5 set to LS mode— please, watch out for the preprocessor directives in C #ifdef and #endif; the difference between them and subtle:
High Speed mode : this mode is implemented in hardware and offers automatic and glitch-free changing of the PWM duty cycle
On the other hand:
Low Speed mode : in this the PWM duty cycle must be changed by the driver in software.
In the end, this will be transparent for the user :/ we will perceive no glitch or effect whatsoever during the channel change in either HS or LS channel…
DUTY
The duty cycle is the ratio of time a load or circuit is ON compared to the time the load or circuit is OFF.
In other words, it can determine the speed at which a motor is running or the intensity of the theater stage lighting in a more exciting scene :)
To set the duty cycle, use the dedicated function ledc_set_duty()
. After that, call ledc_update_duty()
to activate the changes. To check the currently set value, use the corresponding _get_
function ledc_get_duty()
.
Another way to set the duty cycle, as well as some other channel parameters, is by calling ledc_channel_config()
covered in Section Configure Channel.
The range of the duty cycle values passed to functions depends on the selected duty_resolution
and should be from 0
to (2 ** duty_resolution) - 1
. For example, if the selected duty resolution is 10, then the duty cycle values can range from 0 to 1023. This provides a resolution of ~0.1%. (from ESP32 Technical Reference Manual)
The maximum available frequency for The LED PWM Controller is 40 MHz with a duty resolution of 1 bit.
For this test, the PWM duty cycle will be set to:
#define LEDC_TEST_DUTY (4000)
See Section Supported Range of Frequency and Duty Resolutions too.
By setting it to 4000, a 50 % duty cycle has been established, as the counter timer can go up to approximately 8000 (13-bit duty resolution).
In other words, you now have control of the intensity of the LEDs simply by changing this variable (LEDC_TEST_DUTY). Fantastic!
GPIO-NUM
The GPIO18/19 are from the high-speed channel group.
The GPIO4/5 are from the low-speed channel group.
MODE
The GPIO18/19 are set to LEDC high-speed speed_mode.
The GPIO4/5 are set to LEDC low-speed speed_mode,
HPOINT
The code sets all channels’ hpoint to zero, which means that the output will be latched high at the very beginning of the counter (13-bit counts from 0 to 8,192).
.hpoint = 0
Please, read this extract of the ESP32 Technical Reference Manual to understand what is hpoint and lpoint :)
TIMER_SEL
timer_sel
: Timer index (0-3), there are 4 timers in LEDC module.
Now the configuration of the channel.
STRUCTURES
The code uses struct (struct serves to collect correlated variables).
A struct in the C programming language is a composite data type declaration that defines a physically grouped list of variables under one name in a block of memory, allowing the different variables to be accessed via a single pointer or by the struct-declared name which returns the same address. (from Wikipedia)
For a warm-up, here are 4 projects linked below that play with struct using C:
Project 16 - Struct - How to Create Struct in C
Project 17 - 3 ways to declare a struct in C
Project 18 - Chained Struct - How To Declared Nested Struct
Project 19 - How to Align Structs
There are 3 ways to initialize a struct:
First, declare it:
typedef struct Serial{
unsigned char ComPort;
unsigned int BaudRate;
}Serial_t;
Then initialize it by one of these three methods:
Three ways to declare structs:
Serial_t Uart;
Uart.ComPort = 1;
Uart.BaudRate = 9600;
Serial_t Uart = {2, 57600};
Serial_t Uart = {
.ComPort = 3,
.BaudRate = 115200
};
As you see in the Expressif code the method preferred is the last method (which accesses members via a single pointer) because it’s cleaner and clearer.
STRUCT
structledc_channel_config_t
It stores all the necessary and correlated variables for each of the channels.
This is the LEDC_CHANNEL_0 configuration example:
ledc_channel_config_t ledc_channel[0] = {
.channel = LEDC_HS_CH0_CHANNEL,
.duty = 0,
.gpio_num = LEDC_HS_CH0_GPIO,
.speed_mode = LEDC_HS_MODE,
.hpoint = 0,
.timer_sel = LEDC_HS_TIMER
};
The other channels follow the same pattern.
The code analyzed declares a multi-dimensional array of arrays, like this, {{},{},{},{}, {}, {}}and enumerate it; then it select the high and low channel in the preprocessor directives and call each pointer to the channel (remember, LEDC_TEST_CH_NUM=4). Then, passes to the method ledc_channel_config() the settings of each channel struct:
for (ch = 0; ch < LEDC_TEST_CH_NUM; ch++) {
ledc_channel_config(&ledc_channel[ch]);
}
It passes the pointer of the struct address of each channel configured above to the function(pass by reference):
esp_err_t ledc_channel_config(
const ledc_channel_config_t *ledc_conf)LEDC channel configuration Configure LEDC channel with the given:
. channel/
. LEDC duty resolution/
. output gpio_num/
. frequency(Hz)/
. interrupt/
. source timer/
03#Step — Change PWM Signal:
WHILE
It does four loops almost simultaneously; one for each of the effects it prints on the LED attached to the GPIOs 18,19,4, 5.
The result is the synchronization of the four LEDs; first, turn on and off them quickly; then ramp them up; and then ramp them down the brightness of the LEDs.
The implementation loops through the structure of each channel (ledc_channel[LEDC_TEST_CH_NUM]) and retrieves each of the variables requested by the method called.
The first two loops are for HS mode; it is implemented in hardware and offers automatic and glitch-free changing of the PWM:
esp_err_t ledc_set_fade_with_time
(ledc_mode_t speed_mode,
ledc_channel_t channel,
uint32_t target_duty,
int max_fade_time_ms)Set LEDC fade function, with a limited time.
and
esp_err_t ledc_fade_start
(ledc_mode_t speed_mode,
ledc_channel_t channel,
ledc_fade_mode_t fade_mode)Start LEDC fading.
The last two methods are for LS mode— the PWM duty cycle must be changed by the driver in the software:
esp_err_t ledc_set_duty
(ledc_mode_t speed_mode,
ledc_channel_t channel,
uint32_t duty)LEDC set duty This function do not change the hpoint value of this channel. if needed, please call ledc_set_duty_with_hpoint. only after calling ledc_update_duty will the duty update.
And
esp_err_t ledc_update_duty
(ledc_mode_t speed_mode,
ledc_channel_t channel)LEDC update channel parameters.
And that is all!
Hopefully, this post has helped shed some light on why ESP32-IDF stands out from the crowd when it comes to IoT solutions.
Please, send feedback if you wish; we are all ESP32!
Please follow my studies in this series about this impressive framework — ESP-IDF. A lot of efforts have been made to translate it for hobbyists…it is worth the effort though 😜
In the next ESP_IDF_Series Episode let’s study another module: PCNT_MODULE!
See you there!
Bye, for now, o/
Credits & References
ESP32 Technical Reference Manual by wiki/esp32
Related Posts
00#Episode — ESP_IDF_Series — ESP-IDF Programming Guide + WiFi LoRa 32 (v2) — How To Get Started
01#Episode — ESP_IDF_Series — LoRa LPWAN — Long Range Low Power Wide Area Network — LoRa was developed by Semtech, founder LoRa Alliance
02#Episode — ESP_IDF_Series — ESP-IDF Programming Guide + WiFi LoRa 32 (v2) + idf.py utility — How To Really Get Started
03#Episode — ESP_IDF_Series — ESP32-IDF — HelloWorld Get Started — How To Get Started
04#Episode — ESP_IDF_Series — ESP32-IDF — LEDC Get Started — The LED control (LEDC) peripheral (this one:)
In the context of the ESP32 IDF (IoT Development Framework),
PWM (Pulse Width Modulation) is used to control devices like
LEDs, motors, and more by varying the duty cycle of a signal.
The ESP32 offers two modes for PWM control: High Speed mode
and Low Speed mode. Here’s a detailed explanation of each mode:
High Speed Mode
Hardware Implementation: High Speed mode is implemented
directly in hardware. This means the ESP32’s dedicated
hardware peripherals handle the PWM signal generation.
Automatic Duty Cycle Change: The duty cycle (the percentage
of one period in which a signal is active) can be changed
automatically. When you update the duty cycle, the hardware
ensures that the transition is smooth and glitch-free.
Glitch-Free Operation: Since the hardware manages the duty
cycle changes, there are no glitches (unexpected pulses or drops)
when the duty cycle is updated. This is crucial for applications
requiring precise control and smooth operation, such as motor
control or LED dimming.
Efficiency: High Speed mode is more efficient because
it offloads the work from the CPU to the hardware,
reducing the processing load and improving overall performance.
Low Speed Mode
Software Implementation: In Low Speed mode, the PWM signal
generation and duty cycle changes are managed by the software.
The driver (part of the ESP32 IDF) is responsible for controlling
the PWM output.
Manual Duty Cycle Change: The duty cycle must be changed
by the software driver. This involves CPU intervention,
as the software needs to update the PWM settings.
Potential for Glitches: Because the CPU and software are
involved in changing the duty cycle, there is a potential
for glitches. The timing of the software updates may not
be as precise as hardware updates, leading to brief periods
where the PWM signal is not at the desired duty cycle.
Higher CPU Load: Low Speed mode can increase the CPU load
because the processor must handle the timing and updates
for the PWM signals. This can be less efficient and may
impact the performance of other tasks running on the ESP32.
Use Cases
High Speed Mode: Ideal for applications requiring precise
and smooth control of the PWM signal, such as motor control,
LED dimming, and other tasks where stability and efficiency
are critical.
Low Speed Mode: Can be used for less critical applications
where occasional glitches are acceptable, or when the hardware
PWM peripherals are already in use for other tasks.
In summary, High Speed mode leverages dedicated hardware
for efficient, precise, and glitch-free PWM control,
while Low Speed mode relies on software,
which may introduce glitches and increase CPU load.