【普中ESP32S3开发攻略--基于ESP-IDF】-- 第 12 章 定时器中断实验

(1)实验平台:

普中ESP32S3开发板https://item.taobao.com/item.htm?id=803002165011(2)资料下载:普中科技-各型号产品资料下载链接


        前面章节我们介绍了 ESP32 的外部中断使用,本章继续介绍ESP32 的定时器功能。本章分为如下几部分内容:

 

12.1 实验介绍

12.1.1 实验简介

12.1.1.1 系统定时器

12.1.1.2 通用定时器

12.1.2 实验目的

12.1.3 系统定时器函数使用

12.1.3.1 设置 ESP 定时器的回调函数

12.1.3.2 创建一个事件

12.1.3.3 每周期内触发一次

12.1.4 通用定时器函数使用

12.1.4.1 配置通用定时器

12.1.4.2 配置通用定时器的计数值以及定时器周期

12.1.4.3 注册用户回调函数

12.1.4.4 创建一个消息队列,并引入一个事件

12.1.4.5 设置定时器报警动作

12.2 硬件设计

12.3 软件设计

12.3.1 系统定时器实验

12.3.1.1 系统定时器驱动

12.3.1.1.1 esp_time.c 文件

12.3.1.1.2 main 函数

12.3.2 通用定时器实验

12.3.2.1 系统定时器驱动

12.3.2.1.1 gp_time.c 文件

12.3.2.1.2 main 函数

12.4 实验现象

12.4.1 系统定时器实验

12.4.2 通用定时器实验


12.1 实验介绍

12.1.1 实验简介

        定时器,顾名思义就是用来计时的,我们常常会设定计时或闹钟,然后时间到了就告诉我们要做什么。ESP32 也是这样,通过定时器可以完成各种预设好的任务。ESP32 定时器到达指定时间后也会产生中断,然后在回调函数内执行所需功能,这个和外部中断类似。在 ESP32 中有系统定时器和通用定时器。

12.1.1.1 系统定时器

        ESP32-S3 芯片内置一组 52 位系统定时器。该定时器可用于生成操作系统所需的滴答定时中断,也可用作普通定时器生成周期或单次延时中断。下面先来简单了解一下系统定时器结构图,通过学习系统定时器结构图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。

        图中可以看到系统定时器内置两个计数器 UNIT0 和UNIT1 以及三个比较器COMP0、COMP1、COMP2。比较器用于监控计数器的计数值是否达到报警值。

        (1)计数器

        UNIT0、UNIT1 均为 ESP32-S3 系统定时器内置的52 位计数器。计数器使用 XTAL_CLK 作为时钟源(40MHz)。XTAL_CLK 经分频后,在一个计数周期生成频率为 fXTAL_CLK/3 的时钟信号,然后在另一个计数周期生成频率为fXTAL_CLK/2 的时钟信号。因此,计数器使用的时钟 CNT_CLK,其实际平均频率为 fXTAL_CLK/2.5,即 16MHz,如下图所示:

        每个 CNT_CLK 时钟周期,计数递增 1/16µs,即 16 个周期递增1µs。上图展示了系统定时器生成报警的过程。在上述过程中用到一个计数器(Timer Countern)和一个比较器(Timer Comparatorx),比较器将根据比较结果,生成报警中断。

        用户可以通过配置寄存器 SYSTIMER_CONF_REG 中下面三个位来控制计数器UNITn,这三个位分别是:

        ①:SYSTIMER_TIMER_UNITn_WORK_EN

        ②:SYSTIMER_TIMER_UNITn_CORE0_STALL_EN

        ③:SYSTIMER_TIMER_UNITn_CORE1_STALL_EN

        关于这三位的配置请参考《esp32-s3_technical_reference_manual_cn》。

        (2)比较器

        COMP0、COMP1、COMP2 均为 ESP32-S3 系统定时器内置的52 位比较器。比较器同样使用 XTAL_CLK 作为时钟源(40MHz)。

