ESP32计时器
前言
ESP-IDF利用结构体进行各种配置,计时器的配置是一个典型案例。
正文
写在前面
计时器的定义本文不再赘述。本文基于ESP32-S3,编译环境是ESP-IDF v5.3.1
ESP32-S3的计时器从无到有共三种状态,即:init -> enable -> run
计时器通常在以下场景中使用:
- 如同挂钟一般自由运行,随时随地获取高分辨率时间戳;
- 生成周期性警报,定期触发事件;
- 生成一次性警报,在目标时间内响应。
本文的应用场景是第二种。本文所用的示例来自于ESP32-S3用于并口DAC通信的测试
ESP-IDF多使用结构体的方式进行配置,计时器也不例外。ESP-IDF的驱动层高度抽象,因此使用计时器时无需考虑硬件所属的计时器组和计时器,这些均由后台驱动程序管理。
计时器配置
gptimer_config_t waveCounterTimer_config = {
.clk_src = GPTIMER_CLK_SRC_APB, // 时钟源,APB时钟最快不超过80MHz
.direction = GPTIMER_COUNT_UP, // 计时器计数方向
.resolution_hz = TIME_CLOCK_HZ, // 计时器嘀嗒频率配置
.intr_priority = 0, // 中断优先级
};
下面一一解释各项参数
时钟源配置
.clk_src = GPTIMER_CLK_SRC_APB
一般使用APB时钟,频率最快可以到80MHz。
还可以选择晶振作为时钟,即.clk_src = GPTIMER_CLK_SRC_XTAL
不同的时钟源对功耗有所影响,但由于示例并非功率敏感,因此笔者并未就具体的影响进行探索。
计时器计数方向
.direction = GPTIMER_COUNT_UP
计时器无非就是递增或者递减,本例使用了递增计数
如果要使用递减计数,那就配置为.direction = GPTIMER_COUNT_DOWN
计时器嘀嗒频率配置
.resolution_hz = TIME_CLOCK_HZ
设置内部计时器器的分辨率。计数器每滴答一次相当于 1 / resolution_hz
秒。TIME_CLOCK_HZ
是笔者的宏定义。
事实上,这相当于变相配置了预分频器。但值得注意的是,预分频器的最小分频系数为2,即计时器频率的最大值为40MHz
设置中断的优先级
.intr_priority = 0
设置中断的优先级。如果设置为 0
,则会分配一个默认优先级的中断,否则会使用指定的优先级
其他配置
以上是本例中用到的中断配置,但ESP-IDF编程指南还给出了其他配置,即驱动程序是否允许系统在睡眠模式下关闭电源和是否将计时器中断标记为共享源,可能对读者有所帮助。
计时器实例化
简而言之,实例化就是一种注册,即通知系统分配资源给上文所配置的计时器。注意:计时器此时还未开启
gptimer_handle_t waveCounterTimer_handle = NULL;
ESP_ERROR_CHECK(gptimer_new_timer(&waveCounterTimer_config, &waveCounterTimer_handle))
首先,我们需要使用gptimer_handle_t
创建一个计时器句柄,方便我们操作计时器。笔者把句柄理解为指针,故初始化为NULL。
随后,利用函数gptimer_new_timer(const gptimer_config_t *config, gptimer_handle_t *ret_timer)
实例化计时器,简而言之,第一个参数传入上文配置好的计时器配置结构体,第二个参数传入计时器句柄。
最后,使用ESP-IDF的ESP_ERROR_CHECK()
进行检查,使出现计时器资源不足等错误时能及时报错。不用该函数也可以进行配置,但可能为debug带来不便。
设置计时器警报动作
计时器警报配置
所谓的警报动作,就是让计时器计数到某个值时产生警报事件,提醒CPU处理相应的事件。生活中,我们使用闹钟提醒我们按时起床,道理是一样的。
和计时器本身的配置一样,计时器的警报动作也需要使用结构体进行配置,例子的配置如下。
gptimer_alarm_config_t waveCounterTimer_alarm_config = {
.alarm_count = 10, // 到达这个数时警报
.reload_count = 1, // 重载的数值
.flags = {
.auto_reload_on_alarm = true, // 是否自动重载
}
};
.alarm_count
设置警报事件的计数值,到达这个数就会警报。但要注意计时器本身的计数方向。
.reload_count
设置警报事件的重载值,如果启用下面的.auto_reload_on_alarm
,那么一旦计数器警报,计数器就会自动重载到.reload_count
配置的值。
.auto_reload_on_alarm
配置计时器是否自动重载。
计时器警报实例化
`ESP_ERROR_CHECK(gptimer_set_alarm_action(waveCounterTimer_handle, &waveCounterTimer_alarm_config));`
调用gptimer_set_alarm_action(gptimer_handle_t timer, const gptimer_alarm_config_t *config)
实例化计时器警报,第一个参数传入上文提到的计时器句柄,第二个参数传入刚刚配置的计时器警报结构体。
注册事件回调函数
计时器启动后,可动态产生特定事件(如“警报事件”)。如需在事件发生时调用某些函数,就需要通过注册事件回调函数。
配置回调函数
ESP-IDF支持的回调函数格式如下,必须为布尔值,而且传入变量也必须和下文一致。由于此函数在 ISR 上下文中调用,必须确保该函数不会试图阻塞。
bool wave_alarm_cb_t(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx)
{
if (waveindex >= SAMPLE_PER_CYCLE)
{
waveindex = 0;
}
waveindex += wave_index_step;
return true;
}
其中,回调函数的第一个参数为计时器的句柄;第二个参数为计时器警报数据,由驱动传入;第三个参数为用户自定义的传入上下文,由gptimer_register_event_callbacks()
提供。
紧接着,就要配置一个回调函数结构体,格式如下
const gptimer_event_callbacks_t cbs = {
.on_alarm = wave_alarm_cb_t // 回调函数的函数名
};
注册回调函数
ESP_ERROR_CHECK(gptimer_register_event_callbacks(waveCounterTimer_handle, &cbs, NULL));
通过gptimer_register_event_callbacks(gptimer_handle_t timer, const gptimer_event_callbacks_t *cbs, void *user_data)
注册回调函数,第一个参数是配置时钟的句柄地址,第二个是回调函数结构体的地址,第三个是传递给回调函数的上下文。
使能计时器
ESP_ERROR_CHECK(gptimer_enable(waveCounterTimer_handle));
在对计时器进行 IO 控制之前,需要先调用gptimer_enable(gptimer_handle_t timer)
使能计时器,传入计时器句柄,将计时器从init状态切换为enable状态,这样,相应的中断服务会一并使能,时钟的电源管理锁也会启动。
启动计时器
ESP_ERROR_CHECK(gptimer_start(waveCounterTimer_handle));
大功告成,一切准备就绪!调用gptimer_start(gptimer_handle_t timer)
启动计时器,将计时器从enable状态切换为start状态,使计时器开始工作。