通用定时器 (GPTimer)
通用定时器简介
通用定时器可用于准确设定时间间隔、在一定间隔后触发(周期或非周期的)中断或充当硬件时钟。如下图所示,ESP32-S3 包含两个定时器组,即定时器组 0 和定时器组 1。每个定时器组有两个通用定时器(T0,T1)和一个主系统看门狗定时器(WDT)。所有通用定时器均基于 16 位预分频器和 54 位可自动重新加载向上/向下计数器。
通用定时器架构
-
时钟选择器:每个定时器可以通过配置寄存器
TIMG_TxCONFIG_REG
的TIMG_Tx_USE_XTAL
字段,选择APB时钟(APB_CLK
)或外部时钟(XTAL_CLK
)作为时钟源。 -
16位预分频器:时钟源经过16位预分频器分频,产生时基计数器使用的时基计数器时钟(
TB_CLK
)。16位预分频器的分频系数可以通过TIMG_Tx_DIVIDER
字段配置,选取从2到65536之间的任意值。需要注意的是:定时器必须关闭(即TIMG_Tx_EN
必须清零),才能更改16位预分频器。在定时器使能时更改16位预分频器会造成不可预知的结果。 -
54位时基计数器:54位时基计数器基于
TB_CLK
,可通过TIMG_Tx_INCREASE
字段配置为递增或递减。时基计数器可通过置位或清零TIMG_Tx_EN
字段使能或关闭。使能时,时基计数器的值会在每个TB_CLK
周期递增或递减。关闭时,时基计数器暂停计数。注意,TIMG_Tx_EN
置位后,TIMG_Tx_INCREASE
字段还可以更改,时基计数器可立即改变计数方向。 -
比较器:定时器可配置为在当前值与报警值相同时触发报警。报警会产生中断,(可选择)让定时器的当前值自动重新加载。54位报警值可在
TIMG_TxALARMLO_REG
和TIMG_TxALARMHI_REG
配置,两者分别代表报警值的低32位和高22位。但是,只有置位TIMG_Tx_ALARM_EN
字段使能报警功能后,配置的报警值才会生效。为解决报警使能“过晚”(即报警使能时,定时器的值已过报警值),可逆计数器向上计数时,若定时器的当前值高于报警值(在一定范围内),或可逆计数器向下计数时,定时器的当前值低于报警值(在一定范围内),硬件都会立即触发报警。
更多内容可以查看一下官方手册
通用定时器手册
GPtimer的API参考
1. 头文件
#include "driver/gptimer.h"
2. 创建通用计时器
esp_err_t gptimer_new_timer(const gptimer_config_t *config, gptimer_handle_t *ret_timer)
创建一个新的通用计时器,并返回句柄。
注意:
新创建的计时器处于“init”状态。
函数参数:
参数 | 描述 |
---|---|
config | [输入] GPTimer 配置 |
ret_timer | [输出] 返回的计时器句柄 |
函数返回值:
返回值 | 描述 |
---|---|
ESP_OK | 成功创建 GPTimer |
ESP_ERR_INVALID_ARG | 由于参数无效,创建 GPTimer 失败 |
ESP_ERR_NO_MEM | 创建 GPTimer 失败,因为内存不足 |
ESP_ERR_NOT_FOUND | 创建 GPTimer 失败,因为所有硬件计时器都已用完,不再有空闲计时器 |
ESP_FAIL | 由于其他错误,创建 GPTimer 失败 |
该函数使用 gptimer_config_t 类型的结构体变量传入 gptimer 外设的配置参数,该结构体的
定义如下所示:
/**
* @brief 通用定时器配置
*/
typedef struct {
gptimer_clock_source_t clk_src; /* 通用定时器时钟源 */
gptimer_count_direction_t direction; /* 计数方向 */
uint32_t resolution_hz; /* 计数器分辨率(工作频率) */
struct {
uint32_t intr_shared: 1;/* 若置为 1,则计时器中断编号可以与其他外围设备共享 */
} flags; /* 通用定时器配置标志 */
} gptimer_config_t;
该函数的使用实例如下
#include "driver/gpio.h"
void example_fun(void)
{
gptimer_config_t g_tim_handle;
gptimer_handle_t g_tim = NULL;
g_tim_handle.clk_src = GPTIMER_CLK_SRC_DEFAULT; /* 选择定时器时钟源 */
g_tim_handle.direction = GPTIMER_COUNT_UP; /* 递增计数模式 */
g_tim_handle.resolution_hz = resolution; /* 计数器分辨率 */
gptimer_new_timer(&g_tim_handle, &g_tim); /* 创建新的通用定时器 */
}
3. 配置通用定时器
esp_err_t gptimer_set_raw_count(gptimer_handle_t timer, unsigned long long value);
配置通用定时器的计数值以及定时器周期。
函数形参描述:
形参 | 描述 |
---|---|
timer | 计时器句柄由 gptimer_new_timer 创建 |
value | 要设置的计数值 |
函数返回值描述:
返回值 | 描述 |
---|---|
ESP_OK | 返回 0,表示配置成功 |
ESP_ERR_INVALID_ARG | 由于参数无效,获取 GPTimer 原始计数值失败 |
ESP_FAIL | 由于其他错误,设置 GPTimer 原始计数值失败 |
示例用法:
#include "driver/gpio.h"
void example_fun(void) {
gptimer_handle_t g_tim = NULL;
gptimer_set_raw_count(g_tim, 100);
}
4. 注册用户回调函数
g_tim_callbacks.on_alarm = gptimer_callback;
该结构体用于注册用户回调函数,并没有涉及到参与返回值,其调用的 gptimer_callback()
。
该结构体使用 gptimer_event_callbacks_t
类型的结构体对 gptimer 报警进行参数配置,该结
构体的定义如下所示:
/**
* @brief 支持的 GPTimer 回调组
* @note 回调都在 ISR 环境下运行
* @note 当 CONFIG_GPTIMER_ISR_IRAM_SAFE 被启用时,
回调本身及其调用的函数应该被放置在 IRAM 中。
*/
typedef struct {
gptimer_alarm_cb_t on_alarm; /* 定时器报警回调 */
} gptimer_event_callbacks_t;
该结构体的使用示例,如下
#include "driver/gpio.h"
void example_fun(void)
{
gptimer_event_callbacks_t g_tim_callbacks;
g_tim_callbacks.on_alarm = gptimer_callback; /* 注册用户回调函数 */
}
配置通用定时器回调函数
esp_err_t gptimer_register_event_callbacks(gptimer_handle_t timer, const gptimer_event_callbacks_t *cbs, void *user_data)
5. 创建一个消息队列,并引入一个事件
该函数用于创建一个新的队列实例,并返回一个句柄,通过该句柄可以引用新队列,其函数原型如下所示:
QueueHandle_t xQueueCreate( uxQueueLength, uxItemSize );
函数形参描述:
形参 | 描述 |
---|---|
uxQueueLength | 队列可以包含的最大项目数 |
uxItemSize | 队列中每个项目所需的字节数。项目是按副本而非引用排队的,因此这是每个已发布项目将复制的字节数。队列中的每个项目的大小必须相同。 |
函数返回值描述:
如果成功创建了队列,则会返回新创建的队列的句柄。如果无法创建队列,则返回 0。
宏定义:
消息队列的调用方式用到了条件编译的方式,当满足条件编译所需要的条件时才会使用到该消息队列,同时该消息队列通过宏定义的方式进行调用。
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xQueueCreate( uxQueueLength, uxItemSize ) \
xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
#endif
注意事项:
在内部,在 FreeRTOS 实现中,队列使用两个内存块。第一个块用于保存队列的数据结构。第二个块用于保存放入队列中的项目。如果使用 xQueueCreate()
创建队列,那么两个内存块都会在函数内自动动态分配。如果使用 xQueueCreateStatic()
创建队列,则应用程序编写器必须提供队列将使用的内存。因此,xQueueCreateStatic()
允许在不使用任何动态内存分配的情况下创建队列。
使用示例:
#include "driver/gpio.h"
struct Example //示例
{
char ExampleID;
char Data[20];
};
void example_fun(void *pvParameters)
{
QueueHandle_t xQueue1, xQueue2;
/* 创建一个能够包含 10 个 uint32_t 值的队列 */
xQueue1 = xQueueCreate( 10, sizeof(uint32_t));
if(xQueue1 == 0)
{
/* 队列未创建,不得使用 */
}
/* 创建一个能够包含 10 个指向消息结构的指针的队列 */
/* 这些应该通过指针传递,因为它们包含大量数据 */
xQueue2 = xQueueCreate( 10, sizeof(struct Example *));
if(xQueue2 == 0)
{
/* 队列未创建,不得使用 */
}
}
6. 定时器报警,设置报警动作
该函数用于配置通用定时器报事件警,其函数原型如下所示:
esp_err_t gptimer_set_alarm_action(gptimer_handle_t timer, const gptimer_alarm_config_t *config);
函数形参描述:
形参 | 描述 |
---|---|
timer | 指向配置通用定时器的指针 |
config | 指向通用定时器报警配置的指针类型。报警配置,尤其是将配置设置为 NULL 意味着禁用报警功能 |
函数返回值描述:
返回值 | 描述 |
---|---|
ESP_OK | 返回:0,配置成功 |
ESP_ERR_INVALID_ARG | 无效参数 |
ESP_FAIL | 由于其他错误,创建 GPTimer 失败 |
gptimer_alarm_config_t 结构体:
该函数使用 gptimer_alarm_config_t
类型的结构体变量传入 gptimer 外设的报警配置参数
该结构体的定义如下
/**
* @brief 通用定时器报警配置
*/
typedef struct {
uint64_t alarm_count; /* 报警目标计数值 */
uint64_t reload_count; /* 报警重新加载计数值 */
struct {
uint32_t auto_reload_on_alarm: 1; /* 报警事件发生后立即通过硬件重新加载计数值 */
} flags; /* 报警配置标志 */
} gptimer_alarm_config_t;
在这个结构体gptimer_alarm_config_t
中,alarm_count
和reload_count
的含义如下:
-
alarm_count
:这是报警目标计数值。当定时器的计数值达到这个alarm_count
设定的值时,会触发一个报警事件。 -
reload_count
:这是报警重新加载计数值。当报警事件发生后,如果auto_reload_on_alarm
被设置为1,那么定时器的计数值会立即被重置为reload_count
设定的值。
如果定时器在达到alarm_count
设定的值时触发一次报警,而不需要在报警后自动重置计数值。也就是说,这个定时器可能只需要运行一次,而不是周期性地运行。因此,reload_count
就不需要被设置。
使用示例:
#include "driver/gpio.h"
void example_fun(void)
{
gptimer_alarm_config_t alarm_config;
alarm_config.alarm_count = 1000000; /* 报警目标计数值 */
gptimer_set_alarm_action(g_tim, &alarm_config);
}
更多API接口可以去看一下官方文档
通用定时器 (GPTimer)
程序设计
实验将配置通用定时器间隔一定时间返回一次报警值,并通过串口或者 VSCode 终端显示报警值,具体步骤如下:
①:配置通用定时器
②:配置通用定时器的计数值以及定时器周期
③:注册用户回调函数
④:创建一个消息队列,并引入一个事件
⑤:定时器报警,设置报警动作
GPTIM.h
/**
* @file GPTIM.h
* @author 宁子希 (1589326497@qq.com)
* @brief 通用定时器驱动代码
* @version 0.1
* @date 2024-04-17
*
* @copyright Copyright (c) 2024
*
*/
#ifndef __GPTIM_H_
#define __GPTIM_H_
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gptimer.h"
#include "esp_log.h"
//参数引用
typedef struct{
uint64_t event_count;
}gptimer_event_t;
extern QueueHandle_t queue;
//函数声明
void gptim_init(uint64_t count,uint16_t resolution);
bool IRAM_ATTR gptimer_callback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data);
#endif
GPTIM.c
/**
* @file GPTIM.c
* @author 宁子希 (1589326497@qq.com)
* @brief 通用定时器驱动代码
* @version 0.1
* @date 2024-04-17
*
* @copyright Copyright (c) 2024
*
*/
#include "GPTIM.h"
QueueHandle_t queue; //消息队列句柄
/**
* @brief 初始化通用定时器
*
* @param counts 计数值
* @param resolution 定时器周期
*/
void gptim_init(uint64_t counts,uint16_t resolution){
gptimer_config_t g_tim_handle; //gptimer配置结构体
gptimer_alarm_config_t alarm_config; //报警配置结构体
gptimer_event_callbacks_t g_tim_callbacks; //回调函数配置结构体
uint64_t count;
//配置通用定时器
ESP_LOGE("GPTIMER_ALARM", "配置通用定时器");
//创建定时器句柄
gptimer_handle_t g_tim = NULL;
//配置gptimer
g_tim_handle.clk_src = GPTIMER_CLK_SRC_DEFAULT; // 选择定时器时钟源
g_tim_handle.direction = GPTIMER_COUNT_UP; // 递增计数模式
g_tim_handle.resolution_hz = resolution; // 计数器分辨率
//注册用户回调函数
g_tim_callbacks.on_alarm = gptimer_callback;
//配置报警目标计数值
alarm_config.alarm_count = 1000000;
//创建新的通用定时器,并返回句柄
ESP_ERROR_CHECK(gptimer_new_timer(&g_tim_handle, &g_tim));
//创建一个队列,并引入一个事件
queue = xQueueCreate(10, sizeof(gptimer_event_t));
//检查队列是否创建成功
if (!queue)
{
ESP_LOGE("GPTIMER_ALARM", "创建队列失败");
return;
}
//设置和获取计数值
ESP_LOGE("GPTIMER_ALARM", "设置计数值");
ESP_ERROR_CHECK(gptimer_set_raw_count(g_tim, counts)); //设置计数值
ESP_LOGE("GPTIMER_ALARM", "获取计数值");
ESP_ERROR_CHECK(gptimer_get_raw_count(g_tim, &count)); //获取计数值
ESP_LOGE("GPTIMER_ALARM", "定时器计数值: %llu", count);
// 注册事件回调函数
ESP_ERROR_CHECK(gptimer_register_event_callbacks(g_tim, &g_tim_callbacks, queue)); //配置通用定时器回调函数
// 设置报警动作
ESP_LOGE("GPTIMER_ALARM", "使能通用定时器");
ESP_ERROR_CHECK(gptimer_enable(g_tim)); //使能通用定时器
ESP_ERROR_CHECK(gptimer_set_alarm_action(g_tim, &alarm_config)); //配置通用定时器报警事件
ESP_ERROR_CHECK(gptimer_start(g_tim)); //启动通用定时器
}
bool IRAM_ATTR gptimer_callback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data)
{
BaseType_t high_task_awoken = pdFALSE;
queue = (QueueHandle_t)user_data;
// 从事件数据中检索计数值
gptimer_event_t ele = {
.event_count = edata->count_value
};
// 可选:通过操作系统队列将事件数据发送到其他任务
xQueueSendFromISR(queue, &ele, &high_task_awoken);
// 重新配置报警值
gptimer_alarm_config_t alarm_config = {
.alarm_count = edata->alarm_value + 1000000, //在接下来的1秒内报警
};
gptimer_set_alarm_action(timer, &alarm_config);
// 返回是否需要在ISR结束时让步
return high_task_awoken == pdTRUE;
}
main.c
/**
* @file main.c
* @author 宁子希 (1589326497@qq.com)
* @brief 通用定时器驱动代码
* @version 0.1
* @date 2024-04-18
*
* @copyright Copyright (c) 2024
*
*/
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "led.h"
#include "gptim.h"
void app_main(void){
uint8_t record;
esp_err_t ret;
gptimer_event_t g_tim_evente;
ret = nvs_flash_init(); //初始化NVS
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
led_init(); //初始化LED
gptim_init(100, 1000000); //初始化通用定时器
while (1)
{
record = 1;
if (xQueueReceive(queue, &g_tim_evente, 2000))
{
ESP_LOGI("GPTIMER_ALARM", "定时器报警, 计数值: %llu", g_tim_evente.event_count); //打印通用定时器发生一次计数事件后获取到的值
record--;
}
else
{
ESP_LOGW("GPTIMER_ALARM", "错过一次计数事件");
}
}
vQueueDelete(queue);
}