《DNESP32P4开发指南_V1.0》第十六章 TIMG实验

0 阅读19分钟

第十六章 TIMG实验

在本章中,我们将深入探索ESP32-P4芯片中的定时器组(TIMG)功能。定时器不仅用于精确计时,还可用于生成中断和实现定时控制,为嵌入式系统的实时性能提供了重要支持。我们将详细介绍TIMG的结构、特性以及使用方法,通过一系列实验来展示如何配置和操作定时器。通过这些实验,读者将能够理解定时器的基本原理,并掌握在实际应用中如何高效利用这些功能。准备好进入定时器的世界吧!
16.1 TIMG简介
16.2 硬件设计
16.3 程序设计
16.4 下载验证

16.1 TIMG简介

通用定时器可用于精确计时、在特定时间后触发中断(可周期性或非周期性)或作为硬件时钟。ESP32-P4芯片包含两个定时器组,分别是定时器组0和定时器组1(以下简称TIMGn,其中n可以是0或1)。每个定时器组由两个通用定时器(以下简称Tx,其中x可以是0或1)和一个主系统看门狗定时器(MWDT)组成,如下图为TIMG定时器组的简图。

image001.png

图16.1.1 TIMG定时器组简图

TIMG定时器组定时器的特性总结如下:
1)54位时间基准计数器:支持递增或递减配置,提供高精度的时间计量。
2)多种时钟源:可选择PLL_F80M_CLK、XTAL_CLK或RC_FAST_CLK作为时钟源。
3)16位时钟预分频器:预分频器范围从2到65536,允许用户根据具体要求调整计时精度。
4)实时计数器读取:可以实时读取时间基准计数器的当前值,方便监控和控制。
5)暂停与恢复功能:支持在需要时暂停和恢复计数器的计数,适应动态场景需求。
6)可编程报警生成:能够根据计时值生成可编程的报警,提高应用的灵活性。
7)定时器值重载:支持在报警时自动重载或通过软件控制进行即时重载。
8)时钟频率计算:根据晶体时钟测得的频率,可计算并生成TIMG0_CALI_CLK。
9)电平中断生成:能够生成电平中断,便于与其他系统组件进行有效协作。
10)支持多任务和事件:可同时支持多个ETM任务和事件,提升系统的并行处理能力。
在这里,我们已经大致了解了TIMG定时器组的相关特性。接下来,笔者将深入讲解这些定时器组的计时原理,以便读者能够更好地理解定时器的工作机制和应用场景。

16.1.1 定时器组的结构和原理

下图16.1.1.1展示了定时器组TIMGn中的定时器Tx。Tx包含一个16位整数分频器作为预分频器、一个基于定时器的计数器和一个用于报警生成的比较器。

image003.png

图16.1.1.1 定时器组架构

上图展示了定时器组TIMGn的结构,包括定时器Tx的工作原理和各个组件。该架构被分为三个主要部分:时钟源配置、计时器的分频与配置、比较器的警报机制和定时中断。接下来,笔者分别详细说明每一部分的功能和原理。
1,时钟源配置
定时器组可以从三种时钟源中选择一个作为输入时钟:XTAL_CLK、RC_FAST_CLK或PLL_F80M_CLK。通过设置寄存器HP_SYS_CLKRST_PERI_CLK_CTRL20_REG或HP_SYS_CLKRST_PERI_CLK_CTRL21_REG中的HP_SYS_CLKRST_TIMERGRPn_Tx_SRC_SEL字段,可以选择所需的时钟源。这些寄存器位描述如下所示。

image005.png

图16.1.1.2 配置哪个定时器组的哪个定时器时钟源

上图中,定时器的时钟源选择通过一个多路复用器来实现,配置范围为0~1。具体来说,定时器组0的0号定时器的时钟源可以通过HP_SYS_CLKRST_TIMERGRP0_T0_SRC_SEL寄存器进行配置。此寄存器的设置方式如下: 1)0:选择XTAL_CLK(晶体振荡器时钟)作为定时器的时钟源。 2)1:选择RC_FAST_CLK作为定时器的时钟源。 3)2:选择PLL_F80M_CLK作为定时器的时钟源。 注意:上图的字段名称是由笔者根据多个类似的寄存器总结得出的,所以用户请根据《ESP32-P4技术参考手册》的寄存器命名为准。

2,计时器的分频与配置
默认情况下,当定时器选择XTAL_CLK作为时钟源时,输入时钟经过图 16.1.1.1 中的16位预分频器进行分频操作,分频系数范围为2至63336。该分频器由TIMG_TxCONFIG_REG寄存器中的TIMG_Tx_DIVIDER字段决定,该寄存器描述如下所示。

