提示:本博客作为学习笔记,有错误的地方希望指正

一、ESP32定时器介绍

参考资料:ESP IDF编程手册V4.4

1.1、概述

  尽管FreeRTOS提供了软件计时器,但这些计时器有一些限制:

  • 最大分辨率等于RTOS周期
  • 定时器回调从低优先级任务中分派

  硬件计时器不受这两种限制,但通常使用起来不太方便。例如,应用程序组件可能需要定时器事件在未来的特定时间触发,但硬件定时器只包含一个用于中断生成的“compare”值。这意味着需要在硬件计时器之上构建一些工具来管理挂起的事件列表,当相应的硬件中断发生时,可以为这些事件分派回调。
  处理程序的中断级别取决于CONFIG_ESP_TIMER_INTERRUPT_LEVEL选项。它允许设置这个:1,2或3级别(默认为1)。提高级别,中断处理程序可以减少定时器处理延迟。
  esp_timer api集提供了一次性定时器和周期性定时器,微秒时间分辨率和64位范围。
  在内部,esp_timer使用64位硬件计时器,其中实现取决于目标。LAC定时器用于ESP32。
  定时器回调可以通过两种方法分派:

  • ESP_TIMER_TASK
  • ESP_TIMER_ISR。仅当CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD开启(默认关闭)时可用。

  ESP_TIMER_TASK。计时器回调是由高优先级esp_timer任务分派的。因为所有的回调都是从同一个任务调度的,所以建议只从回调本身执行尽可能少的工作,而是使用队列将事件提交给低优先级的任务。
  如果其他优先级高于esp_timer的任务正在运行,回调调度将被延迟,直到esp_timer任务有机会运行。例如,如果正在进行一个SPI Flash操作,就会发生这种情况。
  ESP_TIMER_ISR。定时器回调是直接从定时器中断处理程序调度的。这个方法在一些简单的回调中很有用,目的是降低延迟。
  创建和启动计时器,以及调度回调需要一些时间。因此,一次性esp_timer超时值有一个下限。如果esp_timer_start_once()在超时值小于20us的情况下被调用,回调只会在大约20us之后才会被调度。
  周期性esp_timer还对最小定时器周期施加了50us的限制。周期小于50us的定期软件计时器是不实用的,因为它们会消耗大部分CPU时间。如果需要小周期的定时器,可以考虑使用专用的硬件外设或DMA特性。

1.2、使用esp_timer api

  单个定时器由esp_timer_handle_t类型表示。计时器有一个与之相关的回调函数。每当计时器超时时,esp_timer任务就会调用这个回调函数。

  • 要创建计时器,可以调用esp_timer_create()。
  • 要在不再需要时删除计时器,请调用esp_timer_delete()。
      定时器可以一次性启动,也可以定时启动。
  • 要以一次性模式启动计时器,请调用esp_timer_start_once(),并传递应在之后调用回调的时间间隔。当调用回调时,计时器被认为已停止。
  • 要以周期模式启动计时器,请调用esp_timer_start_periodic(),传递应该调用回调的周期。计时器将一直运行,直到调用esp_timer_stop()。
      注意,当调用esp_timer_start_once()或esp_timer_start_periodic()时,定时器不能运行。要重新启动正在运行的计时器,首先调用esp_timer_stop(),然后调用一个启动函数。

1.3、回调函数

  注解
  保持回调函数尽可能短,否则它会影响所有的计时器。
  由ESP_TIMER_ISR方法处理的定时器回调不应该调用上下文切换调用—portYIELD_FROM_ISR(),而应该使用esp_timer_isr_dispatch_need_yield()函数。如果系统需要,上下文切换将在处理完所有ISR调度计时器之后进行。

1.4、esp_timer在浅睡眠期间

  在浅睡眠期间,esp_timer计数器停止并且不调用回调函数。相反,时间是由RTC计数器计算的。在唤醒时,系统获取计数器之间的差值,并调用一个提高esp_timer计数器的函数。由于计数器已被提前,系统将开始调用睡眠期间未调用的回调函数。回调的数量取决于睡眠的持续时间和定时器的周期。它可能导致一些队列溢出。这只适用于周期性计时器,一次性计时器将被调用一次。
  这种行为可以通过在睡眠前调用 esp_timer_stop() 来改变。在某些情况下,这可能很不方便,您可以在 esp_timer_create() 期间使用 skip_unhandled_events 选项来代替停止功能。当 skip_unhandled_events 为真时,如果周期性计时器在轻度睡眠期间到期一次或多次,则唤醒时仅调用一次回调。
  使用带有自动轻度睡眠的 skip_unhandled_events 选项(请参阅电源管理 API)有助于减少系统处于轻度睡眠时的消耗。浅睡眠的持续时间也由 esp_timers 决定。带有 skip_unhandled_events 选项的计时器不会唤醒系统。

