4. 中断系统与定时器:GPIO中断配置、定时器组(Timer Group)使用、看门狗定时器(WDT)、高性能定时器(HRTimer)

中断和定时器,是嵌入式系统里最基础也最核心的两个概念。说白了,没有中断,CPU就得一直轮询,效率低得吓人;没有定时器,你就没法精确控制时间。ESP32的中断系统和定时器资源相当丰富,我做了这么多年项目,几乎每个产品都离不开它们。

这一章,我会带你逐个击破:GPIO中断怎么配、定时器组怎么用、看门狗怎么喂、高性能定时器又强在哪。嗯,咱们直接开干。

4.1 GPIO中断配置

GPIO中断,就是当某个引脚的电平发生变化时,CPU暂停当前任务,去执行你写的中断服务函数。ESP32的每个GPIO都可以独立配置为上升沿、下降沿或双边沿触发。

核心要点:GPIO中断配置分三步——初始化GPIO、注册中断服务、编写ISR。ISR里尽量少做事,只设标志位,别做延时或打印。

4.1.1 配置步骤

  1. 配置GPIO模式与中断类型:调用 gpio_config() 设置引脚为输入,并指定中断触发边沿。
  2. 安装GPIO中断服务:调用 gpio_install_isr_service(),这是ESP-IDF封装好的中断框架。
  3. 绑定ISR:调用 gpio_isr_handler_add(),把中断处理函数挂到指定引脚上。
// GPIO中断配置示例
gpio_config_t io_conf = {
    .pin_bit_mask = (1ULL << GPIO_NUM_0),  // 选择GPIO0
    .mode = GPIO_MODE_INPUT,
    .pull_up_en = GPIO_PULLUP_ENABLE,
    .intr_type = GPIO_INTR_NEGEDGE          // 下降沿触发
};
gpio_config(&io_conf);

// 安装中断服务
gpio_install_isr_service(0);

// 注册中断处理函数
gpio_isr_handler_add(GPIO_NUM_0, gpio_isr_handler, (void*) GPIO_NUM_0);

我的习惯:ISR函数名前加 isr_ 前缀,方便和普通函数区分。另外,ISR里别用 printf,那玩意儿会阻塞,还可能死锁。用 ESP_EARLY_LOGI 或者直接设一个全局标志位。

4.1.2 避坑指南

  • 中断里别做复杂操作:我曾经在ISR里调用了 vTaskDelay(),结果系统直接崩溃。ISR里不能调用任何可能阻塞的FreeRTOS API。
  • 注意中断优先级:ESP32的中断优先级从1到7,数字越大优先级越高。默认是1,如果你有实时性要求高的中断,记得调高。
  • 共享中断资源:多个GPIO可以共用一个中断服务,但每个引脚要单独注册handler。

4.2 定时器组(Timer Group)使用

ESP32有两个定时器组,每组有两个64位通用定时器。每个定时器都可以独立配置,支持自动重载、单次模式和多种时钟源。我个人觉得,这玩意儿比软件定时器靠谱多了,精度高,还不占CPU。

4.2.1 定时器组结构

定时器组 定时器编号 位宽 时钟源 主要用途
Timer Group 0 TIMER_0, TIMER_1 64位 APB_CLK (80MHz) 或 XTAL 通用定时、PWM、输入捕获
Timer Group 1 TIMER_0, TIMER_1 64位 APB_CLK (80MHz) 或 XTAL 通用定时、看门狗、RTC

4.2.2 配置流程

  1. 初始化定时器参数:设置分频系数、计数方向、是否自动重载。
  2. 设置定时器中断:配置报警值,使能中断。
  3. 启动定时器:调用 timer_start()。
// 定时器配置示例
timer_config_t config = {
    .divider = 80,                // 80分频,得到1MHz计数频率
    .counter_dir = TIMER_COUNT_UP,
    .counter_en = TIMER_PAUSE,
    .alarm_en = TIMER_ALARM_EN,
    .auto_reload = TIMER_AUTORELOAD_EN,
};
timer_init(TIMER_GROUP_0, TIMER_0, &config);

// 设置报警值:1秒
timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, 1000000);

// 注册中断
timer_isr_register(TIMER_GROUP_0, TIMER_0, timer_isr, NULL, ESP_INTR_FLAG_IRAM, NULL);

// 启动
timer_start(TIMER_GROUP_0, TIMER_0);