image007.png

图16.1.1.3 配置分频系数

上图中的TIMG_Tx_DIVCNT_RST字段用于重置预分频器。如果此位被置为1,则必须先禁用计数器,然后通过TIMG_Tx_DIVIDER字段配置预分频数值。时钟频率经过预分频器得到分频的时钟TB_CLK,该时钟作为计数器计数的基准。随后,通过TIMG_TxCONFIG_REG寄存器中的TIMG_Tx_INCREASE和TIMG_Tx_EN字段使能计数器的工作,这两个字段的描述如下所示。

image009.png

图16.1.1.4 配置计数器的方向与使能计数器

上图中,我们通过配置TIMG_Tx_INCREASE字段来设置当前计数器的计数方向(0:向下计数;1:向上计数),而通过TIMG_Tx_EN字段来开启计数器。这样,我们就可以得到计数器在规定时间计数的数值了(图 16.1.1.1中的TIMG_VALUE)。
若我们想设置计数器的初始计数值或者重新装载值,可以通过向TIMG_Tx_LOAD_LO和TIMG_Tx_LOAD_HI寄存器写入起始值来实现。然后,通过向TIMG_TxLOAD_REG写入任意值将其重新加载到定时器中,这些寄存器描述如下图所示。

image011.png

图16.1.1.5 配置重装载值(多个寄存器的合并图)

若我们要读取54位时间基准计数器的值,计时器值必须先被锁存到两个寄存器中,以便CPU读取(由于CPU是32位的,所以只能读取32位以内的数据)。通过向TIMG_TxUPDATE_REG写入任意值,当前54位计时器的值将开始被锁存到TIMG_TxLO_REG和TIMG_TxHI_REG寄存器中,前者包含低32位,后者包含高22位。当TIMG_TxUPDATE_REG被硬件清除时,表示锁存操作已完成,此时可以从TIMG_TxLO_REG和TIMG_TxHI_REG寄存器读取当前计时器值。TIMG_TxLO_REG和TIMG_TxHI_REG寄存器在CPU读取时将保持不变,直到TIMG_TxUPDATE_REG再次被写入。这些寄存器描述如下图所示。

image013.png

图16.1.1.6 读取计数值(多个寄存器合并图)

若读者希望定时器以1µs计时,可以将预分频数值设置为80。其计算公式为:

image015.png

根据上述的公式计算,80M / 80 = 1MHz的定时器时钟频率(图 16.1.1.1 中的TB_CLK),然后根据时间 T与频率F的关系,如下公式所示:

image017.png

经过上述公式的计算,最终计数器计数一次的时钟为1µs。
3,比较器的警报机制
若计数器启动后,TIMG_VALUE的数值会输入到比较器(Comparator)中,比较器根据警报值进行比较。如果TIMG_VALUE数值与警报值相等或出现其他触发情况,则会触发定时组中断(TIMG_Tx_INT)。下表为警报触发情景。

QQ截图20260413095728.png

表16.1.1.1 警报触发情景

图16.1.1.1中,我们通过TIMG_TxCONFIG_REG寄存器中的TIMG_Tx_ALARM_EN字段来使能比较器,而警报值可通过TIMG_TxALARMLO_REG寄存器和TIMG_TxALARMHI_REG寄存器进行配置。这些字段描述如下所示。

image019.png

图16.1.1.7 配置警报数值(多个寄存器合并图)

注意:报警时,TIMG_Tx_ALARM_EN字段会自动清除。这意味着在下次显式重新设置TIMG_Tx_ALARM_EN之前,不会再触发任何报警。这种行为确保每个事件只会触发一次报警。
3,定时中断
当计数值与警报值相等时,将触发中断输出TIMG_Tx_INT至中断矩阵,最后通知CPU触发中断。

16.1.2 定时器重载

当定时器的当前值被覆盖为存储在TIMG_Tx_LOAD_LO和TIMG_Tx_LOAD_HI字段中的重载值时,定时器就会被重载。这两个字段分别对应定时器新值的低32位和高22位。然而,写入重载值到TIMG_Tx_LOAD_LO和TIMG_Tx_LOAD_HI不会导致定时器当前值的变化。相反,重载值在重载事件发生之前会被定时器忽略。
重载事件可以通过软件即时重载或报警自动重载触发。
1)软件即时重载:当CPU向TIMG_TxLOAD_REG写入任何值时,会触发即时重载,这会立即更新定时器的当前值。如果 TIMG_Tx_EN 被设置,定时器将继续从新值开始递增或递减。在这种情况下,如果TIMG_Tx_ALARM_EN被设置,定时器仍然会在表16.1.1.1中列出的场景中触发报警。如果TIMG_Tx_EN被清除,定时器将保持在新值处冻结,直到重新启用计数。
2)报警自动重载:当报警发生时,将导致定时器重载,从而允许定时器从重载值继续递增或递减。这在使用周期性报警时非常有用,可以重置定时器的值。要启用报警自动重载,应该设置TIMG_Tx_AUTORELOAD字段。如果没有启用,定时器的值将在报警后继续递增或递减,超过报警值。

