3、esp32的基础外设

GPIO

简要概述

在 ESP32 上,大部分引脚都可以作为 GPIO 使用,但有些引脚有特殊功能(如 Flash、PSRAM 占用),使用时需要参考数据手册。

可用的引脚号,在文件:gpio_num里面可以查看

/**
 * @brief GPIO number
 */
typedef enum {
    GPIO_NUM_NC = -1,   /*!< Use to signal not connected to S/W */
    GPIO_NUM_0 = 0,     /*!< GPIO0, input and output */
    GPIO_NUM_1 = 1,     /*!< GPIO1, input and output */
    GPIO_NUM_2 = 2,     /*!< GPIO2, input and output */
    GPIO_NUM_3 = 3,     /*!< GPIO3, input and output */
    GPIO_NUM_4 = 4,     /*!< GPIO4, input and output */
    GPIO_NUM_5 = 5,     /*!< GPIO5, input and output */
    GPIO_NUM_6 = 6,     /*!< GPIO6, input and output */
    GPIO_NUM_7 = 7,     /*!< GPIO7, input and output */
    GPIO_NUM_8 = 8,     /*!< GPIO8, input and output */
    GPIO_NUM_9 = 9,     /*!< GPIO9, input and output */
    GPIO_NUM_10 = 10,   /*!< GPIO10, input and output */
    GPIO_NUM_11 = 11,   /*!< GPIO11, input and output */
    GPIO_NUM_12 = 12,   /*!< GPIO12, input and output */
    GPIO_NUM_13 = 13,   /*!< GPIO13, input and output */
    GPIO_NUM_14 = 14,   /*!< GPIO14, input and output */
    GPIO_NUM_15 = 15,   /*!< GPIO15, input and output */
    GPIO_NUM_16 = 16,   /*!< GPIO16, input and output */
    GPIO_NUM_17 = 17,   /*!< GPIO17, input and output */
    GPIO_NUM_18 = 18,   /*!< GPIO18, input and output */
    GPIO_NUM_19 = 19,   /*!< GPIO19, input and output */
    GPIO_NUM_20 = 20,   /*!< GPIO20, input and output */
    GPIO_NUM_21 = 21,   /*!< GPIO21, input and output */
    GPIO_NUM_22 = 22,   /*!< GPIO22, input and output */
    GPIO_NUM_23 = 23,   /*!< GPIO23, input and output */
    GPIO_NUM_25 = 25,   /*!< GPIO25, input and output */
    GPIO_NUM_26 = 26,   /*!< GPIO26, input and output */
    GPIO_NUM_27 = 27,   /*!< GPIO27, input and output */
    GPIO_NUM_28 = 28,   /*!< GPIO28, input and output */
    GPIO_NUM_29 = 29,   /*!< GPIO29, input and output */
    GPIO_NUM_30 = 30,   /*!< GPIO30, input and output */
    GPIO_NUM_31 = 31,   /*!< GPIO31, input and output */
    GPIO_NUM_32 = 32,   /*!< GPIO32, input and output */
    GPIO_NUM_33 = 33,   /*!< GPIO33, input and output */
    GPIO_NUM_34 = 34,   /*!< GPIO34, input mode only */
    GPIO_NUM_35 = 35,   /*!< GPIO35, input mode only */
    GPIO_NUM_36 = 36,   /*!< GPIO36, input mode only */
    GPIO_NUM_37 = 37,   /*!< GPIO37, input mode only */
    GPIO_NUM_38 = 38,   /*!< GPIO38, input mode only */
    GPIO_NUM_39 = 39,   /*!< GPIO39, input mode only */
    GPIO_NUM_MAX,
} gpio_num_t;

配置结构体

结构体

typedef struct {
    uint64_t pin_bit_mask;          /*!< 引脚位掩码,选择要配置的引脚 */
    gpio_mode_t mode;               /*!< 输入/输出模式 */
    gpio_pullup_t pull_up_en;       /*!< 是否使能内部上拉电阻 */
    gpio_pulldown_t pull_down_en;   /*!< 是否使能内部下拉电阻 */
    gpio_int_type_t intr_type;      /*!< 中断触发类型 */
} gpio_config_t;

成员说明

成员 可选值 说明
pin_bit_mask (1ULL << GPIO_NUM_x) 可同时配置多个引脚,用 | 组合。其中:GPIO_NUM_x的x可以为0~39(esp32的是这么多,具体以芯片实际引脚的数量为准)
mode GPIO_MODE_DISABLE
GPIO_MODE_INPUT
GPIO_MODE_OUTPUT
GPIO_MODE_OUTPUT_OD
GPIO_MODE_INPUT_OUTPUT_OD
GPIO_MODE_INPUT_OUTPUT
禁用、输入模式、输出模式、开漏输出、开漏输入、双向模式、
pull_up_en GPIO_PULLUP_DISABLE
GPIO_PULLUP_ENABLE
内部上拉使能
pull_down_en GPIO_PULLDOWN_DISABLE
GPIO_PULLDOWN_ENABLE
内部下拉使能
intr_type GPIO_INTR_DISABLE
GPIO_INTR_POSEDGE
GPIO_INTR_NEGEDGE
GPIO_INTR_ANYEDGE
GPIO_INTR_LOW_LEVEL
GPIO_INTR_HIGH_LEVEL
中断触发类型:禁用、上升沿、下降沿、任意边沿、低电平、高电平

注意pin_bit_mask 是 64 位整数,使用 1ULL << GPIO_NUM_x 避免溢出。同时配置多个引脚时用按位或 |

假设想配置GPIO0和GPIO21,则这样配置:pin_bit_mask = (1ULL << GPIO_NUM_0) | (1ULL << GPIO_NUM_21)

常用API

初始化

  • 函数原型

    esp_err_t gpio_config(const gpio_config_t *pGPIOConfig);
    
  • 参数说明

    1. pGPIOConfig:配置结构体地址
  • 返回值(基本不用管)

    typedef int esp_err_t;
    
    /* Definitions for error constants. */
    #define ESP_OK          0       /*!< esp_err_t表示成功(没有错误)的值 */
    #define ESP_FAIL        -1      /*!< 表示失败的通用esp_err_t代码 */
    
    #define ESP_ERR_NO_MEM              0x101   /*!< 内存不足 */
    #define ESP_ERR_INVALID_ARG         0x102   /*!< 无效参数 */
    #define ESP_ERR_INVALID_STATE       0x103   /*!< 无效状态 */
    #define ESP_ERR_INVALID_SIZE        0x104   /*!< 无效的长度 */
    #define ESP_ERR_NOT_FOUND           0x105   /*!< 请求的资源未找到 */
    #define ESP_ERR_NOT_SUPPORTED       0x106   /*!< 不支持操作或功能 */
    #define ESP_ERR_TIMEOUT             0x107   /*!< 操作超时 */
    #define ESP_ERR_INVALID_RESPONSE    0x108   /*!< 接收到的响应无效 */
    #define ESP_ERR_INVALID_CRC         0x109   /*!< CRC或校验和无效 */
    #define ESP_ERR_INVALID_VERSION     0x10A   /*!< 版本无效 */
    #define ESP_ERR_INVALID_MAC         0x10B   /*!< MAC地址无效 */
    #define ESP_ERR_NOT_FINISHED        0x10C   /*!< 操作尚未完全完成 */
    #define ESP_ERR_NOT_ALLOWED         0x10D   /*!< 不允许操作 */
    
    #define ESP_ERR_WIFI_BASE           0x3000  /*!< 起始WiFi错误码个数 */
    #define ESP_ERR_MESH_BASE           0x4000  /*!< 网格错误码的起始数目 */
    #define ESP_ERR_FLASH_BASE          0x6000  /*!< flash错误码的起始数目 */
    #define ESP_ERR_HW_CRYPTO_BASE      0xc000  /*!< 硬件加密模块错误码的起始数目 */
    #define ESP_ERR_MEMPROT_BASE        0xd000  /*!< 内存保护API错误码的起始数目 */
    
  • 示例代码

    #include <stdio.h>
    #include "driver/gpio.h"
    
    #define LED1            GPIO_NUM_16
    #define LED1_MASK       (1UL << LED1)
    
    void app_main(void)
    {
        gpio_config_t LED_Config = {
            .pin_bit_mask = LED1_MASK,		// 注意这里是掩码
            .mode = GPIO_MODE_OUTPUT,
            .pull_up_en = GPIO_PULLUP_DISABLE,
            .pull_down_en = GPIO_PULLDOWN_DISABLE,
            .intr_type = GPIO_INTR_DISABLE,
        };
        gpio_config(&LED_Config);
    }
    

输出

  • 函数原型

    esp_err_t gpio_set_level(gpio_num_t gpio_num, uint32_t level);
    
  • 参数

    1. gpio_num:引脚号(注意不是掩码,就是引脚号,1、2、3、4、5、6....)
    2. level:输出的电平。1或0
  • 返回值:同上

  • 示例代码

    #include <stdio.h>
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "driver/gpio.h"
    
    #define LED1            GPIO_NUM_16
    #define LED1_MASK       (1UL << LED1)
    
    void task1(void* param) {
        while (1) {
            gpio_set_level(LED1, 1);		// 注意是引脚号
            vTaskDelay(pdMS_TO_TICKS(200));
            gpio_set_level(LED1, 0);
            vTaskDelay(pdMS_TO_TICKS(200));
        }
    }
    
    void app_main(void)
    {
        gpio_config_t LED_Config = {
            .pin_bit_mask = LED1_MASK,				// 注意是掩码
            .mode = GPIO_MODE_OUTPUT,				// 配置为输出
            .pull_up_en = GPIO_PULLUP_DISABLE,		// 关闭上拉
            .pull_down_en = GPIO_PULLDOWN_DISABLE,	// 关闭下拉
            .intr_type = GPIO_INTR_DISABLE,			// 关闭中断
        };
        gpio_config(&LED_Config);	// 初始化
    
        xTaskCreatePinnedToCore(task1, "task1", 2048, NULL, 10, NULL, 1);
    }
    

