1. 简介
LED PWM控制器,简称LEDC,主要用于控制LED的亮度和颜色,当然也可以当作普通的PWM进行使用。
LEDC有16路通道,8路高速通道和8路低速通道,16路通道都能够产生独立的数字波形来驱动LED设备。高速和低速通道分别有8个输出通道,每8个通道共享4个定时器,如下图。
LEDC能够在无须处理器干预的情况下自动逐渐增加或减少占空比,实现亮度和颜色渐变。同时LEDC还支持小数级别的分频。
2. 结构
上面是LEDC一个高速通道的简单框图,分为定时器和通道控制器两部分。
2.1 定时器
定时器接收外部时钟,可以选择外部参考时钟和APB时钟,接着会经过预分频器进行时钟分频,分频后的时钟会负责驱动一个20bit的计数器。
定时器的预分频器是支持小数级别的分频操作的,它的分频系数寄存器有18位,其中高10位表示整数部分(A),低8位表示小数部分(B),分频满足公式:
计数器的计数范围由占空比精度(DUTY_RES)决定,当计数值达到时计数器溢出,并重新从0开始计数。
结合前面提到的参数,我们可以得出定时器的输出频率公式:
通过上面公式,可以推导出占空比精度公式:
其实只需要记住,LEDC的输出频率越高,那么占空比的精度就会越低。就比如,输出频率设置为20MHz,占空比精度就只有2位,只能实现25%的倍数的占空比。
2.2 通道控制器
通道控制器接收计数器的每一次脉冲更新,将计数器寄存器的值跟内部的比较寄存器的值进行比较,控制高电平和低电平的输出。
通道控制器内部有2个比较器——高电平比较器和低电平比较器。由上图可以看到,当计数器的值大于高电平比较器值、低于低电平比较器值时才输出高电平,否则输出低电平。
前面说到,LEDC可以自动更改占空比,实现原理如下图。
更改占空比是通过改变低电平比较器的值实现的,另外,通过配置相关寄存器可以设置占空比更新的频率、次数和方向。
但在使用该功能时要注意,一旦启动了占空比的自动更新,那么必须等到它更新完成才能修改其参数。
3. 函数
3.1 定时器初始化
esp_err_t ledc_timer_config(const ledc_timer_config_t *timer_conf);
- timer_conf:定时器初始化结构体。
typedef struct {
ledc_mode_t speed_mode; // 高速或低速模式
ledc_timer_bit_t duty_resolution; // 占空比分辨率
ledc_timer_t timer_num; // 定时器序号
uint32_t freq_hz; // 输出频率
ledc_clk_cfg_t clk_cfg; // 时钟源选择
bool deconfigure; // 是否去初始化
} ledc_timer_config_t;
3.2 通道初始化
esp_err_t ledc_channel_config(const ledc_channel_config_t *ledc_conf);
- ledc_conf:通道初始化结构体。
typedef struct {
int gpio_num; // 输出GPIO口
ledc_mode_t speed_mode; // 高速或低速模式
ledc_channel_t channel; // 输出通道序号
ledc_intr_type_t intr_type; // 中断类型
ledc_timer_t timer_sel; // 定时器序号
uint32_t duty; // 占空比
int hpoint; // 高电平比较器值
struct {
unsigned int output_invert: 1; // 输出极性反转
} flags;
} ledc_channel_config_t;
3.3 初始化渐变功能
esp_err_t ledc_fade_func_install(int intr_alloc_flags);
- intr_alloc_flags:中断分配标志,默认为0。
3.4 注册事件回调
esp_err_t ledc_cb_register(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_cbs_t *cbs, void *user_arg);
- speed_mode:速度模式;
- channel:输出通道;
- cbs: 回调函数结构体;
typedef bool (*ledc_cb_t)(const ledc_cb_param_t *param, void *user_arg);
typedef struct {
ledc_cb_t fade_cb; // 渐变结束回调
} ledc_cbs_t;
- user_arg:用户回调函数参数。
3.5 注册中断回调
esp_err_t ledc_isr_register(void (*fn)(void*), void *arg, int intr_alloc_flags, ledc_isr_handle_t *handle);
- fn:回调函数;
- arg:回调函数用户参数;
- intr_alloc_flags:中断分配标志,默认为0;
- handle:中断句柄。
3.6 根据时长配置渐变
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);
- speed_mode:速度模式;
- channel:输出通道;
- target_duty:目标占空比;
- max_fade_time_ms:渐变时长,单位毫秒。
3.7 根据步数配置渐变
esp_err_t ledc_set_fade_with_step(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t target_duty, uint32_t scale, uint32_t cycle_num);
- speed_mode:速度模式;
- channel:输出通道;
- target_duty:目标占空比;
- scale:占空比更新跨步,即更新前后占空比的差值;
- cycle_num:占空比更新周期,即当前占空比输出多少次才更新下一个占空比。
3.8 启动渐变更新
esp_err_t ledc_fade_start(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_fade_mode_t fade_mode);
- speed_mode:速度模式;
- channel:输出通道;
- fade_mode:渐变模式,阻塞或非阻塞模式。
3.9 寻找最大占空比分辨率
uint32_t ledc_find_suitable_duty_resolution(uint32_t src_clk_freq, uint32_t timer_freq);
- src_clk_freq:时钟源频率;
- timer_freq:定时器频率。
4. 例程
例程中实现了一个呼吸灯,由熄灭到最亮为3秒,反过来也是3秒的时间。
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "driver/ledc.h"
#define TAG "app"
#define LEDC_SPEED_MODE LEDC_HIGH_SPEED_MODE
#define LEDC_CHANNEL LEDC_CHANNEL_0
static uint8_t cnt;
static SemaphoreHandle_t mutex;
static IRAM_ATTR bool cb_ledc_fade_end_event(const ledc_cb_param_t *param, void *user_arg)
{
BaseType_t taskAwoken = pdFALSE;
if (param->event == LEDC_FADE_END_EVT) {
xSemaphoreTakeFromISR(mutex, &taskAwoken);
ledc_set_fade_with_time(LEDC_SPEED_MODE, LEDC_CHANNEL, cnt == 0 ? 0 : 5000, 3000);
ledc_fade_start(LEDC_SPEED_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT);
cnt = (cnt + 1) % 2;
xSemaphoreGiveFromISR(mutex, &taskAwoken);
}
return (taskAwoken == pdTRUE);
}
void app_main()
{
/* 初始化定时器 */
ledc_timer_config_t ledc_timer = {
.duty_resolution = LEDC_TIMER_13_BIT, // 13位分辨率
.freq_hz = 5000, // 5kHz
.speed_mode = LEDC_SPEED_MODE, // 高速模式
.timer_num = LEDC_TIMER_0, // 定时器0
.clk_cfg = LEDC_AUTO_CLK, // 自动时钟源,默认为APB时钟
};
ledc_timer_config(&ledc_timer);
/* 初始化输出通道 */
ledc_channel_config_t ledc_channel = {
.channel = LEDC_CHANNEL, // 通道0
.duty = 0,
.gpio_num = 2,
.speed_mode = LEDC_SPEED_MODE, // 高速模式
.hpoint = 0,
.timer_sel = LEDC_TIMER_0, // 定时器0
.intr_type = LEDC_INTR_DISABLE, // 不使用中断
.flags.output_invert = 0 // 输出不反转
};
ledc_channel_config(&ledc_channel);
/* 初始化渐变模式 */
mutex = xSemaphoreCreateMutex();
ledc_fade_func_install(0);
ledc_cbs_t callbacks = {
.fade_cb = cb_ledc_fade_end_event
};
ledc_cb_register(LEDC_SPEED_MODE, LEDC_CHANNEL, &callbacks, NULL);
/* 使能渐变模式 */
ledc_set_fade_with_time(LEDC_SPEED_MODE, LEDC_CHANNEL, 5000, 3000);
ledc_fade_start(LEDC_SPEED_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT);
}
首先初始化定时器,我设置的是高速模式,使用定时器0,占空比分辨率为13位,输出频率为5kHz,时钟源选择APB时钟(80MHz)。
接着初始化输出通道,我的开发板上的LED是接在IO2上的,所以设置输出管脚为2,使用通道0,不使用中断,初始占空比和高电平比较器值都为0。
我后面使用的是非阻塞的渐变模式,所以接下来先注册一个回调函数。回调函数的定义前要加上IRAM_ATTR宏定义,把函数放到内部RAM中,提高执行速度。回调函数里面就是设置渐变参数,并启动下一轮的渐变。通过cnt变量判断是从灭到亮还是从亮到灭渐变;因为cnt是全局变量,所以这里用了一个互斥量。这个函数返回是否有更高优先级的任务需要唤醒。
最后就是配置渐变参数并启动渐变功能了。
烧录程序到开发板,就能看到呼吸灯了。