16.2 硬件设计

16.2.1 程序功能

在1s周期内翻转LED0电平状态,并实时打印当前计数值。

16.2.2 硬件资源

1)LED灯
LED 0 - IO51
2)TIMG

16.2.3 原理图

本章实验使用的TIMG为ESP32-P4的片上资源,因此并没有相应的连接原理图。

16.3 程序设计

注意:本书提供了两个TIMG实验,分别为06_1_timg和06_2_timg。其中,06_1_timg实验使用旧版本的API,而06_2_timg实验使用新版API。之所以提供两个版本,是因为旧版和新版API在定时器组的选择方式上存在差异。06_1_timg实验允许用户自由选择定时器组和定时器,可以灵活指定使用哪个定时器组的哪个定时器。而在06_2_timg实验中,使用的新API要求创建定时器时必须按顺序从定时器组0的定时器0开始,依次创建到定时器组1的定时器1。这意味着在新版本中,用户不能随意选择定时器组和定时器,必须按照固定的顺序进行创建和使用。
本章节将以06_2_timg实验中使用的API函数为基准进行讲解,06_1_timg实验仅作为参考。

16.3.1 TIMG的IDF驱动

TIMG外设驱动位于ESP-IDF的components\esp_timer目录。该目录中的include文件夹存放TIMG相关的头文件,声明了TIMG函数和结构体等;而src文件夹则存放实际的TIMG操作函数。要使用TIMG功能,必须先导入以下头文件。

#include "driver/gptimer.h"
#include "../src/gptimer_priv.h"

接下来,作者将介绍一些常用的TIMG函数,这些函数的描述及其作用如下:
1,创建一个新的通用定时器gptimer_new_timer
该函数用于创建一个新的通用定时器,其函数原型如下:

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

函数形参:

QQ截图20260413095745.png

表16.3.1.1 gptimer_new_timer函数形参描述

返回值:
ESP_OK表示成功创建GPTimer。
ESP_ERR_INVALID_ARG表示创建GPTimer失败,因参数无效。
ESP_ERR_NO_MEM表示创建GPTimer失败,因内存不足。
ESP_ERR_NOT_FOUND表示创建GPTimer失败,因所有硬件定时器已被使用,没有可用。
ESP_FAIL表示创建GPTimer失败,因其他错误。
config为指向TIMG配置结构体的指针。接下来,笔者将详细介绍gptimer_config_t结构体中的各个成员变量,如下代码所示:

typedef struct { 
    gptimer_clock_source_t clk_src;      	/* GPTimer时钟源 */
gptimer_count_direction_t direction; 	/* 计数方向 */
/* 计数器分辨率(工作频率),以 Hz 为单位,每次计数的 步长等于 (1/resolution_hz) 秒 */
uint32_t resolution_hz;                
/* GPTimer中断优先级,如果设置为0,驱动程序将尝试分配相对较低优先级的中断(1,2,3) */
    int intr_priority;                   
struct {
uint32_t intr_shared: 1;/* 设为真,则定时器中断号可以与其他外设共享 */
/* 如果设置,驱动程序将在进入/退出睡眠模式之前/之后备份/恢复 GPTimer 寄存器。
通过这种方式,系统可以关闭 GPTimer 的电源域。这可以节省电力,但会消耗更多的 RAM */
        uint32_t backup_before_sleep: 1; 
    } flags;								/* GPTimer 配置标志 */
} gptimer_config_t;

上述结构体用于配置TIMG的定时参数,以下对各个成员做简单介绍。
1)clk_src:
设置GPTimer时钟源。此字段可配置为GPTIMER_CLK_SRC_PLL_F80M、GPTIMER_CLK_SRC_RC_FAST、GPTIMER_CLK_SRC_XTAL和GPTIMER_CLK_SRC_DEFAULT。
2)direction:
设置定时计数方向。可选GPTIMER_COUNT_DOWN向下计数或GPTIMER_COUNT_UP向上计数。
3)resolution_hz:
设置计数器分辨率。
4)intr_priority:
设置定时器中断优先级。
5)flags.intr_shared
设置TIMG中断号共享。
6)flags.backup_before_sleep
设置进入睡眠。
2,通用定时器(GPTimer)设置回调函数gptimer_register_event_callbacks
该函数用于通用定时器(GPTimer)设置回调函数,其函数原型如下:

