一、什么是通用定时器
ESP32 S3 芯片配备了两个通用定时器组,每组均包含两个通用定时器和一个主系统看门狗定时器。每个通用定时器都具备多个通道。通过明确指定定时器号和通道号,用户可以精准地选定所需的定时器和通道。每个定时器均支持独立编程,并且具备微秒级的精确时间中断生成能力。基本的定时器参数设置包括定时器号、通道号、预分频器配置、自动重新加载值的设定,以及定时器中断使能功能的开启。
二、通用定时器架构
【1】、时钟选择器
每个定时器可通过配置寄存器 TIMG_TxCONFIG_REG
的 TIMG_Tx_USE_XTAL
字段,选择 APB 时钟(APB_CLK)或 外部时钟(XTAL_CLK)作为时钟源。
【2】、16 位预分频器
时钟源经过 16 位预分频器分频,产生时基计数器使用的时基计数器时钟(TB_CLK)。16 位预分频器的分频系数可通过 TIMG_Tx_DIVIDER
字段配置,选取从 2 到 65536 之间的任意值。
将
TIMG_Tx_DIVIDER
置 0 后,分频系数会变为 65536。TIMG_Tx_DIVIDER
置 1 时,实际分频系数为 2,计数器的值为实际时间的一半。
【3】、54 位时基计数器
54 位时基计数器基于 TB_CLK,可通过 TIMG_Tx_INCREASE
字段配置为递增或递减。时基计数器可通过置位或清零 TIMG_Tx_EN
字段使能或关闭。使能时,时基计数器的值会在每个 TB_CLK 周期递增或递减。关闭时,时基计数器暂停计数。
时基计数器 54 位定时器的当前值必须被锁入两个寄存器,才能被 CPU 读取。向 TIMG_TxUPDATE_REG 写入任意值时,54 位定时器的值开始被锁入寄存器 TIMG_TxLO_REG
和 TIMG_TxHI_REG
,两个寄存器分别锁存低 32 位和高 22 位。当 TIMG_TxUPDATE_REG
被硬件清零,表明锁存操作已经完成,可以从这两个寄存器中读取当前计数值。在 TIMG_TxUPDATE_REG
被写入新值之前,保持寄存器 TIMG_TxLO_REG
和 TIMG_TxHI_REG
的值不变,以供 32 位的 CPU 读值。
TIMG_Tx_EN
置位后,TIMG_Tx_INCREASE
字段还可以更改,时基计数器可立即改变计数方向。
【4】、比较器
定时器可配置为在当前值与报警值相同时触发报警。报警会产生中断,(可选择)让定时器的当前值自动重新加载。54 位报警值可在 TIMG_TxALARMLO_REG
和 TIMG_TxALARMHI_REG
配置,两者分别代表报警值的低 32 位和高 22 位。但是,只有置位 TIMG_Tx_ALARM_EN
字段使能报警功能后,配置的报警值才会生效。为解决报警使能 “过晚”(即报警使能时,定时器的值已过报警值),可逆计数器向上计数时,若定时器的当前值高于报警值(在一定范围内),或可逆计数器向下计数时,定时器的当前值低于报警值(在一定范围内),硬件都会立即触发报警。
三、通用定时器常用函数
ESP-IDF 提供了一套 API 来配置通用定时器。要使用此功能,需要导入必要的头文件:
#include "driver/gptimer.h"
3.1、配置通用定时器
我们可以使用 gptimer_new_timer()
函数 配置通用定时器,其函数原型如下所示:
/*** @brief 配置通用定时器* * @param config 配置通用定时器指针* @param ret_timer 通用定时器句柄类型* @return esp_err_t ESP_OK表示配置成功,其它表示配置失败*/
esp_err_t gptimer_new_timer(const gptimer_config_t *config, gptimer_handle_t *ret_timer);
形参 config 是指向 gptimer_config_t 类型结构体的指针,它的结构体变量定义如下:
typedef struct
{gptimer_clock_source_t clk_src; // 选择定时器的时钟源gptimer_count_direction_t direction; // 设置定时器的计数方向uint32_t resolution_hz; // 设置内部计数器的分辨率,每个计数器的步长等于(1/resolution_hz)秒int intr_priority; // 设置中断的优先级struct{uint32_t intr_shared: 1; // 设置是否将定时器中断源标记为共享源} flags;
} gptimer_config_t;
成员 clk_src
用来 选择定时器的时钟源,它的可选值如下:
typedef enum
{GPTIMER_CLK_SRC_APB = SOC_MOD_CLK_APB, /*!< Select APB as the source clock */GPTIMER_CLK_SRC_XTAL = SOC_MOD_CLK_XTAL, /*!< Select XTAL as the source clock */GPTIMER_CLK_SRC_DEFAULT = SOC_MOD_CLK_APB, /*!< Select APB as the default choice */
} soc_periph_gptimer_clk_src_t;
typedef soc_periph_gptimer_clk_src_t gptimer_clock_source_t;
成员 direction
用来 设置定时器的计数方向,它的可选值如下:
typedef enum
{GPTIMER_COUNT_DOWN, /*!< Decrease count value */GPTIMER_COUNT_UP, /*!< Increase count value */
} gptimer_count_direction_t;
成员 resolution_hz
用来 设置内部计数器的分辨率,计数器每滴答一次相当于 \(\frac{1}{resolution\_hz}\) 秒。
成员 intr_priority
用来 设置中断的优先级。若设置为 0,则会分配一个默认优先级的中断,否则会使用指定的优先级。
成员 intr_shared
用来 设置是否将定时器中断源标记为共享源。默认为 1。如果该参数设置为 true。那么多个定时器将共享同一个中断源。这意味着当其中一个定时器触发中断时,相应的中断处理程序将被调用,并且不同的定时器中断可以在同一个中断服务函数中进行处理。而如果 intr_shared参数设置为 false,那么每个定时器都会有独立的中断源,并且将有一个相应的中断服务函数用于处理每个定时器的中断。
3.2、设置原始计数值
我们可以使用 gptimer_set_raw_count()
函数 配置通用定时器的原始计数值,其函数原型如下所示:
/*** @brief 设置定时器的计数值* * @param timer 定时器句柄* @param value 要设置的计数值* @return esp_err_t ESP_OK表示配置成功,其它表示配置失败*/
esp_err_t gptimer_set_raw_count(gptimer_handle_t timer, unsigned long long value);
3.3、注册回调函数
我们可以使用 gptimer_register_event_callbacks()
函数 注册用户回调函数,其函数原型如下所示:
/*** @brief 注册回调函数* * @param timer 定时器句柄* @param cbs 需要绑定的回调函数* @param user_data 用户的数据,将直接传递给回调函数* @return esp_err_t ESP_OK表示配置成功,其它表示配置失败*/
esp_err_t gptimer_register_event_callbacks(gptimer_handle_t timer, const gptimer_event_callbacks_t *cbs, void *user_data);
3.4、设置定时器报警动作
我们可以使用 gptimer_set_alarm_action()
函数用于 配置通用定时器报警事件,其函数原型如下所示:
/*** @brief 设置定时器报警动作* * @param timer 定时器句柄* @param config 报警配置* @return esp_err_t ESP_OK表示配置成功,其它表示配置失败*/
esp_err_t gptimer_set_alarm_action(gptimer_handle_t timer, const gptimer_alarm_config_t *config);
形参 config 是 gptimer_alarm_config_t 类型的结构体变量的指针,该结构体的定义如下所示:
typedef struct
{uint64_t alarm_count; // 报警目标计数值uint64_t reload_count; // 报警重新加载计数值struct {uint32_t auto_reload_on_alarm: 1; // 报警事件发生后立即通过硬件重新加载计数值} flags; // 报警配置标志
} gptimer_alarm_config_t;
3.5、使能定时器
我们可以使用 gptimer_enable()
函数来 使能定时器,它的函数原型如下:
/*** @brief 使能定时器* * @param timer 定时器句柄* @return esp_err_t ESP_OK表示使能成功,其它表示使能失败*/
esp_err_t gptimer_enable(gptimer_handle_t timer);
该函数将把定时器驱动程序的状态从初始化切换为使能状态,如果
gptimer_register_event_callbacks()
已经延迟安装回调服务函数,此函数将使能回调服务函数。
3.6、启动定时器
我们可以使用 gptimer_start()
函数来 启动定时器,它的函数原型如下:
/*** @brief 开启定时器* * @param timer 定时器句柄* @return esp_err_t ESP_OK表示开启成功,其它表示开启失败*/
esp_err_t gptimer_start(gptimer_handle_t timer);
四、实验例程
我们修改【components】文件夹下的【peripheral】文件夹下的【inc】文件夹下的 bsp_timer.h
文件和【components】文件夹下的【peripheral】文件夹下的【src】文件夹下 bsp_timer.c
文件。
#ifndef __BSP_TIMER_H__
#define __BSP_TIMER_H__#include "driver/gpio.h"
#include "driver/gptimer.h"extern esp_timer_handle_t g_esp_timer_handle;
extern gptimer_handle_t g_gptimer_handle;void bsp_esp_timer_init(esp_timer_handle_t *handle);
void bsp_gptimer_init(gptimer_handle_t *handle, uint32_t resolution, uint64_t alarm_count, gptimer_alarm_cb_t gptimer_alarm_cbbs);
bool gptimer_alarm_callback(gptimer_handle_t handle, const gptimer_alarm_event_data_t *edata, void *user_data);#endif // !__BSP_TIMER_H__
gptimer_handle_t g_gptimer_handle;static void esp_timer_callback(void *arg);/*** @brief 通用定时器初始化* * @param handle 通用定时器句柄* @param resolution 定时器分辨率* @param alarm_count 报警目标计数值* @param gptimer_alarm_cbbs 报警回调函数* * @note 计数器每滴答一次相当于 1 / resolution 秒,IDF文档要求该值 2 < clk / resolution < 65536*/
void bsp_gptimer_init(gptimer_handle_t *handle, uint32_t resolution, uint64_t alarm_count, gptimer_alarm_cb_t gptimer_alarm_cbbs)
{gptimer_config_t gptimer_config = {0};gptimer_alarm_config_t gptimer_alarm_config = {0};gptimer_event_callbacks_t gptimer_event_callback = {0};// 配置通用定时器gptimer_config.clk_src = GPTIMER_CLK_SRC_DEFAULT; // 选择定时器时钟源gptimer_config.direction = GPTIMER_COUNT_UP; // 递增计数模式gptimer_config.resolution_hz = resolution; // 定时器分辨率gptimer_new_timer(&gptimer_config, handle); // 创建新的通用定时器,并返回句柄// 设置原始计数值gptimer_set_raw_count(*handle, alarm_count);gptimer_alarm_config.alarm_count = alarm_count; // 报警目标计数值gptimer_alarm_config.flags.auto_reload_on_alarm = true; // 当报警发生时,是否自动重载gptimer_set_alarm_action(*handle, &gptimer_alarm_config); // 设置报警动作// 设置报警回调函数gptimer_event_callback.on_alarm = gptimer_alarm_cbbs;gptimer_register_event_callbacks(*handle, &gptimer_event_callback, NULL); // 注册事件回调函数gptimer_enable(g_gptimer_handle); // 使能定时器
}/*** @brief 定时器报警回调函数* * @param handle 定时器句柄* @param edata 报警事件数据* @param user_data 用户传递给报警回调函数的参数* @return true 有高优先级的任务被唤醒* @return false 没有高优先级的任务被唤醒*/
bool gptimer_alarm_callback(gptimer_handle_t handle, const gptimer_alarm_event_data_t *edata, void *user_data)
{gpio_set_level(GPIO_NUM_0, !gpio_get_level(GPIO_NUM_0));return false;
}
修改【main】文件夹下的 main.c
文件。
#include "freertos/FreeRTOS.h"#include "bsp_timer.h"#include "led.h"// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{led_init(GPIO_NUM_0);bsp_gptimer_init(&g_gptimer_handle, 1000000, 1000000, gptimer_alarm_callback);gptimer_start(g_gptimer_handle);while (1){// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数vTaskDelay(pdMS_TO_TICKS(10));}
}