7. 定时器
7.1 定时器介绍
定时器是单片机内部集成,可以通过编程控制。单片机的定时功能是通过计数来实现的,当单片机每一个机器周期产生一个脉冲时,计数器就加一。定时器的主要功能是用来计时,时间到达之后可以产生中断,提醒计时时间到,然后可以在中断函数中去执行功能。比如我们想让一个 led 灯 1 秒钟翻转一次,就可以使用定时器配置为 1 秒钟触发中断,然后在中断函数中执行 led 翻转的程序。
主要作用包括:
- 执行定时任务:定时器的最常见的使用场景就是执行定时任务。例如,如果你希望每隔一定的时间,如 500 毫秒,执行某个特定的任务,那么你可以使用定时器来实现这种需求。
- 时间测量:定时器也可以用于测量时间,例如,你可以使用定时器来测量某个代码段的执行时间或者测量某个事件发生的间隔时间。
- 精确延时:定时器也可用于产生精确的延时。例如,如果你需要一个精确到微秒级的延时,你可以使用定时器来实现这种延时。
- PWM 信号生成:通过定时器,你还可以实现生成 PWM(脉宽调制)信号,该信号可用于驱动电机或者调节 LED 的亮度等。
- 事件触发:很多时候我们需要通过定时器 trigger一些事件,如中断。此外,定时器还用于 watch dog(看门狗)的实现,用于监控或者复位系统。
7.2 硬件定时器和软件定时器
定时器可以基于硬件也可以基于软件实现,两者有各自的特点和适用场景:
- 硬件定时器是由微控制器硬件提供的定时功能,由专门的计时/计数器电路实现。硬件定时器的最大优势在于精确度高和可靠性强,因为它们不受软件任务和操作系统调度的影响。当需要非常精确的定时功能,如产生PWM信号或者获取精确的时间测量时,硬件定时器是首选。由于定时操作由硬件直接完成,即使主CPU忙于其他任务,定时器仍然可以在预定时间到达时准确地执行回调操作。
- 软件定时器是由操作系统或者软件库实现的定时器,它们利用操作系统提供的机制来模拟定时器功能。软件定时器的实现受到当前系统负载和任务调度策略的影响,因此相对来说不如硬件定时器精确。但是软件定时器通常更灵活,可以创建大量的定时器,适用于不需要精确时间控制的场合。 在某些情况下,软件定时器可能会引起定时精度问题,例如在高负载条件下,或者当系统中有许多其他高优先级任务时。对于不需要高精度的简单延时,软件定时器通常足够使用。
7.3 定时器基本参数
ESP32S3芯片具有两个通用定时器组,每个定时器组包含两个通用定时器,例如 Timer0、Timer1 等,每个定时器都包含多个通道。可以通过指定定时器号和通道号来选择具体使用的定时器和通道。每个定时器都可以单独地进行编程,并且每个定时器可以以微秒精度产生基于时间的中断。基本的定时器参数包括:定时器号、通道号、预分频器、自动重新加载值、定时器中断使能等。
以下是一些基本概念和定时器的共有属性:
- 计时器(Counter): 定时器的核心组件,负责持续计数。
- 定时器溢出(Overflow): 当计数器达到其最大值然后归零时发生。
- 预置值(Preset Value): 计数器达到该值时会产生中断或其它事件。
- 分频器(Prescaler): 用于减小计数器接收的时钟信号频率,以延长定时器的最大计时范围。
- 中断(Interrupt): 当定时器达到预置值时,可以配置它来产生一个中断,中断处理程序将执行一些任务。
7.4 定时器的操作流程
7.4.1 导入头文件
ESP-IDF(ESP32的官方开发框架)提供了一套 API 来配置和控制定时器。要使用 ESP32 定时器功能,首先需要在代码中包含必要的头文件,例如:
#include "driver/gptimer.h"
7.4.2 初始化定时器
通用定时器实例由 gptimer_handle_t
表示。后台驱动会在资源池中管理所有可用的硬件资源,这样我们便无需考虑硬件所属的定时器以及定时器组,后台驱动会帮助我们自动分配。示例:
//定义一个通用定时器
gptimer_handle_t gptimer = NULL;
2
初始化定时器需要创建一个定时器参数结构体(gptimer_config_t),其中包含定时器的配置参数。参数说明如下:
- clk_src: 选择定时器的时钟源。gptimer_clock_source_t 中列出多个可用时钟,仅可选择其中一个时钟。以下为可选参数:
GPTIMER_CLK_SRC_APB
: 选择APB作为源时钟(APB=80MHz)GPTIMER_CLK_SRC_XTAL
: 选择XTAL作为源时钟 (XTAL=40MHz)GPTIMER_CLK_SRC_DEFAULT
: 选择APB作为默认选项 - direction 设置定时器的计数方向,gptimer_count_direction_t 中列出多个支持的方向,仅可选择其中一个方向。以下为可选参数:
GPTIMER_COUNT_DOWN
: 向下计数,即从65535到0;GPTIMER_COUNT_UP
: 向上计数,即从0到65535; - resolution_hz: 设置内部计数器的分辨率。计数器每滴答一次相当于 1 / resolution_hz 秒。
intr_priority
: 设置中断的优先级。如果设置为 0,则会分配一个默认优先级的中断,否则会使用指定的优先级。- intr_shared: 设置是否将定时器中断源标记为共享源,默认为1。如果该参数设置为true(1),那么多个定时器将共享同一个中断源。这意味着当其中一个定时器触发中断时,相应的中断处理程序将被调用,并且不同的定时器中断可以在同一个中断服务函数中进行处理。而如果intr_shared参数设置为false(0),那么每个定时器都会有独立的中断源,并且将有一个相应的中断服务函数用于处理每个定时器的中断。
完成上述结构体参数配置之后,可以将结构传递给 gptimer_new_timer() 函数,用以实例化定时器实例并返回定时器句柄。
函数原型:
esp_err_t gptimer_new_timer(const gptimer_config_t *config, gptimer_handle_t *ret_timer)
说明:创建一个新的通用定时器,并返回句柄。 参数:
- config – [] 定时器配置结构体参数地址
- ret_timer – [] 要配置的定时器实例 示例:
//定义一个通用定时器实例
gptimer_handle_t gptimer = NULL;
//配置定时器
gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT, //定时器时钟来源 选择APB作为默认选项
.direction = GPTIMER_COUNT_UP, //向上计数
.resolution_hz = 1000000, // 1MHz, 1 tick=1us
};
//将配置设置到定时器实例中
gptimer_new_timer(&timer_config, &gptimer);
2
3
4
5
6
7
8
9
10
7.4.3 设置定时器报警
定时器报警,指的是当定时器定的时到达我们设置的目标时,会提醒我们。对于大多数通用定时器使用场景而言,应在启动定时器之前设置警报动作(相当于开启中断触发条件)。设置警报动作,需要根据如何使用警报事件来配置 gptimer_alarm_config_t 的不同参数:
- alarm_count 设置触发警报事件的目标计数值。设置警报值时还需考虑计数方向。尤其是当 auto_reload_on_alarm 为 true 时,alarm_count 和 reload_count 不能设置为相同的值,因为警报值和重载值相同时没有意义。
- reload_count 代表警报事件发生时要重载的计数值。此配置仅在 auto_reload_on_alarm 设置为 true 时生效。
- auto_reload_on_alarm 标志设置是否使能自动重载功能。如果使能,硬件定时器将在警报事件发生时立即将 reload_count 的值重载到计数器中。例如,设置为向上计数,当前值是0,目标值是10,自动重载值为5。当计数值从0向上增加到10时,就会触发一次报警事件,报警事件之后就计数值不会变为0,而是变为了5。
需要注意的是,要使警报配置生效,需要调用 gptimer_set_alarm_action()。特别是当 gptimer_alarm_config_t 设置为 NULL
时,报警功能将被禁用。
使用示例:
//通用定时器的报警值设置
gptimer_alarm_config_t alarm_config = {
.reload_count = 0, //重载计数值为0
.alarm_count = 1000000, // 报警目标计数值 period = 1s
.flags.auto_reload_on_alarm = true, //开启重加载
};
//设置触发报警动作
gptimer_set_alarm_action(gptimer, &alarm_config);
2
3
4
5
6
7
8
其中,gptimer_set_alarm_action()
函数的原型如下:
esp_err_t gptimer_set_alarm_action(gptimer_handle_t timer, const gptimer_alarm_config_t *config)
说明:设置GPTimer的告警事件动作。包括触发条件值,是否重载。 参数
- timer – 要设置的定时器;
- config – gptimer_alarm_config_t参数的配置; 返回
- 请参考 esp_err_t 结构体的参数定义;
7.4.4 注册回调函数
定时器启动后,可动态产生特定事件(如“警报事件”)。如需在事件发生时调用某些函数,可以通过 gptimer_register_event_callbacks() 将函数挂载到中断服务例程 (ISR)。
esp_err_t gptimer_register_event_callbacks(gptimer_handle_t timer, const gptimer_event_callbacks_t *cbs, void *user_data)
说明:设置定时器回调函数。 参数
- timer – 要设置的定时器;
- cbs – 要绑定的回调函数;
- user_data – 用户的数据,将直接传递给回调函数;
返回
- 请参考 esp_err_t 结构体的参数定义;
需要注意的是,对这个函数的第一次调用需要在调用gptimer_enable() 函数之前。
7.4.5 使能和禁用定时器
使能定时器
在对定时器进行控制之前,需要先调用 gptimer_enable() 使能定时器。此函数功能如下:
esp_err_t gptimer_enable(gptimer_handle_t timer)
- 此函数将把定时器驱动程序的状态从 初始化 切换为 使能状态。
- 如果 gptimer_register_event_callbacks() 已经延迟安装回调服务函数,此函数将使能回调服务函数。
失能定时器
调用 gptimer_disable() 会进行相反的操作,即将定时器驱动程序恢复到 初始化 状态,禁用回调服务并释放定时器。
7.4.6 启动和停止定时器
我们使能了定时器之后,并没有代表定时器已经开始运行,还需要通过调用 gptimer_start() 函数使内部计数器开始工作。而 gptimer_stop() 可以使计数器停止工作。
7.5 定时器验证
在main/hardware/timer目录下(如果没有该目录则新建),新建两个文件,bsp_timer.c 和 bsp_timer.h。
记得配置头文件的路径
在bsp_timer.h中编写如下代码。
#ifndef _BSP_TIMER_H_
#define _BSP_TIEMR_H_
#include "driver/gptimer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "freertos/queue.h"
/**
* @函数说明 定时器初始化配置
* @传入参数 resolution_hz=定时器的分辨率 alarm_count=触发警报事件的目标计数值
* @函数返回 创建的定时器回调队列
*/
QueueHandle_t timerInitConfig(uint32_t resolution_hz, uint64_t alarm_count);
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在bsp_timer.c中编写如下代码。
#include "bsp_timer.h"
/**
* @函数说明 定时器回调函数
* @传入参数
* @函数返回
*/
static bool IRAM_ATTR TimerCallback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data)
{
BaseType_t high_task_awoken = pdFALSE;
//将传进来的队列保存
QueueHandle_t queue = (QueueHandle_t)user_data;
static int time = 0;
time++;
//从中断服务程序(ISR)中发送数据到队列
xQueueSendFromISR(queue, &time, &high_task_awoken);
return (high_task_awoken == pdTRUE);
}
/**
* @函数说明 定时器初始化配置
* @传入参数 resolution_hz=定时器的分辨率 alarm_count=触发警报事件的目标计数值
* @函数返回 创建的定时器回调队列
*/
QueueHandle_t timerInitConfig(uint32_t resolution_hz, uint64_t alarm_count)
{
//定义一个通用定时器
gptimer_handle_t gptimer = NULL;
//创建一个队列
QueueHandle_t queue = xQueueCreate(10, sizeof(10));
//如果创建不成功
if (!queue) {
ESP_LOGE("queue", "Creating queue failed");
return NULL;
}
//配置定时器参数
gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT, //定时器时钟来源 选择APB作为默认选项
.direction = GPTIMER_COUNT_UP, //向上计数
//计数器分辨率(工作频率)以Hz为单位,因此,每个计数滴答的步长等于(1 / resolution_hz)秒
//假设 resolution_hz = 1000 000
//1 / resolution_hz = 1 / 1000000 = 0.000001(秒) = 1(微秒) ( 1 tick= 1us )
.resolution_hz = resolution_hz,
};
//将配置设置到定时器
gptimer_new_timer(&timer_config, &gptimer);
//绑定一个回调函数
gptimer_event_callbacks_t cbs = {
.on_alarm = TimerCallback,
};
//设置定时器gptimer的 回调函数为cbs 传入的参数为NULL
gptimer_register_event_callbacks(gptimer, &cbs, queue);
//使能定时器
gptimer_enable(gptimer);
//通用定时器的报警值设置
gptimer_alarm_config_t alarm_config = {
.reload_count = 0, //重载计数值为0
.alarm_count = alarm_count, // 报警目标计数值 1000000 = 1s
.flags.auto_reload_on_alarm = true, //开启重加载
};
//设置触发报警动作
gptimer_set_alarm_action(gptimer, &alarm_config);
//开始定时器开始工作
gptimer_start(gptimer);
return queue;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
在main.c中编写如下代码。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "bsp_timer.h"
static const char *TAG = "example";
void app_main(void)
{
int number = 0;
QueueHandle_t queue = 0;
// 初始化定时器 1秒进入回调函数一次
queue = timerInitConfig(1000000,1000000);
while(1)
{
//从队列中接收一个数据,不能在中断服务函数使用
if (xQueueReceive(queue, &number, pdMS_TO_TICKS(2000)))
{
ESP_LOGI(TAG, "Timer stopped, count=%d", number);
} else {
ESP_LOGW(TAG, "Missed one count event");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
在上面示例代码中,使用到了关于FreeRTOS相关的内容。xQueueSendFromISR()
是一个用于在FreeRTOS实时操作系统中从中断服务程序(ISR)中发送数据到队列。
这个函数的原型如下:
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken);
参数说明:
xQueue
:要发送数据的队列。pvItemToQueue
:指向要发送的数据的指针。pxHigherPriorityTaskWoken
:一个指向BaseType_t
类型变量的指针,在发送数据的过程中,如果有更高优先级的任务需要唤醒,通过传递指向该变量的指针,可以通知内核唤醒任务。 函数返回:- 成功时,返回
pdPASS
,表示数据成功发送到队列。 - 失败时,返回
errQUEUE_FULL
,表示队列已满,无法发送数据。 需要注意的是,xQueueSendFromISR()
函数只能从中断服务程序中调用,而不能从普通任务中调用。这是因为在中断服务程序中发送数据到队列时需要采取特殊的操作,以及保证在多任务环境中的数据同步和使用的正确性。
xQueueReceive()
是一个用于从FreeRTOS实时操作系统的队列中接收数据。
这个函数的原型如下:
BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
参数说明:
xQueue
:要读取数据的队列的句柄。pvBuffer
:指向用于接收数据的缓冲区。xTicksToWait
:等待数据可用的超时时间,以FreeRTOS时钟节拍为单位。如果设置为portMAX_DELAY
,则表示一直等待直到有数据可读。
函数返回:- 成功时,返回一个正数,表示已经成功接收了数据项的数量。
- 失败时,返回
errQUEUE_EMPTY
,表示队列为空,无法获取数据项。
需要注意的是,xQueueReceive()
函数通常会阻塞等待队列中有数据可读,在数据可读后获取数据,并将该数据从队列中删除。如果设置了超时时间xTicksToWait
,则在指定的时间内等待有数据可读,并在超时前返回。如果没有设置超时时间,那么这个函数会一直阻塞,直到队列中有数据可读。
此外,需要在使用xQueueReceive()
函数时注意内存安全性。由于该函数的参数中包含指向缓冲区的指针,因此需要确保缓冲区大小足够,以避免内存越界和数据损坏的问题。
7.6 定时器验证效果
烧录以上代码之后,可以通过串口助手串口日志。每个消息都是以1秒的间隔进行发送。