输入

  • 函数原型

    int gpio_get_level(gpio_num_t gpio_num);
    
  • 参数

    1. gpio_num:引脚号(注意不是掩码,就是引脚号,1、2、3、4、5、6....)
  • 返回值:同上

  • 示例代码

    #include <stdio.h>
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "driver/gpio.h"
    
    #define LED1            GPIO_NUM_16
    #define LED1_MASK       (1UL << LED1)
    
    #define KEY1            GPIO_NUM_12
    #define KEY1_MASK       (1UL << KEY1)
    
    void task1(void* param) {
        while (1) {
            if (gpio_get_level(KEY1) == 0) {
                gpio_set_level(LED1, 1);
            }
            else {
                gpio_set_level(LED1, 0);
            }
            vTaskDelay(pdMS_TO_TICKS(20));
        }
    }
    
    void app_main(void)
    {
        gpio_config_t KEY_Config = {
            .pin_bit_mask = KEY1_MASK,				// 注意是掩码
            .mode = GPIO_MODE_INPUT,				// 配置为输入
            .pull_up_en = GPIO_PULLUP_ENABLE,		// 开启上拉
            .pull_down_en = GPIO_PULLDOWN_DISABLE,	// 关闭下拉
            .intr_type = GPIO_INTR_DISABLE,			// 关闭中断
        };
        gpio_config(&KEY_Config);
    
        gpio_config_t LED_Config = {
            .pin_bit_mask = LED1_MASK,				// 注意是掩码
            .mode = GPIO_MODE_OUTPUT,				// 配置为输出
            .pull_up_en = GPIO_PULLUP_DISABLE,		// 关闭上拉
            .pull_down_en = GPIO_PULLDOWN_DISABLE,	// 关闭下拉
            .intr_type = GPIO_INTR_DISABLE,			// 关闭中断
        };
        gpio_config(&LED_Config);
    
        xTaskCreatePinnedToCore(task1, "task1", 2048, NULL, 10, NULL, 1);
    }
    

设置独立属性

函数原型 说明
esp_err_t gpio_set_direction(gpio_num_t gpio_num, gpio_mode_t mode); 设置方向
esp_err_t gpio_set_pull_mode(gpio_num_t gpio_num, gpio_pull_mode_t pull); 设置上下拉
esp_err_t gpio_set_intr_type(gpio_num_t gpio_num, gpio_int_type_t intr_type); 设置中断类型

参数: 参考结构体

返回值: 同上


中断

安装GPIO的中断服务程序

  • 函数原型

    esp_err_t gpio_install_isr_service(int intr_alloc_flags);
    
  • 参数: intr_alloc_flags 说明。

    • 可写值:(在路径:./esp-idf/components/esp_hw_support/include/esp_intr_alloc.h下)

      //Keep the LEVELx values as they are here; they match up with (1<<level)
      #define ESP_INTR_FLAG_LEVEL1        (1<<1)  ///< Accept a Level 1 interrupt vector (lowest priority)
      #define ESP_INTR_FLAG_LEVEL2        (1<<2)  ///< Accept a Level 2 interrupt vector
      #define ESP_INTR_FLAG_LEVEL3        (1<<3)  ///< Accept a Level 3 interrupt vector
      #define ESP_INTR_FLAG_LEVEL4        (1<<4)  ///< Accept a Level 4 interrupt vector
      #define ESP_INTR_FLAG_LEVEL5        (1<<5)  ///< Accept a Level 5 interrupt vector
      #define ESP_INTR_FLAG_LEVEL6        (1<<6)  ///< Accept a Level 6 interrupt vector
      #define ESP_INTR_FLAG_NMI           (1<<7)  ///< Accept a Level 7 interrupt vector (highest priority)
      #define ESP_INTR_FLAG_SHARED        (1<<8)  ///< Interrupt can be shared between ISRs
      #define ESP_INTR_FLAG_EDGE          (1<<9)  ///< Edge-triggered interrupt
      #define ESP_INTR_FLAG_IRAM          (1<<10) ///< ISR can be called if cache is disabled
      #define ESP_INTR_FLAG_INTRDISABLED  (1<<11) ///< Return with this interrupt disabled
      
      #define ESP_INTR_FLAG_LOWMED    (ESP_INTR_FLAG_LEVEL1|ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3) ///< Low and medium prio interrupts. These can be handled in C.
      #define ESP_INTR_FLAG_HIGH      (ESP_INTR_FLAG_LEVEL4|ESP_INTR_FLAG_LEVEL5|ESP_INTR_FLAG_LEVEL6|ESP_INTR_FLAG_NMI) ///< High level interrupts. Need to be handled in assembly.
      
      #define ESP_INTR_FLAG_LEVELMASK (ESP_INTR_FLAG_LEVEL1|ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3| \
                                      ESP_INTR_FLAG_LEVEL4|ESP_INTR_FLAG_LEVEL5|ESP_INTR_FLAG_LEVEL6| \
                                      ESP_INTR_FLAG_NMI) ///< Mask for all level flags
      
  • 说明

    宏定义 含义
    ESP_INTR_FLAG_LEVEL1 优先级 1(最低)
    ESP_INTR_FLAG_LEVEL2 优先级 2
    ESP_INTR_FLAG_LEVEL3 优先级 3(常用,普通外设中断)
    ESP_INTR_FLAG_LEVEL4 优先级 4(较高,一般不用)
    ESP_INTR_FLAG_LEVEL5 优先级 5
    ESP_INTR_FLAG_LEVEL6 优先级 6
    ESP_INTR_FLAG_NMI 非屏蔽中断(最高优先级 7)
    ESP_INTR_FLAG_SHARED 允许多个中断源共享同一个中断向量(即多个设备共用同一中断号)
    ESP_INTR_FLAG_EDGE 边沿触发(如果不指定,默认为电平触发)
    ESP_INTR_FLAG_IRAM 中断服务函数可以放在 IRAM 中运行,即使 Cache 被禁用也能执行(用于 SPI Flash 读写等场景)
    ESP_INTR_FLAG_INTRDISABLED 分配中断后保持禁用状态,需要手动使能
  • 常用组合:

    // 默认配置(优先级 1~3,可共享,非 IRAM)
    gpio_install_isr_service(0);
    
    // 指定优先级 3,不共享,边缘触发
    gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3 | ESP_INTR_FLAG_EDGE);
    
    // 高优先级,且 ISR 可运行于 IRAM
    gpio_install_isr_service(ESP_INTR_FLAG_LEVEL4 | ESP_INTR_FLAG_IRAM);
    
    // 允许共享中断(比如多个 GPIO 共用同一中断线)
    gpio_install_isr_service(ESP_INTR_FLAG_SHARED | ESP_INTR_FLAG_LEVEL3);
    

    一般来说直接写0就可以了

  • 建议:

    • 大多数 GPIO 中断场景:直接传 0 即可,系统会自动分配一个合适的中断(优先级 1~3,非共享)。
    • 如果你需要更高优先级或者确保不被 Cache 缺失影响,可以组合使用 ESP_INTR_FLAG_LEVEL3 | ESP_INTR_FLAG_IRAM
    • 注意:ESP32 的 GPIO 中断默认是电平触发,如果不指定 ESP_INTR_FLAG_EDGE,则按照你在 gpio_config_t 中设置的 intr_type(上升沿/下降沿/任意沿)仍然有效,因为 GPIO 硬件本身就是边沿或电平,这个标志是用来告诉中断分配器选择合适的触发方式,通常可以省略。
  • 返回值: 同上


卸载中断服务程序

  • 函数原型

    void gpio_uninstall_isr_service(void);
    

绑定中断处理函数

  • 函数原型

    esp_err_t gpio_isr_handler_add(gpio_num_t gpio_num, gpio_isr_t isr_handler, void *args);
    
  • 参数说明

    • gpio_num:GPIO端口
    • isr_handler:中断回调函数
    • *args:传递给中断回调函数的参数

移除中断处理函数

  • 函数原型

    esp_err_t gpio_isr_handler_remove(gpio_num_t gpio_num);
    
  • 参数: gpio_num:GPIO端口


中断回调函数

必须用 IRAM_ATTR 修饰,且只能调用 FromISR 版本的 API。

示例如下

static void IRAM_ATTR gpio_isr_handler(void* arg) {
    // ...
}

其中函数名(gpio_isr_handler)可以自定义

中断示例

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "freertos/event_groups.h"

#define LED1            GPIO_NUM_16
#define LED1_MASK       (1UL << LED1)

#define KEY1            GPIO_NUM_12
#define KEY1_MASK       (1UL << KEY1)

#define KEY_PRESS       (1UL << 0)          // 事件组中用于表示按键按下的位

EventGroupHandle_t event_handle;            // 事件组句柄

// GPIO 中断服务函数(必须加 IRAM_ATTR)
static void IRAM_ATTR gpio_key1_isr_handler(void* arg) {
    uint32_t gpio_num = (uint32_t)arg;                      // 获取引脚编号(本例中为 KEY1)
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;          // 记录是否唤醒高优先级任务

    // 从 ISR 中设置事件组位,注意使用 FromISR 版本
    xEventGroupSetBitsFromISR(event_handle, KEY_PRESS, &xHigherPriorityTaskWoken);

    // 如果唤醒了一个更高优先级的任务,则在 ISR 结束后请求任务切换
    if (xHigherPriorityTaskWoken == pdTRUE) {
        portYIELD_FROM_ISR();
    }
}

// 任务1:等待事件组中的 KEY_PRESS 位,然后使 LED 闪烁一次
void task1(void* param) {
    EventBits_t event_bit;
    while (1) {
        // 等待事件位 KEY_PRESS,等待到后自动清除该位(pdTRUE),或关系(pdFALSE 表示只需任意一位即可,这里只有一位)
        event_bit = xEventGroupWaitBits(event_handle, KEY_PRESS, pdTRUE, pdFALSE, portMAX_DELAY);
        if (event_bit & KEY_PRESS) {
            gpio_set_level(LED1, 1);            // LED 亮
            vTaskDelay(pdMS_TO_TICKS(200));     // 延时 200ms
            gpio_set_level(LED1, 0);            // LED 灭
        }
    }
}

