什么是中断?
中断是现代计算机系统中一个非常核心的概念,它解决了传统轮询方式的诸多弊端,使得系统能够更高效、更及时地响应各种事件。
为什么要有中断?
想象一下你正在厨房里做饭。传统轮询(Polling)的方式就像你每隔几秒钟就去看一眼烤箱里的蛋糕好了没,或者水烧开了没。这个过程非常耗费精力,而且你还得不断地重复这个动作,无法去做其他事情。
而中断(Interrupts)则像是你给烤箱和水壶设置了定时器,或者安装了报警器。当蛋糕烤好或者水烧开时,它们会发出响声,主动通知你。这时,你就可以立即放下手头的事情,去处理这个紧急的状况。处理完后,你再回来继续做之前的事情。
在计算机世界中,中断的存在有以下几个主要原因:
- 提高CPU效率: 没有中断,CPU需要不断地检查外部设备或内部状态(比如,串口有没有数据、按钮有没有按下、定时器时间到没到)。这种持续的检查叫做“轮询”。轮询会浪费大量的CPU时间,因为在绝大多数情况下,设备并没有准备好,CPU只是在空转等待。中断机制允许CPU在没有事件发生时执行其他有用的任务,只有当事件发生时才被“打断”去处理。
- 实时响应: 某些事件需要立即处理,比如紧急停止按钮、传感器阈值报警等。轮询可能会导致响应延迟,因为CPU可能在检查其他地方,直到下一次轮询到这个事件才能发现。中断可以确保系统对关键事件做出即时响应。
- 并发性: 中断使得CPU能够在处理一个任务的同时,也能对突发事件做出反应。比如,Arduino正在执行一个复杂的计算,但同时检测到按钮被按下,中断机制就能让它暂停计算,处理按钮事件,然后回到计算中。这模拟了一种并发执行的能力。
- 简化编程: 没有中断,开发者需要编写复杂的轮询逻辑来管理多个设备和事件,代码会变得非常复杂且难以维护。中断将事件处理逻辑封装到中断服务程序 (ISR) 中,使代码结构更清晰。
中断的使用场景
中断在嵌入式系统(如 Arduino)和更复杂的计算机系统中都有广泛的应用。以下是一些常见的Arduino中断使用场景:
- 外部事件响应:
- 按钮输入: 检测用户按下按钮,如开关LED、改变模式、启动/停止某个功能等。这是最常见的外部中断使用场景,比轮询更加灵敏和可靠。
- 传感器触发: 当传感器(如红外传感器、超声波传感器、门磁传感器)检测到特定事件(如有人经过、障碍物靠近、门被打开)时,立即触发动作。
- 编码器读取: 准确检测旋转编码器的脉冲,用于测量角度、速度或位置。
- 通信协议: 在一些低速或自定义通信协议中,可以用中断来检测数据线的边沿变化,从而接收数据。
- 精确时间控制:
- 定时任务: 以精确的时间间隔定期执行某个任务,如数据采样、PID控制循环、发送数据包等。这通常通过定时器中断实现。
- 生成PWM信号: 虽然Arduino的analogWrite()函数已经集成了PWM,但通过定时器中断可以生成更灵活、更高精度的PWM信号。
- 频率测量: 通过中断来测量外部信号的频率或周期。
- 节能:在一些低功耗应用中,微控制器可以在大部分时间进入睡眠模式。只有当外部事件发生时(如按下按钮或接收到数据),中断才能唤醒微控制器进行处理,从而大大降低功耗。
- 避免阻塞:当程序中有需要等待外部事件发生(如等待用户输入)才能继续执行的部分时,使用中断可以避免程序在等待过程中“卡住”(阻塞)。主程序可以继续执行其他任务,直到中断通知事件发生。
总之,中断是嵌入式编程中不可或缺的工具,它让你的Arduino项目能够更智能、更高效地与外部世界交互,并实现复杂的时序控制。
Arduino 的中断
Arduino 中断是一种强大的机制,可以让你编写的程序更高效地响应事件,而不是持续地循环检测。这对于处理时间敏感的任务和优化处理器利用率非常有益。我们将一起学习两种主要的中断类型:外部中断和定时器中断。
外部中断
外部中断允许你的 Arduino 在特定的外部事件发生时(例如按钮按下、传感器触发等)立即暂停当前正在执行的代码,并跳转到一个专门的函数(称为中断服务程序,或 ISR)来处理这个事件。这意味着你不再需要通过 loop() 函数不断地检查某个引脚的状态,从而节省了处理器资源并提高了响应速度。
外部中断的工作原理
- 中断引脚: Arduino 板子上有特定的引脚被配置为外部中断引脚。当这些引脚上的电平发生变化时,会触发中断。
- 中断触发模式: 你可以设置中断在何时触发:
- LOW: 当引脚为低电平时触发。
- CHANGE: 当引脚电平发生变化时(从高到低或从低到高)触发。
- RISING: 当引脚电平从低到高跳变时触发。
- FALLING: 当引脚电平从高到低跳变时触发。
- 中断服务程序 (ISR): 当中断被触发时,处理器会立即跳转到你指定的 ISR 函数。在 ISR 中,你应该执行尽可能少且快速的代码,因为在 ISR 执行期间,其他中断默认是禁用的,并且 delay() 函数和一些库函数可能无法正常工作。
如何使用外部中断
使用外部中断主要涉及两个函数:
- attachInterrupt(digitalPinToInterrupt(pin), ISR, mode): 这个函数用于将一个特定的引脚与一个 ISR 函数关联起来,并指定触发模式。
- digitalPinToInterrupt(pin): 将数字引脚号转换为中断号。
- ISR: 你要执行的中断服务函数的名称。
- mode: 中断触发模式(LOW, CHANGE, RISING, FALLING)。
- detachInterrupt(digitalPinToInterrupt(pin)): 这个函数用于禁用特定引脚上的中断。
示例:按钮控制 LED
让我们通过一个例子来学习如何使用外部中断。我们将用一个按钮来控制一个 LED 的开关状态。当按钮被按下时,LED 的状态会反转。
const int buttonPin = 2; // 按钮连接到数字引脚 2 const int ledPin = 13; // LED 连接到数字引脚 13 (内置 LED) volatile bool ledState = false; // 使用 volatile 关键字,因为这个变量会在中断中修改 void setup() { pinMode(ledPin, OUTPUT); pinMode(buttonPin, INPUT_PULLUP); // 使用内部上拉电阻 // 将引脚 2 (中断 0) 附加到 buttonISR 函数,在按钮按下 (FALLING 模式) 时触发 attachInterrupt(digitalPinToInterrupt(buttonPin), buttonISR, FALLING); Serial.begin(9600); Serial.println("外部中断示例:按钮控制 LED"); } void loop() { // 在 loop 中执行其他任务 // 中断会在后台自动处理按钮事件 digitalWrite(ledPin, ledState); } // 中断服务程序 (ISR) void buttonISR() { // 简单地反转 LED 状态 ledState = !ledState; Serial.print("按钮被按下!LED 状态:"); Serial.println(ledState ? "ON" : "OFF"); }
代码解释:
- buttonPin 连接到数字引脚 2,因为大多数 Arduino 板(如 Uno)将数字引脚 2 和 3 分配为外部中断引脚(中断 0 和 1)。
- INPUT_PULLUP 将按钮引脚配置为输入并启用内部上拉电阻,这样当按钮未按下时引脚为高电平,按下时为低电平。
- volatile bool ledState = false;:volatile 关键字告诉编译器 ledState 变量可能会在程序正常流程之外被修改(即在 ISR 中),因此编译器不会对其进行不必要的优化,确保每次访问都从内存中读取最新值。
- attachInterrupt(digitalPinToInterrupt(buttonPin), buttonISR, FALLING);:当 buttonPin 检测到从高电平到低电平的下降沿时(即按钮被按下),buttonISR 函数将被调用。
- buttonISR() 函数只执行一个简单的任务:反转 ledState 的值。
定时器中断
定时器中断允许你以精确的间隔定期执行一个任务,而无需在 loop() 函数中使用 delay() 或 millis() 进行复杂的计时。这对于需要周期性采样数据、生成 PWM 信号或执行后台任务非常有用。
定时器中断的工作原理
Arduino 内部有多个定时器/计数器 (Timer/Counter) 硬件模块。这些定时器可以配置为以特定的频率计数,当计数达到预设值时,它们可以触发一个中断。
- 定时器配置:你可以配置定时器的工作模式(例如,正常模式、CTC 模式等)、预分频器(分频时钟以降低计数速度)和比较值。
- 比较匹配中断:当定时器计数到预设的比较值时,会触发一个中断。
- 溢出中断:当定时器计数到最大值并溢出时,会触发一个中断。
如何使用定时器中断
直接操作 AVR 单片机的寄存器来配置定时器中断会比较复杂。幸运的是,有一些库可以简化这个过程,例如 TimerOne 或 MsTimer2。这里我们以 TimerOne 库为例,因为它提供了相对直观的接口来配置定时器1(通常是 16 位定时器,可以实现更长的定时)。
首先,你需要安装 TimerOne 库。在 Arduino IDE 中,通过 Sketch -> Include Library -> Manage Libraries… 搜索并安装 “TimerOne”。
示例:LED 闪烁
我们将使用 TimerOne 库让一个 LED 每秒闪烁一次,而不是使用 delay()。
#include <TimerOne.h> // 包含 TimerOne 库 const int ledPin = 13; // LED 连接到数字引脚 13 void setup() { pinMode(ledPin, OUTPUT); // 初始化 TimerOne,设置中断周期为 1,000,000 微秒 (1 秒) Timer1.initialize(1000000); // 将 timerIsr 函数附加到 Timer1 的中断 Timer1.attachInterrupt(timerIsr); Serial.begin(9600); Serial.println("定时器中断示例:LED 闪烁"); } void loop() { // 在 loop 中执行其他任务 // LED 的闪烁由定时器中断在后台处理 } // 定时器中断服务程序 (ISR) void timerIsr() { // 反转 LED 状态 digitalWrite(ledPin, !digitalRead(ledPin)); }
代码解释:
- #include <TimerOne.h>: 包含了 TimerOne 库。
- initialize(1000000);: 初始化 Timer1,并设置中断周期为 1,000,000 微秒(即 1 秒)。
- attachInterrupt(timerIsr);: 将 timerIsr 函数设置为 Timer1 的中断服务程序。每当定时器计数达到预设的周期时,timerIsr 就会被调用。
- timerIsr() 函数简单地反转了 LED 的状态。
中断使用的注意事项
在使用中断时,有一些重要的注意事项需要牢记:
- ISR 应该尽可能短和快: 在 ISR 中执行的代码应该尽可能少。长时间的 ISR 会阻塞其他中断的执行,并可能导致数据丢失或系统不稳定。
- 避免在 ISR 中使用 delay(): delay() 函数依赖于定时器,而在 ISR 中定时器可能会被暂停或重新配置,导致 delay() 无法正常工作。
- 避免在 ISR 中使用串行通信: 串行通信函数(如print())也可能很慢,不适合在 ISR 中使用。如果你需要在 ISR 中调试,可以考虑通过改变引脚状态来指示事件发生。
- 使用 volatile 关键字: 任何在 ISR 中修改并在 loop() 或其他函数中使用的变量,都应该用 volatile 关键字声明,以确保编译器不会对其进行不当优化。
- 临界区: 如果 ISR 和主程序都访问同一个变量,并且该变量是多字节的(如 long 或 float),那么在主程序访问该变量时,应该暂时禁用中断 (noInterrupts()),并在访问完成后重新启用中断 (interrupts()),以避免数据损坏。
volatile long counter = 0; void setup() { Serial.begin(9600); // ... 其他设置 attachInterrupt(digitalPinToInterrupt(2), myISR, RISING); } void loop() { // 禁用中断,读取 counter noInterrupts(); long currentCounter = counter; interrupts(); // 重新启用中断 Serial.println(currentCounter); delay(1000); } void myISR() { counter++; }