由于 ESP-IDF 基于的 FreeRTOS 最大的时钟频率为 1 kHz,当需要生成更精确的时钟中断时(比如每 100 us 产生一个中断)可使用 ESP32 的 timer group。参考资料如下:

timer group 示例

通用定时器文档

ESP32 芯片提供两组硬件定时器,每组包含两个通用硬件定时器。所有定时器均为 64 位通用定时器,包括 16 位预分频器和 64 位自动重载向上/向下计数器。这篇博客将从阅读 通用定时器文档 和 查看 timer group 示例 代码两部分出发来实现生成更精确的时钟中断。

1. 配置和操作定时器的常规步骤

首先阅读 通用定时器文档,了解配置和操作定时器的常规步骤分为以下四部分:

  • 定时器初始化:启动定时器前应设置的参数,以及每个设置提供的具体功能
  • 定时器控制:如何读取定时器的值,如何暂停/启动定时器以及如何改变定时器的操作方式
  • 警报:如何设置和使用警报
  • 处理中断事务:如何使用中断提供的回调函数

1.1 定时器初始化

ESP32 定时器组的类型为 timer_group_t,每组中的个体定时器类型为 timer_idx_t。首先调用 timer_init() 函数,并将 timer_config_t 结构体传递给此函数,用于定义定时器的工作方式,实现定时器初始化。在 timer group 示例中对应的代码如下:

timer_config_t config = {
        .divider = TIMER_DIVIDER,
        .counter_dir = TIMER_COUNT_UP,
        .counter_en = TIMER_PAUSE,
        .alarm_en = TIMER_ALARM_EN,
        .auto_reload = auto_reload,
    }; // default clock source is APB
    timer_init(group, timer, &config);

以下定时器参数可设置为:

  • 时钟源(默认为 APB): 选择时钟源,它同时钟分频器一起决定了定时器的分辨率。默认的时钟源是 APB_CLK (一般是 80 MHz)。
  • 分频器(divider): 设置定时器中计数器计数的速度,divider 的设置将用作输入时钟源的除数。
  • 模式(counter_dir): 设置计数器是递增还是递减。可通过从 timer_count_dir_t 中选取一个值,后使用 counter_dir 来选择模式。
  • 计数器使能(counter_en): 如果计数器已使能,则在调用 timer_init() 后计数器将立即开始递增/递减。您可通过从 timer_start_t 中选取一个值,后使用 counter_en 改变此行为。
  • 报警使能(alarm_en): 可使用 alarm_en 设置。
  • 自动重载(auto_reload): 设置计数器是否应该在定时器警报上使用 auto_reload 自动重载首个计数值,还是继续递增或递减。

注:可使用函数 timer_get_config() 获取定时器设置的当前值。

1.2 定时器控制

定时器使能后便开始计数。要使能定时器,可首先设置 counter_en 为 true,然后调用函数 timer_init(),或者直接调用函数 timer_start()。您可通过调用函数 timer_set_counter_value() 来指定定时器的首个计数值。代码如下:

/* Timer's counter will initially start from value below.
       Also, if auto_reload is set, this value will be automatically reload on alarm */
	timer_set_counter_value(group, timer, 0);

可通过调用函数 timer_pause() 随时暂停定时器。若要再次启动它,调用函数 timer_start()。

注:要检查定时器的当前值,调用函数 timer_get_counter_value() 或 timer_get_counter_time_sec()。

同时可通过使用专有函数更改个别设置来重新配置定时器:

  • timer_set_divider() 配置分频器来更改计数频率
  • timer_set_counter_mode()配置计数器技术模式为递增或递减
  • timer_set_auto_reload() 配置是否应在定时器警报上重载首个计数值,关于警报部分会在下一节提到

1.3 警报

要设置警报,先调用函数 timer_set_alarm_value(),然后使用 timer_set_alarm() 使能警报。代码如下:

/* Configure the alarm value and the interrupt on alarm. */
    timer_set_alarm_value(group, timer, timer_interval_sec * TIMER_SCALE);

当调用函数 timer_init() 时,也可以在定时器初始化阶段使能警报。

警报已使能且定时器达到警报值后,根据配置,可能会出现以下两种行为:

  • 如果先前已配置,此时将触发中断
  • 如 auto_reload 已使能,定时器的计数器将重新加载,从先前配置好的值开始再次计数。应使用函数 timer_set_counter_value() 预先设置该值

如果已设置警报值且定时器已超过该值,则将立即触发警报。一旦触发后,警报将自动关闭,需要重新使能以再次触发。

注:要检查某特定的警报值,调用函数 timer_get_alarm_value()。

1.4 处理中断事务

调用 timer_isr_callback_add() 函数可以给某个定时器注册一个中断回调函数。代码如下:

timer_enable_intr(group, timer);
    
    example_timer_info_t *timer_info = calloc(1, sizeof(example_timer_info_t));
    timer_info->timer_group = group;
    timer_info->timer_idx = timer;
    timer_info->auto_reload = auto_reload;
    timer_info->alarm_interval = timer_interval_sec;
    timer_isr_callback_add(group, timer, timer_group_isr_callback, timer_info, 0);

    timer_start(group, timer);

对应的中断函数为 static bool IRAM_ATTR timer_group_isr_callback(void *args)。顾名思义,该函数会在中断上下文中被执行,因此用户不能在回调函数中调用任何会阻塞 CPU 的 API。 相较于从头编写中断处理程序,使用中断回调函数的好处是,用户无需检测和处理中断的状态位,这些操作会由驱动中默认的中断处理程序替我们完成。

2 相关示例

  • ESP32 timer group 生成 100 ms 的中断