void app_main(void)
{
    // 创建事件组
    event_handle = xEventGroupCreate();

    // 配置按键引脚(输入,上拉,下降沿中断)
    gpio_config_t KEY_Config = {
        .pin_bit_mask = KEY1_MASK,
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,       // 使能上拉,未按下时读为高电平
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_NEGEDGE,         // 下降沿触发(按键按下时电平从高变低)
    };
    gpio_config(&KEY_Config);

    // 配置 LED 引脚(输出)
    gpio_config_t LED_Config = {
        .pin_bit_mask = LED1_MASK,
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    gpio_config(&LED_Config);

    // 安装 GPIO 中断服务(参数 0 表示默认中断属性:优先级 1~3,非 IRAM 等)
    gpio_install_isr_service(0);

    // 为 KEY1 引脚添加中断处理函数,传入引脚编号作为参数
    gpio_isr_handler_add(KEY1, gpio_key1_isr_handler, (void*)KEY1);

    // 创建任务 task1,优先级 10,绑定到核心 1
    xTaskCreatePinnedToCore(task1, "task1", 2048, NULL, 10, NULL, 1);
}

GPTimer(通用定时器)

概述

全称:General Purpose Timer,通用定时器

头文件:#include "driver/gptimer.h"

作用: 定时中断、输入捕获、精准计时

数量: 可以通过创建多个定时器句柄,每个句柄对应一个独立的硬件定时器

芯片型号 GPTimer 数量
ESP32 2 个硬件定时器组,每组包含 2 个定时器,共 4 个
ESP32-S3 同 ESP32,4 个
ESP32-C3 2 个(只有一组)
ESP32-C6 2 个

配置结构体

定时器配置

/**
 * @brief 通用定时器配置
 */
typedef struct {
    gptimer_clock_source_t clk_src;      /*!< 时钟源 */
    gptimer_count_direction_t direction; /*!< 计数方向 */
    uint32_t resolution_hz;              /*!< 计数器工作频率,单位为Hz。因此每个计数值所用的时长为(1/resolution_hz)秒 */
    int intr_priority;                   /*!< 优先级。如果设置为0则自动分配较低的优先级(1、2、3) */
    struct {
        uint32_t intr_shared: 1;         /*!< 设置为1。则定时器中断号可以与其他外设共享 */
        uint32_t allow_pd: 1;            /*!< 如果设置,当系统进入休眠模式时,驱动器允许电源域断电。
                                                这可以节省功耗,但以消耗更多RAM来保存寄存器上下文为代价。 */
        uint32_t backup_before_sleep: 1; /*!< @已弃用, 功能与allow_pd相同 */
    } flags;                             /*!< GPTimer 配置标志*/
} gptimer_config_t;

成员说明

成员 可写参数
clk_src TIMER_SRC_CLK_APB
GPTIMER_CLK_SRC_DEFAULT
时钟源选择APB、选择默认时钟
direction GPTIMER_COUNT_DOWN
GPTIMER_COUNT_UP
向下计数、向上计数
resolution_hz uint32_t类型的数据 计时一次的时长为(1/resolution_hz)秒。
注意不要太小了,太小的话会超过分频系数导致系统重启,一般来说1MHz的频率就可以

注意: 关于时钟源的选择。两个可选参数都是一样的,因为esp32只有这一种时钟源,如果是其他芯片就有其他选择,具体查看源代码即可

ESP32 的 GPTimer 时钟源是 80 MHz。驱动内部会计算分频系数 divider = 80,000,000 / resolution_hz

  • 但硬件限制分频系数只能在 2 ~ 65536 之间,超出上限硬件直接断言失败 → 系统重启。

报警配置

/**
 * @brief 通用定时器报警配置
 */
typedef struct {
    uint64_t alarm_count;  /*!< 报警目标计数值 */
    uint64_t reload_count; /*!< 报警后重载值。需要auto_reload_on_alarm=1时才有效 */
    struct {
        uint32_t auto_reload_on_alarm: 1; /*!< 是否启动自动重载 */
    } flags;                              /*!< Alarm config flags*/
} gptimer_alarm_config_t;

常用API

创建定时器

/**
 * @说明 创建通用定时器,并返回句柄
 *
 * @注意 这个新创建的定时器会被重置为init状态
 *
 * @参数[输入] config 定时配置结构体
 * @参数[输出] ret_timer 返回的定时器句柄
 * @返回值
 *      - ESP_OK: 创建成功
 *      - ESP_ERR_INVALID_ARG: 创建失败,因为参数错误
 *      - ESP_ERR_NO_MEM: 创建失败,因为内存不足
 *      - ESP_ERR_NOT_FOUND: 创建失败,因为没有可用的定时器
 *      - ESP_FAIL: 创建失败,其他错误
 */
esp_err_t gptimer_new_timer(const gptimer_config_t *config, gptimer_handle_t *ret_timer);

删除定时器

/**
 * @brief 删除已创建的定时器句柄
 *
 * @note 这个定时器必须处于init状态才能被删除
 *
 * @param[in] timer 由“gptimer_new_timer”创建的句柄
 * @return
 *      - ESP_OK: 删除成功
 *      - ESP_ERR_INVALID_ARG: 删除失败,因为参数错误
 *      - ESP_ERR_INVALID_STATE: 删除失败,因为定时器没有处于init状态
 *      - ESP_FAIL: 删除失败,其他错误
 */
esp_err_t gptimer_del_timer(gptimer_handle_t timer);

更新定时器的原始计数

/**
 * @brief 设置定时器的原始计数值
 *
 * @note 更新活动定时器的原始计数时,定时器将立即从新值开始计数。
 * @note 该函数允许在ISR上下文中运行
 * @note 如果`CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM`被启用,该函数将被链接器放置在IRAM中,即使在Flash缓存被禁用时,也可以执行。
 *
 * @param[in] timer 用gptimer_new_timer创建的句柄
 * @param[in] value 要设置的Count值
 * @return
 *      - ESP_OK: 设置成功
 *      - ESP_ERR_INVALID_ARG: 设置失败,参数错误
 *      - ESP_FAIL: 设置失败,其他错误
 */
esp_err_t gptimer_set_raw_count(gptimer_handle_t timer, uint64_t value);

获取定时器的原始计数值

/**
 * @brief 获取定时器的原始计数值
 *
 * @note 这个函数将触发一个软件捕获事件,然后返回捕获的计数值。
 * @note 把获取到的count,和从gptimer_get_resolution函数返回的频率,可以把count转化为秒
 * @note 该函数允许在ISR上下文中运行
 * @note 如果`CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM`被启用,该函数将被链接器放置在IRAM中,即使在Flash缓存被禁用时,也可以执行。
 *
 * @param[in] timer 用gptimer_new_timer创建的定时器句柄
 * @param[out] value 返回获取的原始计数值
 * @return
 *      - ESP_OK: 获取成功
 *      - ESP_ERR_INVALID_ARG: 失败,参数错误
 *      - ESP_FAIL: 失败,其他错误
 */
esp_err_t gptimer_get_raw_count(gptimer_handle_t timer, uint64_t *value);

获取定时器的频率

/**
 * @brief 获取定时器的频率
 *
 * @note 通常定时器的分辨率与你在`gptimer_config_t::resolution_hz`中配置的相同,但是一些不稳定的时钟源(例如RC_FAST)会进行校准,实际的分辨率可能与配置的不同。
 *
 * @param[in] timer 由`gptimer_new_timer`创建的定时器句柄
 * @param[out] out_resolution 返回的定时器分辨率,单位为Hz
 * @return
 *      - ESP_OK: 获取成功
 *      - ESP_ERR_INVALID_ARG: 失败,参数错误
 *      - ESP_FAIL: 失败,其他错误
 */
esp_err_t gptimer_get_resolution(gptimer_handle_t timer, uint32_t *out_resolution);

设置定时器的报警事件

实际就是定时中断,只不过esp把定时,和中断给分开了

/**
 * @brief 设置GPTimer告警事件
 *
 * @note 该函数允许在ISR上下文中运行,因此您可以在任何ISR回调中立即更新新的警报动作。
 * @note 如果`CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM`被启用,该函数将被链接器放置在IRAM中,即使在Flash缓存被禁用时,也可以执行。
 *       在这种情况下,还请确保将`gptimer_alarm_config_t`实例放置在静态数据部分
 *       而不是在只读数据部分。例如:static gptimer_alarm_config_t alarm_config ={…};
 *
 * @param[in] timer 由`gptimer_new_timer`创建的定时器句柄
 * @param[in] config 报警配置结构体。注意如果传入NULL意味着禁用报警功能
 * @return
 *      - ESP_OK: 设置成功
 *      - ESP_ERR_INVALID_ARG: 失败,参数错误
 *      - ESP_FAIL: 失败,其他错误
 */
esp_err_t gptimer_set_alarm_action(gptimer_handle_t timer, const gptimer_alarm_config_t *config);

使能定时器

/**
 * @brief 使能定时器
 *
 * @note 该函数的可以将定时器的状态从“init”转换为“enable”
 * @note 如果`gptimer_register_event_callbacks`是惰性安装的,这个函数将启用中断服务。
 * @note 如果在`gptimer_config_t`中选择了特定的源时钟(例如APB),而`CONFIG_PM_ENABLE`被启用,则此函数将获得一个PM锁。
 * @note 启用计时器并不意味着启动它。请参阅`gptimer_start`以了解如何使定时器开始计数。
 *
 * @param[in] timer 由`gptimer_new_timer`创建的定时器句柄
 * @return
 *      - ESP_OK: 成功
 *      - ESP_ERR_INVALID_ARG: 失败,参数错误
 *      - ESP_ERR_INVALID_STATE: 启动失败,因为已经启动了
 *      - ESP_FAIL: 失败,其他错误
 */
esp_err_t gptimer_enable(gptimer_handle_t timer);

惰性安装

“惰性安装 (Lazy Install)” 是 ESP-IDF 驱动里的一种延迟执行策略。用大白话说就是:注册回调的时候,只是把任务登记在册,但并没有真正去“打通”硬件中断,直到你调用 enable 的时候才一次性把所有硬件配置好

  • 节省资源:一个定时器可能会先创建好、配置好,但过一段时间才真正使用。如果注册回调时就立刻占用中断向量、使能中断,可能导致系统中断资源紧张,或者在你还没准备好时意外响应中断。
  • 安全有序enable 是定时器从“初始化”到“就绪”的最终步骤,所有准备工作(时钟源选择、分辨率设置、回调注册)都应该在此之前完成,enable 一次性把硬件激活,避免中间状态。

禁用定时器

/**
 * @brief 禁用定时器
 *
 * @note 该函数可以将定时器的状态从enable转化为init
 * @note 如果安装了中断服务,这个函数将禁用它
 * @note 如果在`gptimer_enable`函数中获得PM锁,该函数将释放PM锁。
 * @note 禁用计时器并不意味着停止它。请参阅`gptimer_stop`以了解如何使定时器停止计数。
 *
 * @param[in] timer 由`gptimer_new_timer`创建的定时器句柄
 * @return
 *      - ESP_OK: 成功
 *      - ESP_ERR_INVALID_ARG: 失败,参数错误
 *      - ESP_ERR_INVALID_STATE: 禁用失败,因为还没有启动定时器。或者定时器计数没有停止
 *      - ESP_FAIL: 其他错误
 */
esp_err_t gptimer_disable(gptimer_handle_t timer);

定时器开始计数

/**
 * @brief 启动定时器 (内部计数器开始计数)
 *
 * @note 这个函数将定时器的状态从enable转为run
 * @note 该函数允许在ISR上下文中运行
 * @note 如果`CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM`被启用,该函数将被链接器放置在IRAM中,即使在Flash缓存被禁用时,也可以执行。
 *
 * @param[in] timer 由`gptimer_new_timer`创建的定时器句柄
 * @return
 *      - ESP_OK: 成功
 *      - ESP_ERR_INVALID_ARG: 失败,参数错误
 *      - ESP_ERR_INVALID_STATE: 失败,因为定时器没有启用或已经在运行中
 *      - ESP_FAIL: 失败,其他错误
 */
esp_err_t gptimer_start(gptimer_handle_t timer);

定时器停止计数

/**
 * @brief 停止定时器 (内部计数器停止计数)
 *
 * @note 这个函数将定时器的状态从run转为enable
 * @note 该函数允许在ISR上下文中运行
 * @note 如果`CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM`被启用,该函数将被链接器放置在IRAM中,即使在Flash缓存被禁用时,也可以执行。
 *
 * @param[in] timer 由`gptimer_new_timer`创建的定时器句柄
 * @return
 *      - ESP_OK: 成功
 *      - ESP_ERR_INVALID_ARG: 失败,参数错误
 *      - ESP_ERR_INVALID_STATE: 失败,定时器没有运行
 *      - ESP_FAIL: 失败,其他错误
 */
esp_err_t gptimer_stop(gptimer_handle_t timer);

中断

回调函数组

/**
 * @brief GPTimer回调函数组
 * @note 这些回调函数都运行在ISR环境下
 * @note 当启用CONFIG_GPTIMER_ISR_CACHE_SAFE时,回调函数本身和它调用的函数应该放在IRAM中
 */
typedef struct {
    gptimer_alarm_cb_t on_alarm; /*!< 定时器告警回调函数 */
    // 有的型号会有输入捕获的回调函数
} gptimer_event_callbacks_t;

回调函数类型

/**
 * @brief 定时报警中断回调函数原型
 *
 * @param[in] timer 由`gptimer_new_timer`创建的定时器句柄
 * @param[in] edata GPTimer告警事件数据,内部自己获取
 * @param[in] user_ctx 用户数据,通过`gptimer_register_event_callbacks`传递
 * @return 高优先级任务是否被该功能唤醒
 */
typedef bool (*gptimer_alarm_cb_t)(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx);

GPTimer告警事件数据

/**
 * @brief GPTimer告警事件数据
 */
typedef struct {
    uint64_t count_value; /*!< 当前计数值 */
    uint64_t alarm_value; /*!< 当前报警值 */
} gptimer_alarm_event_data_t;

关于返回值的使用

/*
true	告诉系统:"我在 ISR 中进行了需要任务切换的操作,请帮我在 ISR 结束时触发 portYIELD_FROM_ISR()"
false	告诉系统:"不需要任务切换"(最常见的返回值)
*/
    
// 报警中断回调函数
static bool IRAM_ATTR gptimer_alarm_isr(gptimer_handle_t timer, 
                                         const gptimer_alarm_event_data_t* edata, 
                                         void* user_param) 
{
    // 1. 如果需要在 ISR 中操作 FreeRTOS 对象(信号量、队列等),
    //    必须定义这个变量
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    // 2. 执行简短的中断操作
    // 例如:翻转一个 GPIO
    gpio_set_level(LED_GPIO, !gpio_get_level(LED_GPIO));

    // 或者:释放一个信号量给任务去处理
    // xSemaphoreGiveFromISR(my_semaphore, &xHigherPriorityTaskWoken);

    // 3. 判断是否需要任务切换
    //    如果返回 false,系统会自动忽略 xHigherPriorityTaskWoken 的值
    //    如果返回 true,系统会检查 xHigherPriorityTaskWoken 并决定是否切换
    return false;  // 不触发任务切换(最常用)
    // 或者,返回 true,告诉系统需要检查是否要做任务切换
    // return (xHigherPriorityTaskWoken == pdTRUE);
}

中断回调注册函数

/**
 * @brief 为GPTimer设置回调函数
 *
 * @note 用户注册的回调函数应该可以在ISR上下文中运行
 * @note 第一次调用这个函数需要在调用`gptimer_enable`函数之前。
 * @note 用户可以通过调用此函数并将`cbs`结构中的回调成员设置为NULL来注销先前注册的回调函数。
 *
 * @param[in] timer 由`gptimer_new_timer`创建的定时器句柄
 * @param[in] cbs 回调函数组
 * @param[in] user_data 用户数据,它将直接传递给回调函数
 * @return
 *      - ESP_OK: 成功
 *      - ESP_ERR_INVALID_ARG: 错误,参数错误
 *      - ESP_ERR_INVALID_STATE: 定时器未处于init状态,设置事件回调失败
 *      - ESP_FAIL: 由于其他错误,设置事件回调失败
 */
esp_err_t gptimer_register_event_callbacks(gptimer_handle_t timer, const gptimer_event_callbacks_t *cbs, void *user_data);

示例

定时器定时中断完整流程

#include "freertos/FreeRTOS.h"
#include "driver/gptimer.h"

gptimer_handle_t gptimer1;  // 定时器1句柄

// 报警中断回调函数
static bool IRAM_ATTR gptimer_alarm_isr(gptimer_handle_t timer, const gptimer_alarm_event_data_t* edata, void* user_ctx) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;          // 记录是否唤醒高优先级任务

    /*
    逻辑处理,注意只能
    */
    
    // 返回 true,告诉系统需要检查是否要做任务切换
    return (xHigherPriorityTaskWoken == pdTRUE);
}

