8. PWM
8.1 Introduction to PWM
PWM (Pulse Width Modulation) is a very effective technique that uses the digital output of a microprocessor to control an analog circuit. It is a method of digitally encoding the level of an analog signal. It refers to the proportion of time that the high level (i.e., the "1" state) of the waveform occupies within a certain period of time. Through the use of a high-resolution counter, the duty cycle of a square wave is modulated to encode the level of an analog signal. The PWM signal is still digital, because at any given moment, the full-amplitude DC supply is either fully on (1) or fully off (0). For example, if our voltage output is 5 V, then by changing the duty cycle of the PWM, we can achieve the effect of outputting 3.3 V or 1.3 V within a certain period of time. For example Imagine you have an LED and a switch. You quickly toggle the switch on and off at a speed that the naked eye cannot distinguish, so the LED is on for half the time and off for half the time. If you perform this operation quickly, for the observer, the LED appears to be continuously on at half brightness. This is the basic principle of PWM. If you keep the switch off for most of the time, the LED will appear dimmer; conversely, if you keep the switch on for most of the time, the LED will appear brighter. This is the process of PWM adjusting the duty cycle to control the brightness.
8.2 Basic Parameters of PWM
PWM is pulse width modulation and has two very important parameters: frequency and duty cycle.
- Frequency: The period length of the PWM signal, usually expressed in Hertz (Hz), indicating how many pulses there are per second. The frequency of PWM is the reciprocal of the entire period. It refers to the number of times the signal goes from a high level to a low level and back to a high level (one cycle) within 1 second.
- Duty cycle: The duty cycle refers to the proportion of the high level within one period.
- Resolution: The PWM signal resolution supported by the ESP32 refers to the number of different duty cycle levels that the device can output. For example, 8-bit resolution means the device can output 2^8 different duty cycle levels, that is, 0%, 1/256, 2/256 ... up to 100%.
8.3 PWM on the ESP32-S3
In the ESP32-S3, there are two hardware peripherals that can output PWM signals: the LED PWM Controller (LEDC) and the Motor Control Pulse Width Modulator (MCPWM). Each has its own characteristics and uses:
- LED PWM Controller (LEDC): The main design goal of this module is to generate high-precision PWM waveforms to control the brightness of LEDs or generate sound. The resolution of LEDC can reach 16 bits, capable of producing accurate and smooth changes, suitable for controlling LED brightness and generating sound. In addition, LEDC supports up to 8 channels of PWM output and supports any GPIO pin. Users can configure the frequency and duty cycle of each channel.
- Motor Control Pulse Width Modulator (MCPWM): This module is mainly used for motor control, including servo motors, stepper motors, and ordinary motors. MCPWM supports more complex control modes, such as forward/backward driving of motors, coasting, braking, etc., and supports closed-loop control modes to meet more complex motor control requirements. MCPWM supports up to 6 channels of independent PWM output, and supports dead-time control and external signal capture. In summary, the two are different in handling PWM and are applied in different scenarios. LEDC is more suitable for controlling linear devices such as lights and sound, while MCPWM contains more advanced functions and is suitable for motor control. In this chapter, we use the LED PWM Controller as an example to output PWM, hereafter referred to as LEDC.
8.4 PWM Operation Flow
- Timer configuration: Specify the frequency and duty cycle resolution of the PWM signal.
- Channel configuration: Bind the timer and the GPIO that outputs the PWM signal.
- Change the PWM signal: Output the PWM signal to drive the LED. The brightness of the LED can be changed through software control or by using the hardware fade function.
8.4.1 Timer Configuration
To set the timer, call the function ledc_timer_config(), and pass the data structure ledc_timer_config_t including the following configuration parameters to this function. Description of the relevant parameters of ledc_timer_config_t:
speed_mode: Speed mode. Note that unlike the ESP32, the ESP32-S3 only supports setting the channel to low-speed mode, i.e.,LEDC_LOW_SPEED_MODE.timer_num: The timer source of the channel. Timer index ledc_timer_t. Available parameters are as follows:LEDC_TIMER_0LEDC_TIMER_1LEDC_TIMER_2LEDC_TIMER_3LEDC_TIMER_MAX
freq_hz: The PWM signal frequency, indicating the timer clock frequency setting of the LEDC module, in Hz.duty_resolution: PWM duty cycle resolution. The duty cycle resolution is usually set with ledc_timer_bit_t, in the range of 10 to 15 bits. For lower duty cycle resolutions (up to 10, down to 1), you can directly enter the corresponding value. For related parameters, please refer to ledc_timer_bit_t.clk_cfg: The clock source of the LED PWM. The following parameters are available:LEDC_AUTO_CLK: When starting the timer, the LEDC source clock will be automatically selected based on the given resolution and duty cycle parameters;LEDC_USE_APB_CLK: Select APB as the source clock;LEDC_USE_RC_FAST_CLK: Select "RC_FAST" as the source clock;LEDC_USE_XTAL_CLK: Select XTAL as the source clock;LEDC_USE_RTC8M_CLK: An alias for "LEDC_USE_RC_FAST_CLK";
The frequency and duty cycle resolution are interrelated. The higher the PWM frequency, the lower the duty cycle resolution, and vice versa. This relationship may be important if the API is not used to change the LED brightness but for other purposes. For more information, see the section Supported Range of Frequency and Duty Cycle Resolution.
The clock source can also limit the PWM frequency. The higher the frequency of the selected clock source, the higher the upper limit of the PWM frequency that can be configured.
Notes
- If the timer of the ESP32-S3 uses RC_FAST_CLK as its clock source, the driver will know the actual frequency of this clock source through internal calibration. This ensures the accuracy of the output PWM signal frequency.
- All timers of the ESP32-S3 share one clock source. Therefore, the ESP32-S3 does not support configuring different clock sources for different timers.
8.4.2 Channel Configuration
After the timer is set up, you need to configure the desired channel (one of ledc_channel_t). To configure the channel, call the function ledc_channel_config().
The channel configuration is similar to the timer setup. You need to pass a structure including the channel configuration parameters to the channel configuration function.
ledc_channel_config_tAt this point, the channel will start operating according to the configuration of ledc_channel_config_t, and generate a PWM signal with the frequency and duty cycle specified by the timer settings on the selected GPIO. During the operation of the channel, you can pause it at any time by calling the function ledc_stop().
The parameters of ledc_channel_config_t are described as follows: gpio_num:
Configure the output pin; for example, if you use the GPIO6 pin, then gpio_num = 6;
speed_mode:
LEDC speed mode selection. Available parameters are high-speed mode (LEDC_HIGH_SPEED_MODE) or low-speed mode (LEDC_LOW_SPEED_MODE).
channel:
The output channel of the LEDC (PWM output channel). Available parameters are 0 to 7.
intr_type:
Configure the interrupt. Available parameters are enable interrupt (LEDC_INTR_FADE_END) and disable interrupt (LEDC_INTR_DISABLE).
timer_sel:
Select the timer source of the channel. Timer index ledc_timer_t. Available parameters are as follows:
LEDC_TIMER_0LEDC_TIMER_1LEDC_TIMER_2LEDC_TIMER_3LEDC_TIMER_MAX
duty: The duty cycle setting of the LEDC channel. The duty cycle setting range is 0 to 2 raised to the power of duty_resolution.
hpoint: The hpoint value of the LED channel. hpoint is called the duty cycle action point. It represents the clock count value corresponding to the duty cycle. The so-called duty cycle action point means that during the process of the LEDC module outputting a PWM signal, it will compare the counter with the duty cycle value to get a result, compare it with hpoint, and then output the PWM signal. The specific operations are as follows:
- If the counter value is less than hpoint, the PWM signal output of the LEDC module is logic high.
- If the counter value is greater than or equal to hpoint, the PWM signal output of the LEDC module is logic low.
- When the counter reaches the maximum value (0xfffff), the LEDC module will clear the count value and output the PWM signal as logic low.
Therefore, the smaller the hpoint value, the smaller the duty cycle, and the smaller the duty cycle when the PWM signal is output. The larger the hpoint value, the larger the duty cycle, and the larger the duty cycle when the PWM signal is output.
output_invert:
Enable (1) or disable (0) gpio output inversion. Enabled by default.
Example:
// Prepare and apply the LEDC PWM channel configuration
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_LOW_SPEED_MODE, // LED mode: low-speed mode
.channel = LEDC_CHANNEL_0, // Channel 0
.timer_sel = LEDC_TIMER_0, // Timer source: timer 0
.intr_type = LEDC_INTR_DISABLE, // Disable interrupt
.gpio_num = 5, // Output pin: GPIO5
.duty = 0, // Set duty cycle to 0
.hpoint = 0
};
ledc_channel_config(&ledc_channel);2
3
4
5
6
7
8
9
10
11
8.4.3 Changing the PWM Signal
After the channel starts running and generating a PWM signal with a constant duty cycle and frequency, there are several ways to change this signal. When driving an LED, the brightness is mainly changed by changing the duty cycle.
8.4.3.1 Changing the PWM Duty Cycle
Call the function ledc_set_duty() to set a new duty cycle. After that, call the function ledc_update_duty() to make the new configuration take effect. To view the currently set duty cycle, use the get function ledc_get_duty().
- Set the duty cycle
esp_err_t ledc_set_duty(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t duty)Parameter description: speed_mode: LEDC speed mode selection. Available parameters are high-speed mode (LEDC_HIGH_SPEED_MODE) or low-speed mode (LEDC_LOW_SPEED_MODE). channel - LEDC channel (0 - LEDC_CHANNEL_MAX-1), selected from ledc_channel_t; duty - Set the LED duty load. The duty load setting range is 0 to (2 raised to the power of duty_resolution) - 1;
- Update the duty cycle
esp_err_t ledc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel)speed_mode- LEDC speed mode selection. Available parameters are high-speed mode (LEDC_HIGH_SPEED_MODE) or low-speed mode (LEDC_LOW_SPEED_MODE).channel- LEDC channel (0 - LEDC_CHANNEL_MAX-1), selected fromledc_channel_t;
Example: Assuming the resolution is set to 13 bits. Then 50% duty cycle = ((2^13) - 1) 50% = (8,192-1) 0.5 = 4095.5;
// Set the duty cycle to 50%
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 4095);
// Update the channel duty cycle
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);2
3
4
Another way to set the duty cycle and other channel parameters is to call the function ledc_channel_config() mentioned in the Channel Configuration section. The range of duty cycle values passed to the function depends on the selected duty_resolution and should be 0 to (2 raised to the power of duty_resolution) - 1. For example, if the selected duty cycle resolution is 10, then the duty cycle value range is 0 to 1023. At this time, the resolution is ~0.1%.
8.4.3.2 Changing the PWM Frequency
The LED PWM Controller API provides several ways to change the PWM frequency on the fly:
- Set the frequency by calling the function ledc_set_freq(). You can use the function ledc_get_freq() to view the current frequency.
esp_err_t ledc_set_freq(ledc_mode_t speed_mode, ledc_timer_t timer_num, uint32_t freq_hz)Parameters - speed_mode - LEDC speed mode selection. Available parameters are high-speed mode (LEDC_HIGH_SPEED_MODE) or low-speed mode (LEDC_LOW_SPEED_MODE). - timer_num – LEDC timer (0-3), selected from ledc_timer_t. - freq_hz – Set the LED frequency. 2. Change the frequency and duty cycle resolution by binding other timers to the channel by calling the function ledc_bind_channel_timer().
esp_err_t ledc_bind_channel_timer(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_timer_t timer_sel)Parameters
speed_mode- LEDC speed mode selection. Available parameters are high-speed mode (LEDC_HIGH_SPEED_MODE) or low-speed mode (LEDC_LOW_SPEED_MODE).channel- LEDC channel (0 - LEDC_CHANNEL_MAX-1), selected fromledc_channel_t.timer_sel– LEDC timer (0-3), selected fromledc_timer_t.
- Change the timer of the channel by calling the function ledc_channel_config().
8.5 Hardware Connection and Preparation
This example uses the onboard LED for the breathing light test. Generally, the human eye has no sense of flicker for refresh frequencies above 80 Hz. Because the flicker cannot be seen at high frequencies, the larger the duty cycle, the brighter the LED; the smaller the duty cycle, the dimmer the LED. Therefore, at a certain frequency, you can use different duty cycles to change the brightness of the LED, achieving a breathing light effect (gradually brighter and then gradually dimmer, repeating this process). The onboard LED is connected to GPIO48, so we need to bind the LEDC function to GPIO48 during initialization.
8.6 PWM Breathing Light Verification
In the main/hardware/pwm directory (if it does not exist, create it), create two new files: bsp_pwm.c and bsp_pwm.h.
Remember to configure the header file path
Write the following code in bsp_pwm.h.
#ifndef _BSP_PWM_H_
#define _BSP_PWM_H_
#include "driver/ledc.h"
#define LEDC_TIMER LEDC_TIMER_0 // Timer 0
#define LEDC_MODE LEDC_LOW_SPEED_MODE // Low-speed mode
#define LEDC_OUTPUT_IO (48) // Define the output GPIO as GPIO48
#define LEDC_CHANNEL LEDC_CHANNEL_0 // Use channel 0 of LEDC
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT // Set LEDC resolution to 13 bits
#define LEDC_DUTY (4095) // Set duty cycle to 50%. ((2^13) - 1) * 50% = 4095
#define LEDC_FREQUENCY (100) // Frequency unit is Hz. Set frequency to 5000 Hz
/**
* @brief LEDC function initialization
* @param None
* @retval None
* @note The higher the PWM frequency, the lower the available duty cycle resolution
*/
void LedcInitConfig(void);
#endif2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Write the following code in bsp_pwm.c.
#include "bsp_pwm.h"
/**
* @brief LEDC function initialization
* @param None
* @retval None
* @note The higher the PWM frequency, the lower the available duty cycle resolution
*/
void LedcInitConfig(void)
{
// Prepare and apply the LED PWM timer configuration
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_MODE, // LED mode: low-speed mode
.timer_num = LEDC_TIMER, // Timer source of the channel: timer 0
.duty_resolution = LEDC_DUTY_RES, // Set duty cycle resolution to 13 bits
.freq_hz = LEDC_FREQUENCY, // Set output frequency to 5 kHz
.clk_cfg = LEDC_AUTO_CLK // Set the LED PWM clock source to auto
// LEDC_AUTO_CLK = When starting the timer, the LED source clock will be automatically selected based on the given resolution and duty cycle parameters
};
ledc_timer_config(&ledc_timer);
// Prepare and apply the LEDC PWM channel configuration
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_MODE, // LED mode: low-speed mode
.channel = LEDC_CHANNEL, // Channel 0
.timer_sel = LEDC_TIMER, // Timer source: timer 0
.intr_type = LEDC_INTR_DISABLE, // Disable interrupt
.gpio_num = LEDC_OUTPUT_IO, // Output pin: GPIO5
.duty = 0, // Set duty cycle to 0
.hpoint = 0
};
ledc_channel_config(&ledc_channel);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Write the following code in main.c.
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "bsp_pwm.h"
void app_main(void)
{
int duty_value = 0;
// Set the LED peripheral configuration
LedcInitConfig();
// Set the duty cycle to 50
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, LEDC_DUTY);
// Update the channel duty cycle
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
while(1)
{
// Achieve a gradual brightening effect
for(duty_value=0;duty_value<8191;duty_value+=100)
{
// Set the brightness analog value; duty cycle keeps increasing
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty_value);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
// Delay 10 ms
vTaskDelay(10 / portTICK_PERIOD_MS);
}
// Achieve a gradual dimming effect
for(duty_value=8191;duty_value>=0;duty_value-=100)
{
// Set the brightness analog value; duty cycle keeps decreasing
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty_value);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
// Delay 10 ms
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37