1.5、处理回调

  esp_timer 旨在实现高分辨率低延迟计时器和处理延迟事件的能力。如果计时器迟到了,那么回调将尽快被调用,它不会丢失。在最坏的情况下,当定时器超过一个周期(对于周期性定时器)没有被处理时,在这种情况下,回调将一个接一个地被调用,而无需等待设置的周期。这对某些应用程序来说可能很糟糕,并且引入了 skip_unhandled_events 选项来消除这种行为。如果设置了skip_unhandled_events,那么一个已过期多次而无法调用回调的周期性定时器在可以处理时仍将仅导致一个回调事件。

1.6、获取当前时间

  esp_timer 还提供了一个方便的函数来获取自启动以来经过的时间,精度为微秒:esp_timer_get_time()。此函数返回自 esp_timer 初始化以来的微秒数,这通常发生在调用 app_main 函数之前不久。
  与 gettimeofday 函数不同,esp_timer_get_time() 返回的值:

  1. 芯片从深度睡眠唤醒后从零开始
  2. 未应用时区或 DST 调整

二、硬件设计

  这里只涉及到ESP32系统资源,不需要外部硬件。

三、实现代码

  在历程中实现两个定时器的初始化,创建定时器,然后执行定时器,第二个定时器中断回调中改变第一个定时器的频率,其中还有将定时器挂起,系统进入休眠状态。
  初始化流程
在这里插入图片描述
  初始化代码如下

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_sleep.h"
#include "sdkconfig.h"

static void periodic_timer_callback(void* arg);
static void oneshot_timer_callback(void* arg);

static const char* TAG = "example";
/**
 * 创建两个定时器:
 * 1. 一个周期定时器,每 0.5 秒运行一次,并打印一条消息
 * 2. 单发定时器,5秒后触发,以1s为周期重新启动周期性定时器。
*/
void app_main(void)
{
    const esp_timer_create_args_t periodic_timer_args = {
            .callback = &periodic_timer_callback,   //定时器回调函数
            .name = "periodic"                      //定时器名称
    };
    esp_timer_handle_t periodic_timer;              //创建一个定时器变量
    //创建一个定时器
    ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));   
    const esp_timer_create_args_t oneshot_timer_args = {
            .callback = &oneshot_timer_callback,    //定时器回调函数                             
            .arg = (void*) periodic_timer,          //传递给回调函数的参数                     
            .name = "one-shot"                      //定时器名称         
    };                              
    esp_timer_handle_t oneshot_timer;               //创建一个定时器变量
    //创建一个定时器
    ESP_ERROR_CHECK(esp_timer_create(&oneshot_timer_args, &oneshot_timer));
    //开启定时器
    ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, 500000));
    //启动一次性定时器
    ESP_ERROR_CHECK(esp_timer_start_once(oneshot_timer, 5000000));
    ESP_LOGI(TAG, "Started timers, time since boot: %lld us", esp_timer_get_time());
    //每2秒向控制台打印一次定时器的调试信息
    for (int i = 0; i < 5; ++i) {
        ESP_ERROR_CHECK(esp_timer_dump(stdout));    //将计时器列表转储到流中
        usleep(2000000);                            //休眠 us
    }
    //在浅睡眠中计时继续,计时器被调度浅睡后正确。
    ESP_LOGI(TAG, "Entering light sleep for 0.5s, time since boot: %lld us",esp_timer_get_time());
    //开启定时唤醒功能
    ESP_ERROR_CHECK(esp_sleep_enable_timer_wakeup(500000));
    //定时器开启轻度睡眠
    esp_light_sleep_start();
    ESP_LOGI(TAG, "Woke up from light sleep, time since boot: %lld us",esp_timer_get_time());
    //让计时器再运行一会儿
    usleep(2000000);
    //清理并完成示例
    ESP_ERROR_CHECK(esp_timer_stop(periodic_timer));
    ESP_ERROR_CHECK(esp_timer_delete(periodic_timer));
    ESP_ERROR_CHECK(esp_timer_delete(oneshot_timer));
    ESP_LOGI(TAG, "Stopped and deleted timers");
}