void app_main(void) {
    // 配置定时1
    gptimer_config_t gptimer1_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT, // 默认时钟源
        .direction = GPTIMER_COUNT_UP,      // 向上计数
        .resolution_hz = 1 * 1000 * 1000,   // 1us计数一次
        .intr_priority = 0,                 // 自动分配较低的优先级
    };

    // 创建定时器
    gptimer_new_timer(&gptimer1_config, &gptimer1);

    // 配置报警
    gptimer_alarm_config_t gptimer_alarm_config = {
        .alarm_count = 1000,        // 计数一千次中断一次。1ms
        .reload_count = 0,          // 重装载值,触发报警后回到该值
        .flags.auto_reload_on_alarm = 1,    // 自动重装载
    };

    // 设置报警事件,绑定到定时器1
    gptimer_set_alarm_action(gptimer1, &gptimer_alarm_config);

    // 绑定编写的中断回调函数
    gptimer_event_callbacks_t gptiemr_cbs = {
        .on_alarm = gptimer_alarm_isr,
    };

    // 注册中断回调函数
    gptimer_register_event_callbacks(gptimer1, &gptiemr_cbs, NULL);

    // 启动定时器,相当于给硬件上电
    gptimer_enable(gptimer1);

    // 定时器开始工作
    gptimer_start(gptimer1);
}

定时器控制io口定时1s翻转

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "driver/gptimer.h"
#include "esp_log.h"

#define LED_MASK        (0x01UL << GPIO_NUM_16)
#define LED_BIT         GPIO_NUM_16

gptimer_handle_t gptimer1;  // 定时器1句柄
SemaphoreHandle_t bit_sem;  // 二值信号量

// 报警中断回调函数
static bool IRAM_ATTR gptimer_alarm_isr(gptimer_handle_t timer, const gptimer_alarm_event_data_t* edata, void* user_ctx) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;          // 记录是否唤醒高优先级任务

    // 释放一个二值信号量。FromISR版本
    xSemaphoreGiveFromISR(bit_sem, &xHigherPriorityTaskWoken);

    // 返回 true,告诉系统需要检查是否要做任务切换
    return (xHigherPriorityTaskWoken == pdTRUE);
}

// 任务,定时时间到翻转io
void LedFlicker(void* param) {
    uint8_t level = 0;
    while (1) {
        xSemaphoreTake(bit_sem, portMAX_DELAY);
        level = !level;
        gpio_set_level(LED_BIT, level);
        ESP_LOGI("main", "翻转");
    }
}

void app_main(void) {
    // 创建二值信号量
    bit_sem = xSemaphoreCreateBinary();

    // 配置GPIO
    gpio_config_t led_config = {
        .pin_bit_mask = LED_MASK,               // 选择引脚
        .mode = GPIO_MODE_OUTPUT,               // 输出模式
        .intr_type = GPIO_INTR_DISABLE,         // 关闭中断
        .pull_down_en = GPIO_PULLDOWN_DISABLE,  // 关闭下拉
        .pull_up_en = GPIO_PULLUP_DISABLE,      // 关闭上拉
    };

    // 初始化io
    gpio_config(&led_config);

    // 创建任务
    xTaskCreatePinnedToCore(LedFlicker, "led闪烁", 2048, NULL, 3, NULL, 0);

    // 配置定时1
    gptimer_config_t gptimer1_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT, // 默认时钟源
        .direction = GPTIMER_COUNT_UP,      // 向上计数
        .resolution_hz = 1 * 1000 * 1000,   // 1us计数一次
        .intr_priority = 0,                 // 自动分配较低的优先级
    };

    // 创建定时器
    gptimer_new_timer(&gptimer1_config, &gptimer1);

    // 配置报警
    gptimer_alarm_config_t gptimer_alarm_config = {
        .alarm_count = 1000000,        // 计数1s中断一次,
        .reload_count = 0,              // 重装载值,触发报警后回到该值
        .flags.auto_reload_on_alarm = 1,    // 自动重装载
    };

    // 设置报警事件,绑定到定时器1
    gptimer_set_alarm_action(gptimer1, &gptimer_alarm_config);

    // 绑定编写的中断回调函数
    gptimer_event_callbacks_t gptiemr_cbs = {
        .on_alarm = gptimer_alarm_isr,
    };

    // 注册中断回调函数
    gptimer_register_event_callbacks(gptimer1, &gptiemr_cbs, NULL);

    // 启动定时器,相当于给硬件上电
    gptimer_enable(gptimer1);

    // 定时器开始工作
    gptimer_start(gptimer1);
}

