4. 中断系统与定时器:GPIO中断配置、定时器组(Timer Group)使用、看门狗定时器(WDT)、高性能定时器(HRTimer)
中断和定时器,是嵌入式系统里最基础也最核心的两个概念。说白了,没有中断,CPU就得一直轮询,效率低得吓人;没有定时器,你就没法精确控制时间。ESP32的中断系统和定时器资源相当丰富,我做了这么多年项目,几乎每个产品都离不开它们。
这一章,我会带你逐个击破:GPIO中断怎么配、定时器组怎么用、看门狗怎么喂、高性能定时器又强在哪。嗯,咱们直接开干。
4.1 GPIO中断配置
GPIO中断,就是当某个引脚的电平发生变化时,CPU暂停当前任务,去执行你写的中断服务函数。ESP32的每个GPIO都可以独立配置为上升沿、下降沿或双边沿触发。
核心要点:GPIO中断配置分三步——初始化GPIO、注册中断服务、编写ISR。ISR里尽量少做事,只设标志位,别做延时或打印。
4.1.1 配置步骤
- 配置GPIO模式与中断类型:调用
gpio_config()设置引脚为输入,并指定中断触发边沿。 - 安装GPIO中断服务:调用
gpio_install_isr_service(),这是ESP-IDF封装好的中断框架。 - 绑定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 配置流程
- 初始化定时器参数:设置分频系数、计数方向、是否自动重载。
- 设置定时器中断:配置报警值,使能中断。
- 启动定时器:调用
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,每个模块的依赖关系和配置要点一目了然。
4.6 小结
这一章的内容,说实话,每个点单独拿出来都能讲一节课。但核心就一句话:中断是系统的「神经」,定时器是系统的「心跳」。GPIO中断让你能及时响应外部事件,定时器组给你精确的时间控制,看门狗保你不死机,HRTimer则让你玩出高精度的花样。
我个人建议,初学者先把GPIO中断和普通定时器玩熟,这两个是日常开发最常用的。看门狗在项目后期加上,别一开始就开,否则调试时老复位,烦得很。HRTimer嘛,等你需要做电机控制或高频信号时再深入研究。
最后一个小技巧:调试中断相关代码时,可以在ISR里翻转一个GPIO,用示波器看波形。这样能直观看到中断响应时间和执行时间,比打印日志靠谱多了。