12.1.1.2 通用定时器

        通用定时器可用于准确设定时间间隔、在一定间隔后触发(周期或非周期的)中断或充当硬件时钟。如下图所示:

        ESP32-S3 包含两个定时器组,即定时器组 0 和定时器组1。每个定时器组有两个通用定时器(下文用 Tx 表示,x 为 0 或 1)和一个主系统看门狗定时器。所有通用定时器均基于 16 位预分频器和 54 位可自动重新加载向上/向下计数器。

        下面先来学习通用定时器架构,通过学习通用定时器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。

        (1)时钟选择器

        每个定时器可通过配置寄存器 TIMG_TxCONFIG_REG 的TIMG_Tx_USE_XTAL字段,选择 APB 时钟(APB_CLK)或外部时钟(XTAL_CLK)作为时钟源。

        (2)16 位预分频器

        时钟源经过 16 位预分频器分频,产生时基计数器使用的时基计数器时钟(TB_CLK)。16 位预分频器的分频系数可通过 TIMG_Tx_DIVIDER 字段配置,选取从 2 到 65536 之间的任意值。注意,将 TIMG_Tx_DIVIDER 置0 后,分频系数会变为 65536。TIMG_Tx_DIVIDER 置 1 时,实际分频系数为2,计数器的值为实际时间的一半。

        需要注意的是:定时器必须关闭(即 TIMG_Tx_EN 必须清零),才能更改16位预分频器。在定时器使能时更改 16 位预分频器会造成不可预知的结果。

        (3)54 位时基计数器

        54 位时基计数器基于 TB_CLK,可通过 TIMG_Tx_INCREASE 字段配置为递增或递减。时基计数器可通过置位或清零 TIMG_Tx_EN 字段使能或关闭。使能时,时基计数器的值会在每个 TB_CLK 周期递增或递减。关闭时,时基计数器暂停计数。注意,TIMG_Tx_EN 置位后,TIMG_Tx_INCREASE 字段还可以更改,时基计数器可立即改变计数方向。

        时基计数器 54 位定时器的当前值必须被锁入两个寄存器,才能被CPU读取(因为 CPU 为 32 位)。向 TIMG_TxUPDATE_REG 写入任意值时,54 位定时器的值开始被锁入寄存器 TIMG_TxLO_REG 和 TIMG_TxHI_REG,两个寄存器分别锁存低 32 位和高 22 位。当 TIMG_TxUPDATE_REG 被硬件清零,表明锁存操作已经完成,可以从这两个寄存器中读取当前计数值。在TIMG_TxUPDATE_REG被写入新值之前,保持寄存器 TIMG_TxLO_REG 和 TIMG_TxHI_REG 的值不变,以供32 位的 CPU 读值。

        (4)比较器

        定时器可配置为在当前值与报警值相同时触发报警。报警会产生中断,(可选择)让定时器的当前值自动重新加载。54 位报警值可在TIMG_TxALARMLO_REG和 TIMG_TxALARMHI_REG 配置,两者分别代表报警值的低32 位和高22 位。但是,只有置位 TIMG_Tx_ALARM_EN 字段使能报警功能后,配置的报警值才会生效。为解决报警使能“过晚”(即报警使能时,定时器的值已过报警值),可逆计数器向上计数时,若定时器的当前值高于报警值(在一定范围内),或可逆计数器向下计数时,定时器的当前值低于报警值(在一定范围内),硬件都会立即触发报警。

12.1.2 实验目的

        通过定时器让 LED 周期性每秒闪烁 1 次。

12.1.3 系统定时器函数使用

        具体的步骤如下:

        ①:设置 ESP 定时器的回调函数

        ②:创建一个事件

        ③:每周期内触发一次

12.1.3.1 设置 ESP 定时器的回调函数

        这里有些特殊,用于配置 ESP 定时器的回调函数,其结构体如下所示:

esp_timer_create_args_t tim_periodic_arg;

        该函数的形参描述,如下表所示:

        无返回值。

        该结构体使用 esp_timer_create_args_t 类型的结构体变量传入定时器的配置参数,该结构体的定义如下所示:

/**
* @brief 计时器配置已传递给 esp_Timer_create
*/
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;

        使用方法如下:

/* 定义一个定时器结构体 */
esp_timer_create_args_t tim_periodic_arg = {
    .callback = &esptim_callback, /* 设置回调函数*/
    .arg = NULL, /* 不携带参数*/
};

12.1.3.2 创建一个事件

        该函数用于创建指定定时器事件,其函数原型如下所示:

esp_err_t esp_timer_create(const esp_timer_create_args_t* create_args,esp_timer_handle_t* out_handle);

        使用方法如下:

esp_timer_create(&tim_periodic_arg, &esp_tim_handle); /* 创建一个事件*/