LEDC

概述

头文件: #include "driver/ledc.h"

作用: 生成PWM信号

数量:

芯片型号 LEDC 定时器数量 LEDC 通道数量
ESP32 4 个 8 个
ESP32-S3 4 个 8 个
ESP32-C3 4 个 6 个
ESP32-C6 4 个 6 个

注意与 GPTimer 的关系: LEDC 和 GPTimer 是两套完全独立的硬件外设,各自有各自的定时器,互不占 用。使用 LEDC 不需要配置 GPTimer,内部自动调用硬件。

配置结构体

定时器配置

typedef struct {
    ledc_mode_t speed_mode;                /*!< 速度模式 */
    ledc_timer_bit_t duty_resolution;      /*!< 占空比分辨率 */
    ledc_timer_t  timer_num;               /*!< 选择定时器。范围: (0 - LEDC_TIMER_MAX-1) */
    uint32_t freq_hz;                      /*!< PWM频率 (Hz) */
    ledc_clk_cfg_t clk_cfg;                /*!< 为定时器选择时钟源 */
    bool deconfigure;                      /*!< 为True时表示注销该定时器(注意PWM必须在关闭状态) */
} ledc_timer_config_t;
成员 参数 说明
speed_mode LEDC_LOW_SPEED_MODE
LEDC_HIGH_SPEED_MODE
低速模式(通用性好,所有芯片支持)
高速模式
duty_resolution LEDC_TIMER_x_BIT(其中x可以为1~20) 占空比分辨率。
例如:13位的最大占空比为8191,14位的最大占空比为16383
timer_num LEDC_TIMER_x(其中x可以为0~3) 定时器编号,选择硬件定时器中的一个
freq_hz uint32类型数据 PWM的频率,单位Hz
clk_cfg LEDC_AUTO_CLK(推荐)
LEDC_APB_CLK
LEDC_USE_RC_FAST_CLK
LEDC_USE_XTAL_CLK
根据设置的分辨率和占空比自动选择时钟源
选择APB作为时钟源
选择RC_FAST作为时钟源
选择REF_TICK作为时钟源
deconfigure false(正常配置)
true(反配置/注销)
设为 true 时,表示注销该定时器。
此时 duty_resolutionfreq_hzclk_cfg 都会被忽略。
注销前必须确保定时器已暂停。

注意,配置的占空比分辨率和频率一定要合理,这两个决定了LEDC内部的分频系数,设置不合理会导致配置失败

分辨率越高,一个周期内可分的份数就越多,比如13bit的分辨率,表示可以把一个周期分成8191份;频率越高,一个周期的时间就越短;

假设定时器的时钟源是80MHz,频率需要达到100kHz。想要得到最大分辨率,分频系数需要为1,也就是不分频,LEDC的时钟频率达到最大80MHz,因此当输出频率需要100KHz时,最大计数值 = 时钟源频率 / PWM频率 = 80MHz / 100KHz = 800,所以2^分辨率必须 <= 800,也就是分辨率最大是9位

通道配置

/**
 * @brief 为LEDC通道配置参数
 */
typedef struct {
    int gpio_num;                   /*!< LEDC输出的GPIO。假设使用gpio16,则:gpio_num = 16 */
    ledc_mode_t speed_mode;         /*!< LEDC速度模式选择。高速模式(仅esp32拥有)或者低速模式 */
    ledc_channel_t channel;         /*!< LEDC的通道。可写值:0 ~ LEDC_CHANNEL_MAX-1*/
    ledc_intr_type_t intr_type;     /*!< 中断配置。渐变中断使能和失能 */
    ledc_timer_t timer_sel;         /*!< 为通道选择定时器(绑定定时器)。可写值: 0 ~ LEDC_TIMER_MAX-1 */
    uint32_t duty;                  /*!< 通道的占空比。可设置的范围是:0 ~(2^duty_resolution) */
    int hpoint;                     /*!< 输出跳变点。 可设置的范围是:0 ~(2^duty_resolution)-1 */
    ledc_sleep_mode_t sleep_mode;   /*!< 选择该通道在系统进入轻度睡眠时的状态 */
    struct {
        unsigned int output_invert: 1;/*!< 是否反转输出的极性。 */
    } flags;                        /*!< LEDC flags */

} ledc_channel_config_t;
成员 可写值 说明
gpio_num GPIO_NUM_x 选择输出PWM的引脚
speed_mode LEDC_LOW_SPEED_MODE
LEDC_SPEED_MODE_MAX
高速模式
低速模式
注意必须与定时器一致
channel 0 ~ LEDC_CHANNEL_MAX-1
或者:LEDC_CHANNEL_x
通道编号
intr_type LEDC_INTR_DISABLE
LEDC_INTR_FADE_END
关闭中断
开启渐变中断
timer_sel 0 ~ LEDC_TIMER_MAX-1
或者:LEDC_TIMER_x
该通道绑定的定时器编号
duty 0 ~ (2^duty_resolution) 占空比
hpoint 0 ~(2^duty_resolution)-1 PWM 在一个周期内,计数器达到多少时,输出开始翻转为高电平。一般默认给0即可(下面有详细解释)
sleep_mode LEDC_SLEEP_MODE_NO_ALIVE_NO_PD
LEDC_SLEEP_MODE_NO_ALIVE_ALLOW_PD
LEDC_SLEEP_MODE_KEEP_ALIVE
进入休眠时不断电也不工作(默认模式)
不工作,可以断电(低功耗模式)
保持工作(高功耗模式)
output_invert 0、1 是否反转输出的极性

hpoint详解

一个 PWM 周期内,LEDC 默认的行为是:

  • 周期开始时输出高电平
  • 计数器达到 duty 值时,输出翻转为低电平

hpoint 就是用来打破这个默认行为的。它规定了“输出跳变为高电平的时机”。

场景 hpoint duty 波形描述
标准 PWM(默认) 0 你设的值 周期开始时立即输出高电平,到达 duty 时变低
有 hpoint 的 PWM 非零值 你设的值 计数器到达 hpoint 时才输出高电平,到达 duty 时变低

如果 hpoint = 0,就是标准的 PWM,和你之前理解的一样。

总结: hpoint 是 LEDC 输出由低变高的计数器阈值。一般设为 0(默认行为)。 仅在需要精细控制多路 PWM 相位差或同步时修改。

