3. ESP32 中断系统:中断概念、GPIO外部中断、定时器中断、中断服务函数编写规范
中断,说白了就是让CPU停下手里的事,先去处理一个紧急任务。我刚开始学单片机时,总觉得轮询也能用,干嘛非要搞中断?直到有一次做工业数据采集,轮询丢包丢到怀疑人生……嗯,从那以后我再也不敢轻视中断了。
ESP32的CPU是双核Xtensa LX6,中断控制器支持多达32个中断源。你想想看,如果不用中断,CPU就得一直盯着某个引脚看电平变化,多浪费算力。中断就是让CPU该干嘛干嘛,有事再叫它。
3.1 中断的基本概念
中断分为硬件中断和软件中断。硬件中断由外设触发,比如GPIO电平变化、定时器溢出、UART收到数据。软件中断由程序指令触发,比如ESP32的esp_intr_alloc()函数。
中断处理流程大致是这样:
- 中断源产生中断请求信号
- CPU保存当前上下文(寄存器、PC指针等)
- CPU跳转到中断向量表,找到对应的中断服务函数(ISR)
- 执行ISR
- ISR执行完毕,恢复上下文,返回断点继续执行
ESP32的中断控制器支持优先级设置,优先级从1到5,数字越大优先级越高。高优先级中断可以打断低优先级中断——这叫中断嵌套。我在项目中遇到过一个问题:两个中断同时触发,低优先级的ISR还没跑完就被高优先级打断,结果共享资源被搞乱了。后来加了互斥锁才解决。
| 中断特性 | 说明 |
|---|---|
| 中断源数量 | 32个(可配置) |
| 优先级范围 | 1~5(5最高) |
| 中断嵌套 | 支持(高优先级可打断低优先级) |
| 中断延迟 | 约12个时钟周期(典型值) |
3.2 GPIO外部中断
GPIO外部中断是IoT设备最常用的中断类型。按键检测、传感器触发、脉冲计数……都离不开它。
ESP32的GPIO中断支持以下触发模式:
- 上升沿触发:电平从低变高时触发
- 下降沿触发:电平从高变低时触发
- 任意沿触发:电平变化时触发(上升或下降都行)
- 低电平触发:电平为低时持续触发
- 高电平触发:电平为高时持续触发
我个人习惯用沿触发,尤其是下降沿。为什么?因为电平触发容易产生抖动,你按一下按键,电平可能来回跳好几次,ISR就被反复调用。沿触发配合硬件去抖或软件延时,效果就好很多。
配置GPIO中断的步骤:
- 配置GPIO为输入模式
- 设置中断触发模式
- 注册中断服务函数
- 使能中断
来看代码:
#include "driver/gpio.h"
#define BUTTON_GPIO 0 // GPIO0 接按键
// 中断服务函数
static void IRAM_ATTR gpio_isr_handler(void* arg) {
uint32_t gpio_num = (uint32_t) arg;
// 处理中断逻辑
// 注意:ISR中不要做耗时操作
}
void app_main() {
// 1. 配置GPIO
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_NEGEDGE; // 下降沿触发
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pin_bit_mask = (1ULL << BUTTON_GPIO);
io_conf.pull_up_en = 1;
gpio_config(&io_conf);
// 2. 安装GPIO中断驱动
gpio_install_isr_service(0);
// 3. 注册中断服务函数
gpio_isr_handler_add(BUTTON_GPIO, gpio_isr_handler, (void*) BUTTON_GPIO);
}
IRAM_ATTR 属性,把函数放到IRAM中运行。否则Flash访问延迟会导致中断响应变慢。
3.3 定时器中断
定时器中断是另一种常用中断。你想让LED每隔100ms闪烁一次?用定时器中断。你想每隔1秒采集一次传感器数据?用定时器中断。你想做PWM输出?还是用定时器中断。
ESP32有4个64位通用定时器,每个定时器都可以独立配置。定时器的工作原理很简单:计数器从0开始,每个时钟周期加1,当计数值达到预设的阈值时,产生中断。
配置定时器中断的步骤:
- 初始化定时器配置结构体
- 设置分频系数和计数模式
- 设置报警值(即阈值)
- 注册中断服务函数
- 启动定时器
来看代码:
#include "esp_timer.h"
// 定时器中断回调函数
static void timer_callback(void* arg) {
// 每100ms执行一次
// 可以在这里翻转LED、采集数据等
}
void app_main() {
const esp_timer_create_args_t timer_args = {
.callback = &timer_callback,
.arg = NULL,
.name = "my_timer"
};
esp_timer_handle_t timer_handle;
esp_timer_create(&timer_args, &timer_handle);
// 启动定时器,每100ms触发一次
esp_timer_start_periodic(timer_handle, 100 * 1000); // 单位:微秒
}
3.4 中断服务函数编写规范
中断服务函数(ISR)的编写规范,是嵌入式开发的基本功。写不好ISR,系统就会出各种诡异问题。我见过有人把printf写在ISR里,结果系统直接死机……
ISR编写有以下几个铁律:
- ISR要短小精悍:ISR里只做最必要的事,比如设置标志位、读取数据。耗时操作放到主循环或任务里做。
- ISR里不能调用阻塞函数:比如
vTaskDelay()、printf()、malloc()等。这些函数可能引起任务切换或等待,而ISR里不允许任务切换。 - ISR里不能使用互斥锁:互斥锁可能导致死锁。用信号量或队列代替。
- ISR要加IRAM_ATTR属性:确保ISR代码在IRAM中运行,避免Flash访问延迟。
- ISR里要避免浮点运算:浮点运算耗时且可能使用FPU寄存器,保存和恢复上下文开销大。
来看一个规范的ISR示例:
static QueueHandle_t gpio_evt_queue = NULL;
// ISR:只发送数据到队列
static void IRAM_ATTR gpio_isr_handler(void* arg) {
uint32_t gpio_num = (uint32_t) arg;
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
// 任务:处理队列中的数据
void gpio_task_example(void* arg) {
uint32_t io_num;
while(1) {
if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
// 在这里处理中断事件
printf("GPIO %d 被触发\n", io_num);
}
}
}
void app_main() {
// 创建队列
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
// 创建处理任务
xTaskCreate(gpio_task_example, "gpio_task", 2048, NULL, 10, NULL);
// 配置GPIO中断...
}
3.5 中断优先级与中断嵌套
ESP32的中断优先级从1到5。默认情况下,所有中断的优先级都是1。如果你有多个中断源,需要根据紧急程度分配优先级。
中断嵌套的规则:
- 高优先级中断可以打断低优先级中断
- 同优先级中断不能互相打断
- 低优先级中断不能打断高优先级中断
我在项目中遇到过一个问题:一个高优先级的定时器中断频繁触发,导致低优先级的GPIO中断一直得不到响应。后来我把GPIO中断的优先级调高了一级,问题就解决了。
设置中断优先级的方法:
// 在注册ISR时指定优先级
gpio_isr_handler_add_with_level(BUTTON_GPIO, gpio_isr_handler,
(void*) BUTTON_GPIO, ESP_INTR_FLAG_LEVEL3);
3.6 中断系统的常见陷阱
写中断代码,踩坑是难免的。我把自己踩过的坑列出来,你遇到了可以少走弯路:
- 中断里调用printf:printf会阻塞,而且可能触发任务切换,导致系统崩溃。用ESP_LOG代替,或者干脆别在ISR里打印。
- 中断里操作全局变量没加volatile:编译器可能优化掉你的变量读写。全局变量一定要加volatile关键字。
- 中断里使用FreeRTOS API没加FromISR后缀:比如
xQueueSend和xQueueSendFromISR是不同的。在ISR里必须用FromISR版本。 - 中断里做浮点运算:浮点运算在ISR里开销巨大,而且可能破坏FPU状态。能不用就不用。
- 中断里做延时:
vTaskDelay()、delay()这些函数在ISR里不能用。用硬件定时器或计数器代替。
3.7 本章知识结构图
下面这张图总结了ESP32中断系统的核心知识点和流程关系:
这张图展示了中断系统的三个核心环节:中断源产生中断信号 → 中断控制器仲裁优先级 → CPU执行ISR。同时列出了常见的中断类型、ISR编写规范以及最佳实践。你可以把它当作开发时的快速参考。