static void periodic_timer_callback(void* arg)
{
    int64_t time_since_boot = esp_timer_get_time();     //获取定时器的时间
    ESP_LOGI(TAG, "Periodic timer called, time since boot: %lld us", time_since_boot);
}

static void oneshot_timer_callback(void* arg)
{
    int64_t time_since_boot = esp_timer_get_time();         //获取定时器的时间
    ESP_LOGI(TAG, "One-shot timer called, time since boot: %lld us", time_since_boot);
    esp_timer_handle_t periodic_timer_handle = (esp_timer_handle_t) arg;
    //要启动正在运行的计时器,需要先停止它
    ESP_ERROR_CHECK(esp_timer_stop(periodic_timer_handle)); //停止定时器
    ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer_handle, 1000000));  //开启定时器
    time_since_boot = esp_timer_get_time();                 //获取定时器的时间
    ESP_LOGI(TAG, "Restarted periodic timer with 1s period, time since boot: %lld us",time_since_boot);
}

四、定时器实验演示结果

&esmp;&esmp;定时器执行的结果以log的形式打印显示的
在这里插入图片描述

五、ESP32 Timer函数API

5.1、esp_timer.h文件中的内容的API

  esp_timer.h文件中的简约版本的API不带参数解释的部分,内容基本上初始化定时器结构体,这里面使用了typedef的形式定义了typedef void (esp_timer_cb_t)(void arg);的函数指针的,在定时器初始化中申明函数,运用挺巧妙的。


struct esp_timer {
    uint64_t alarm;
    uint64_t period:56;
    flags_t flags:8;
    union {
        esp_timer_cb_t callback;
        uint32_t event_id;
    };
    void* arg;
#if WITH_PROFILING
    const char* name;
    size_t times_triggered;
    size_t times_armed;
    size_t times_skipped;
    uint64_t total_callback_run_time;
#endif // WITH_PROFILING
    LIST_ENTRY(esp_timer) list_entry;
};
//取别名结构体
typedef struct esp_timer* esp_timer_handle_t;
//取别名函数指针
typedef void (*esp_timer_cb_t)(void* arg);

typedef enum {
    ESP_TIMER_TASK,
} esp_timer_dispatch_t;

typedef struct {
    esp_timer_cb_t callback;               // !<计时器回调函数
    void* arg;                             // !传递给回调函数的参数
    esp_timer_dispatch_t dispatch_method;  // !<从任务或ISR调用回调
    const char* name;                      // !<定时器名称,在esp_timer_dump函数中使用
    bool skip_unhandled_events;            // !<跳过定期计时器的未处理事件
} esp_timer_create_args_t;
/*
* @brief esp_timer最小初始化
* 这个函数在启动代码中被调用。应用程序不需要
* 在使用其他esp_timer api之前调用该函数。
* 这个函数可以在启动过程的早期调用,在这个调用之后
* 只能使用esp_timer_get_time函数。
*/
esp_err_t esp_timer_early_init(void);
/*
* 初始化esp_timer库
* 这个函数在启动代码中被调用。应用程序不需要
* 在使用其他esp_timer api之前调用该函数。
* 在调用这个函数之前,esp_timer_early_init必须被调用
* 启动代码。
*/
esp_err_t esp_timer_init(void);
//反初始化esp_timer库,通常这个函数不应该被应用程序调用
esp_err_t esp_timer_deinit(void);
/*
*创建一个esp_timer实例
*当使用定时器完成时,使用esp_timer_delete函数删除它。
@param create_args指针指向一个带有定时器创建参数的结构体。
*不被库保存,可以在堆栈上分配。
*/
esp_err_t esp_timer_create(const esp_timer_create_args_t* create_args,esp_timer_handle_t * out_handle);
//启动一次性定时器,定时器不应该运行时,该函数被调用。
esp_err_t esp_timer_start_once(esp_timer_handle_t timer, uint64_t timeout_us);
//调用此函数时不应运行计时器。 此函数将启动计时器,该计时器将每“周期”微秒触发一次。
esp_err_t esp_timer_start_periodic(esp_timer_handle_t timer, uint64_t period);
/*
* @brief停止定时器
* 这个函数停止之前使用esp_timer_start_once启动的计时器
* 或esp_timer_start_periodic。
*/
esp_err_t esp_timer_stop (esp_timer_handle_t timer);
/*
* @brief删除esp_timer实例
* 删除前必须停止定时器。已过期的一次性计时器不需要停止。
*/
esp_err_t esp_timer_delete (esp_timer_handle_t timer);
//以微秒为单位获取启动后的时间
int64_t esp_timer_get_time(void);
//获取预计下一次超时发生的时间戳
int64_t esp_timer_get_next_alarm(void);
//获取下一个超时预计发生的时间戳,跳过那些有skip_unhandled_events标志的
int64_t esp_timer_get_next_alarm_for_wake_up(void);
/*
* 将计时器列表转储到流中
* 如果CONFIG_ESP_TIMER_PROFILING选项被启用,将打印所有选项的列表
* 现有的计时器。否则,只打印列表活动的计时器。
* 格式为:
* name周期告警times_armed times_triggered total_callback_run_time
* 所在:
* name -计时器名称(如果定义了CONFIG_ESP_TIMER_PROFILING),或计时器指针
* period -定时器的周期,单位为微秒,单次定时器为0
* alarm -下一个告警的时间,以微秒为单位,如果定时器为0
* 未启动
* 如果定义了CONFIG_ESP_TIMER_PROFILING,会打印以下字段:
* times_armed -通过esp_timer_start_X设置定时器武装的次数
* times_triggered - callback被调用的次数
* total_callback_run_time -跨所有调用执行回调所需的总时间
*/
esp_err_t esp_timer_dump(FILE* stream);

