【快速上手ESP32(基于ESP-IDF&VSCode)】03-定时器

ESP32中的通用定时器

通用定时器是 ESP32 定时器组外设的驱动程序。ESP32 硬件定时器分辨率高,具有灵活的报警功能。定时器内部计数器达到特定目标数值的行为被称为定时器报警。定时器报警时将调用用户注册的不同定时器回调函数。

在ESP32-S3中,一共有两个定时器组,每个定时器组中各有两个通用定时器以及一个看门狗定时器。

每个通用定时器都有16位预分频器和54位可自动重新加载向上/向下计数器。

通用定时器通常在以下场景中使用:

  • 如同挂钟一般自由运行,随时随地获取高分辨率时间戳;

  • 生成周期性警报,定期触发事件;

  • 生成一次性警报,在目标时间内响应。

使用通用定时器

#include "driver/gptimer.h"

我们跟着编程指南来,第一步需要实例化一个定时器句柄。

可以配置的有五个点。

第一个是时钟源,我顺着编程指南的链接往上找,结果找到的结果就两个,甚至我觉得这两个是一个意思。

但是立创开发板提供的文档中有三个选项。

但是其实也不用纠结使用什么,我们就选默认的就行。因为ESP32的定时器与STM32的定时器在设置上不一样,STM32中我们需要通过时钟源频率去计算该如何配置自动重装载计数器和预分频器,但是在ESP32中我们可以不用计算,直接指定溢出频率,也就是上面的第三个。

第三个结构体成员设置内部计数器的分辨率,其实也就是溢出频率。

第二个结构体成员指定计数方向,一般我们就选向上计数。

第四个指定优先级,第五个设置是否将中断源共享,这俩我们在中断比较少的情况下可以不设置。

优先级不必说,数值越小越大(0是分配一个默认的优先级,并不是优先级最高)。

第五个是否中断源共享其实我们在STM32中就使用过了,不过STM32是默认就是共享的,因为不同的中断可能会进入同一个中断函数,这就是中断源共享。

配置完之后需要使用 gptimer_new_timer 实例化定时器并获取句柄,传入的参数就是刚刚结构体变量和定时器句柄的地址,因此除了上面的配置,我们还需要定义一个定时器句柄,可以参考下面的代码。

    // 定义一个通用定时器
    gptimer_handle_t gptimer = NULL;

    // 配置定时器参数
    gptimer_config_t timer_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT, // 定时器时钟来源 选择APB作为默认选项
        .direction = GPTIMER_COUNT_UP,      // 向上计数
        .resolution_hz = 1e6,
    };
    // 将配置设置到定时器
    gptimer_new_timer(&timer_config, &gptimer);

也可以参考编程指南的示例代码。

当我们不再需要定时器的时候,需要先禁用定时器,然后删除,具体流程在下面。

我们也可以通过下面两个函数去获取和修改计数器的值。

做完上面的准备工作之后,按照STM32中的流程,我们应该要写中断函数了吧。

在ESP32中,我们换了个叫法,我们叫警报。所以流程就是配置定时器,然后配置警报,接着绑定警报的回调函数,这个回调函数可以理解成就是STM32中的中断函数。

一共需要配置的结构体成员有三个。

第一个设置触发警报事件的目标计数值,也就是当计数器的值到达这个目标值的时候触发警报。

第二个设置重装载的值,一般咱就选择重装成0,然后计数模式是向上计数。

第三个选择是否自动重载,也就是说我们是否要这个定时器警报周期性的给我们警报。

给结构体的成员配置完之后,还需要使用gptimer_set_alarm_action这个函数去激活,第一个参数是定时器的句柄,第二个参数是上面配置好的结构体变量的地址。

可以参考下面的代码。

    // 通用定时器的报警值设置
    gptimer_alarm_config_t alarm_config = {
        .reload_count = 0,                  // 重载计数值为0
        .alarm_count = 5e5,         // 报警目标计数值 500000 = 500ms
        .flags.auto_reload_on_alarm = true, // 开启重加载
    };
    // 设置触发报警动作
    gptimer_set_alarm_action(gptimer, &alarm_config);

接下来就该绑定警报事件的回调函数了。

这个回调函数虽然不像STM32那样定死了,但是也是给我们限制住了,必须确保这个函数不会试图阻塞,甚至是使用FreeRTOS的API。

没错,ESP-IDF给我们自带了FreeRTOS的API,我们导入头文件之后就可以直接使用FreeRTOS了,这一点还是非常方便的。