esp_err_t gptimer_register_event_callbacks(gptimer_handle_t timer, 
const gptimer_event_callbacks_t *cbs, 
void *user_data);

函数形参:

QQ截图20260413095755.png

表16.3.1.2 gptimer_register_event_callbacks函数形参描述

返回值:
ESP_OK表示成功设置事件回调。
ESP_ERR_INVALID_ARG表示由于无效参数导致设置回调失败。
ESP_ERR_INVALID_STATE表示定时器不处于初始化状态,无法设置回调。
ESP_FAIL表示由于其他错误导致设置回调失败。
3,启用通用定时器gptimer_enable
该函数用于启用通用定时器,其函数原型如下:

esp_err_t gptimer_enable(gptimer_handle_t timer);

函数形参:

QQ截图20260413095824.png

表16.3.1.3 gptimer_enable函数形参描述

返回值:
ESP_OK表示成功启用GPTimer。
ESP_ERR_INVALID_ARG表示由于无效参数导致启用失败。
ESP_ERR_INVALID_STATE表示定时器已经处于启用状态,无法再次启用。
ESP_FAIL表示由于其他错误导致启用失败。
4,通用定时器(GPTimer)设置报警事件的操作gptimer_set_alarm_action
该函数用于通用定时器(GPTimer)设置报警事件的操作,其函数原型如下:

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

函数形参:

QQ截图20260413095834.png

表16.3.1.4 gptimer_set_alarm_action函数形参描述

返回值:
ESP_OK表示成功为GPTimer设置报警操作。
ESP_ERR_INVALID_ARG表示由于无效参数导致设置失败。
ESP_FAIL表示由于其他错误导致设置失败。
config为指向报警配置结构体的指针。接下来,笔者将详细介绍gptimer_alarm_config_t结构体中的各个成员变量,如下代码所示:

typedef struct {
    uint64_t alarm_count; /* 报警目标计数值,当定时器计数达到此值时,将触发报警事件 */
    uint64_t reload_count;/* 报警重载计数值,仅在auto_reload_on_alarm设为真时有效 */
    struct {
        uint32_t auto_reload_on_alarm: 1; /* 报警事件发生时,硬件是否立即重载计数值 */
    } flags;                              /* 报警配置标志 */
} gptimer_alarm_config_t;

gptimer_alarm_config_t结构体用于传递通用定时器(GPTimer)的报警配置参数。以下是各个参数的说明。

QQ截图20260413095844.png

表16.3.1.5 gptimer_alarm_config_t结构体的各个参数描述及可选项

5,启动通用定时器gptimer_start
该函数用于启动通用定时器,其函数原型如下:

esp_err_t gptimer_start(gptimer_handle_t timer);

函数形参:

QQ截图20260413095855.png

表16.3.1.6 gptimer_start函数形参描述

返回值: ESP_OK表示成功启动GPTimer。 ESP_ERR_INVALID_ARG表示启动GPTimer失败,原因是无效参数。 ESP_ERR_INVALID_STATE表示启动GPTimer失败,因为定时器未启用或已处于运行状态。 ESP_FAIL表示启动GPTimer失败,原因是其他错误。
16.3.2 程序流程图

image022.png

图16.3.2.1 TIMG实验程序流程图

16.3.3 程序解析

在06_2_timg例程中,作者在06_2_timg\components\BSP路径下新建TIMG文件夹,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。

1,TIMG驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。ESPTIMER驱动源码包括两个文件:timg.c和timg.h。

timg.h主要用于声明timg_init等函数,以便在其他文件中调用,具体内容不再赘述。
下面我们再解析timg.c的程序,看一下初始化函数timg_init,代码如下:

/**
 * @brief      	初始化定时器
 * @param     	alarm_count:触发警报事件的目标计数值
 * @param     	resolution: 定时器的分辨率
 * @retval   	定时器的ID
 */