配置流程

  1. 配定时器(ledc_timer_config
    决定 PWM 的频率占空比分辨率,时钟源推荐直接用 LEDC_AUTO_CLK。这是所有通道的公共基础。

  2. 配通道(ledc_channel_config
    决定 PWM 从哪个 GPIO 输出初始占空比是多少,以及绑定到哪个定时器

    ⚠️ speed_modetimer_sel 必须与第 1 步定时器的设置完全一致

  3. 多通道共用一个定时器
    如果多个通道需要相同的频率和分辨率,只需配一个 ledc_timer_config_t,然后定义多个 ledc_channel_config_t,将它们的 timer_sel 都指向同一个定时器即可。它们的占空比可以各自独立设置。

常用API

辅助函数:查找最大可用的占空比分辨率

/**
 * @brief 辅助函数,用于查找ledc_timer_config()的最大可能占空比分辨率
 *
 * @param src_clk_freq LEDC定时器的时钟源频率 (Hz) (详见 ledc_clk_cfg_t 的 doxygen 注释,或通过 esp_clk_tree_src_get_freq_hz 函数获取。)
 * @param timer_freq 所需要设置的PWM频率 (Hz)
 *
 * @return
 *     - 0 无法实现该定时器的频率
 *     - 非0 返回的是最大可用的占空比分辨率位数(比如返回 13 表示最高可选 13 位)
 */
uint32_t ledc_find_suitable_duty_resolution(uint32_t src_clk_freq, uint32_t timer_freq);

使用时机

  • 你明确知道需要的频率(比如舵机要 50Hz,LED 要 1KHz),但不想自己算分辨率,直接调这个函数帮你算。
  • 你想在代码里动态适配不同频率,而不是写死一个分辨率值。

创建LEDC的定时器

/**
 * @brief LEDC定时器配置
 *
 * @param  timer_conf LEDC定时器配置结构体指针
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_ARG 参数错误
 *     - ESP_FAIL 根据给定的频率和当前的duty_resolution无法找到一个合适的预分频器。
 *     - ESP_ERR_INVALID_STATE 定时器不能取消配置,因为定时器没有配置或没有暂停
 */
esp_err_t ledc_timer_config(const ledc_timer_config_t *timer_conf);

创建LEDC通道

/**
 * @brief 配置LEDC通道
 *
 * @param ledc_conf LEDC通道配置结构的指针
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_ARG 参数错误
 */
esp_err_t ledc_channel_config(const ledc_channel_config_t *ledc_conf);

设置占空比(非线程安全)

/**
 * @brief 设置LEDC的占空比
 *        only after calling ledc_update_duty will the duty update.
 *
 * @note  这个函数不是线程安全的。多任务中需要调用线程安全的版本:ledc_set_duty_and_update
 * @note  需要调用ledc_update_duty后才能生效设置
 *
 * @param speed_mode 速度模式
 * @param channel 选择要设置的通道
 * @param duty 占空比,范围: [0, (2^duty_resolution)]
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_ARG 参数错误
 */
esp_err_t ledc_set_duty(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t duty);

更新占空比(非线程安全)

/**
 * @brief 更新LEDC通道参数
 *
 * @note  调用此函数激活已经更新的LEDC参数
 *        调用ledc_set_duty后,需要使用该函数更新设置
 *        新的LEDC参数直到下一个PWM周期才生效。
 * @note  这个函数不是线程安全的。多任务中需要调用线程安全的版本:ledc_set_duty_and_update
 * @note  如果`CONFIG_LEDC_CTRL_FUNC_IN_IRAM`被启用,该函数将被链接器放置在IRAM中,
 *        即使在缓存被禁用时,也可以执行。
 * @note  该函数允许在ISR上下文中运行。
 *
 * @param speed_mode 设置速度模式。注意并非所有目标都支持高速模式
 * @param channel LEDC通道。范围 (0 - LEDC_CHANNEL_MAX-1), 从ledc_channel_t中选择
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_ARG 参数错误
 */
esp_err_t ledc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel);

设置并更新占空比(线程安全版)

/**
 * @brief 线程安全的API,用于设置LEDC通道的占空比,并在占空比更新时返回。
 *
 * @note  对于ESP32,硬件不支持对正在执行渐变的通道进行修改占空比
 *
 * @param speed_mode 设置速度模式。注意并非所有目标都支持高速模式
 * @param channel LEDC通道。范围 (0 - LEDC_CHANNEL_MAX-1), 从ledc_channel_t中选择
 * @param duty 设置LEDC的占空比,范围是 [0, (2**duty_resolution)]
 * @param hpoint 设置输出跳变点, 范围是 [0, (2**duty_resolution)-1]
 *
 * @return
 *      - ESP_OK 成功
 *      - ESP_ERR_INVALID_STATE 通道未初始化
 *      - ESP_ERR_INVALID_ARG 参数错误
 *      - ESP_FAIL 函数初始化错误
 */
esp_err_t ledc_set_duty_and_update(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t duty, uint32_t hpoint);

停止PWM输出

/**
 * @brief 关闭LEDC
 *        关闭LEDC的输出,并设置引脚闲置时的电平
 *
 * @note  如果`CONFIG_LEDC_CTRL_FUNC_IN_IRAM`被启用,该函数将被链接器放置在IRAM中,
 *        即使在缓存被禁用时,也可以执行。
 * @note  该函数允许在ISR上下文中运行。
 *
 * @param  speed_mode 设置速度模式。注意并非所有目标都支持高速模式
 * @param  channel LEDC通道。范围 (0 - LEDC_CHANNEL_MAX-1), 从ledc_channel_t中选择
 * @param  idle_level LEDC停止后,设置输出空闲电平。
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_ARG 参数错误
 */
esp_err_t ledc_stop(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t idle_level);

停止后重新启动需要重新设置占空比

再次调用 ledc_set_duty + ledc_update_duty

或者直接使用ledc_set_duty_and_update一步到位

如果之前启动了硬件渐变,重新启动一个渐变也会恢复PWM

不常用API简介

函数 作用
ledc_timer_pause() / ledc_timer_resume() 暂停/恢复定时器计数
ledc_timer_rst() 重置定时器
ledc_bind_channel_timer() 手动绑定通道和定时器(一般用 channel_config 就够了)
ledc_get_duty() / ledc_get_hpoint() 读取当前占空比 / hpoint 值
ledc_set_duty_with_hpoint() 同时设置 duty 和 hpoint
函数 作用
ledc_set_freq() 动态修改 PWM 频率
ledc_get_freq() 读取当前频率
ledc_set_pin() 单独给某个通道换一个 GPIO 引脚

硬件渐变

LEDC只有硬件渐变完成中断这一个中断

配置步骤

  1. 配置定时器 (ledc_timer_config)
  2. 配置通道 (ledc_channel_config)
  3. 安装渐变中断 (ledc_fade_func_install) ← 必须!只装一次
  4. 如果需要知道渐变完成,就要注册一个中断函数
  5. 设置渐变参数
  6. 启动渐变 (ledc_fade_start)

安装硬件渐变功能

整个系统只安装一次

/**
 * @brief 安装LEDC的渐变功能。此函数将占用LEDC模块的中断。
 *
 * @param intr_alloc_flags 中断属性配置
 *        ESP_INTR_FLAG_* 类型的值。详细信息在esp_intr_alloc.h中
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_ARG 中断属性错误
 *     - ESP_ERR_NOT_FOUND 找不到可用中断源
 *     - ESP_ERR_INVALID_STATE 已经开启渐变中断
 */
esp_err_t ledc_fade_func_install(int intr_alloc_flags);

intr_alloc_flags在本文的GPIO中断中有详细介绍,下面只是简介

标志位 含义 何时用
0 默认配置 最常用,啥都不用管
ESP_INTR_FLAG_LEVEL1 ~ ESP_INTR_FLAG_LEVEL3 中断优先级 1~3 需要指定优先级时
ESP_INTR_FLAG_SHARED 允许多个外设共享中断 多个设备共用一个中断源时
ESP_INTR_FLAG_EDGE 边沿触发 配合边沿型中断使用
ESP_INTR_FLAG_IRAM ISR 放在 IRAM 中运行 需要 Flash Cache 禁用时也能响应中断

设置渐变(指定目标占空比和总时长)(非线程安全版)

/**
 * @brief 设置渐变功能,根据时间控制
 *
 * @note  调用这个函数之前需要先调用一次ledc_fade_func_install()
 *        再次之后调用ledc_fade_start()开始渐变
 * @note  ledc_set_fade_with_step, ledc_set_fade_with_time 和 ledc_fade_start 不是线程安全的,如果多任务操作同一个通道,需要使用线程安全版:ledc_set_fade_step_and_start
 * @note  对于ESP32,硬件不支持对正在执行渐变的通道进行修改占空比
 *
 * @param speed_mode 设置速度模式。注意并非所有目标都支持高速模式
 * @param channel LEDC通道。范围 (0 - LEDC_CHANNEL_MAX-1), 从ledc_channel_t中选择
 * @param target_duty 渐变的目标占空比。范围: [0, (2**duty_resolution)]
 * @param desired_fade_time_ms 预期的渐变时间 ( ms ).
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_ARG 参数错误
 *     - ESP_ERR_INVALID_STATE 通道为初始化
 *     - ESP_FAIL 函数初始化错误
 */
esp_err_t ledc_set_fade_with_time(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t target_duty, int desired_fade_time_ms);

关于期望的渐变时间的误差说明

注意: 由于内部计算存在舍入误差,实际完成渐变的时间可能与预期有差异,最快可能比预期快 2 倍,最慢可能比预期慢 2 倍。

具体来说:

  • 总周期数 = 期望时间(ms) × PWM频率(Hz) / 1000
  • 占空比差值 = |目标占空比 - 当前占空比|

如果总周期数 > 占空比差值,渐变可能比预期更快完成(因为步长被放大,步子迈得更大)。
反之,如果总周期数 < 占空比差值,渐变可能比预期更慢完成(因为步长被缩小,需要更多步才能走完)。

优化建议:总周期数 / 占空比差值 的比值越接近整数,实际时间就越接近预期时间。
如果需要一个非常精确的渐变时间,建议把整个渐变拆分成多个小的线性渐变,让每一段的 总周期数 / 占空比差值 比值都能被整除。

总结: 如果你需要精确的渐变时间,让 PWM频率 × 期望秒数 能被 占空比差值 整除,或者把渐变拆成多段,让每一段都满足这个条件。

设置渐变(指定步长和每步周期数)(非线程安全版)

/**
 * @brief 设置渐变功能,根据步长和周期数控制
 *
 * @note  调用这个函数之前需要先调用一次ledc_fade_func_install()
 *        再次之后调用ledc_fade_start()开始渐变
 * @note  ledc_set_fade_with_step, ledc_set_fade_with_time 和 ledc_fade_start 不是线程安全的,如果多任务操作同一个通道,需要使用线程安全版:ledc_set_fade_step_and_start
 * @note  对于ESP32,硬件不支持对正在执行渐变的通道进行修改占空比
 *
 * @param speed_mode 设置速度模式。注意并非所有目标都支持高速模式
 * @param channel LEDC通道。范围 (0 - LEDC_CHANNEL_MAX-1), 从ledc_channel_t中选择
 * @param target_duty 渐变的目标占空比。范围: [0, (2**duty_resolution)]
 * @param scale 每一步的占空比变化量。值越大,每一步变化越剧烈,渐变越快
 * @param cycle_num 每隔多少个PWM周期走一步。值越大,每一步持续越久,渐变越慢
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_ARG 参数错误
 *     - ESP_ERR_INVALID_STATE 通道未初始化
 *     - ESP_FAIL 函数初始化错误
 */
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);

关于渐变速度和精度的控制说明

核心概念:

  • 渐变总步数 = |目标占空比 - 当前占空比| / scale
  • 渐变总时长 = 总步数 × cycle_num 个 PWM 周期

参数选择建议:

你想要的效果 scale 调法 cycle_num 调法
更平滑的渐变 减小 scale(每一步变得少,走更多步) 减小 cycle_num(步伐更紧凑)
更快的渐变 增大 scale(每一步变得多,步子大) 减小 cycle_num(步伐更紧凑)
更慢的渐变 减小 scale(每一步变得少) 增大 cycle_num(每一步停更久)

举例:PWM 频率 1KHz(每秒 1000 个周期),13 位分辨率(最大 8191)

  • scale=100, cycle_num=20:每一步变 100,每 20ms 走一步。从 0 渐变到 8000 需要 80 步,总时长 = 80 × 20ms = 1.6 秒
  • scale=50, cycle_num=10:每一步变 50,每 10ms 走一步。从 0 渐变到 8000 需要 160 步,总时长 = 160 × 10ms = 1.6 秒(和上面一样快,但更平滑)。
  • scale=500, cycle_num=5:每一步变 500,每 5ms 走一步。从 0 渐变到 8000 需要 16 步,总时长 = 16 × 5ms = 0.08 秒(非常快)。

注意:scale 和 cycle_num 都必须是整数。如果 |目标占空比 - 当前占空比| 不能被 scale 整除,最后一步的占空比变化量会小于 scale,硬件会自动处理。

卸载渐变中断

/**
 * @brief Uninstall LEDC fade function.
 */
void ledc_fade_func_uninstall(void);

开始渐变

/**
 * @brief 开始渐变
 *
 * @note  调用这个函数之前需要先调用一次 ledc_fade_func_install()
 *        在 ledc_set_fade_with_time 或 ledc_set_fade_with_step 之后立即调用此函数来启动渐变
 * @note  这个 API 不是线程安全的,使用时请注意
 * @note  对于 ESP32,硬件不支持对正在执行渐变的通道进行修改占空比
 *
 * @param speed_mode 设置速度模式。注意并非所有目标都支持高速模式
 * @param channel LEDC 通道编号
 * @param fade_mode 选择阻塞或非阻塞模式。详见 ledc_types.h 中的 ledc_fade_mode_t
 *       如果选择 LEDC_FADE_WAIT_DONE 模式,此函数会一直阻塞直到渐变完成才返回
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_STATE 通道未初始化 或 渐变功能未安装
 *     - ESP_ERR_INVALID_ARG 参数错误
 */