不过我在编程指南中没找到提供的可用的回调函数,估计是我英文看不懂,翻译成中文有些偏差,搞得我迟迟没有找到。

不过没关系,我们直接抄别人现成的写好的来分析。

下面是立创开发板提供的文档中的代码。最后面我会贴出出完整版的修改过的代码,下面把代码切开来是为了方便分析。

首先先是包含头文件。

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

 可以看得出包含的头文件里有freertos的字样,也就是说我们通过ESP-IDF创建的工程确实可以直接使用FreeRTOS。

下面这一段是绑定回调函数,第三行中的TimerCallBback是回调函数的名字。

后面创建了一个队列,是用在最后设置定时器的回调函数时的第三个参数,这个参数是用户数据,会直接传递给回调函数。

    // 绑定一个回调函数
    gptimer_event_callbacks_t cbs = {
        .on_alarm = TimerCallback,
    };
    // 创建一个队列
    QueueHandle_t queue = xQueueCreate(10, sizeof(10));
    // 设置定时器gptimer的 回调函数为cbs  传入的参数为NULL
    gptimer_register_event_callbacks(gptimer, &cbs, queue);

下面是回调函数,函数名,返回值,形参的类型是固定的,我们可以在下面我写注释的地方写上我们要执行的逻辑,因为这个回调函数里不能有阻塞的风险,因此我们最好就是只写简单的逻辑甚至是不写,那么我们应该写在哪里呢?

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;
    // 从中断服务程序(ISR)中发送数据到队列
    xQueueSendFromISR(queue, &time, &high_task_awoken);

    return (high_task_awoken == pdTRUE);
}

写在主循环里面。

    int number = 0;
    while(1){
        //从队列中接收一个数据,不能在中断服务函数使用
        if (xQueueReceive(queue, &number, pdMS_TO_TICKS(2000))) {
            //触发了警报之后这里会执行
        } else {
            //出现错误
        }
    }

剩下就是使能定时器以及启动了。

    // 使能定时器
    gptimer_enable(gptimer);
    // 开始定时器开始工作
    gptimer_start(gptimer);

那么接下来就完整地展示一下通过定时器来让LED闪烁的代码。

#include <stdio.h>
#include "driver/gptimer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

#include "driver/gpio.h"

void initGPIO(){
    gpio_config_t init;
    init.intr_type=GPIO_INTR_DISABLE;             //失能中断;
    init.mode=GPIO_MODE_OUTPUT|GPIO_MODE_INPUT;   
    init.pin_bit_mask=(1ULL<<18);                 //GPIO18
    init.pull_down_en=GPIO_PULLDOWN_DISABLE;      //失能下拉模式
    init.pull_up_en=GPIO_PULLUP_ENABLE;           //使能上拉模式

    gpio_config(&init);
}

/**
 * @函数说明        定时器回调函数
 * @传入参数
 * @函数返回
 */
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;
    // 从中断服务程序(ISR)中发送数据到队列
    xQueueSendFromISR(queue, &time, &high_task_awoken);

    return (high_task_awoken == pdTRUE);
}

void app_main(void){
    initGPIO();
    // 定义一个通用定时器
    gptimer_handle_t gptimer = NULL;
    // 配置定时器参数
    gptimer_config_t timer_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT, // 定时器时钟来源 选择APB作为默认选项
        .direction = GPTIMER_COUNT_UP,      // 向上计数
        .resolution_hz = 1e6,
    };
    // 将配置设置到定时器
    gptimer_new_timer(&timer_config, &gptimer);


    // 通用定时器的报警值设置
    gptimer_alarm_config_t alarm_config = {
        .reload_count = 0,                  // 重载计数值为0
        .alarm_count = 5e5,         // 报警目标计数值 500000 = 500ms
        .flags.auto_reload_on_alarm = true, // 开启重加载
    };
    // 设置触发报警动作
    gptimer_set_alarm_action(gptimer, &alarm_config);
    

    // 绑定一个回调函数
    gptimer_event_callbacks_t cbs = {
        .on_alarm = TimerCallback,
    };
    // 创建一个队列
    QueueHandle_t queue = xQueueCreate(10, sizeof(10));
    // 设置定时器gptimer的 回调函数为cbs  传入的参数为NULL
    gptimer_register_event_callbacks(gptimer, &cbs, queue);


    // 使能定时器
    gptimer_enable(gptimer);
    // 开始定时器开始工作
    gptimer_start(gptimer);
    
    int number = 0;
    
    while(1){
        //从队列中接收一个数据,不能在中断服务函数使用
        if (xQueueReceive(queue, &number, pdMS_TO_TICKS(2000))) {
           gpio_set_level(18,!gpio_get_level(18));
        } else {
            printf("error\r\n");
        }
    }
}

