07. 系统定时器

一、什么是定时器

  定时器是单片机内部集成的功能,它能够通过编程进行灵活控制。单片机的定时功能依赖于内部的计数器实现,每当单片机经历一个机器周期并产生一个脉冲时,计数器就会递增。定时器的主要作用在于计时,当设定的时间到达后,它会触发中断,从而通知系统计时完成。在中断服务函数中,我们可以编写特定的程序以实现所需的功能。

  定时器的主要作用包括但不限于:(1)、执行定时任务。(2)、时间测量。(3)、精确延时。(4)、PWM 信号生成。(5)、事件触发与监控。

  定时器既可通过硬件实现,也可基于软件进行设计,二者各具特色,适用于不同场景:

  硬件定时器,依托微控制器的内置硬件机制,通过专门的计时/计数器电路达成定时功能。其显著优势在于高精度与高可靠性,这是因为硬件定时器的工作独立于软件任务和操作系统调度,故而不受它们的影响。在追求极高定时精确度的场合,如生成 PWM 信号或进行精确时间测量时,硬件定时器无疑是最佳选择。其工作原理确保即便主 CPU 忙于其他任务,定时器也能在预设时间准确触发相应操作。

  而 软件定时器,则是通过操作系统或软件库模拟实现的定时功能。这类定时器的性能受系统当前负载和任务调度策略制约,因此在精度上较硬件定时器稍逊一筹。然而,软件定时器在灵活性方面更胜一筹,允许创建大量定时器,适用于对时间控制要求不那么严格的场景。

软件定时器在某些情况下可能面临定时精度问题,特别是在系统负载较重或存在众多高优先级任务时。

二、系统定时器框架

系统定时器结构图

  系统定时器内置两个计数器 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。

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

SYSTIMER_TIMER_UNITn_WORK_EN                // 来启动计数器
SYSTIMER_TIMER_UNITn_CORE0_STALL_EN         // 当CPU0暂停工作时,计数器会停止计数;一旦CPU0恢复,计数器会继续计数
SYSTIMER_TIMER_UNITn_CORE1_STALL_EN         // 当CPU1暂停工作时,计数器会停止计数;一旦CPU1恢复,计数器会继续计数

  当计数器 UNITn 工作时,计数值递增;停止时则保持不变。初始计数值的低 32 位和高 20 位从寄存器 SYSTIMER_TIMER_UNITn_LOAD_LOSYSTIMER_TIMER_UNITn_LOAD_HI 中加载。写入 1SYSTIMER_TIMER_UNITn_LOAD触发重新加载事件,当前计数值立即更新;若 UNITn 正在工作,计数器将从新加载值继续计数。

  此外,若向 SYSTIMER_TIMER_UNITn_UPDATE 写入 1触发更新事件,则当前计数值的低 32 位和高 20 位将锁定到寄存器 SYSTIMER_TIMER_UNITn_VALUE_LOSYSTIMER_TIMER_UNITn_VALUE_HI 中,并将 SYSTIMER_TIMER_UNITn_VALUE_VALID 置为有效。值在下一次更新事件之前保持不变。

【2】、比较器

  COMP0、COMP1、COMP2 均为 ESP32 S3 系统定时器内置的 52 位比较器。比较器同样使用 XTAL_CLK 作为时钟源(40MHz)。计数器生成的计时值实时传递到比较器。

系统定时器生成报警

  在上述过程中用到一个计数器(Timer Countern)和一个比较器(Timer Comparatorx),比较器将根据比较结果,生成报警中断。