esp_err_t ledc_fade_start(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_fade_mode_t fade_mode);

两种渐变模式对比

模式 含义 何时用
LEDC_FADE_NO_WAIT 非阻塞:启动后立即返回,渐变在后台由硬件自动完成 常用,不卡任务
LEDC_FADE_WAIT_DONE 阻塞:一直等到渐变完成才返回,期间调用此函数的任务进入阻塞状态 需要渐变完成后再做下一步操作时

非阻塞模式下如何知道渐变完成?

如果需要获知渐变完成的通知,可以注册渐变结束中断回调:

c

// 渐变完成回调函数
static bool fade_end_cb(const ledc_cb_param_t *param, void *user_arg) {
   // 渐变完成后的处理逻辑
   BaseType_t high_task_awoken = pdFALSE;
   xSemaphoreGiveFromISR(my_semaphore, &high_task_awoken);
   return (high_task_awoken == pdTRUE);
}

// 注册回调
ledc_cbs_t cbs = {
   .fade_cb = fade_end_cb
};
ledc_cb_register(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, &cbs, NULL);

注意

  • 回调函数运行在 ISR 环境中,不能有阻塞操作
  • 如果只是简单场景,用阻塞模式让任务等待更简单直接

设置并开启渐变(指定目标占空比和总时长)(线程安全)

/**
 * @brief 设置并启动渐变,根据时间控制(线程安全版)
 *
 * @note  调用这个函数之前需要先调用一次 ledc_fade_func_install()
 * @note  对于ESP32,硬件不支持对正在执行渐变的通道进行修改占空比
 * @note  本函数将"设置渐变参数"和"启动渐变"两步合并为一步原子操作,保证线程安全
 *
 * @param speed_mode 设置速度模式。注意并非所有目标都支持高速模式
 * @param channel LEDC通道。范围 (0 - LEDC_CHANNEL_MAX-1), 从 ledc_channel_t 中选择
 * @param target_duty 渐变的目标占空比。范围: [0, (2**duty_resolution)]
 * @param desired_fade_time_ms 预期的渐变时间 ( ms )
 * @param fade_mode 选择阻塞或非阻塞模式
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_ARG 参数错误
 *     - ESP_ERR_INVALID_STATE 通道未初始化
 *     - ESP_FAIL 函数初始化错误
 */
esp_err_t ledc_set_fade_time_and_start(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t target_duty, uint32_t desired_fade_time_ms, ledc_fade_mode_t fade_mode);

设置并开启渐变(指定步长和每步周期数)(线程安全版)

/**
 * @brief 设置并启动渐变,根据步长和周期数控制(线程安全版)
 *
 * @note  调用这个函数之前需要先调用一次 ledc_fade_func_install()
 * @note  对于ESP32,硬件不支持对正在执行渐变的通道进行修改占空比
 * @note  本函数将"设置渐变参数"和"启动渐变"两步合并为一步原子操作,保证线程安全
 *
 * @param speed_mode 设置速度模式。注意并非所有目标都支持高速模式
 * @param channel LEDC通道。范围 (0 - LEDC_CHANNEL_MAX-1), 从 ledc_channel_t 中选择
 * @param target_duty 渐变的目标占空比。范围: [0, (2**duty_resolution)]
 * @param scale 每一步的占空比变化量。值越大,每一步变化越剧烈,渐变越快
 * @param cycle_num 每隔多少个PWM周期走一步。值越大,每一步持续越久,渐变越慢
 * @param fade_mode 选择阻塞或非阻塞模式
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_ARG 参数错误
 *     - ESP_ERR_INVALID_STATE 通道未初始化
 *     - ESP_FAIL 函数初始化错误
 */
esp_err_t ledc_set_fade_step_and_start(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t target_duty, uint32_t scale, uint32_t cycle_num, ledc_fade_mode_t fade_mode);

渐变使用流程总结

前提:已配置好定时器和通道(ledc_timer_config + ledc_channel_config

1. 安装渐变功能(整个系统只需一次)

ledc_fade_func_install(0);   // 参数为中断分配标志,一般填0即可

2. 设置并启动渐变

▶ 通道仅被一个任务调用(可使用分步,也可用合并版)

以时间为准(非线程安全,分步)

// 设置渐变参数:目标占空比 + 期望时长(ms)
ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 8000, 2000);
// 启动渐变(可选择阻塞/非阻塞)
ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT); // 或 LEDC_FADE_WAIT_DONE

以步长为单位(非线程安全,分步)

// 设置渐变参数:目标占空比 + 每步变化量 + 每步周期数
ledc_set_fade_with_step(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 8000, 50, 20);
// 启动渐变
ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT); // 或 LEDC_FADE_WAIT_DONE

也可直接使用下方线程安全合并版,单任务下同样有效。

▶ 通道被多个任务调用(必须使用线程安全合并版)

以时间为准(线程安全,一步到位)

ledc_set_fade_time_and_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 8000, 2000, LEDC_FADE_NO_WAIT);

以步长为单位(线程安全,一步到位)

ledc_set_fade_step_and_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 8000, 50, 20, LEDC_FADE_NO_WAIT);

3. 阻塞与非阻塞模式选择

模式 函数末尾参数 行为
非阻塞 LEDC_FADE_NO_WAIT 函数立即返回,渐变由硬件后台完成
阻塞 LEDC_FADE_WAIT_DONE 任务卡在这里直到渐变结束
  • 非阻塞常用于不需要等待渐变结束的场景;

  • 若需要知道渐变何时完成,可注册回调:

    ledc_cbs_t cbs = { .fade_cb = fade_end_cb };
    ledc_cb_register(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, &cbs, NULL);
    

中断

概述

LEDC 的中断源只有一种:渐变结束中断。当硬件自动渐变(Fade)到达目标占空比时,触发该中断。用户可注册回调函数,在渐变完成时执行自定义操作(如通知任务)。

回调函数组

/**
 * @brief LEDC 支持的回调函数组
 * @note 这些回调函数都运行在 ISR 环境下
 */
typedef struct {
    ledc_cb_t fade_cb;  /**< LEDC 渐变结束回调函数 */
} ledc_cbs_t;

回调函数类型

/**
 * @brief LEDC 事件回调函数原型
 *
 * @param param LEDC 回调参数,包含事件类型、通道号、当前占空比等信息
 * @param user_arg 用户通过 ledc_cb_register 传入的自定义数据
 * @return 是否唤醒了更高优先级的任务
 *      - true:唤醒了高优先级任务,退出 ISR 后立即任务切换
 *      - false:无需任务切换
 */
typedef bool (*ledc_cb_t)(const ledc_cb_param_t *param, void *user_arg);

关于回调参数结构体

/**
* @brief LEDC 回调参数
*/
typedef struct {
   ledc_cb_event_t event;   /**< 事件类型,目前只有 LEDC_FADE_END_EVT(渐变结束) */
   uint32_t speed_mode;     /**< 当前通道的速度模式 */
   uint32_t channel;        /**< 触发回调的通道号 (0 - LEDC_CHANNEL_MAX-1) */
   uint32_t duty;           /**< 当前占空比 [0, (2**duty_resolution)] */
} ledc_cb_param_t;

关于回调返回值的使用

/*
true  → 告诉系统:“我在 ISR 中唤醒了更高优先级的任务,请帮我切换”
false → 告诉系统:“不需要任务切换”(最常用)
*/

// 渐变结束中断回调函数示例
static bool fade_end_cb(const ledc_cb_param_t *param, void *user_arg) {
     // 1. 如果需要在 ISR 中操作 FreeRTOS 对象(信号量、队列等),
     //    必须定义这个变量
     BaseType_t xHigherPriorityTaskWoken = pdFALSE;

     // 2. 执行简短的中断操作
     // 例如:翻转一个 GPIO
     gpio_set_level(LED_GPIO, !gpio_get_level(LED_GPIO));

     // 或者:释放一个信号量给任务去处理
     // xSemaphoreGiveFromISR(my_semaphore, &xHigherPriorityTaskWoken);

     // 3. 判断是否需要任务切换
     //    如果返回 false,系统会自动忽略 xHigherPriorityTaskWoken 的值
     //    如果返回 true,系统会检查 xHigherPriorityTaskWoken 并决定是否切换
     return false;  // 不触发任务切换(最常用)
     // 或者,返回 true,告诉系统需要检查是否要做任务切换
     // return (xHigherPriorityTaskWoken == pdTRUE);
}

中断回调注册函数

/**
 * @brief 注册 LEDC 渐变中断回调函数
 *
 * @note 回调函数运行在 ISR 环境中,绝对不能有阻塞操作,且任何 FreeRTOS API 必须使用 ISR 安全版本(即以 FromISR 结尾的版本)
 * @note 该函数必须在通道初始化完成后、启动渐变前调用
 *
 * @param speed_mode 速度模式。并非所有目标都支持高速模式,推荐使用 LEDC_LOW_SPEED_MODE
 * @param channel LEDC 通道。范围 (0 - LEDC_CHANNEL_MAX-1), 从 ledc_channel_t 中选择
 * @param cbs LEDC 回调函数组指针,结构体成员 fade_cb 指定回调函数
 * @param user_arg 用户自定义数据,会原样传递给回调函数,通常可传信号量句柄、队列句柄等
 *
 * @return
 *     - ESP_OK 成功
 *     - ESP_ERR_INVALID_ARG 参数错误
 *     - ESP_ERR_INVALID_STATE 通道未初始化
 *     - ESP_FAIL 函数初始化错误
 */
esp_err_t ledc_cb_register(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_cbs_t *cbs, void *user_arg);

完整示例:渐变完成通知任务

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/ledc.h"

SemaphoreHandle_t fade_sem;

// 渐变结束回调
static bool fade_end_cb(const ledc_cb_param_t *param, void *user_arg) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(fade_sem, &xHigherPriorityTaskWoken);
    return (xHigherPriorityTaskWoken == pdTRUE);
}