# ifdef CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
/*
* 请求从一个定时器回调函数切换上下文。
* 这只适用于有ISR调度方法的定时器。
* 在所有ISR调度计时器处理完毕后,上下文切换将被调用。
*/
void esp_timer_isr_dispatch_need_yield(void);
# endif // CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
//返回定时器的状态,激活或不激活,此函数用于识别计时器是否仍处于活动状态。
bool esp_timer_is_active (esp_timer_handle_t计时器);

  esp_timer.h文件中的带参数版本的API部分


struct esp_timer {
    uint64_t alarm;
    uint64_t period:56;
    flags_t flags:8;
    union {
        esp_timer_cb_t callback;
        uint32_t event_id;
    };
    void* arg;
#if WITH_PROFILING
    const char* name;
    size_t times_triggered;
    size_t times_armed;
    size_t times_skipped;
    uint64_t total_callback_run_time;
#endif // WITH_PROFILING
    LIST_ENTRY(esp_timer) list_entry;
};
//取别名结构体
typedef struct esp_timer* esp_timer_handle_t;
//取别名函数指针
typedef void (*esp_timer_cb_t)(void* arg);

typedef enum {
    ESP_TIMER_TASK,
} esp_timer_dispatch_t;

typedef struct {
    esp_timer_cb_t callback;               // !<计时器回调函数
    void* arg;                             // !传递给回调函数的参数
    esp_timer_dispatch_t dispatch_method;  // !<从任务或ISR调用回调
    const char* name;                      // !<定时器名称,在esp_timer_dump函数中使用
    bool skip_unhandled_events;            // !<跳过定期计时器的未处理事件
} esp_timer_create_args_t;

/*
* @brief esp_timer最小初始化
* 这个函数在启动代码中被调用。应用程序不需要
* 在使用其他esp_timer api之前调用该函数。
* 这个函数可以在启动过程的早期调用,在这个调用之后
* 只能使用esp_timer_get_time函数。
* @return
* - ESP_OK表示成功
*/
esp_err_t esp_timer_early_init(void);

/*
* 初始化esp_timer库
* 这个函数在启动代码中被调用。应用程序不需要
* 在使用其他esp_timer api之前调用该函数。
* 在调用这个函数之前,esp_timer_early_init必须被调用
* 启动代码。
* @return
* - ESP_OK表示成功
* -如果分配失败,则为ESP_ERR_NO_MEM
* 如果已经初始化,则为ESP_ERR_INVALID_STATE
* 中断分配器的其他错误
*/
esp_err_t esp_timer_init(void);

/*
* @brief反初始化esp_timer库
* @note通常这个函数不应该被应用程序调用
* @return
* - ESP_OK表示成功
* 如果未初始化,则为ESP_ERR_INVALID_STATE
*/
esp_err_t esp_timer_deinit(void);

/*
创建一个esp_timer实例
当使用定时器完成时,使用esp_timer_delete函数删除它。
@param create_args指针指向一个带有定时器创建参数的结构体。
*不被库保存,可以在堆栈上分配。
* @param[out] out_handle输出,指向esp_timer_handle_t变量
*将保存已创建的定时器句柄。
* @return
* - ESP_OK表示成功
* -如果某些create_args无效,则为ESP_ERR_INVALID_ARG
* -如果esp_timer库没有初始化,则为ESP_ERR_INVALID_STATE
* -如果内存分配失败,则为ESP_ERR_NO_MEM
*/
esp_err_t esp_timer_create(const esp_timer_create_args_t* create_args,esp_timer_handle_t * out_handle);

