本文只针对 ESP32-C3。
Espressif 素来以完善的文档著称,但在使用 PWM 用来控灯时却产生了诸多疑问,翻遍 datasheet 和 ESP-IDF Programming Guide 也没有找到想要的答案,无奈只能自己手撸一下代码。
PWM 输出频率与什么因素有关?
从 datasheet 可知,PWM 的输出频率满足以下公式:
从上述公式可以知道,PWM 输出频率只跟三个因素有关:
- 定时器的时钟源
LEDC_CLKx
- 时钟分频系数
LEDC_CLK_DIVx
- 计数器的计数范围
LEDC_TIMERx_DUTY_RES
我们在来看下 LEDC 的整体框图:
整体的逻辑就很好理解了,PWM 通过定时器 Timer 来实现。Timer 的时钟源(即 LEDC_CLKx
)可以来自 APB_CLK
、FOSC_CLK
或者 XTAL_CLK
。经过时钟分频器(最大为 18 bits)之后产生 ref_pulsex
信号供计数器(最大为 14 bits)使用。
PWM 输出频率如何设置?
我们现在知道了 PWM 的输出频率是受以下三个因素的影响:
- 定时器的时钟源
LEDC_CLKx
- 时钟分频系数
LEDC_CLK_DIVx
- 计数器的计数范围
LEDC_TIMERx_DUTY_RES
那我们应该如何来设置这三个因素来实现我们想要输出的频率呢?其实 Espressif 能让我们设置的仅仅只有 LEDC_CLKx
及 LEDC_TIMERx_DUTY_RES
这两个参数,至于 LEDC_CLK_DIVx
参数通过公式来判断是否满足要求即可。一般来说,LEDC_CLKx
设置为 APB_CLK
即可,在 ESP32-C3 上为 80 MHz
。
这里就以 IDF v4.4 为例,假设我们要输出的频率为 10 KHz,占空比为 50%。
我在代码里设置时钟源为 APB_CLK
,计数器的计数范围为 14
bit,占空比为 8191
(((2 ** 14) - 1) * 50% = 8191),输出频率为 10
KHz。
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_OUTPUT_IO (5) // Define the output GPIO
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_14_BIT
#define LEDC_DUTY (8191)
#define LEDC_FREQUENCY (10000) // Frequency in Hertz. Set frequency at 10 kHz
static void example_ledc_init(void)
{
// Prepare and then apply the LEDC PWM timer configuration
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY, // Set output frequency at 10 kHz
.clk_cfg = LEDC_USE_APB_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
// Prepare and then apply the LEDC PWM channel configuration
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = LEDC_OUTPUT_IO,
.duty = LEDC_DUTY, // Set duty to 50%
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
}
运行的时候肯定会报以下错误:
E (269) ledc: requested frequency and duty resolution can not be achieved, try reducing freq_hz or duty_resolution. div_param=125
其实这里就是我不明白的地方,明明设置的都对,但为什么却给报这种错误呢?其实这里的真正原因就是没有满足 PWM 的输出频率公式的要求。
我们在看回看一下 PWM 的输出频率公式,在这个公式中,其实我们已经确定了几个变量:
- fPWM = 10000
- fLEDC_CLKx = 80000000
- 2LEDC_TIMERx_DUTY_RES = 214 = 16384
则 LEDC_CLK_DIVx
即为 0
。显然不满足公式的要求。在 IDF 提供的 LEDC 源文件 ledc.c 中对时钟分频系数 LEDC_CLK_DIVx
做了检查:
// Setting the LEDC timer divisor with the given source clock, frequency and resolution.
static esp_err_t ledc_set_timer_div(ledc_mode_t speed_mode, ledc_timer_t timer_num, ledc_clk_cfg_t clk_cfg, int freq_hz, int duty_resolution)
{
uint32_t div_param = 0;
uint32_t precision = ( 0x1 << duty_resolution );
ledc_clk_src_t timer_clk_src = LEDC_APB_CLK;
// Calculate the divisor
// User specified source clock(RTC8M_CLK) for low speed channel
if ((speed_mode == LEDC_LOW_SPEED_MODE) && (clk_cfg == LEDC_USE_RTC8M_CLK)) {
if(s_ledc_slow_clk_8M == 0) {
if (ledc_slow_clk_calibrate() == false) {
goto error;
}
}
div_param = ( (uint64_t) s_ledc_slow_clk_8M << 8 ) / freq_hz / precision;
} else {
// Automatically select APB or REF_TICK as the source clock.
if (clk_cfg == LEDC_AUTO_CLK) {
// Try calculating divisor based on LEDC_APB_CLK
div_param = ( (uint64_t) LEDC_APB_CLK_HZ << 8 ) / freq_hz / precision;
if (div_param > LEDC_TIMER_DIV_NUM_MAX) {
// APB_CLK results in divisor which too high. Try using REF_TICK as clock source.
timer_clk_src = LEDC_REF_TICK;
div_param = ((uint64_t) LEDC_REF_CLK_HZ << 8) / freq_hz / precision;
} else if (div_param < 256) {
// divisor is too low
goto error;
}
// User specified source clock(LEDC_APB_CLK_HZ or LEDC_REF_TICK)
} else {
timer_clk_src = (clk_cfg == LEDC_USE_REF_TICK) ? LEDC_REF_TICK : LEDC_APB_CLK;
uint32_t src_clk_freq = ledc_get_src_clk_freq(clk_cfg);
div_param = ( (uint64_t) src_clk_freq << 8 ) / freq_hz / precision;
}
}
if (div_param < 256 || div_param > LEDC_TIMER_DIV_NUM_MAX) {
goto error;
}
if (speed_mode == LEDC_LOW_SPEED_MODE) {
portENTER_CRITICAL(&ledc_spinlock);
ledc_hal_set_slow_clk(&(p_ledc_obj[speed_mode]->ledc_hal), clk_cfg);
portEXIT_CRITICAL(&ledc_spinlock);
}
//Set the divisor
ledc_timer_set(speed_mode, timer_num, div_param, duty_resolution, timer_clk_src);
// reset the timer
ledc_timer_rst(speed_mode, timer_num);
return ESP_OK;
error:
ESP_LOGE(LEDC_TAG, "requested frequency and duty resolution can not be achieved, try reducing freq_hz or duty_resolution. div_param=%d",
(uint32_t ) div_param);
return ESP_FAIL;
}
所以,要满足输出 10 KHz 频率,占空比为 50% 的 PWM 波,只能改变计数器的计数范围 LEDC_TIMERx_DUTY_RES
。以时钟分频系数 LEDC_CLK_DIVx
为 1 来计算:
- fPWM = 10000
- fLEDC_CLKx = 80000000
- LEDC_CLK_DIVx = 1
则 2LEDC_TIMERx_DUTY_RES 要不大于 8000,则计数器的计数范围 LEDC_TIMERx_DUTY_RES
最大只能取到 12
bits。
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_OUTPUT_IO (5) // Define the output GPIO
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_12_BIT
#define LEDC_DUTY (2047)
#define LEDC_FREQUENCY (10000) // Frequency in Hertz. Set frequency at 10 kHz
static void example_ledc_init(void)
{
// Prepare and then apply the LEDC PWM timer configuration
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY, // Set output frequency at 10 kHz
.clk_cfg = LEDC_USE_APB_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
// Prepare and then apply the LEDC PWM channel configuration
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = LEDC_OUTPUT_IO,
.duty = LEDC_DUTY, // Set duty to 50%
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
}