在计算机系统中,中断(Interrupt)是指某个硬件设备或软件程序发出一个信号,通知 CPU 暂停当前正在执行的任务并转而执行另一个任务。中断用于处理一些需要立即响应、优先级较高的事件,例如输入设备(例如键盘、鼠标)、输出设备(例如显示器、打印机)或者网络数据包等。
当硬件设备或软件程序需要向 CPU 发送中断请求时,它会向 CPU 发送一个中断信号,CPU 会暂停执行当前指令,并根据中断信号的类型调用相应的中断服务程序(ISR)。中断服务程序是一个预定义的程序,用于处理特定类型的中断请求,并返回到之前的任务执行状态。
在处理完中断请求后,CPU 会恢复之前被暂停的任务,继续执行之前的指令。如果存在多个中断请求,CPU 会根据事先设置的优先级来依次处理。
中断对于计算机系统的性能和可靠性至关重要。它可以提高系统的响应速度,使系统能够在同时处理多个任务时更加高效,同时也提高了系统的可靠性,可以及时处理一些关键事件,如故障处理、系统监视等。
需要注意的是,在编写中断服务程序时,需要考虑到中断与当前任务之间的上下文切换,防止出现竞态条件和死锁等问题,确保系统的稳定性和可靠性。同时,还需要合理设置中断优先级、中断屏蔽、中断嵌套等相关参数,以充分发挥中断的性能和特性。
ESP32 支持多种类型的中断,包括外部中断、定时器中断、任务通知中断、硬件定时器中断等。其中,外部中断是最常用的类型,它通过 GPIO 端口引脚接受外部的中断信号,在 ESP32 内部触发中断服务程序执行。
ESP32 的中断主要分为以下几类:
ESP32的每个内核共有 32 个中断。每个中断都有一定的优先级,大多数(但不是全部)中断都连接到中断多路复用器。因为中断源比中断多,所以有些中断是与多个中断源共享的。
在 ESP32 中,外部中断有以下几种触发类型:
上升沿触发:当 GPIO 引脚从低电平变为高电平时触发中断。
下降沿触发:当 GPIO 引脚从高电平变为低电平时触发中断。
双边沿(跳变沿)触发:当 GPIO 引脚从低电平变为高电平或从高电平变为低电平时触发中断。
低电平触发:当 GPIO 引脚保持低电平状态时触发中断。
高电平触发:当 GPIO 引脚保持高电平状态时触发中断。
ESP32 还提供了一些专门用于中断处理的 API 接口,例如:
但在本阶段的教程中,我们只使用Arduino封装好的中断函数操作,以上函数待我们学习ESP-IDF编程的时候再详细讲。
ESP32 支持中断嵌套,可以在一个中断服务函数(ISR)的执行过程中,再触发另一个中断服务函数的执行。这种机制可以帮助开发者快速响应多个事件,提高系统的实时性和响应能力。
ESP32 中断嵌套的具体原理是基于 CPU 的中断向量表(Interrupt Vector Table, IVT)实现的。当 CPU 执行到一个中断服务函数时,会自动禁用当前中断,并将中断服务函数的入口地址写入 IVT 中对应的中断向量项中。如果在当前中断服务函数执行的过程中,又触发了一个新的中断请求,则会根据新中断的优先级来选择是否暂停当前中断服务函数,并转而执行新的中断服务函数。当新的中断服务函数执行完毕后,CPU 会从上一个中断服务函数的执行点继续执行程序,即回到原来的中断服务函数继续执行。
需要注意的是,在 ESP32 中,除 TIMG0_TIMER1_IRQ 和 TIMG0_TIMER2_IRQ 外,所有的中断都支持中断嵌套。此外,由于中断嵌套会增加系统复杂度和不确定性,因此在实际应用中需要谨慎使用,确保程序稳定性和可靠性。
关于 ESP32 中断嵌套的更多介绍和应用实例,可以参考 Espressif 官方文档以及社区中的相关教程和文章。
中断优先级是指在处理多个中断请求时,按照一定的规则确定哪个中断请求具有更高的优先级。在 ESP32 中,一个中断的优先级可以通过设置中断控制器(Interrupt Controler, INTCTL)中的相应寄存器来进行配置。
ESP32 中,每个中断通道都有一个独立的中断控制器,可以设置该中断通道的优先级、中断类型、中断触发方式等参数。具体来说,ESP32 中使用 5 位二进制数表示中断优先级,其中优先级编号越小,优先级越高。中断优先级的具体设置需要参考 ESP32 芯片手册和相关文档。
大多数情况下,开发者不需要修改中断优先级的默认设置,但在某些特殊情况下,根据需求对中断优先级进行适当配置可以提高系统的实时性和响应能力。
ESP32 中的中断嵌套和中断优先级可以帮助开发者快速响应多个事件,提高系统的实时性和响应能力。在使用这两个特性时,需要根据具体的应用场景和需求进行配置和调整,以确保系统的稳定性和可靠性。
在Arduino中,中断服务函数和其他函数的格式是一样的,但是在FreeRTOS框架中,中断如无函数有严格的格式要求,如下:
void IRAM_ATTR ISR_function() {
// 中断服务程序的具体实现
}
其中,void 表示 ISR_function() 函数没有返回值,IRAM_ATTR 表示将该函数放在 IRAM(内部 RAM)中,以提高执行速度。ISR_function() 即为中断服务函数的名称,开发者需要根据具体的中断类型和事件命名。
注意,在使用 Arduino FreeRTOS 的情况下,中断服务函数与常规任务处理函数的执行方式略有不同。在中断服务函数中,可以通过 xHigherPriorityTaskWoken 参数来指定是否唤醒更高优先级的任务。例如,如果需要在中断服务函数中唤醒一个阻塞在等待信号量的任务,可以将 xHigherPriorityTaskWoken 参数设置为 pdTRUE,并在中断服务函数结束时调用 portYIELD_FROM_ISR() 函数来切换任务上下文。
void attachInterrupt(uint8_t pin, void (*ISR)(void), int mode);
该函数与用于安装中断函数,在对应的引脚上启用中断响应。
参数 | 描述 |
---|---|
pin | 表示外部中断触发的引脚编号,在不同开发板编号不一致,ESP32的需要对照开发板丝印确定(编号在引脚下面书写) |
ISR | 表示中断服务函数的指针,使用函数指针是因为中断服务函数需要具有固定的格式和命名规则。在编写中断服务函数时,需要使用 void 类型作为返回值,不带任何参数 |
mode | 表示中断触发模式,可以是下列四种之一 |
LOW:电平低,当引脚为低电平时,触发中断 | |
CHANGE:跳变沿,当引脚电平发生改变时,触发中断 | |
RISING:上升沿,当引脚由低电平变为高电平时,触发中断 | |
FALLING:下降沿,当引脚由高电平变为低电平时,触发中断 |
void detachInterrupt(uint8_t pin);
该函数用于删除绑定在引脚的中断服务,并关闭该引脚的中断,pin 表示要禁用的引脚
void interrupts(); // 开启全局中断
void noInterrupts(); // 关闭全局中断
其中,interrupts() 函数用于开启全局中断,noInterrupts() 函数用于关闭全局中断。在开启了全局中断时,Arduino 系统可以响应所有外部中断请求,并会调用相应的中断服务函数;而在关闭全局中断时,Arduino 系统将无法响应任何外部中断请求,即使外部中断触发也不会执行中断服务函数。
为了保证中断服务程序的响应速度和稳定性,开发者需要遵循以下几点原则:
本例中我们使用四个按键模拟四种中断触发方式:
红色按键上升沿触发,其中一个引脚连接到VCC上
绿色按键下降沿触发,其中一个引脚连接在GND上
蓝色按键跳变沿触发,其中一个引脚连接GND或者VCC都可以
黄色按钮低电平触发,其中一个引脚连接GND
黑色按键下降沿触发,这个用于删除所有中断服务程序
代码共享位置:https://wokwi.com/projects/364683897847685121
#define KEY1_PIN 4 // 红色:上升沿触发
#define KEY2_PIN 11 // 绿色:下降沿触发
#define KEY3_PIN 2 // 蓝色:跳变沿触发
#define KEY4_PIN 35 // 黄色:低电平触发
#define KEY5_PIN 19 // 黑色:退出
#define RISING_EVENT 1
#define FALLING_EVENT 2
#define CHANGE_EVENT 4
#define LOW_EVENT 8
#define CLEAR_EVENT 16
#define ALL_EVENT 31 //RISING_EVENT | FALLING_EVENT | CHANGE_EVENT | LOW_EVENT | CLEAR_EVENT
TaskHandle_t xKeyHandler = NULL; // 按键驱动任务句柄
// 上升沿触发中断服务函数
void IRAM_ATTR ISR_RISING(){
xTaskNotifyFromISR(xKeyHandler, RISING_EVENT, eSetBits, NULL);
}
// 下降沿触发中断服务函数
void IRAM_ATTR ISR_FALLING(){
xTaskNotifyFromISR(xKeyHandler, FALLING_EVENT, eSetBits, NULL);
}
// 跳变沿触发中断服务函数
void IRAM_ATTR ISR_CHANGE(){
xTaskNotifyFromISR(xKeyHandler, CHANGE_EVENT, eSetBits, NULL);
}
// 低电平触发中断服务函数
void IRAM_ATTR ISR_LOW(){
xTaskNotifyFromISR(xKeyHandler, LOW_EVENT, eSetBits, NULL);
}
// 下降沿触发,删除所有中断
void IRAM_ATTR ISR_CLEAN(){
xTaskNotifyFromISR(xKeyHandler, CLEAR_EVENT, eSetBits, NULL);
}
// 键盘驱动线程
void key_driver_entry(void *params){
pinMode(KEY1_PIN, INPUT_PULLDOWN); // 上升沿触发,所以必须下拉
pinMode(KEY2_PIN, INPUT_PULLUP); // 下降沿触发,所以必须上拉
pinMode(KEY3_PIN, INPUT); // 跳变沿触发,用哪种拉无所谓
pinMode(KEY4_PIN, INPUT_PULLUP); // 低电平触发,所以必须上拉
pinMode(KEY5_PIN, INPUT_PULLUP); // 下降沿触发,电阻上拉
// 安装中断
attachInterrupt(KEY1_PIN, ISR_RISING, RISING);
attachInterrupt(KEY2_PIN, ISR_FALLING, FALLING);
attachInterrupt(KEY3_PIN, ISR_CHANGE, CHANGE);
attachInterrupt(KEY4_PIN, ISR_LOW, LOW);
attachInterrupt(KEY5_PIN, ISR_CLEAN, FALLING);
// 初始化按键情况
printf("红色按键情况:%d\n", digitalRead(KEY1_PIN));
printf("绿色按键情况:%d\n", digitalRead(KEY2_PIN));
printf("蓝色按键情况:%d\n", digitalRead(KEY3_PIN));
printf("黄色按键情况:%d\n", digitalRead(KEY4_PIN));
uint32_t events = 0;
while(1){
// 接收任务通知前,先清空所有位,接收后也清空所有位
if (xTaskNotifyWait(UINT32_MAX, UINT32_MAX, &events, portMAX_DELAY) == pdTRUE){
if(events>0){
// 当收到任务通知后,等待10ms,如果该按键在查看电平状态,则表示真的触发了,真机中可以调整这个值达到去抖目的
delay(50);
if((events & RISING_EVENT) > 0){
// 上升沿按键可能被触发
if(digitalRead(KEY1_PIN)==HIGH){
printf("您按下了红色按键!\n");
}
}else if((events & FALLING_EVENT) > 0){
if(digitalRead(KEY2_PIN)==LOW){
printf("您按下了绿色按键!\n");
}
}else if((events & CHANGE_EVENT) > 0){
printf("您按下了蓝色按键!\n");
}else if((events & LOW_EVENT) > 0){
if(digitalRead(KEY4_PIN)==LOW){
printf("黄色按键被按下!\n");
}
}else if((events & CLEAR_EVENT) > 0){
if(digitalRead(KEY5_PIN)==LOW){
printf("终结所有按键中断!\n");
//卸载中断
detachInterrupt(KEY1_PIN);
detachInterrupt(KEY2_PIN);
detachInterrupt(KEY3_PIN);
detachInterrupt(KEY4_PIN);
detachInterrupt(KEY5_PIN);
}
}
}
xTaskNotifyStateClear(NULL); // 清空所有状态
}
events = 0;
delay(1);
}
vTaskDelete(NULL);
}
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Serial.println("Hello, ESP32-S3!");
xTaskCreate(key_driver_entry, "KD", 10240, NULL, 1, &xKeyHandler);
vTaskDelete(NULL);
}
void loop() {
}
代码中一共定义了5个中断服务函数,分别对用了四种中断触发类型(还有一个是KEY5,下降沿触发,用于删除所有中断的)。
五个中断服务函数中,都是使用 xTaskNotifyFromISR 发送任务通知,这个函数是 xTaskNotify 在中断中的用法,后缀是 FromISR,并且比 xTaskNotify 多了最后一个 pxHigherPriorityTaskWoken 参数,这个参数来指定是否唤醒更高优先级的任务。例如,如果需要在中断服务函数中唤醒一个阻塞在等待信号量的任务,可以将 xHigherPriorityTaskWoken 参数设置为 pdTRUE,并在中断服务函数结束时调用 portYIELD_FROM_ISR() 函数来切换任务上下文,这里我们不做任何处理,传入一个 NULL 即可。
在 key_driver_entry 中,开始对引脚进行初始化,因为触发中断的方式不同,所以我们初始化引脚的上下拉电阻方式也不同原则如下:
上升沿触发,使用下拉电阻方式初始化引脚(INPUT_PULLDOWN),确保初始化为低电平,另一个引脚接VCC
下降沿触发,使用上拉电阻方式初始化引脚(INPUT_PULLUP),确保初始化为高电平,另一个引脚接GND
跳变沿触发,使用哪种方式都行,但必须是INPUT方式,如果使用上拉初始化,另一个引脚接GND,如果使用下拉初始化,另一个引脚接VCC
低电平触发,使用上拉电阻方式初始化引脚(INPUT_PULLUP),确保初始化为高电平,另一个引脚接GND。
通过初始化后的电平状态输出,我们可以看到上下拉的作用。
大循环中,使用 xTaskNotifyWait 等待通知事件的到达,在接收前后都进行数据位的清空,处理后再次把所有状态位清空。
在之前的例程中,以及实验中得知,物理按键有抖动的情况,为了消除这些抖动,因为在按键的过程中,引脚电平可能会出现多次的高低翻转,也就意味着可能出现多次中断,这是我们不希望看到的,所以我们采用很多种方式协助去抖动:
这段程序有个BUG,LOW 方式的中断始终无法触发,不知道是模拟器问题还是程序问题,大家可以真机测试一下看。
程序运行后,红色按键和绿色按键每次按下的会后都会触发一次,松开不触发,而蓝色按键不管是按下还是松开都会被触发,因为他是跳变沿触发,只要电平状态改变就会触发。
用 delay 延时检测电平状态的形式是比常用的,也是比较偷懒和影响效率的方式,在不惜成本的情况下尽量还是用硬件去抖,如果非得用软件去抖,大家还可以使用定时器辅助完成。
代码共享位置:https://wokwi.com/projects/364697074528877569
#define KEY1_PIN 4 // 红色:上升沿触发
#define RISING_EVENT 1
TaskHandle_t xKeyHandler = NULL; // 按键驱动任务句柄
TimerHandle_t xKeyTimer = NULL; // 去抖定时器
void key_timer(TimerHandle_t xTimer){
if(digitalRead(KEY1_PIN)==HIGH){
xTaskNotify(xKeyHandler, RISING_EVENT, eSetBits); // 发送通知
}
}
// 上升沿触发中断服务函数
void IRAM_ATTR ISR_RISING(){
xTimerStartFromISR(xKeyTimer, NULL); // 启动定时器,可能多次启动
}
// 键盘驱动线程
void key_driver_entry(void *params){
// 初始化按键去抖定时器
xKeyTimer = xTimerCreate("KEY1_TIMER", 50, pdFALSE, NULL, key_timer);
pinMode(KEY1_PIN, INPUT_PULLDOWN);
// 安装中断
attachInterrupt(KEY1_PIN, ISR_RISING, RISING);
uint32_t events = 0;
while(1){
// 接收任务通知前,先清空所有位,接收后也清空所有位
if (xTaskNotifyWait(UINT32_MAX, UINT32_MAX, &events, portMAX_DELAY) == pdTRUE){
if((events & RISING_EVENT) > 0){
printf("按键被触发了!\n");
}
xTaskNotifyStateClear(NULL); // 清空所有状态
}
events = 0;
delay(1);
}
vTaskDelete(NULL);
}
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Serial.println("Hello, ESP32-S3!");
xTaskCreate(key_driver_entry, "KD", 10240, NULL, 1, &xKeyHandler);
vTaskDelete(NULL);
}
void loop() {
}
代码 key_event_task_entry 首先初始化了一个按键的定时器,这个定时器在 Start 后50ms执行一次(这个就是去抖时间,根据实际硬件情况修改),函数中判断如果该引脚仍然是高电平,则说明确实按下了键,而不是抖动,这时候向任务发送一个通知,表示按键被按下了。而 xTimerStartFromISR 是可以重复调用的,如果第二次调用在,则会reset之后重新计时,不会重复启动多个定时器,这正好符合我们的要求。
注意: 在实际开发中,应该还对按键的抬起进行去抖动,尤其是跳变沿触发方式上,上面两段代码仅仅是在模拟器上运行,问题不大,但真机测试的时候可能会存在抬起扰动的风险。
抬起抖动可能不会超过 50ms ,所以如果抬起发生扰动的时候,第二次判断按键状态的时候一定是低电平(如果去抖时间短就不好说了),比较保守的做法是在按键抬起后,第一次出现低电平在进行一个定时器的判断进行去抖。