【3】、触发中断信号

  当计数器的值达到比较器设置的目标值时,比较器会生成一个 Timer Interrupt 警报信号,并将该信号发送至 CPU 中断矩阵(CPU Interrupt Matrix)。中断矩阵负责处理中断信号,并触发相应的中断服务程序(ISR)。

  每个 COMPx 的工作模式可通过配置寄存器 SYSTIMER_TARGETx_PERIOD_MODE 来选择,主要包括 周期模式单次模式

  • 周期模式:警报周期(δt)由寄存器 SYSTIMER_TARGETx_PERIOD 提供。假设当前计数值为 t1,当计数值达到(t1 + δt)时,会生成一个警报中断;当计数值再次达到(t1 + 2*δt)时,另一个警报中断将生成,从而实现周期性警报。
  • 单次模式:警报值(t)的低 32 位和高 20 位由 SYSTIMER_TIMER_TARGETx_LOSYSTIMER_TIMER_TARGETx_HI 提供。假设当前计数值为 t2(t2 ≤ t),当计数值 t2 达到警报值(t)时,将生成一次警报中断。与周期模式不同,单次模式仅生成一次警报中断。

  此外,SYSTIMER_TARGETx_TIMER_UNIT_SEL 用于选择用于比较生成警报的计数值来源:

  • 1 表示使用来自 UNIT1 的计数值。
  • 0 表示使用来自 UNIT0 的计数值(。

  设置 SYSTIMER_TARGETx_WORK_EN 可启用 COMPx 进行计数值比较。在单次模式下,COMPx 将与警报值(t)进行比较;在周期模式下,则与起始值加上 n 倍警报周期(t1 + n * δt)进行比较。

  警报生成的条件是,当计数值等于警报值(t)时(在单次模式)或等于起始值加上 n 倍警报周期(在周期模式)时,警报将被触发。如果寄存器中设置的警报值(t)小于当前计数值,即目标已过,当当前计数值大于警报值(t)并在范围(0 ~ 251 – 1)内时,也会立即生成警报中断。

无论是在目标模式还是周期模式,实际警报值的低 32 位和高 20 位始终可以从 SYSTIMER_TARGETx_LO_ROSYSTIMER_TARGETx_HI_RO 读取。

三、系统定时器常用函数

  ESP-IDF 提供了一套 API 来配置高精度定时器。要使用此功能,我们需要在 CMakeLists.txt 文件中导入 esp_timer 依赖库,然后还需要导入必要的头文件:

# 注册组件到构建系统的函数
idf_component_register(
    # 依赖库的路径
    REQUIRES esp_timer
)
#include "esp_timer.h"

3.1、创建一个定时器

  我们可以使用 esp_timer_create() 函数来 创建一个定时器,其函数原型如下:

/**
 * @brief 创建一个定时器
 * 
 * @param args 指向arg外设结构体的指针
 * @param out_handle 指定使能的中断
 * @return esp_err_t ESP_OK 表示创建成功
 *                   ESP_ERR_INVALID_ARG 表示某些create_args无效
 *                   ESP_ERR_INVALID_STAT 表示esp_timer库尚未初始化
 *                   ESP_ERR_NO_MEM 表示内存分配失败
 */
esp_err_t esp_timer_create(const esp_timer_create_args_t* args, esp_timer_handle_t* out_handle);

  成员 args指向 esp_timer_create_args_t 结构体的成员变量的指针,该结构体的成员变量描述如下:

typedef struct 
{
    esp_timer_cb_t callback;                // 定时器到期时执行的回调函数
    void * arg;                             // 传递给回调的参数
    esp_timer_dispatch_t dispatch_method;   // 从任务或ISR调度回调的方法,如果未指定,将使用esp_timer任务
    const char * name;                      // 定时器名称,在esp_timer_dump()函数中使用
    bool skip_unhandled_events;             // 设为跳过在轻睡眠状态下的周期性定时器未处理事件
} esp_timer_create_args_t;

  回调函数的格式如下:

void (*esp_timer_cb_t)(void* arg);

3.2、一次触发

  我们可以使用 esp_timer_start_once() 函数 单次启动系统定时器,其函数原型如下:

/**
 * @brief 单次启动系统定时器
 * 
 * @param timer 使用esp_timer_create()创建的定时器句柄
 * @param period_us 计时器周期,以微秒为单位
 * @return esp_err_t ESP_OK表示开启定时器成功
 *                   ESP_ERR_INVALID_ARG 表示句柄无效
 *                   ESP_ERR_INVALID_STATE 表示定时器已经在运行
 */
esp_err_t IRAM_ATTR esp_timer_start_once(esp_timer_handle_t timer, uint64_t timeout_us);

3.3、周期触发

  我们可以使用 esp_timer_start_periodic() 函数 周期启动系统定时器,其函数原型如下:

/**
 * @brief 周期启动系统定时器
 * 
 * @param timer 使用esp_timer_create()创建的定时器句柄
 * @param period_us 计时器周期,以微秒为单位
 * @return esp_err_t ESP_OK表示开启定时器成功
 *                   ESP_ERR_INVALID_ARG 表示句柄无效
 *                   ESP_ERR_INVALID_STATE 表示定时器已经在运行
 */
esp_err_t IRAM_ATTR esp_timer_start_periodic(esp_timer_handle_t timer, uint64_t period_us);

3.4、停止定时器

  我们可以使用 esp_timer_stop() 函数 停止运行的定时器,其函数原型如下:

/**
 * @brief 停止定时器
 * 
 * @param timer 使用esp_timer_create()创建的定时器句柄
 * @return esp_err_t ESP_OK 表示停止成功
 *                   ESP_ERR_INVALID_STATE 表示定时器未在运行
 */
esp_err_t IRAM_ATTR esp_timer_stop(esp_timer_handle_t timer);

3.5、重新启动定时器

  我们可以使用 esp_timer_restart() 函数 重新启动系统定时器,其函数原型如下:

/**
 * @brief 重新启动系统定时器
 * 
 * @param timer 使用esp_timer_create创建的定时器句柄
 * @param period_us 计时器周期,以微秒为单位
 * @return esp_err_t ESP_OK表示开启定时器成功,其它表示开启失败
 */
esp_err_t IRAM_ATTR esp_timer_restart(esp_timer_handle_t timer, uint64_t timeout_us);

3.6、删除一个定时器

  我们可以使用 esp_timer_delete() 函数 删除一个 esp_timer 实例,其函数原型如下:

/**
 * @brief 删除一个定时器
 * 
 * @param timer 使用esp_timer_create()创建的定时器句柄
 * @return esp_err_t ESP_OK 表示删除成功
 *                   ESP_ERR_INVALID_STATE 表示定时器仍在运行
 */
esp_err_t esp_timer_delete(esp_timer_handle_t timer);

四、实验例程

  这里,我们在【components】文件夹下的【peripheral】文件夹下的【inc】文件夹(用来存放头文件)新建一个 bsp_timer.h 文件,在【components】文件夹下的【peripheral】文件夹下的【src】文件夹(用来存放源文件)新建一个 bsp_timer.c 文件。

#ifndef __BSP_TIMER_H__
#define __BSP_TIMER_H__

#include "esp_timer.h"
#include "driver/gpio.h"

extern esp_timer_handle_t g_esp_timer_handle;

void bsp_esp_timer_init(esp_timer_handle_t *handle);

#endif // !__BSP_TIMER_H__
#include "bsp_timer.h"

static void esp_timer_callback(void *arg);

esp_timer_handle_t g_esp_timer_handle;

/**
 * @brief 系统定时器初始化
 * 
 * @param handle 系统定时器句柄
 */
void bsp_esp_timer_init(esp_timer_handle_t *handle)
{
    esp_timer_create_args_t esp_timer_create_args = {0};

    esp_timer_create_args.callback = esp_timer_callback;
    esp_timer_create_args.arg = NULL;
    esp_timer_create(&esp_timer_create_args, handle);
}

/**
 * @brief 系统定时器回调函数
 * 
 * @param arg 传入系统定时器回调函数的参数
 */
static void esp_timer_callback(void *arg)
{
    gpio_set_level(GPIO_NUM_0, !gpio_get_level(GPIO_NUM_0));
}

  然后,我们修改【components】文件夹下【peripheral】文件夹下的 CMakeLists.txt 文件。

# 源文件路径
set(src_dirs src)

# 头文件路径
set(include_dirs inc)

# 设置依赖库
set(requires 
    driver
    esp_timer
)

# 注册组件到构建系统的函数
idf_component_register(
    # 源文件路径
    SRC_DIRS ${src_dirs}
    # 自定义头文件的路径
    INCLUDE_DIRS ${include_dirs}
    # 依赖库的路径
    REQUIRES ${requires}
)

# 设置特定组件编译选项的函数
# -ffast-math: 允许编译器进行某些可能减少数学运算精度的优化,以提高性能。
# -O3: 这是一个优化级别选项,指示编译器尽可能地进行高级优化以生成更高效的代码。
# -Wno-error=format: 这将编译器关于格式字符串不匹配的警告从错误降级为警告。
# -Wno-format: 这将完全禁用关于格式字符串的警告。
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)

  修改【main】文件夹下的 main.c 文件。

#include "freertos/FreeRTOS.h"

#include "bsp_timer.h"

#include "led.h"

// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
    led_init(GPIO_NUM_0);
    bsp_esp_timer_init(&g_esp_timer_handle);
    esp_timer_start_periodic(g_esp_timer_handle, 1000000);

    while (1)
    {
        // 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}
posted @ 2025-03-13 21:13  星光映梦  阅读(81)  评论(0)    收藏  举报