第十五章 GPTIMER实验
通用定时器可用于准确设定时间间隔、在一定间隔后触发(周期或非周期的)中断或充当硬件时钟。通过本章的学习,开发者将学习到通用定时器的使用。 本章分为如下几个小节: 15.1 定时器简介 15.2 硬件设计 15.3 程序设计 15.4 下载验证
15.1 定时器简介 在上一章节中,我们介绍了定时器的一些基础知识,在这里便不再详细赘述了,请读者们自行阅读上一章节的内容,这一章节我们介绍一下ESP32的通用定时器。 1,ESP32-S3的定时器基本参数 ESP32-S3芯片配备了两个通用定时器组,每组均包含两个通用定时器(例如Timer0、Timer1等),且每个定时器都具备多个通道。通过明确指定定时器号和通道号,用户可以精准地选定所需的定时器和通道。每个定时器均支持独立编程,并且具备微秒级的精确时间中断生成能力。基本的定时器参数设置包括定时器号、通道号、预分频器配置、自动重新加载值的设定,以及定时器中断使能功能的开启。这些参数为用户提供了丰富的配置选项,以满足不同应用场景的需求。 图15.1.1 定时器组 2,ESP32-S3的通用定时器架构 我们先来学习通用定时器架构,通过学习通用定时器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。 图15.1.2 通用定时器架构 (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字段使能报警功能后,配置的报警值才会生效。为解决报警使能“过晚”(即报警使能时,定时器的值已过报警值),可逆计数器向上计数时,若定时器的当前值高于报警值(在一定范围内),或可逆计数器向下计数时,定时器的当前值低于报警值(在一定范围内),硬件都会立即触发报警。 15.2 硬件设计 15.2.1 例程功能 实现现象:程序运行后配置通用定时器,在一定的周期内触发报警事件。 15.2.2 硬件资源 1. LED LED - IO1 2. 通用定时器 15.2.3 原理图 本章实验使用的通用定时器为ESP32-S3的片上资源,因此没有对应的连接原理图。 15.3 程序设计 15.3.1 程序流程图 程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。下面看看本实验的程序流程图: 图15.3.1.1 GPTIMER实验程序流程图 15.3.2 GPTIMER函数解析 ESP-IDF提供了一套API来配置通用定时器。要使用此功能,需要导入必要的头文件: #include "driver/gptimer.h" 接下来,作者将介绍一些常用的GPTIMER函数,这些函数的描述及其作用如下: 1,配置通用定时器 该函数用于配置通用定时器,其函数原型如下所示: esp_err_t gptimer_new_timer(const gptimer_config_t *config, gptimer_handle_t *ret_timer); 该函数的形参描述,如下表所示: | | | | | 通用定时器句柄类型。如果没有特定需求一般设置为NULL |
表15.3.2.1 函数gptimer_new_timer()形参描述 返回值:ESP_OK表示配置成功。其他表示配置失败。 下表是gptimer_config_t类型的结构体变量描述。 | | | | | GPTIMER_CLK_SRC_APB: 选择APB作为时钟源 | GPTIMER_CLK_SRC_XTAL: 选择XTAL作为时钟源 | GPTIMER_CLK_SRC_DEFAULT: 选择APB作为默认选项 | | GPTIMER_COUNT_DOWN: 向下计数,即从65535到0 | GPTIMER_COUNT_UP: 向上计数,即从0到65535 | 设置内部计数器的分辨率。计数器每滴答一次相当于 1 / resolution_hz 秒 | | intr_priority: 设置中断的优先级。若设置为 0,则会分配一个默认优先级的中断,否则会使用指定的优先级。 | | 设置是否将定时器中断源标记为共享源,默认为1。如果该参数设置为true(1),那么多个定时器将共享同一个中断源。这意味着当其中一个定时器触发中断时,相应的中断处理程序将被调用,并且不同的定时器中断可以在同一个中断服务函数中进行处理。而如果intr_shared参数设置为false(0),那么每个定时器都会有独立的中断源,并且将有一个相应的中断服务函数用于处理每个定时器的中断。 | |
表15.3.2.2 gptimer_config_t结构体参数值描述 2,计数值以及定时器周期 该函数用于配置通用定时器的计数值以及定时器周期,其函数原型如下所示: esp_err_t gptimer_set_raw_count(gptimer_handle_t timer, unsigned long long value); 该函数的形参描述,如下表所示: | | | 计时器句柄由gptimer_new_timer创建 | | |
表15.3.2.3 函数gptimer_set_raw_count()形参描述 返回值:ESP_OK表示配置成功,其他表示配置失败。 3,注册用户回调函数 该结构体用于注册用户回调函数,其结构体原型如下所示: esp_err_t gptimer_register_event_callbacks(gptimer_handle_t timer, const gptimer_event_callbacks_t *cbs, void *user_data); 该函数的形参描述,如下表所示: | | | 需要设置的定时器,计时器句柄由gptimer_new_timer创建 | | | | |
表15.3.2.4 函数gptimer_register_event_callbacks ()形参描述 返回值:ESP_OK表示配置成功。其他表示配置失败。 当定时器启动后,可动态产生特定事件(如“警报事件”)。如需在事件发生时调用某些函数,便可通过该函数将函数挂载至中断服务例程(ISR)。 4,定时器报警,设置报警动作 该函数用于配置通用定时器报事件警,其函数原型如下所示: esp_err_t gptimer_set_alarm_action(gptimer_handle_t timer, const gptimer_alarm_config_t *config); 该函数的形参描述,如下表所示: | | | | | 指向通用定时器报警配置的指针类型。报警配置,尤其是将配置设置为NULL意味着禁用报警功能 |
表15.3.2.5 函数gptimer_set_alarm_action()形参描述 返回值:ESP_OK表示配置成功,其他表示失败。 该函数使用gptimer_alarm_config_t类型的结构体变量传入gptimer外设的报警配置参数,该结构体的定义如下所示: /** */ typedef struct { uint64_t alarm_count; /* 报警目标计数值 */ uint64_t reload_count; /* 报警重新加载计数值 */ struct { uint32_t auto_reload_on_alarm: 1;/* 报警事件发生后立即通过硬件重新加载计数值 */ } flags; /* 报警配置标志 */ } gptimer_alarm_config_t; 5,使能定时器 该函数用于配置通用定时器报事件警,其函数原型如下所示: esp_err_t gptimer_enable(gptimer_handle_t timer); 该函数的形参描述,如下表所示: 表15.3.2.6 函数gptimer_enable()形参描述 返回值:ESP_OK表示配置成功,其他表示失败。 6,启动定时器 该函数用于配置通用定时器报事件警,其函数原型如下所示: esp_err_t gptimer_start(gptimer_handle_t timer); 该函数的形参描述,如下表所示: 表15.3.2.7 函数gptimer_enable()形参描述 返回值:ESP_OK表示配置成功,其他表示失败。 15.3.3 GPTIMER驱动解析 在IDF版的06_gp_timer例程中,作者在06_gp_timer \components\BSP路径下新增了一个GPTIM文件夹,用于存放gptim.c和gptim.h这两个文件。其中,gptim.h文件负责声明GPTIM相关的函数和变量,而gptim.c文件则实现了GPTIM的驱动代码。下面,我们将详细解析这两个文件的实现内容。 1,gptim.h文件 /* 参数引用 */ typedef struct { uint64_t event_count; } gptimer_event_t; extern QueueHandle_t queue; /* 函数声明 */ void gptim_int_init(uint16_t counts, uint16_t resolution); /* 初始化通用定时器 */ bool IRAM_ATTR gptimer_callback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data); /* 定时器回调函数 */ 2,gptim.c文件 /** * @brief 初始化通用定时器 * @param counts: 计数值 * @param resolution: 定时器周期,resolution = 1s = 1000000μs (此处,定时器以微秒作为计算单位,) * @retval 无 */ void gptim_int_init(uint16_t counts, uint16_t resolution) { gptimer_alarm_config_t alarm_config; uint64_t count; /* 配置通用定时器 */ ESP_LOGI("GPTIMER_ALARM", "配置通用定时器"); /* 创建通用定时器句柄 */ gptimer_handle_t g_tim = NULL; gptimer_config_t g_tim_handle = { .clk_src = GPTIMER_CLK_SRC_DEFAULT, /* 选择定时器时钟源 */ .direction = GPTIMER_COUNT_UP, /* 递增计数模式 */ .resolution_hz = resolution, /* 计数器分辨率 */ }; gptimer_event_callbacks_t 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; } 15.3.4 CMakeLists.txt文件 打开本实验BSP下的CMakeLists.txt文件,其内容如下所示: set(src_dirs GPTIM LED) set(include_dirs GPTIM LED) set(requires driver esp_timer) idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires}) component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format) 上述的红色GPTIM驱动需要由开发者自行添加,以确保GPTIM驱动能够顺利集成到构建系统中。需要注意的是我们的依赖库(requires)需要添加上ESP32-S3定时器定时器的库,这一步骤是必不可少的,它确保了GPTIM驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。 15.3.5 实验应用代码 打开main/main.c文件,该文件定义了工程入口函数,名为app_main。该函数代码如下。 /** * @brief 程序入口 * @param 无 * @retval 无 */ void app_main(void) { uint8_t record; 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) { record = 1; /* 打印通用定时器发生一次计数事件后获取到的值 */ if (xQueueReceive(queue, &g_tim_evente, 2000)) { ESP_LOGI("GPTIMER_ALARM", "定时器报警, 计数值: %llu", g_tim_evente.event_count); record--; } else { ESP_LOGW("GPTIMER_ALARM", "错过一次计数事件"); } } vQueueDelete(queue); } 从上面的代码中可以看到,通用定时器的计数值为100,定时器周期设置为1000000微秒并通过创建消息队列的方式引入一个定时器事件。 15.4 下载验证 在完成编译和烧录操作后,可以看到板子上的LED在闪烁,在一定周期内串口打印输出定时器报警事件,报警值以及计数值等信息。
|