12.1.3.3 每周期内触发一次

        该函数用于使能定时器并启动,其函数原型如下所示:

esp_err_t IRAM_ATTR esp_timer_start_periodic(esp_timer_handle_t timer,uint64_tperiod_us);

        使用方法如下:

esp_timer_start_periodic(esp_tim_handle, 1000000); /* 每周期内触发一次*/

12.1.4 通用定时器函数使用

12.1.4.1 配置通用定时器

        该函数用于配置通用定时器,其函数原型如下所示:

esp_err_t gptimer_new_timer(const gptimer_config_t *config,gptimer_handle_t *ret_timer);

        该函数使用 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;

        使用方法如下:

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); /* 创建新的通用定时器 */

12.1.4.2 配置通用定时器的计数值以及定时器周期

        该函数用于配置通用定时器,其函数原型如下所示:

esp_err_t gptimer_set_raw_count(gptimer_handle_t timer,unsigned long long value);

        使用方法如下:

gptimer_set_raw_count(g_tim, 1000);

12.1.4.3 注册用户回调函数

        该结构体用于注册用户回调函数,其结构体原型如下所示:

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;

        使用方法如下:

gptimer_event_callbacks_t g_tim_callbacks;
g_tim_callbacks.on_alarm = gptimer_callback; /* 注册用户回调函数*/

12.1.4.4 创建一个消息队列,并引入一个事件

        该函数用于创建一个新的队列实例,并返回一个句柄,通过该句柄可以引用新队列,其函数原型如下所示:

xQueueCreate( uxQueueLength, uxItemSize );

        消息队列的调用方式用到了条件编译的方式,当满足条件编译所需要的条件时才会使用到该消息队列,同时该消息队列通过宏定义的方式进行调用,该宏定义如下所示:

#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
    #define xQueueCreate( uxQueueLength, uxItemSize )
    xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
#endif

        在内部,在 FreeRTOS 实现中,队列使用两个内存块。第一个块用于保存队列的数据结构。第二个块用于保存放入队列中的项目。如果使用xQueueCreate()创建队列,那么两个内存块都会在 xQueueCreate()函数内自动动态分配。(请参照 https://www.freertos.org/a00111.html)。如果使用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)
    {
        /* 队列未创建,不得使用 */
    }
}

12.1.4.5 设置定时器报警动作

        该函数用于配置通用定时器报事件警,其函数原型如下所示:

esp_err_t gptimer_set_alarm_action(gptimer_handle_t timer,const gptimer_alarm_config_t *config);

        该函数使用 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_config;
alarm_config.alarm_count = 1000000; /* 报警目标计数值 */
gptimer_set_alarm_action(g_tim, &alarm_config);

12.2 硬件设计

        本实验使用到硬件资源如下:

        (1)LED 模块

        (2)ESP32 GPIO

        LED 模块电路前面已介绍,此处不再重复。

12.3 软件设计

12.3.1 系统定时器实验

        下面我们打开“\4--实验程序\4--ESP-IDF 实验\1--基础实验\08-time\1-esp_time”程序,在 components/bsp 文件夹内新建了一个esp_time文件夹,该文件夹内存放的是系统定时器驱动。

        在 bsp 文件夹内还有一个 CMakeLists.txt 文件,里面用来设置对应的文件路径,每当新建文件夹,都需要将该文件夹名称添加到CMakeLists.txt 内,这个在前面已经介绍过,类似与 KEIL5 MDK 开发 STM32 中路径的添加。如下所示:

12.3.1.1 系统定时器驱动

12.3.1.1.1 esp_time.c 文件

        代码如下:

/**
 ****************************************************************************************************
 * @file        esp_time.c
 * @author      普中科技
 * @version     V1.0
 * @date        2024-03-01
 * @brief       ESP32系统定时器驱动代码
 * @license     Copyright (c) 2024-2034, 深圳市普中科技有限公司
 ****************************************************************************************************
 * @attention
 *
 * 实验平台:普中科技 ESP32-S3 开发板
 * 在线视频:https://space.bilibili.com/2146492485
 * 技术论坛:www.prechin.net
 * 公司网址:www.prechin.cn
 * 购买地址:
 *
 ****************************************************************************************************
 */

#include "esp_time.h"
#include "esp_timer.h"
#include "led.h"


