Skip to content

Arduino - 中断

在微控制器编程中,中断是一种信号,它会暂时暂停处理器当前的任务,以响应一个重要事件并执行一段特殊的代码。一旦特殊代码执行完毕,处理器会准确地从中断前的位置恢复其原始任务。

想象一下您正在读一本书(您的主 loop() 函数),然后门铃响了(中断事件)。您放上书签,去开门(中断服务程序),然后回到您的书,从书签所在的位置继续阅读。如果没有门铃,您就不得不每隔几秒钟停下来检查门,这效率很低。中断通过让事件通知您来解决这个问题。

中断的主要优势是响应性。微控制器可以执行长时间任务(例如管理显示屏或计算数据),同时仍能立即响应关键事件,例如:

  • 用户按下按钮。
  • 传感器数据到达。
  • 旋转编码器的信号。
  • 定时器达到特定值。

如果没有中断,您将不得不在主循环中不断轮询(检查)这些输入的状态,这可能会很慢,并且如果循环繁忙,则可能导致事件丢失。

本教程重点介绍外部中断,它由特定数字引脚上的电压变化触发。大多数 Arduino 开发板只有数量有限的专用中断引脚。

开发板中断引脚
Arduino UNO, Nano, Mini2, 3
Arduino Mega 25602, 3, 18, 19, 20, 21
Arduino MKR 和 Zero 系列所有数字引脚都可使用。
树莓派 Pico (RP2040)所有 GPIO 引脚都可使用。

您可以使用 attachInterrupt() 函数配置中断。现代且推荐的语法是:

attachInterrupt(digitalPinToInterrupt(pin), ISR, mode);
  • digitalPinToInterrupt(pin):一个有用的函数,它将开发板的数字引脚号(例如 2)转换为其特定的内部中断号。使用此函数可使您的代码在不同的 Arduino 开发板之间具有可移植性。

  • ISR:特殊函数——中断服务程序——的名称,当中断被触发时将调用此函数。此函数不得接受任何参数,也不得返回任何值(void myISR())。

  • mode:定义何种信号变化触发中断:

  • LOW:当引脚保持低电平时触发。(谨慎使用,因为它可能重复触发)。

  • CHANGE:当引脚值改变时触发(从低电平到高电平或从高电平到低电平)。

  • RISING:当引脚值从低电平变为高电平时触发。

  • FALLING:当引脚值从高电平变为低电平时触发。这对于带上拉电阻的按钮按下非常常见。

ISR 是特殊的,并有严格的规则:

  • 保持它们简短快速。 ISR 应该只做所需的最少工作,例如设置一个标志或更新一个变量,然后退出。
  • 将共享变量声明为 volatile。 如果一个变量在 ISR 和主循环中都被使用,您必须使用 volatile 关键字声明它(例如,volatile bool eventOccurred = false;)。这会告诉编译器变量的值可以随时改变,从而防止可能导致错误的优化。
  • 避免使用 delay() 和 Serial 函数。 需要定时器(如 delay())或其他中断(如 Serial.print())的函数在 ISR 内部表现不可靠,应避免使用。请在主循环中进行打印和延迟操作。

此草图演示了中断如何立即响应按钮按下以切换 LED,即使主循环因长时间延迟而“繁忙”时也能做到。

/*
中断示例:响应式 LED 切换
此草图演示了中断如何提供即时响应性。
即使主循环被长时间延迟阻塞,按下引脚 2 上的按钮也会立即切换内置 LED。
电路:
- 一个按钮连接在引脚 2 和 GND 之间。
*/
// 引脚定义
const int interruptPin = 2;
const int ledPin = LED_BUILTIN;
// 此变量在 ISR 和主循环之间共享,因此必须是 volatile。
volatile bool ledState = LOW;
void setup() {
// 将 LED 引脚设置为输出
pinMode(ledPin, OUTPUT);
// 将中断引脚设置为带上拉电阻的输入模式
pinMode(interruptPin, INPUT_PULLUP);
// 附加中断。
// digitalPinToInterrupt() 使代码可移植。
// 我们在下降沿(当按钮按下时)触发。
attachInterrupt(digitalPinToInterrupt(interruptPin), toggleLed, FALLING);
Serial.begin(9600);
Serial.println("系统准备就绪。请按下按钮。");
}
void loop() {
// 主循环可以执行其他任务。
// 这里我们模拟一个长时间的阻塞任务。
Serial.println("主循环正在忙碌...");
delay(5000);
}
// --- 中断服务程序 (ISR) ---
// 当按钮按下时,此函数会自动被调用。
// 它必须简短、快速,并且是无参数的 void 返回类型。
void toggleLed() {
// ISR 应该只做最少的工作。这里,我们只是翻转状态。
ledState = !ledState;
digitalWrite(ledPin, ledState);
}
  • 忘记 volatile:如果您的共享变量没有正确更新,您很可能忘记将其声明为 volatile。
  • 过长的 ISR:如果您的程序在中断触发时冻结或行为异常,您的 ISR 可能做了太多工作。将 Serial 调用、delay() 或复杂计算移到主 loop() 中,并且仅使用 ISR 来设置一个标志。
  • 按钮抖动:机械按钮在按下时可能会发生物理“抖动”,在几微秒内产生多个高/低电平转换。这可能导致您的 ISR 在一次按压中多次触发。这称为“抖动”。解决方案包括在电路中添加一个小电容(硬件去抖)或添加逻辑来忽略快速触发(软件去抖)。