【ESP32】ESP-IDF开发 | LED PWM控制器+呼吸灯例程

时间:2024-10-13 18:51:56

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),分频满足公式:

DIV=A+\frac{B}{256}

         计数器的计数范围由占空比精度(DUTY_RES)决定,当计数值达到2^{DUTY\_RES-1}时计数器溢出,并重新从0开始计数。

        结合前面提到的参数,我们可以得出定时器的输出频率公式:

f_{sig\_out}=\frac{f_{LEDC\_CLK}}{CLK\_DIV\cdot 2^{DUTY\_RES}}

        通过上面公式,可以推导出占空比精度公式:

DUTY\_RES=log_{2}(\frac{f_{LEDC\_CLK}}{f_{sig_out}\cdot CLK\_DIV}) 

        其实只需要记住,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是全局变量,所以这里用了一个互斥量。这个函数返回是否有更高优先级的任务需要唤醒。

        最后就是配置渐变参数并启动渐变功能了。

        烧录程序到开发板,就能看到呼吸灯了。