/**
 * @brief       定时器回调函数
 * @param       arg: 不携带参数
 * @retval      无
 */
void esptim_callback(void *arg)
{
    LED_TOGGLE();
}

/**
 * @brief       初始化高精度定时器(ESP_TIMER)
 * @param       arr: 自动重装值
 * @param       tps: 定时器周期,以微妙为单位(μs),以一秒为定时器周期来执行一次定时器中断,
 *                   那此处tps = 1s = 1000000μs
 * @retval      无
 */
void esptim_int_init(uint16_t arr, uint64_t tps)
{
    esp_timer_handle_t esp_tim_handle;                      /* 定时器回调函数句柄 */

    /* 定义一个定时器结构体 */
    esp_timer_create_args_t tim_periodic_arg = {
        .callback = &esptim_callback,                       /* 设置回调函数 */
        .arg = NULL,                                        /* 不携带参数 */
    };

    esp_timer_create(&tim_periodic_arg, &esp_tim_handle);   /* 创建一个事件 */
    esp_timer_start_periodic(esp_tim_handle, tps);          /* 每周期内触发一次 */
}

        esptim_int_init 函数调用结构体 esp_timer_create_args_t 配置ESP定时器时,传入的参数 tim_periodic_arg 调用了定时器服务函数,esp_timer_create 配置了自动重装载值,这是因为对于ESP 定时器而言,只有这两个成员变量是有效的,因此仅需配置这两个成员变量。并且开启了ESP定时器的更新中断,因此每当 ESP 定时器计数溢出后都会产生一次更新中断。

        esptim_callback 函数为回调函数。在 ESP 定时器每次计数溢出后都会翻转一次 LED1 的状态。

12.3.1.1.2 main 函数

        代码如下:

/**
 ****************************************************************************************************
 * @file        main.c
 * @author      普中科技
 * @version     V1.0
 * @date        2024-03-01
 * @brief       
 * @license     Copyright (c) 2024-2034, 深圳市普中科技有限公司
 ****************************************************************************************************
 * @attention
 *
 * 实验平台:普中科技 ESP32-S3 开发板
 * 在线视频:https://space.bilibili.com/2146492485
 * 技术论坛:www.prechin.net
 * 公司网址:www.prechin.cn
 * 购买地址:
 *
 ****************************************************************************************************
 * 实验名称:ESP32系统定时器实验
 * 
 * 接线说明:
 *         
 *         LED模块-->ESP32 IO
 *         (D1)-->(3)
 * 
 * 实验现象:程序下载成功后,D1指示灯亮灭;
 * 
 * 注意事项:
 * 
 *
 ****************************************************************************************************
 */

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include "led.h"
#include "esp_time.h"


/**
 * @brief       程序入口
 * @param       无
 * @retval      无
 */
void app_main(void)
{
    esp_err_t ret;

    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 */
    esptim_int_init(5000, 1000000); /* 初始化高分辨率定时器,此处设置定时器周期为1秒,
                                       但该函数事宜微妙为单位进行计算,
                                       故而1秒钟换算为1000000微秒 */  

    while(1)
    {
        vTaskDelay(10);  /* 延时10ms */
    }
}

        从上面的代码中可以看到,ESP 定时器的自动重装载配置为5000,ESP定时器的周期值配置为 1000000,因为 ESP32-S3 高分辨率定时器的计数周期是以微秒作为基础单位进行运算,所以当我们设定计数周期为1 秒时需要将单位换算为微秒。因此 LED 的闪烁周期为 1 秒。

12.3.2 通用定时器实验

        下面我们打开“\4--实验程序\4--ESP-IDF 实验\1--基础实验\08-time\2-gp_time”程序,在 components/bsp 文件夹内新建了一个gp_time文件夹,该文件夹内存放的是系统定时器驱动。

        在 bsp 文件夹内还有一个 CMakeLists.txt 文件,里面用来设置对应的文件路径,每当新建文件夹,都需要将该文件夹名称添加到CMakeLists.txt 内,这个在前面已经介绍过,类似与 KEIL5 MDK 开发 STM32 中路径的添加。如下所示:

12.3.2.1 系统定时器驱动

12.3.2.1.1 gp_time.c 文件

        代码如下:

/**
 ****************************************************************************************************
 * @file        gp_time.c
 * @author      普中科技
 * @version     V1.0
 * @date        2024-03-01
 * @brief       通用定时器驱动代码
 * @license     Copyright (c) 2024-2034, 深圳市普中科技有限公司
 ****************************************************************************************************
 * @attention
 *
 * 实验平台:普中科技 ESP32-S3 开发板
 * 在线视频:https://space.bilibili.com/2146492485
 * 技术论坛:www.prechin.net
 * 公司网址:www.prechin.cn
 * 购买地址:
 *
 ****************************************************************************************************
 */

#include "gp_time.h"
#include "esp_log.h"

QueueHandle_t queue;

/**
 * @brief       初始化通用定时器
 * @param       counts: 计数值
 * @param       resolution: 定时器周期,resolution = 1s = 1000000μs(此处,定时器以微秒作为计算单位,)
 * @retval      无
 */
void gptim_int_init(uint16_t counts, uint32_t resolution)
{
    gptimer_config_t g_tim_handle;
    gptimer_alarm_config_t alarm_config;
    gptimer_event_callbacks_t g_tim_callbacks;

    uint64_t count;

    /* 配置通用定时器 */
    ESP_LOGI("GPTIMER_ALARM", "配置通用定时器");                    
    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;                       /* 计数器分辨率 */
    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_LOGI("GPTIMER_ALARM", "设置计数值");
    ESP_ERROR_CHECK(gptimer_set_raw_count(g_tim, counts));        /* 设置计数值 */
    ESP_LOGI("GPTIMER_ALARM", "获取计数值");
    ESP_ERROR_CHECK(gptimer_get_raw_count(g_tim, &count));       /* 获取计数值 */
    ESP_LOGI("GPTIMER_ALARM", "定时器计数值: %llu", count);

    /* 注册事件回调函数 */
    ESP_ERROR_CHECK(gptimer_register_event_callbacks(g_tim, &g_tim_callbacks, queue));  /* 配置通用定时器回调函数 */

    /* 设置报警动作 */
    ESP_LOGI("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));                                /* 启动通用定时器 */
}

/**
 * @brief       定时器回调函数
 * @param       无
 * @retval      无
 */
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;
}

        对于大多数通用定时器使用场景而言,应在启动定时器之前设置警报动作,但不包括简单的挂钟场景,该场景仅需自由运行的定时器。设置警报动作,需要根据如何使用警报事件来配置 gptimer_alarm_config_t 的不同参数:

        ①:gptimer_alarm_config_t::alarm_count 设置触发警报事件的目标计数值。设置警报值时还需考虑计数方向。尤其是当 gptimer_alarm_config_t::auto_reload_on_alarm 为true 时,gptimer_alarm_config_t::alarm_count 和 gptimer_alarm_config_t::reload_count 不能设置为相同的值,因为警报值和重载值相同时没有意义。

        ②:gptimer_alarm_config_t::reload_count 代表警报事件发生时要重载的计数值。此配置仅在 gptimer_alarm_config_t::auto_reload_on_alarm 设置为 true 时生效。

        ③:gptimer_alarm_config_t::auto_reload_on_alarm 标志设置是否使能自动重载功能。如果使能,硬件定时器将在警报事件发生时立即将gptimer_alarm_config_t::reload_count 的值重载到计数器中。

        要使警报配置生效,需要调用 gptimer_set_alarm_action()。特别是当gptimer_alarm_config_t 设置为 NULL 时,报警功能将被禁用。

        定时器启动后,可动态产生特定事件(如“警报事件”)。如需在事件发生时调用某些函数,请通过 gptimer_register_event_callbacks() 将函数挂载到中断服务例程(ISR)。gptimer_event_callbacks_t 中列出了所有支持的事件回调函数:gptimer_event_callbacks_t 设置警报事件的回调函数。由于此函数在 ISR 上下文中调用,必须确保该函数不会试图阻塞(例如,确保仅从函数内调用具有 ISR 后缀的 FreeRTOSAPI)。函数原型在 gptimer_alarm_cb_t 中有所声明。也可以通过参数 user_data,将自己的上下文保存到gptimer_register_event_callbacks()中。用户数据将直接传递给回调函数。

        此功能将为定时器延迟安装中断服务,但不使能中断服务。所以,请在gptimer_enable()之前调用这一函数,否则将返回 ESP_ERR_INVALID_STATE错误。

12.3.2.1.2 main 函数

        代码如下:

/**
 ****************************************************************************************************
 * @file        main.c
 * @author      普中科技
 * @version     V1.0
 * @date        2024-03-01
 * @brief       
 * @license     Copyright (c) 2024-2034, 深圳市普中科技有限公司
 ****************************************************************************************************
 * @attention
 *
 * 实验平台:普中科技 ESP32-S3 开发板
 * 在线视频:https://space.bilibili.com/2146492485
 * 技术论坛:www.prechin.net
 * 公司网址:www.prechin.cn
 * 购买地址:
 *
 ****************************************************************************************************
 * 实验名称:通用定时器实验
 * 
 * 接线说明:
 *         
 *         LED模块-->ESP32 IO
 *         (D1)-->(3)
 * 
 * 实验现象:程序下载成功后,D1指示灯指定时间闪烁;
 * 
 * 注意事项:
 * 
 *
 ****************************************************************************************************
 */

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include "led.h"
#include "gp_time.h"
#include "esp_log.h"


/**
 * @brief       程序入口
 * @param       无
 * @retval      无
 */
void app_main(void)
{
    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_int_init(100, 1000000);      /* 初始化通用定时器 */
    
    while (1)
    {
        if (xQueueReceive(queue, &g_tim_evente, 2000))
        {
            ESP_LOGI("GPTIMER_ALARM", "定时器报警, 计数值: %llu", g_tim_evente.event_count);   /* 打印通用定时器发生一次计数事件后获取到的值 */
            LED_TOGGLE();
        }
        else
        {
            ESP_LOGW("GPTIMER_ALARM", "错过一次计数事件");
        }
    }
    vQueueDelete(queue);
}

        从上面的代码中可以看到,通用定时器的计数值为100,定时器周期设置为1000000 微秒并通过创建消息队列的方式引入一个定时器事件。

12.4 实验现象

12.4.1 系统定时器实验

        下载程序前,按照如下接线:

        将程序下载到开发板内(可参考“2.2.4 程序下载运行”章节),可以看到D1 指示灯间隔 1S 闪烁。

12.4.2 通用定时器实验

        下载程序前,按照如下接线:

        将程序下载到开发板内(可参考“2.2.4 程序下载运行”章节),可以看到D1 指示灯间隔 1S 闪烁。在一定周期内串口打印输出定时器报警事件,报警值以及计数值等信息。