uint8_t timg_init(uint64_t alarm_count, uint32_t resolution)
{
    gptimer_handle_t gptimer = NULL;                 	/* 定义一个通用定时器实例 */
    uint8_t group_index, timer_index, timer_id = 0;

    gptimer_config_t timer_config =          	/* 配置定时器参数 */
    {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT,  	/* 选择定时器的时钟源 */
        .direction = GPTIMER_COUNT_UP,        	/* 设置定时器的计数方向 */
/* 设置内部计数器的分辨率,若为1000000即1MHz,1tick = 1us */
        .resolution_hz = resolution,	 
    };
    ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer));/* 实例化定时器 */
    
    /* 获取定时器ID信息 */
    group_index = gptimer->group->group_id;
    timer_index = gptimer->timer_id;
    timer_id |= (group_index << 1) | timer_index;
ESP_LOGI("timer", "group_index:%d  timer_index:%d  timer_id:%d",
         group_index, timer_index, timer_id); 

    gptimer_event_callbacks_t callback_func =                 
    {
        .on_alarm = timeout_callback,      	/* 设置警报事件的回调函数 */
};
/* 将函数挂载到中断服务例程 (ISR) */
ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer,
&callback_func, NULL));  

    gptimer_enable(gptimer);                            /* 使能定时器 */

    gptimer_alarm_config_t alarm_config = 
    {
        .alarm_count = alarm_count,                     /* 报警目标计数值 */
        .reload_count = 0,                              /* 重载计数值为0 */
        .flags.auto_reload_on_alarm = true,             /* 开启重加载 */
    };   
    gptimer_set_alarm_action(gptimer, &alarm_config);   /* 设置触发报警条件 */

    gptimer_start(gptimer);                             /* 定时器开始工作 */

    return timer_id;
}

timg_init函数用于初始化一个通用定时器,配置其时钟源、计数方向和分辨率。它注册一个报警回调函数,使能定时器,并设置报警条件,包括目标计数值和自动重载功能。最后,函数启动定时器并返回定时器的ID,以便后续使用。如下是GPTimer回调函数timeout_callback的处理任务。

/**
 * @brief    	定时器回调函数
 * @param     	timer:通用定时器对象
 * @param     	edata:通用定时器警报事件数据
 * @param    	user_data:用户数据
 * @retval    	布尔类型
 */
static bool IRAM_ATTR timeout_callback( gptimer_handle_t timer, 
const gptimer_alarm_event_data_t *edata, 
void *user_data)
{
    if (timer->group->group_id == 0)        /* 定时器组0 */
    {
        if ((timer->timer_id))              /* 定时器1 */
        {
            /* 执行定时器1定时操作 */
            ESP_DRAM_LOGI("TimerGroup0", "timer1 time out");
        }
        else                                /* 定时器0 */
        {
            /* 执行定时器0定时操作 */
            ESP_DRAM_LOGI("TimerGroup0", "timer0 time out");
        }
    }
    else                                    /* 定时器组1 */
    {
        if ((timer->timer_id))              /* 定时器1 */
        {
            /* 执行定时器1定时操作 */
            ESP_DRAM_LOGI("TimerGroup1", "timer1 time out");
        }
        else                                /* 定时器0 */
        {
            /* 执行定时器0定时操作 */
            ESP_DRAM_LOGI("TimerGroup1", "timer0 time out");
        }
    }

    return pdTRUE;
}

timeout_callback是一个定时器回调函数,当定时器触发报警事件时被调用。该函数根据定时器所属的组和ID,判断是哪个定时器超时,并打印相应的日志信息。返回值为pdTRUE,表示回调执行成功。

2,CMakeLists.txt文件
本例程的功能实现主要依靠GPTimer驱动。要在main函数中,成功调用GPTimer文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件(重点看红色内容),修改如下:

set(src_dirs
            LED
            TIMG)

set(include_dirs
            LED
            TIMG)

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)

3,main.c驱动代码
在main.c里面编写如下代码。

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());
        ESP_ERROR_CHECK(nvs_flash_init());
    }
    led_init();                 	/* 初始化LED */
    /* 执行顺序会自动分配定时器 */
    timg_init(500000, 1000000);  	/* 定时器0初始化 */
    timg_init(1000000, 1000000); 	/* 定时器1初始化 */
    timg_init(2000000, 1000000); 	/* 定时器2初始化 */
    timg_init(4000000, 1000000);  	/* 定时器3初始化 */
    while(1)
    {
        LED0_TOGGLE();
        vTaskDelay(200);
    }
}

该函数首先按顺序初始化两个定时器组的四个定时器,每个定时器被设置为不同的定时时间。具体实现通过timeout_callback回调函数处理定时器超时事件,根据定时器的组和ID执行不同的操作。这样可以灵活管理多个定时器的行为,满足不同的定时需求。

16.4 下载验证

程序下载完成后,我们可打开监视器查看各个定时器的操作任务,如下图所示。

image023.png

图16.4.1 四个通用定时器运行消息