void app_main(void) {
    // ... 配置定时器和通道(略)...

    fade_sem = xSemaphoreCreateBinary();
    ledc_fade_func_install(0);

    ledc_cbs_t cbs = {
        .fade_cb = fade_end_cb
    };
    ledc_cb_register(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, &cbs, NULL);

    // 启动非阻塞渐变
    ledc_set_fade_time_and_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 8000, 2000, LEDC_FADE_NO_WAIT);

    // 等待渐变完成
    xSemaphoreTake(fade_sem, portMAX_DELAY);
    ESP_LOGI("main", "渐变完成");
}

注意事项

  1. 回调函数运行在 ISR 上下文:必须短小精悍,不能有延时、打印、阻塞,只能使用 FromISR 版本 API。
  2. 返回值作用:若回调中调用了 xSemaphoreGiveFromISR 等可能唤醒高优先级任务的函数,需按规则返回 true;若只是简单翻转 GPIO 等无需任务切换的操作,直接返回 false
  3. 不需要手动加 IRAM_ATTR:LEDC 驱动内部已处理,注册时无需像 GPTimer 那样手动添加。
  4. 先安装渐变功能再注册回调ledc_fade_func_install(0) 要在 ledc_cb_register 之前调用。
  5. 同一通道重复注册:新注册会覆盖旧回调,但需注意在多任务环境下可能存在竞争,建议在初始化阶段一次性完成注册。

注意事项

  1. ledc_set_duty_and_update 内部走的是 fade 通道,所以使用前必须调用 ledc_fade_func_install(0) 安装 fade 服务(不一定要注册回调,但服务必须装)。
  2. 频繁调用 ledc_set_duty_and_updateledc_set_duty + ledc_update_duty 来实现平滑渐变是错误做法,会占用大量 CPU,导致看门狗复位。
  3. 正确的平滑渐变应该使用硬件 fade 功能ledc_set_fade_with_timeledc_set_fade_with_step + ledc_fade_start),硬件自动完成,不占 CPU。
需求 正确做法
设置固定占空比 ledc_set_duty + ledc_update_duty(分步) 或 ledc_set_duty_and_update(需先 ledc_fade_func_install
实现平滑渐变 使用硬件 fade:ledc_set_fade_with_time / ledc_set_fade_with_step + ledc_fade_start
多任务操作同一通道 线程安全版:ledc_set_duty_and_updateledc_set_fade_xxx_and_start

注意:即使使用分步 API(ledc_set_duty + ledc_update_duty),也不应该高频调用(比如每 1ms 改一次),因为每次调用仍会占用 CPU 时间。硬件 fade 是唯一能实现“后台平滑渐变”的方式。

使用示例

实现固定占空比

#include "driver/ledc.h"
#include "esp_log.h"

#define PWM_FREQ_HZ         100000      // 输出PWM所需的频率(100kHz)

#define PWM_IN_0            GPIO_NUM_16 // 通道0的PWM输出引脚
#define PWM_IN_1            GPIO_NUM_17 // 通道1的PWM输出引脚

void app_main(void) {
    // ================= 1. 计算最大占空比分辨率 =================
    // 参数1:时钟源频率(80MHz = 80 * 1000 * 1000)
    // 参数2:期望的PWM频率
    // 返回:该频率下最大可用的分辨率位数(如 9 表示 0~511)
    uint8_t duty_re = ledc_find_suitable_duty_resolution(80 * 1000 * 1000, PWM_FREQ_HZ);

    // 计算最大可配置的占空比
    uint16_t duty_max = 1UL << duty_re;

    ESP_LOGI("main", "最大占空比:%d", duty_max);

    // ================= 2. 配置LEDC定时器 =================
    ledc_timer_config_t ledc_timer1 = {
        .clk_cfg = LEDC_AUTO_CLK,           // 时钟源:自动选择(推荐)
        .deconfigure = false,               // false=正常配置;true=注销定时器
        .duty_resolution = duty_re,         // 占空比分辨率(位数)
        .freq_hz = PWM_FREQ_HZ,             // PWM频率,单位Hz
        .speed_mode = LEDC_LOW_SPEED_MODE,  // 速度模式:低速(兼容所有芯片)
        .timer_num = LEDC_TIMER_0,          // 使用硬件定时器0
    };
    ledc_timer_config(&ledc_timer1);

    // ================= 3. 配置LEDC通道0 =================
    ledc_channel_config_t ledc_channel_0 = {
        .channel = LEDC_CHANNEL_0,          // 选择通道0
        .duty = duty_max / 4,               // 初始占空比:25%
        .gpio_num = PWM_IN_0,               // PWM输出引脚 GPIO16
        .hpoint = 0,                        // 输出跳变点:0表示周期开始就输出高
        .intr_type = LEDC_INTR_DISABLE,     // 关闭中断(不需要渐变完成中断)
        .speed_mode = LEDC_LOW_SPEED_MODE,  // 必须与定时器一致
        .timer_sel = LEDC_TIMER_0,          // 绑定到定时器0(与上面配置的对应)
        .flags.output_invert = 0,           // 不反转输出极性
        .sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD,  // 休眠时停止输出但不断电
    };
    ledc_channel_config(&ledc_channel_0);

    // ================= 4. 配置LEDC通道1 =================
    ledc_channel_config_t ledc_channel_1 = {
        .channel = LEDC_CHANNEL_1,          // 选择通道1
        .duty = duty_max / 2,               // 初始占空比:50%
        .gpio_num = PWM_IN_1,               // PWM输出引脚 GPIO17
        .hpoint = 0,
        .intr_type = LEDC_INTR_DISABLE,
        .speed_mode = LEDC_LOW_SPEED_MODE,  // 必须与定时器一致
        .timer_sel = LEDC_TIMER_0,          // 绑定到同一个定时器0
        .flags.output_invert = 0,
        .sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD,
    };
    ledc_channel_config(&ledc_channel_1);
}

渐变

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/ledc.h"
#include "esp_log.h"

#define PWM_FREQ_HZ         100000      // 输出PWM所需的频率(100kHz)

#define PWM_IN_0            GPIO_NUM_16 // 呼吸灯使用的GPIO引脚

SemaphoreHandle_t sema_bit;            // 二值信号量,用于通知任务渐变完成
uint16_t duty_max;                     // 最大有效占空比(全局,供任务和初始化使用)
volatile uint8_t dir;                  // 渐变方向:1=渐亮,0=渐灭(volatile因在ISR中修改)

// 渐变中断回调函数(当硬件渐变完成时触发)
static bool fade_end_isr(const ledc_cb_param_t* param, void* user_arg) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    // 判断是否是通道0产生的渐变完成事件
    if (param->channel == LEDC_CHANNEL_0) {
        // 释放信号量,唤醒等待渐变完成的任务
        xSemaphoreGiveFromISR(sema_bit, &xHigherPriorityTaskWoken);

        // 根据当前占空比判断上一次渐变的方向,从而决定下一次方向
        if (param->duty == 0) {
            dir = 1;    // 上次渐变到了0,所以下一次应该是渐亮
        } else {
            dir = 0;    // 上次渐变到了最大值,所以下一次应该是渐灭
        }
    }
    // 返回是否需要触发任务切换
    return (xHigherPriorityTaskWoken == pdTRUE);
}

// 呼吸灯控制任务
void task(void* param) {
    dir = 1;  // 初始方向设为渐亮
    while (1) {
        // 根据方向启动非阻塞渐变
        if (dir) {
            // 渐亮:在1000ms内将占空比渐变到duty_max
            ledc_set_fade_time_and_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty_max, 1000, LEDC_FADE_NO_WAIT);
        } else {
            // 渐灭:在1000ms内将占空比渐变到0
            ledc_set_fade_time_and_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0, 1000, LEDC_FADE_NO_WAIT);
        }
        // 等待渐变完成中断(信号量被释放)
        xSemaphoreTake(sema_bit, portMAX_DELAY);
    }
}

void app_main(void) {
    // 创建二值信号量,初始值为0(表示没有渐变完成事件)
    sema_bit = xSemaphoreCreateBinary();

    // 获取当前频率所支持的最大占空比分辨率(例如100kHz时可能返回9)
    uint8_t duty_re = ledc_find_suitable_duty_resolution(80 * 1000 * 1000, PWM_FREQ_HZ);

    // 计算最大可配置的占空比
    duty_max = (1UL << duty_re);

    ESP_LOGI("main", "最大占空比:%d", duty_max);

    // 配置LEDC定时器
    ledc_timer_config_t ledc_timer1 = {
        .clk_cfg = LEDC_AUTO_CLK,           // 自动选择时钟源
        .deconfigure = false,               // 正常配置
        .duty_resolution = duty_re,         // 设置分辨率位数
        .freq_hz = PWM_FREQ_HZ,             // PWM频率
        .speed_mode = LEDC_LOW_SPEED_MODE,  // 低速模式(兼容性最好)
        .timer_num = LEDC_TIMER_0,          // 使用硬件定时器0
    };
    ledc_timer_config(&ledc_timer1);

    // 配置LEDC通道0
    ledc_channel_config_t ledc_channel_0 = {
        .channel = LEDC_CHANNEL_0,          // 使用通道0
        .duty = 0,                          // 初始占空比为0
        .gpio_num = PWM_IN_0,               // PWM输出引脚
        .hpoint = 0,                        // 跳变点为0,周期开始就输出高
        .intr_type = LEDC_INTR_DISABLE,     // 关闭渐变中断(我们使用回调注册)
        .speed_mode = LEDC_LOW_SPEED_MODE,  // 与定时器模式一致
        .timer_sel = LEDC_TIMER_0,          // 绑定到定时器0
        .flags.output_invert = 0,           // 不反转输出极性
        .sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD,  // 睡眠时停止输出但不断电
    };
    ledc_channel_config(&ledc_channel_0);

    // 安装渐变功能(使用渐变API前必须调用)
    ledc_fade_func_install(0);

    // 准备回调结构体
    ledc_cbs_t fade_cbs = {
        .fade_cb = fade_end_isr,            // 指定渐变完成回调
    };

    // 为通道0注册渐变完成回调
    ledc_cb_register(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, &fade_cbs, NULL);

    // 创建呼吸灯控制任务,绑定到核心1,优先级3
    xTaskCreatePinnedToCore(task, "task", 2048, NULL, 3, NULL, 1);
}
posted @ 2026-08-17 19:45  7七柒  阅读(3)  评论(0)    收藏  举报