<think> 我们已经在之前的回答中介绍了ESP32-S3ESP-IDF环境下使用单次定时器的基本步骤。现在,用户明确要求提供示例代码。我们将根据ESP-IDF的GPTimer(通用定时器)示例,编写一个单次启动的定时器代码。 注意:用户要求的是单次启动的定时器,即触发一次后就停止。 步骤回顾: 1. 配置定时器(包括时钟源、计数方向、分辨率和自动重载标志) 2. 创建定时器实例 3. 设置报警值并注册回调函数(在回调函数中我们可以停止定时器,因为是单次) 4. 启动定时器 另外,在回调函数中,我们可以进行一些操作(比如打印日志),然后停止定时器(因为单次触发后我们不需要再运行)。 参考引用[1]中提到的示例:esp-idf/examples/peripherals/timer_group/gptimer。我们将以此为基础,修改为单次定时器。 注意:在配置中,我们需要设置`auto_reload_on_alarm`为false(在定时器配置结构体中)和`auto_reload`为false(在报警配置结构体中)以确保单次模式。 我们将编写一个完整的示例代码,包括初始化、回调函数和启动定时器,并在回调函数中释放定时器资源(但注意:通常我们不会在回调函数中删除定时器,因为删除操作可能会涉及到资源释放,而回调函数在中断上下文中,不能进行复杂的操作。因此,我们通常是在回调函数中设置一个标志,然后在任务中处理定时器的停止和删除)。 但是,为了简化,我们可以在回调函数中停止定时器(gptimer_stop)并禁用定时器(gptimer_disable),但删除定时器(gptimer_del_timer)建议在任务中进行。不过,由于是单次定时器,我们可能希望它在触发后自动停止,并不再使用,所以我们可以选择在回调函数中停止定时器,然后在主任务中删除。 然而,为了简单演示,我们在回调函数中停止定时器,然后在主循环中删除定时器并退出。另一种做法是:在回调函数中发送一个信号量或任务通知,通知主任务定时器已经触发,然后由主任务来删除定时器。 这里我们采用第二种方式:使用FreeRTOS的信号量来通知主任务。 代码结构如下: 1. 包含必要的头文件 2. 定义定时器句柄和信号量句柄 3. 编写定时器回调函数(在中断中触发,发送信号量) 4. 创建定时器,配置定时器 5. 创建信号量 6. 启用并启动定时器 7. 主任务等待信号量,然后停止并删除定时器 注意:在中断回调函数中不能调用可能引起阻塞的函数,所以信号量必须使用二值信号量,并且使用`xSemaphoreGiveFromISR`给予信号量。 下面我们提供示例代码: </think> 以下是 ESP32-S3ESP-IDF 环境下实现的单次启动定时器完整示例代码,基于 GPTimer 外设: ```c #include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #include "driver/gptimer.h" static const char* TAG = "SingleShot Timer"; static gptimer_handle_t gptimer = NULL; // 定时器中断回调函数 (需放在 IRAM) static bool IRAM_ATTR timer_callback( gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) { BaseType_t high_task_awoken = pdFALSE; // 此处执行定时触发后的操作 ESP_DRAM_LOGI(TAG, "Timer triggered! Count=%llu", edata->count_value); // 停止定时器 (单次模式) gptimer_stop(timer); return high_task_awoken == pdTRUE; } void app_main(void) { ESP_LOGI(TAG, "Creating single-shot timer"); // 1. 配置定时器参数 gptimer_config_t timer_config = { .clk_src = GPTIMER_CLK_SRC_DEFAULT, .direction = GPTIMER_COUNT_UP, .resolution_hz = 1 * 1000 * 1000, // 1MHz 分辨率 (1微秒/计数) .flags.auto_reload_on_alarm = false // 禁用自动重载 → 单次模式[^1] }; ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer)); // 2. 设置警报参数 gptimer_alarm_config_t alarm_config = { .alarm_count = 3 * 1000 * 1000, // 定时3秒 (3e6 微秒) .flags.auto_reload = false // 确保单次触发[^1] }; ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer, &alarm_config)); // 3. 注册回调函数 gptimer_event_callbacks_t cbs = { .on_alarm = timer_callback, }; ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer, &cbs, NULL)); // 4. 启动定时器 ESP_ERROR_CHECK(gptimer_enable(gptimer)); ESP_ERROR_CHECK(gptimer_start(gptimer)); ESP_LOGI(TAG, "Timer started, will trigger after 3 seconds"); // 等待定时器触发 vTaskDelay(pdMS_TO_TICKS(4000)); // 5. 清理资源 ESP_ERROR_CHECK(gptimer_disable(gptimer)); ESP_ERROR_CHECK(gptimer_del_timer(gptimer)); ESP_LOGI(TAG, "Timer resources released"); } ``` ### 关键代码说明: 1. **定时器配置** - `resolution_hz = 1e6`:设置定时器分辨率为 1MHz(1微秒/计数) - `auto_reload_on_alarm = false`:禁用自动重载,实现单次触发[^1] 2. **警报配置** - `alarm_count = 3e6`:设置 3 秒定时(3,000,000 微秒) - `auto_reload = false`:确保单次触发[^1] 3. **中断处理** - `IRAM_ATTR`:确保回调函数在 RAM 中执行(避免闪存访问延迟) - `gptimer_stop()`:触发后立即停止定时器 ### 使用说明: 1. 将代码保存为 `main.c` 2. 在项目目录中创建 `CMakeLists.txt`: ```cmake cmake_minimum_required(VERSION 3.16) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(single_shot_timer) ``` 3. 编译并烧录: ```bash idf.py set-target esp32s3 idf.py build idf.py -p PORT flash monitor ``` ### 预期输出: ``` I (327) SingleShot Timer: Creating single-shot timer I (337) SingleShot Timer: Timer started, will trigger after 3 seconds I (3007) SingleShot Timer: Timer triggered! Count=3000000 I (4007) SingleShot Timer: Timer resources released ``` > 📌 **注意**: > 1. 回调函数必须标记 `IRAM_ATTR`,否则可能导致崩溃 > 2. 分辨率可调整(如 `10e6` 为 100ns 精度) > 3. 定时结束后调用 `gptimer_del_timer()` 释放资源[^3] > 4. 完整示例参考:[esp-idf/examples/peripherals/timer_group/gptimer][^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值