/*
* @brief启动一次性定时器
* 定时器不应该运行时,该函数被调用。
* @param timer定时器句柄创建esp_timer_create
* @param timeout_us定时器超时时间,以微秒相对于当前时刻
* @return
* - ESP_OK表示成功
* -如果句柄无效,则执行ESP_ERR_INVALID_ARG
* -如果计时器已经在运行,则为ESP_ERR_INVALID_STATE
*/
esp_err_t esp_timer_start_once(esp_timer_handle_t timer, uint64_t timeout_us);

/*
* @brief启动定时定时器
* 定时器不应该运行时,该函数被调用。该函数将
* 启动计时器,每“周期”微秒触发一次。
* @param timer定时器句柄创建esp_timer_create
* @param period定时器周期,以微秒为单位
* @return
* - ESP_OK表示成功
* -如果句柄无效,则执行ESP_ERR_INVALID_ARG
* -如果计时器已经在运行,则为ESP_ERR_INVALID_STATE
*/
esp_err_t esp_timer_start_periodic(esp_timer_handle_t timer, uint64_t period);

/*
* @brief停止定时器
* 这个函数停止之前使用esp_timer_start_once启动的计时器
* 或esp_timer_start_periodic。
* @param timer定时器句柄创建esp_timer_create
* @return
* - ESP_OK表示成功
* -如果定时器没有运行,则为ESP_ERR_INVALID_STATE
*/
esp_err_t esp_timer_stop (esp_timer_handle_t timer);

/*
* @brief删除esp_timer实例
* 删除前必须停止定时器。已过期的一次性计时器
* 不需要停止。
* @param timer定时器句柄分配使用esp_timer_create
* @return
* - ESP_OK表示成功
* -如果定时器正在运行,则为ESP_ERR_INVALID_STATE
*/
esp_err_t esp_timer_delete (esp_timer_handle_t timer);

/*
* 以微秒为单位获取启动后的时间
* @返回自底层计时器启动以来的微秒数
*/
int64_t esp_timer_get_time(void);

/*
* @brief获取预计下一次超时发生的时间戳
* @return最近的定时器事件的时间戳,以微秒为单位。
* timebase与esp_timer_get_time返回的值相同。
*/
int64_t esp_timer_get_next_alarm(void);

/*
* @brief获取下一个超时预计发生的时间戳,跳过那些有skip_unhandled_events标志的
* @return最近的定时器事件的时间戳,以微秒为单位。
* timebase与esp_timer_get_time返回的值相同。
*/
int64_t esp_timer_get_next_alarm_for_wake_up(void);

/*
* 将计时器列表转储到流中
* 如果CONFIG_ESP_TIMER_PROFILING选项被启用,将打印所有选项的列表
* 现有的计时器。否则,只打印列表活动的计时器。
* 格式为:
* name周期告警times_armed times_triggered total_callback_run_time
* 所在:
* name -计时器名称(如果定义了CONFIG_ESP_TIMER_PROFILING),或计时器指针
* period -定时器的周期,单位为微秒,单次定时器为0
* alarm -下一个告警的时间,以微秒为单位,如果定时器为0
* 未启动
* 如果定义了CONFIG_ESP_TIMER_PROFILING,会打印以下字段:
* times_armed -通过esp_timer_start_X设置定时器武装的次数
* times_triggered - callback被调用的次数
* total_callback_run_time -跨所有调用执行回调所需的总时间
* @param stream stream(例如stdout)将信息转储到
* @return
* - ESP_OK表示成功
* 如果不能为输出分配临时缓冲区,则为ESP_ERR_NO_MEM
*/
esp_err_t esp_timer_dump(FILE* stream);

# ifdef CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD
/*
* 请求从一个定时器回调函数切换上下文。
* 这只适用于有ISR调度方法的定时器。
* 在所有ISR调度计时器处理完毕后,上下文切换将被调用。
*/
void esp_timer_isr_dispatch_need_yield(void);
# endif // CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD

/*
* @brief返回定时器的状态,激活或不激活
* 此函数用于识别计时器是否仍处于活动状态。
* @param timer定时器句柄创建esp_timer_create
* @return
* - 1如果定时器仍然是活动的
* - 0如果定时器是不活跃的。
*/
bool esp_timer_is_active (esp_timer_handle_t计时器);

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