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_DISABLEGPIO_MODE_INPUTGPIO_MODE_OUTPUTGPIO_MODE_OUTPUT_ODGPIO_MODE_INPUT_OUTPUT_ODGPIO_MODE_INPUT_OUTPUT |
禁用、输入模式、输出模式、开漏输出、开漏输入、双向模式、 |
pull_up_en |
GPIO_PULLUP_DISABLE GPIO_PULLUP_ENABLE |
内部上拉使能 |
pull_down_en |
GPIO_PULLDOWN_DISABLEGPIO_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); -
参数说明
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); -
参数
gpio_num:引脚号(注意不是掩码,就是引脚号,1、2、3、4、5、6....)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); -
参数
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 硬件本身就是边沿或电平,这个标志是用来告诉中断分配器选择合适的触发方式,通常可以省略。
- 大多数 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_APBGPTIMER_CLK_SRC_DEFAULT |
时钟源选择APB、选择默认时钟 |
direction |
GPTIMER_COUNT_DOWNGPTIMER_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_MODELEDC_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_CLKLEDC_USE_RC_FAST_CLKLEDC_USE_XTAL_CLK |
根据设置的分辨率和占空比自动选择时钟源 选择APB作为时钟源 选择RC_FAST作为时钟源 选择REF_TICK作为时钟源 |
| deconfigure | false(正常配置)true(反配置/注销) |
设为 true 时,表示注销该定时器。此时 duty_resolution、freq_hz、clk_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就是用来打破这个默认行为的。它规定了“输出跳变为高电平的时机”。
场景 hpointduty波形描述 标准 PWM(默认) 0你设的值 周期开始时立即输出高电平,到达 duty时变低有 hpoint 的 PWM 非零值 你设的值 计数器到达 hpoint时才输出高电平,到达duty时变低如果
hpoint = 0,就是标准的 PWM,和你之前理解的一样。总结:
hpoint是 LEDC 输出由低变高的计数器阈值。一般设为0(默认行为)。 仅在需要精细控制多路 PWM 相位差或同步时修改。
配置流程
-
配定时器(
ledc_timer_config)
决定 PWM 的频率和占空比分辨率,时钟源推荐直接用LEDC_AUTO_CLK。这是所有通道的公共基础。 -
配通道(
ledc_channel_config)
决定 PWM 从哪个 GPIO 输出、初始占空比是多少,以及绑定到哪个定时器。⚠️
speed_mode和timer_sel必须与第 1 步定时器的设置完全一致。 -
多通道共用一个定时器
如果多个通道需要相同的频率和分辨率,只需配一个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只有硬件渐变完成中断这一个中断
配置步骤
- 配置定时器 (ledc_timer_config)
- 配置通道 (ledc_channel_config)
- 安装渐变中断 (ledc_fade_func_install) ← 必须!只装一次
- 如果需要知道渐变完成,就要注册一个中断函数
- 设置渐变参数
- 启动渐变 (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_IRAMISR 放在 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", "渐变完成");
}
注意事项
- 回调函数运行在 ISR 上下文:必须短小精悍,不能有延时、打印、阻塞,只能使用
FromISR版本 API。 - 返回值作用:若回调中调用了
xSemaphoreGiveFromISR等可能唤醒高优先级任务的函数,需按规则返回true;若只是简单翻转 GPIO 等无需任务切换的操作,直接返回false。 - 不需要手动加
IRAM_ATTR:LEDC 驱动内部已处理,注册时无需像 GPTimer 那样手动添加。 - 先安装渐变功能再注册回调:
ledc_fade_func_install(0)要在ledc_cb_register之前调用。 - 同一通道重复注册:新注册会覆盖旧回调,但需注意在多任务环境下可能存在竞争,建议在初始化阶段一次性完成注册。
注意事项
ledc_set_duty_and_update内部走的是 fade 通道,所以使用前必须调用ledc_fade_func_install(0)安装 fade 服务(不一定要注册回调,但服务必须装)。- 频繁调用
ledc_set_duty_and_update或ledc_set_duty+ledc_update_duty来实现平滑渐变是错误做法,会占用大量 CPU,导致看门狗复位。 - 正确的平滑渐变应该使用硬件 fade 功能(
ledc_set_fade_with_time或ledc_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_update 或 ledc_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);
}

浙公网安备 33010602011771号