注意:定时器中断处理函数必须放在IRAM中,否则可能因为flash访问冲突导致崩溃。在函数前加 IRAM_ATTR 即可。

4.3 看门狗定时器(WDT)

看门狗,说白了就是系统的一个「保命符」。如果程序跑飞或者死锁,WDT会在超时后复位系统。ESP32里有三种WDT:TWDT(任务看门狗)、IWDT(中断看门狗)和RWDT(RTC看门狗)。

4.3.1 三种WDT对比

类型 触发条件 默认状态 典型场景
TWDT 某个任务长时间不释放CPU 默认开启 检测任务死循环
IWDT 中断服务执行时间过长 默认关闭 检测ISR异常
RWDT RTC周期内未喂狗 默认关闭 深度睡眠唤醒检测

4.3.2 喂狗的正确姿势

喂狗不是随便调个函数就完事了。你想想看,如果喂狗放在主循环里,但某个子函数死锁了,主循环还在跑,那看门狗就形同虚设。

我的建议:在关键任务里单独喂狗,或者用多级看门狗策略。比如主循环喂一次,高优先级任务喂一次,这样能精确定位哪个模块出了问题。

// 喂狗示例
#include "esp_task_wdt.h"

void task_function(void *arg) {
    // 将当前任务添加到TWDT监控
    esp_task_wdt_add(NULL);

    while (1) {
        // 业务逻辑
        // ...

        // 喂狗
        esp_task_wdt_reset();

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

我曾经踩过的坑:调试阶段把TWDT超时时间设得太短,结果每次断点停下来就复位。后来我把超时时间设成10秒,调试完再改回来。嗯,这个习惯一直保留到现在。

4.4 高性能定时器(HRTimer)

HRTimer,全称High Resolution Timer,是ESP32-S2/S3/C3等芯片才有的外设。它的精度能达到纳秒级,适合做精确的脉冲生成、PWM和输入捕获。如果你在做电机控制或激光测距,这东西就是神器。

4.4.1 HRTimer vs 普通定时器

特性 普通定时器 HRTimer
分辨率 微秒级(1MHz) 纳秒级(最高80MHz)
通道数 4个(两组各2个) 4个独立通道
输出模式 仅中断 PWM、单脉冲、连续脉冲
适用场景 通用定时、延时 精密控制、信号生成

4.4.2 配置示例

// HRTimer配置示例(ESP32-S3)
timer_hal_context_t hal = {
    .dev = TIMERG0,
};
timer_hal_init(&hal);

// 设置分频:80MHz / 80 = 1MHz
timer_hal_set_divider(&hal, 0, 80);

// 设置报警值
timer_hal_set_alarm_value(&hal, 0, 1000);  // 1ms

// 使能中断并启动
timer_hal_set_alarm_enable(&hal, 0, true);
timer_hal_set_counter_enable(&hal, 0, true);

核心区别:HRTimer的计数器是硬件直接驱动的,不经过CPU。所以它能做到极低的抖动(jitter),适合对时序要求苛刻的场景。普通定时器虽然也能用,但受中断响应时间影响,精度会打折扣。

4.5 本章知识结构图

下面这张图帮你理清本章的核心脉络。从GPIO中断到HRTimer,每个模块的依赖关系和配置要点一目了然。

中断系统与定时器知识结构图 中断系统与定时器知识结构 中断系统 GPIO中断 定时器组 看门狗WDT 配置 → 注册 → ISR Timer0 / Timer1 × 2组 TWDT / IWDT / RWDT HRTimer(纳秒级) HRTimer是定时器组的增强版 适用于高精度PWM和脉冲生成

4.6 小结

这一章的内容,说实话,每个点单独拿出来都能讲一节课。但核心就一句话:中断是系统的「神经」,定时器是系统的「心跳」。GPIO中断让你能及时响应外部事件,定时器组给你精确的时间控制,看门狗保你不死机,HRTimer则让你玩出高精度的花样。

我个人建议,初学者先把GPIO中断和普通定时器玩熟,这两个是日常开发最常用的。看门狗在项目后期加上,别一开始就开,否则调试时老复位,烦得很。HRTimer嘛,等你需要做电机控制或高频信号时再深入研究。

最后一个小技巧:调试中断相关代码时,可以在ISR里翻转一个GPIO,用示波器看波形。这样能直观看到中断响应时间和执行时间,比打印日志靠谱多了。