3. ESP32 中断系统:中断概念、GPIO外部中断、定时器中断、中断服务函数编写规范

中断,说白了就是让CPU停下手里的事,先去处理一个紧急任务。我刚开始学单片机时,总觉得轮询也能用,干嘛非要搞中断?直到有一次做工业数据采集,轮询丢包丢到怀疑人生……嗯,从那以后我再也不敢轻视中断了。

ESP32的CPU是双核Xtensa LX6,中断控制器支持多达32个中断源。你想想看,如果不用中断,CPU就得一直盯着某个引脚看电平变化,多浪费算力。中断就是让CPU该干嘛干嘛,有事再叫它。

核心观点:中断是实时系统的基石。没有中断,你就别想做好IoT设备。

3.1 中断的基本概念

中断分为硬件中断和软件中断。硬件中断由外设触发,比如GPIO电平变化、定时器溢出、UART收到数据。软件中断由程序指令触发,比如ESP32的esp_intr_alloc()函数。

中断处理流程大致是这样:

  1. 中断源产生中断请求信号
  2. CPU保存当前上下文(寄存器、PC指针等)
  3. CPU跳转到中断向量表,找到对应的中断服务函数(ISR)
  4. 执行ISR
  5. ISR执行完毕,恢复上下文,返回断点继续执行

ESP32的中断控制器支持优先级设置,优先级从1到5,数字越大优先级越高。高优先级中断可以打断低优先级中断——这叫中断嵌套。我在项目中遇到过一个问题:两个中断同时触发,低优先级的ISR还没跑完就被高优先级打断,结果共享资源被搞乱了。后来加了互斥锁才解决。

中断特性 说明
中断源数量 32个(可配置)
优先级范围 1~5(5最高)
中断嵌套 支持(高优先级可打断低优先级)
中断延迟 约12个时钟周期(典型值)

3.2 GPIO外部中断

GPIO外部中断是IoT设备最常用的中断类型。按键检测、传感器触发、脉冲计数……都离不开它。

ESP32的GPIO中断支持以下触发模式:

  • 上升沿触发:电平从低变高时触发
  • 下降沿触发:电平从高变低时触发
  • 任意沿触发:电平变化时触发(上升或下降都行)
  • 低电平触发:电平为低时持续触发
  • 高电平触发:电平为高时持续触发

我个人习惯用沿触发,尤其是下降沿。为什么?因为电平触发容易产生抖动,你按一下按键,电平可能来回跳好几次,ISR就被反复调用。沿触发配合硬件去抖或软件延时,效果就好很多。

配置GPIO中断的步骤:

  1. 配置GPIO为输入模式
  2. 设置中断触发模式
  3. 注册中断服务函数
  4. 使能中断

来看代码:

#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);
}
提示:ISR函数前一定要加 IRAM_ATTR 属性,把函数放到IRAM中运行。否则Flash访问延迟会导致中断响应变慢。

3.3 定时器中断

定时器中断是另一种常用中断。你想让LED每隔100ms闪烁一次?用定时器中断。你想每隔1秒采集一次传感器数据?用定时器中断。你想做PWM输出?还是用定时器中断。

ESP32有4个64位通用定时器,每个定时器都可以独立配置。定时器的工作原理很简单:计数器从0开始,每个时钟周期加1,当计数值达到预设的阈值时,产生中断。

配置定时器中断的步骤:

  1. 初始化定时器配置结构体
  2. 设置分频系数和计数模式
  3. 设置报警值(即阈值)
  4. 注册中断服务函数
  5. 启动定时器

来看代码:

#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);  // 单位:微秒
}
注意:定时器中断的精度受系统时钟影响。ESP32的APB时钟默认80MHz,分频后定时器精度可以达到微秒级。但如果你开启了WiFi或蓝牙,系统时钟可能会动态调整,导致定时器偏差。我在做智能家居网关时就踩过这个坑——定时器跑着跑着就偏了,后来改用硬件定时器才稳定。

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中断...
}
最佳实践:ISR + 队列 + 任务 是ESP32中断处理的黄金组合。ISR只负责把数据塞进队列,任务负责处理。这样既保证了中断响应速度,又避免了ISR里做耗时操作。

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里不能用。用硬件定时器或计数器代替。
经验之谈:写ISR时,脑子里要时刻绷着一根弦——这个函数是在中断上下文里跑的。任何可能引起阻塞、任务切换、内存分配的操作,都不要出现在ISR里。

3.7 本章知识结构图

下面这张图总结了ESP32中断系统的核心知识点和流程关系:

ESP32 中断系统知识结构 中断源 中断控制器 CPU 执行ISR GPIO外部中断 定时器中断 UART中断 ADC/DAC中断 ISR编写规范 优先级设置 中断嵌套 避坑指南 ISR + 队列 任务处理 IRAM_ATTR volatile变量 核心原则 ISR要短小精悍,只做最必要的事 耗时操作放到任务中处理,ISR通过队列传递数据

这张图展示了中断系统的三个核心环节:中断源产生中断信号 → 中断控制器仲裁优先级 → CPU执行ISR。同时列出了常见的中断类型、ISR编写规范以及最佳实践。你可以把它当作开发时的快速参考。