3. ESP32 中断编程:从入门到实战
中断,说白了就是让CPU停下手里的事,先去处理一个紧急任务。我刚开始学单片机时,总觉得轮询也能干活,干嘛非要用中断?直到有一次做多传感器采集,轮询导致数据丢包严重……嗯,从那以后我才真正理解了中断的价值。
ESP32的中断系统比传统单片机灵活得多。它有两个核心处理器,每个核都有自己的中断控制器。你想想看,这相当于你有两个秘书,可以同时处理不同的紧急事务。
3.1 中断的基本概念
中断的本质是什么?我个人的理解是:事件驱动的优先级抢占。CPU平时跑主循环,当外设或内部事件触发时,硬件自动保存当前现场,跳转到中断服务函数执行,执行完再回来继续干活。
ESP32的中断系统有几个关键参数:
| 参数 | 说明 | 取值范围 |
|---|---|---|
| 中断源 | 触发中断的外设或事件 | GPIO、定时器、UART、I2C等 |
| 中断优先级 | 数字越小优先级越高 | 0~31(ESP32) |
| 中断类型 | 电平触发或边沿触发 | 上升沿、下降沿、高电平、低电平 |
| 中断服务函数 | 处理中断的代码块 | ISR(Interrupt Service Routine) |
这里有个容易踩的坑:中断服务函数里不能做耗时操作。我曾经见过有人直接在ISR里调用printf,结果系统直接崩溃。为什么?因为printf涉及系统调用和锁,中断上下文里调用这些函数会死锁。
3.2 GPIO中断配置
GPIO中断是嵌入式开发中最常用的中断类型。ESP32的每个GPIO都可以配置为中断源,这一点比很多传统MCU都灵活。
配置GPIO中断的步骤,我习惯这样记:
- 初始化GPIO:设置引脚模式为输入
- 配置中断触发方式:上升沿、下降沿、或双边沿
- 注册中断服务函数:告诉CPU中断来了该干什么
- 使能中断:打开中断开关
来看一个实际代码示例:
#include "driver/gpio.h"
#define BUTTON_GPIO 0 // 开发板上的BOOT按钮
// 中断服务函数
static void IRAM_ATTR gpio_isr_handler(void* arg) {
uint32_t gpio_num = (uint32_t) arg;
// 这里只做最轻量级操作
// 实际处理放到主循环
}
void app_main(void) {
// 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(指令RAM)里。为什么?因为中断发生时,Flash可能正在被其他操作占用,如果ISR在Flash里,就会导致取指失败。我调试时遇到过这个问题,折腾了半天才发现是ISR没放到IRAM里。
3.3 中断服务函数编写规范
写ISR和写普通函数完全不同。我总结了几个铁律:
- 快进快出:ISR执行时间越短越好,最好在微秒级别
- 禁止阻塞:不能用delay、vTaskDelay等阻塞函数
- 禁止调用FreeRTOS API:大部分API不能在ISR里用,除非带FromISR后缀
- 注意临界区:如果ISR和主循环共享变量,要用volatile和临界区保护
来看一个规范的ISR写法:
// 共享变量,用volatile防止编译器优化
static volatile uint32_t g_button_press_count = 0;
// 事件标志组,用于通知主循环
static EventGroupHandle_t g_event_group;
#define BUTTON_PRESS_BIT (1 << 0)
static void IRAM_ATTR gpio_isr_handler(void* arg) {
// 1. 清除中断标志(硬件自动完成,但有些外设需要手动清)
// 2. 更新共享变量
g_button_press_count++;
// 3. 通知主循环(使用FromISR版本)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xEventGroupSetBitsFromISR(g_event_group, BUTTON_PRESS_BIT,
&xHigherPriorityTaskWoken);
// 4. 如果需要上下文切换
if (xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
你可能会问:为什么不用全局变量直接传数据?嗯,这里有个关键点——中断和主循环是异步的。如果不用事件标志或队列,主循环可能错过中断,或者读到不完整的数据。
3.4 中断优先级与嵌套
ESP32的中断优先级系统比传统单片机复杂。它有32个优先级(0~31),数字越小优先级越高。但实际可用的优先级范围取决于你用的FreeRTOS配置。
我画了一张图,帮你理解中断优先级和嵌套的关系:
中断嵌套是怎么回事?说白了就是:高优先级中断可以打断正在执行的低优先级中断。比如你正在处理按键中断(优先级15),突然来了一个定时器中断(优先级8),CPU会暂停按键中断,先去处理定时器中断,处理完再回来继续处理按键中断。
配置中断优先级的方法:
// 在安装中断服务时设置优先级
gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1); // 设置优先级为1
// 或者使用更底层的API
// intr_alloc(0, ESP_INTR_FLAG_LEVEL3, &handle);
3.5 实战中的避坑指南
做中断编程这些年,我踩过的坑能写一本书。这里挑几个最常见的:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| ISR里调用printf | 系统死机或重启 | 用事件标志通知主循环打印 |
| 忘记加IRAM_ATTR | 偶发性的崩溃 | 所有ISR都加上IRAM_ATTR |
| 共享变量没加volatile | 编译器优化导致变量值不对 | 所有中断共享变量加volatile |
| 中断触发抖动 | 一次按键触发多次中断 | 硬件加RC滤波,软件做去抖 |
| 优先级设置不当 | 实时性要求高的任务被延迟 | 按实时性需求分配优先级 |
关于去抖,我多说两句。机械按键按下时,触点会弹跳几十毫秒。如果不做处理,一次按下可能触发几十次中断。我的做法是:硬件去抖 + 软件去抖。硬件加一个RC低通滤波,软件在ISR里启动一个50ms的定时器,定时器到期后才确认按键有效。
最后说一个很多人忽略的问题:中断的延迟。从硬件触发中断到ISR开始执行,中间有几十个时钟周期的延迟。这包括:CPU完成当前指令、保存上下文、跳转到中断向量表、查找ISR地址等。如果你的应用对实时性要求极高(比如微秒级),就要考虑这个延迟。
ESP32的中断延迟一般在50~100个时钟周期左右。对于240MHz的主频来说,大约是0.2~0.4微秒。大多数应用场景下完全够用。但如果你的系统要求纳秒级的响应,那就要考虑用硬件外设或者专用芯片了。