ESP32中的软件定时器

上面介绍的通用定时器实际上是硬件定时器,所以相对的,我们能用的还有软件定时器。

软件定时器就是系统模拟出来的定时器,而通用定时器是实实在在有的硬件定时器,因此硬件定时器的精度会更高,而软件定时器使用起来可以有很多个,并且代码编写方面也比较简单,但是相对的,精度会略微下降。

在编程指南里面,我们可以在系统API里找到软件定时器的用法,而我们之前的通用定时器的用法在外设API里,这也侧面说明了两种用法的区别。

使用软件定时器

使用软件定时器首先需要包含头文件。

#include "esp_timer.h"

接着我们需要构造一个esp_timer示例。

我们分别看看需要哪两个参数。

第一个esp_timer_create_args_t

由它来设置我们的回调函数,我们只需要设置前两个结构体成员即可,剩下的可以不配置。

第二个是软件定时器的句柄,我们直接创建一个,然后取地址放进去即可。

可以参考下面的例子。

    esp_timer_handle_t  timer1=0;
    
    esp_timer_create_args_t timer1_arg = {
            .callback = &timer1Callback,
            .arg = NULL
    };

	esp_timer_create(&timer1_arg , &timer1);

下一步有了配置好了的定时器的句柄之后我们就可以直接启动了。

 一共有两种启动方式,一种是周期性定时,另一种是一次性定时。

传入的第一个参数是定时器句柄,第二个是启动的时间,单位是微秒。

剩下就是上面三个函数:停止定时器,重新启动定时器,删除定时器,看看上面的解释就可以知道三者之间的联系与区别了。

然后就结束了。软件定时器就是这么简单。

接下来大家结合着我下面的示例代码就可以理解了。

#include "driver/gpio.h"
#include <unistd.h>
#include "esp_timer.h"

void initGPIO(){
    gpio_config_t init = {
        .intr_type = GPIO_INTR_DISABLE,             // 失能中断;
        .mode = GPIO_MODE_OUTPUT | GPIO_MODE_INPUT, // 输出模式&输入模式(为了读取电平来翻转)
        .pin_bit_mask = (1ULL << 18),               // GPIO18
        .pull_down_en = GPIO_PULLDOWN_DISABLE,      // 失能下拉模式
        .pull_up_en = GPIO_PULLUP_ENABLE,           // 使能上拉模式
    };
    gpio_config(&init);
}

esp_timer_handle_t timer1 = 0;
esp_timer_handle_t timer2 = 0;

void timer1Callback(void *arg){
    esp_timer_stop(timer2);   // 删除前需要停止
    esp_timer_delete(timer2); // 删除定时器
}

void timer2Callback(void *arg){
    gpio_set_level(18, !gpio_get_level(18)); // 翻转GPIO口电平
}

void initTimer(void){
    esp_timer_create_args_t timer1_arg = {
        .callback = &timer1Callback,
        .arg = NULL
    };

    esp_timer_create_args_t timer2_arg = {
        .callback = &timer2Callback,
        .arg = NULL
    };

    esp_timer_create(&timer1_arg, &timer1);
    esp_timer_start_once(timer1, 5 * 1000 * 1000);  //5s后执行一次

    esp_timer_create(&timer2_arg, &timer2);
    esp_timer_start_periodic(timer2, 1000 * 1000);  //1s执行一次,周期执行
}
void app_main(void){
    initGPIO();
    initTimer();
    while (1){
        usleep(1000*10);        //加个10ms延时,以免运行时产生报错
    }
}

相关推荐

  1. ESP32 Arduino 定时器中断

    2024-04-09 12:34:02       14 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-09 12:34:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-09 12:34:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-09 12:34:02       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-09 12:34:02       20 阅读

热门阅读

  1. 头歌:共享单车之数据可视化

    2024-04-09 12:34:02       15 阅读
  2. 计算机网络-ICMP和ARP协议——沐雨先生

    2024-04-09 12:34:02       15 阅读
  3. Ubuntu22.04 安装 Supabase

    2024-04-09 12:34:02       14 阅读
  4. 【力扣】238. 除自身以外数组的乘积

    2024-04-09 12:34:02       14 阅读
  5. npm的一些经常使用的命令

    2024-04-09 12:34:02       14 阅读
  6. CSS变换

    CSS变换

    2024-04-09 12:34:02      11 阅读
  7. 7.网络编程-安全

    2024-04-09 12:34:02       13 阅读