|
第十五章 System Timer实验
1)实验平台:正点原子DNESP32P4开发板
2)章节摘自【正点原子】ESP32-P4开发指南— V1.0
3)购买链接:https://detail.tmall.com/item.htm?id=873309579825
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32P4.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子DNESP32S3开发板技术交流群:132780729
系统定时器(System Timer)是ESP32-P4芯片中的核心外设之一,专为实现精确计时和事件触发而设计。系统定时器提供高分辨率的计数功能,可生成精确的时间中断,广泛应用于操作系统的Tick计时、定时任务的触发、低功耗模式下的时间管理等场景。在本章节中,我们将详细探讨系统定时器的结构、时钟源配置、计数器与比较器的工作机制、以及如何利用这些功能进行应用开发。通过学习本章节,读者将能够掌握如何利用ESP32-P4的系统定时器实现精确的时间控制,为构建更高效的应用打下坚实基础。
15.1 System Timer简介
15.2 硬件设计
15.3 程序设计
15.4 下载验证
15.1 System Timer简介
ESP32-P4的系统定时器为52位计数器,主要用于操作系统的Tick中断生成,也可作为通用定时器,用于生成周期性或一次性中断。它具有以下特点:
1)双计数器与三比较器:具备两个计数器(UNIT0和UNIT1)和三个比较器(COMP0、COMP1、COMP2),支持多种中断触发配置。
2)APB_CLK依赖:系统定时器的寄存器配置等软件操作依赖APB_CLK时钟信号。
3)CNT_CLK生成:CNT_CLK计数时钟由40 MHz的XTAL_CLK提供,经过分频后,频率平均为16 MHz。
4)报警模式:支持目标模式(一次性报警)和周期模式(周期性报警)。
5)软件配置支持:在轻度休眠唤醒后,可加载RTC计时器的睡眠时间。
接下来,我们将查看ESP32-P4系统定时器的结构简图,如下图所示。
图15.1.1 系统定时器结构简图
上图展示了ESP32-P4系统定时器的结构,包括计数单元和比较器的连接关系,主要组件有:
1)计数单元:Timer Counter0(UNIT0)和Timer Counter1(UNIT1),分别用于独立记录计时信息,并可根据需求进行配置。
2)MUX(多路复用器):选择计数单元(UNIT0或UNIT1)的输出,并将其发送至不同的比较器,以灵活设置报警事件。
3)比较器:Timer Comparator0、Timer Comparator1和Timer Comparator2,用于接收计数单元的输出,并将其与预设报警值进行比较,生成独立的定时中断。
接下来,笔者将讲解ESP32-P4系统定时器的时钟源选择和控制方法。
15.1.1 系统定时器的时钟源选择
声明:以下章节的寄存器均来自《ESP32-P4技术参考手册》。
在图15.1.1中,计数器和比较器可以选择XTAL_CLK(外部石英晶振)或RC_FAST_CLK(快速RC时钟)作为时钟源。时钟源选择通过配置寄存器HP_SYS_CLKRST_PERI_CLK_CTRL21_REG中的HP_SYS_CLKRST_REG_SYSTIMER_CLK_SRC_SEL字段实现,如下图所示。
图15.1.1配置系统定时器的时钟源
当选择XTAL_CLK作为计数器的时钟源时,通过将XTAL_CLK频率缩小2.5倍,生成频率为CNT_CLK的计数器时钟信号,其平均频率为16 MHz。在每个CNT_CLK周期内,计数器会递增1,计数间隔为1/16µs(计数器每计数16次为1µs)。在15.1.3小节中,笔者将重点讲解计数器与比较器如何协同开启定时器的警报机制。
15.1.2 系统定时器的控制和操作时钟源
系统定时器的软件操作(如寄存器配置)使用APB_CLK作为时钟源。关于APB_CLK的详细信息,请参考《ESP32-P4技术参考手册》中的第8章“Reset and Clock”。为使系统定时器接收APB_CLK信号,需要在HP_SYS_CLKRST_SOC_CLK_CTRL2_REG寄存器中设置HP_SYS_CLKRST_REG_SYSTIMER_APB_CLK_EN位。此外,要重置系统定时器,可在HP_SYS_CLKRST_HP_RST_EN1_REG寄存器中设置HP_SYS_CLKRST_REG_RST_EN_STIMER位(见下图所示)。注意:定时器复位后,其寄存器将恢复到默认值。
图15.1.2.1 控制和操作系统定时器
15.1.3 系统定时器的警报生成机制
系统定时器的定时警报功能至关重要,它可以在特定时间点触发事件或中断处理。系统定时器通过计数器和比较器的配合实现警报生成与触发。图15.1.3.1展示了系统定时器中警报的生成流程,本文将详细讲解该流程的工作机制。
图15.1.3.1 系统定时器中警报的生成流程
ESP32-P4系统定时器包含两个计数器(Timer Counter)和三个比较器(Timer Comparator)。警报生成的具体步骤如下:
1)时钟源与计数器
系统定时器通过分频器(DIV)将XTAL_CLK的主时钟信号频率缩小2.5倍,生成频率为16 MHz的CNT_CLK计数时钟信号。在每个CNT_CLK周期内,计数器(Timer Counter)递增计数值以形成基准时间。
如前所述,系统定时器配备两个52位的计数器,计数时钟源为16 MHz,意味着每次计数的时间间隔为1/16 µs。计数器与CPUx(x:0~1)的工作状态可通过SYSTIMER_CONF_REG寄存器控制,主要设置包括:
①:启用计数器(SYSTIMER_TIMER_UNITn_WORK_EN):你可以通过设置这个选项来启动计数器。
②:CPU0暂停控制(SYSTIMER_TIMER_UNITn_CORE0_STALL_EN):如果你设置了这个选项,当CPU0暂停工作时,计数器会停止计数;一旦CPU0恢复,计数器会继续计数。
③:CPU1暂停控制(SYSTIMER_TIMER_UNITn_CORE1_STALL_EN):如果你设置了这个选项,当CPU1暂停工作时,计数器会停止计数;一旦CPU1恢复,计数器会继续计数。
下图展示了在CPU暂停时对计数器UNITn(n:0~1)的配置位。
图15.1.3.2 计数器与CPU的联系
通过上述寄存器位机制,系统定时器能够精确跟踪时间,并灵活适应CPUx的工作状态。接下来,笔者将详细讲解计数器的工作原理及相关寄存器。
当计数器UNITn工作时,计数值递增;停止时则保持不变。初始计数值的低32位和高20位从寄存器SYSTIMER_TIMER_UNITn_LOAD_LO和SYSTIMER_TIMER_UNITn_LOAD_HI中加载。写入1到SYSTIMER_TIMER_UNITn_LOAD会触发重新加载事件,当前计数值立即更新;若UNITn正在工作,计数器将从新加载值继续计数。
此外,若向SYSTIMER_TIMER_UNITn_UPDATE写入1会触发更新事件,则当前计数值的低32位和高20位将锁定到寄存器SYSTIMER_TIMER_UNITn_VALUE_LO和SYSTIMER_TIMER_UNITn_VALUE_HI中,并将SYSTIMER_TIMER_UNITn_VALUE_VALID置为有效。值在下一次更新事件之前保持不变。
2)计数器与比较器的比较操作
计数器生成的计时值实时传递到比较器(Timer Comparator)。比较器具备多种配置寄存器(如:SYSTIMER_TARGETx_WORK_EN、SYSTIMER_TARGETx_PERIOD_MODE、SYSTIMER_TARGETx_PERIOD、SYSTIMER_TARGETx_LO_REG、SYSTIMER_TARGETx_HI_REG等),用于设置比较器的工作模式和目标值(x:0~2)。
SYSTIMER_CONF_REG寄存器中的SYSTIMER_TARGETx_WORK_EN位如图所示。
图15.1.3.3 开启比较器COMP0、COMP1和COMP2
上图的寄存器位是用来开启系统定时器的三个比较器。
SYSTIMER_TARGETx_CONF_REG寄存器中的SYSTIMER_TARGETx_PERIOD_MODE和SYSTIMER_TARGETx_PERIOD(x为比较器0~2的索引)位描述如下图所示。
图15.1.3.4 配置比较器
上图中,SYSTIMER_TARGETx_PERIOD位用于设置定时警报周期(δt)。当模式为周期模式时,定时器将在设定时间间隔(t1 + n*δt)内持续触发警报。SYSTIMER_TARGETx_PERIOD_MODE位用于配置比较器为单次触发或周期性触发。同时,SYSTIMER_TARGETx_TIMER_UNIT_SEL用于指定计数器,以获取其计数值进行比较。SYSTIMER_TARGETx_LO_REG和SYSTIMER_TARGETx_HI_REG寄存器用于设定52位警报数值的低32位和高20位。在单次触发模式下,通过对这两个寄存器的配置可以设定所需的警报值,如下图所示。
图15.1.3.5 配置警报值
3)触发中断信号
当计数器的值达到比较器设置的目标值时,比较器会生成一个Timer Interrupt警报信号,并将该信号发送至CPU中断矩阵(CPU Interrupt Matrix)。中断矩阵负责处理中断信号,并触发相应的中断服务程序(ISR)。
通过对比较器的配置,开发者可以灵活地设置警报时间和触发的中断,利用系统定时器进行高效的事件调度。
15.1.4 系统定时器的比较器及警报生成机制解析
以下是根据前面小节内容总结的系统定时器原理,希望读者能对其有更深入的认识和学习。
根据前面的知识,我们了解到ESP32-P4的系统定时器配备三个52位比较器(COMPx,x = 0, 1, 2),能够根据不同的警报值(t)或警报周期(δt)生成独立的中断。每个COMPx的工作模式可通过配置寄存器SYSTIMER_TARGETx_PERIOD_MODE来选择,主要包括周期模式和单次模式:
1)周期模式:警报周期(δt)由寄存器SYSTIMER_TARGETx_PERIOD提供。假设当前计数值为t1,当计数值达到(t1 + δt)时,会生成一个警报中断;当计数值再次达到(t1 + 2*δt)时,另一个警报中断将生成,从而实现周期性警报。
2)单次模式:警报值(t)的低32位和高20位由SYSTIMER_TIMER_TARGETx_LO和SYSTIMER_TIMER_TARGETx_HI提供。假设当前计数值为t2(t2 ≤ t),当计数值t2达到警报值(t)时,将生成一次警报中断。与周期模式不同,单次模式仅生成一次警报中断。
此外,SYSTIMER_TARGETx_TIMER_UNIT_SEL用于选择用于比较生成警报的计数值来源:1表示使用来自UNIT1的计数值,0表示使用来自UNIT0的计数值(请参考图15.1.3.4)。设置SYSTIMER_TARGETx_WORK_EN可启用COMPx进行计数值比较。在单次模式下,COMPx将与警报值(t)进行比较;在周期模式下,则与起始值加上n倍警报周期(t1 + n * δt)进行比较。
警报生成的条件是,当计数值等于警报值(t)时(在单次模式)或等于起始值加上n倍警报周期(在周期模式)时,警报将被触发。如果寄存器中设置的警报值(t)小于当前计数值,即目标已过,当当前计数值大于警报值(t)并在范围(0 ~ 251 – 1)内时,也会立即生成警报中断,如下表所示。
表15.1.4.1 系统定时器警报触发点
注意:无论是在目标模式还是周期模式,实际警报值的低32位和高20位始终可以从SYSTIMER_TARGETx_LO_RO和SYSTIMER_TARGETx_HI_RO读取。
15.2 硬件设计
15.2.1 例程功能
开启高精度定时器,并设置时间为1s,在定时周期内翻转LED0电平状态。
15.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)Systimer
15.2.3 原理图
本章实验使用的系统定时器为ESP32-P4的片上资源,因此并没有相应的连接原理图。
15.3 程序设计
15.3.1 SysTimer的IDF驱动
SysTimer外设驱动位于ESP-IDF的components\esp_timer目录。该目录中的include文件夹存放SysTimer相关的头文件,声明了SysTimer函数和结构体等;而src文件夹则存放实际的SysTimer操作函数。要使用SysTimer功能,必须先导入以下头文件。
接下来,作者将介绍一些常用的SysTimer函数,这些函数的描述及其作用如下:
1,创建系统定时器esp_timer_create
该函数用于创建一个 ESP 定时器实例,其函数原型如下:
- <font size="3">esp_err_t esp_timer_create(const esp_timer_create_args_t* create_args,</font>
- <font size="3"> esp_timer_handle_t* out_handle);</font>
复制代码 函数形参:
表15.3.1.1 esp_timer_create函数形参描述
返回值:
ESP_OK表示成功。
ESP_ERR_INVALID_ARG表示如果某些create_args无效。
ESP_ERR_INVALID_STAT表示esp_timer库尚未初始化。
ESP_ERR_NO_MEM表示内存分配失败。
create_args为指向SysTimer配置结构体的指针。接下来,笔者将详细介绍esp_timer_create_args_t结构体中的各个成员变量,如下代码所示:
- typedef struct {
- esp_timer_cb_t callback; /* 定时器到期时执行的回调函数 */
- void* arg; /* 传递给回调的参数 */
- /* 从任务或 ISR 调度回调的方法;
- 如果未指定,将使用 esp_timer 任务,
- 要使 ISR 工作,还需设置 Kconfig 选项
- `CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD` */
- esp_timer_dispatch_t dispatch_method;
- const char* name; /* 定时器名称,在esp_timer_dump()函数中使用 */
- bool skip_unhandled_events; /* 设为跳过在轻睡眠状态下的周期性定时器未处理事件 */
- } esp_timer_create_args_t;
复制代码 上述结构体用于配置SysTimer的定时参数,以下对各个成员做简单介绍。
1)callback:
设置回调函数。根据esp_timer_cb_t类型编写回调。
2)arg:
设置回调函数传入形参。
3)dispatch_method:
设置调度模式。此字段可配置为ESP_TIMER_TASK定时器任务调度和ESP_TIMER_MA中断调度。
4)name:
定时器名称。
5)skip_unhandled_events
设置执行未处理事件。Flase:关闭;true:开启。
2,启动一个周期性定时器esp_timer_start_periodic
该函数用于启动周期性定时器,其函数原型如下:
- esp_err_t esp_timer_start_periodic(esp_timer_handle_t timer, uint64_t period);
复制代码 函数形参:
表15.3.1.2 esp_timer_start_periodic函数形参描述
返回值:
ESP_OK表示成功。
ESP_ERR_INVALID_ARG表示句柄无效。
ESP_ERR_INVALID_STATE表示定时器已经在运行。
3,启动一次性定时器esp_timer_start_once
该函数用于启动单次性定时器,其函数原型如下:
- esp_err_t esp_timer_start_once(esp_timer_handle_t timer, uint64_t timeout_us);
复制代码 函数形参:
表15.3.1.3 esp_timer_start_once函数形参描述
返回值:
ESP_OK表示成功。
ESP_ERR_INVALID_ARG表示句柄无效。
ESP_ERR_INVALID_STATE表示定时器已经在运行。
4,重新启动当前运行的定时器esp_timer_restart
该函数用于重新启动当前运行的定时器,其函数原型如下:
- esp_err_t esp_timer_restart(esp_timer_handle_t timer, uint64_t timeout_us);
复制代码 函数形参:
表15.3.1.4 esp_timer_restart函数形参描述
返回值:
ESP_OK表示表示成功。
ESP_ERR_INVALID_ARG表示句柄无效。
ESP_ERR_INVALID_STATE表示定时器已经在运行。
5,停止运行的定时器esp_timer_stop
该函数用于停止运行的定时器,其函数原型如下:
- esp_err_t esp_timer_stop(esp_timer_handle_t timer);
复制代码 函数形参:
表15.3.1.5 esp_timer_stop函数形参描述
返回值:
ESP_OK表示成功。
ESP_ERR_INVALID_STATE表示定时器未在运行。
6,删除一个 esp_timer 实例esp_timer_delete
该函数删除一个 esp_timer 实例,其函数原型如下:
- esp_err_t esp_timer_delete(esp_timer_handle_t timer);
复制代码 函数形参:
表15.3.1.6 esp_timer_delete函数形参描述
返回值:
ESP_OK表示成功。
ESP_ERR_INVALID_STATE表示定时器仍在运行。
15.3.2 程序流程图
图15.3.2.1 SysTimer实验程序流程图
15.3.3 程序解析
在05_esptimer例程中,作者在05_esptimer\components\BSP路径下新建ESPTIMER文件夹,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。
1,SysTimer驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ESPTIMER驱动源码包括两个文件:esptimer.c和esptimer.h。
esptimer.h主要用于声明esptimer_init等函数,以便在其他文件中调用,具体内容不再赘述。
下面我们再解析esptimer.c的程序,看一下初始化函数esptimer_init,代码如下:
- <font size="3">/**</font>
- <font size="3"> * @brief 初始化高分辨率定时器(ESP_TIMER)</font>
- <font size="3"> * [url=home.php?mod=space&uid=271674]@param[/url] tps: 定时器周期,以微秒为单位(μs).</font>
- <font size="3"> * 若以一秒为定时器周期来执行一次定时器中断,那此处tps = 1s = 1000000μs</font>
- <font size="3"> * @retval 无</font>
- <font size="3"> */</font>
- <font size="3">void esptimer_init(uint64_t tps)</font>
- <font size="3">{</font>
- <font size="3"> esp_timer_handle_t esp_tim_handle; /* 定义定时器句柄 */</font>
- <font size="3"> /* 定义一个定时器结构体设置定时器配置参数 */</font>
- <font size="3"> esp_timer_create_args_t timer_arg = {</font>
- <font size="3"> .callback = &esptimer_callback, /* 计时时间到达时执行的回调函数 */</font>
- <font size="3"> .arg = NULL, /* 传递给回调函数的参数 */</font>
- <font size="3"> .dispatch_method = ESP_TIMER_TASK, /* 进入回调方式,从定时器任务进入 */</font>
- <font size="3"> .name = "Timer", /* 定时器名称 */</font>
- <font size="3">};</font>
- <font size="3">/* 创建定时器 */</font>
- <font size="3">ESP_ERROR_CHECK(esp_timer_create(&timer_arg, &esp_tim_handle)); </font>
- <font size="3">/* 启动周期性定时器,tps设置定时器周期(us单位) */</font>
- <font size="3"> ESP_ERROR_CHECK(esp_timer_start_periodic(esp_tim_handle, tps)); </font>
- <font size="3">}</font>
复制代码 esptimer_init函数用于初始化并启动一个周期性定时器。它接受一个微秒为单位的周期参数 tps,通过定义定时器句柄和配置参数,设置了回调函数、参数传递方式以及定时器名称。该函数内部首先创建定时器,然后启动它,使其按照设定的周期触发回调,执行特定的任务。整个过程通过错误检查确保创建和启动的成功,适用于需要定时执行操作的场景,如数据采集或状态监测。如下是系统定时器回调函数esptimer_callback的处理任务。
- <font size="3">/**</font>
- <font size="3"> * @brief 定时器回调函数</font>
- <font size="3"> * @param arg: 不携带参数</font>
- <font size="3"> * @retval 无</font>
- <font size="3"> */</font>
- <font size="3">void esptimer_callback(void *arg)</font>
- <font size="3">{</font>
- <font size="3"> LED0_TOGGLE();</font>
- <font size="3">}</font>
复制代码 esptimer_callback函数是定时器到期时执行的回调函数。它的参数arg可以用于传递任何额外的信息,但在这个实现中未使用。函数内部调用LED0_TOGGLE(),用于切换LED0的状,实现定时闪烁的效果。
2,CMakeLists.txt文件
本例程的功能实现主要依靠ESPTIMER驱动。要在main函数中,成功调用ESPTIMER文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件(重点看红色内容),修改如下:
- <font size="3">set(src_dirs</font>
- <font size="3"> LED</font>
- <font size="3"> ESPTIMER)</font>
- <font size="3">set(include_dirs</font>
- <font size="3"> LED</font>
- <font size="3"> ESPTIMER)</font>
- <font size="3">set(requires</font>
- <font size="3"> driver</font>
- <font size="3"> esp_timer)</font>
- <font size="3">idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs}</font>
- <font size="3"> REQUIRES ${requires})</font>
- <font size="3">component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)</font>
复制代码
3,main.c驱动代码
在main.c里面编写如下代码。
- <font size="3">void app_main(void)</font>
- <font size="3">{</font>
- <font size="3"> esp_err_t ret;</font>
- <font size="3"> </font>
- <font size="3"> ret = nvs_flash_init(); /* 初始化NVS */</font>
- <font size="3"> if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_erase());</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_init());</font>
- <font size="3"> }</font>
- <font size="3"> led_init(); /* 初始化LED */</font>
- <font size="3"> esptimer_init(1000000); /* 初始化ESP_TIMER */</font>
- <font size="3"> while(1)</font>
- <font size="3"> {</font>
- <font size="3"> vTaskDelay(pdMS_TO_TICKS(10)); /* 延时10ms */</font>
- <font size="3"> }</font>
- <font size="3">}</font>
复制代码 app_main函数首先调用led_init和esptimer_init,分别用于初始化LED和配置系统定时器,设定定时需求为1秒。接着,通过一个无限循环与10毫秒的延时保持主任务的运行,这样应用程序在后台持续待命,以便能够及时响应定时器产生的中断事件。这种结构确保了程序的高效运行和对定时任务的及时处理。
15.4 下载验证
程序下载完成后,LED0在1s周期内电平状态翻转。
|