中断、MsTimer2、Ticker、多任务功能详解
Arduino中断
CPU执行时原本是按程序指令一条一条向下顺序执行的。 但如果此时发生了某一事件B请求CPU迅速去处理(中断发生),CPU暂时中断当前的工作,转去处理事件B(中断响应和中断服务). 待CPU将事件B处理完毕后, 再回到原来被中断的地方继续执行程序(中断返回),这一过程称为中断 。
Arduino有两种形式的中断:
● 外部输入
● 引脚状态变化
1.外部中断(INT0、INT1)
1. 本质与特点
- 硬件专属中断引脚:ATmega168/328 芯片仅支持 2 个外部中断通道,分别命名为 INT0 和 INT1。
- 对应物理引脚:这两个中断通道固定映射到 Arduino 开发板的引脚 2(INT0)和引脚 3(INT1),不可更改。
- 触发方式:通过
attachInterrupt()
函数配置,可选择以下触发模式:LOW
(低电平)CHANGE
(电平变化)RISING
(上升沿)FALLING
(下降沿)HIGH
:高电平触发(仅适用于 Arduino Due)。
2. 适用场景
- 需要对特定引脚的状态变化进行高优先级响应(如紧急按钮、高速脉冲计数)。
- 优势:中断响应速度极快,因为硬件直接支持。
2.引脚变化中断(Pin Change Interrupts)
1. 本质与特点
- 全局中断机制:ATmega168/328 芯片支持3 组引脚变化中断(PCINT0、PCINT1、PCINT2),每组对应多个物理引脚:
- PCINT0 组:引脚 0~7(对应 Arduino 引脚 D0~D7)。
- PCINT1 组:引脚 8~13(对应 Arduino 引脚 D8~D13)。
- PCINT2 组:引脚 14~20(仅适用于 ATmega328P,对应 Arduino 引脚 A0~A5 和未定义引脚)。
- 关键特性:
- 任何引脚均可触发:只要属于某一组 PCINT,该组内的所有引脚状态变化都会触发中断。
- 无法指定单个引脚:只能按组配置中断,无法单独监听某个引脚(需在中断服务程序中自行判断具体是哪根引脚变化)。
2. 触发方式
- 仅支持电平变化触发:只要引脚电平发生变化(上升沿或下降沿),该组中断就会被触发。
- 不支持高 / 低电平持续触发:无法像外部中断一样配置为
LOW
或HIGH
模式。
3. 如何使用?
-
需手动配置寄存器:Arduino 核心库未直接提供
attachPinChangeInterrupt()
之类的函数,需通过底层寄存器操作实现。-
示例代码(监听引脚 4 的电平变化):
volatile bool pin4Changed = false;void setup() {pinMode(4, INPUT_PULLUP); // 假设使用上拉电阻// 使能 PCINT0 组中断(引脚 0~7 属于 PCINT0)PCMSK0 |= (1 << PCINT4); // 使能引脚 4 的中断掩码PCICR |= (1 << PCIE0); // 使能 PCINT0 组的中断sei(); // 开启全局中断 }// 引脚变化中断服务程序(需通过 ISR 关键字声明组别) ISR(PCINT0_vect) {if (digitalRead(4) == LOW) { // 假设下降沿触发pin4Changed = true;} }void loop() {if (pin4Changed) {// 处理引脚 4 的变化事件pin4Changed = false;} }
-
总结:
1.两种中断的核心区别
特性 | 外部中断(INT0/INT1) | 引脚变化中断(PCINT) |
---|---|---|
可配置引脚 | 仅引脚 2(INT0)、3(INT1) | 任意引脚(按组划分) |
触发模式 | 支持 LOW/CHANGE/RISING/FALLING | 仅支持 CHANGE(电平变化) |
响应速度 | 更快(硬件直接处理) | 稍慢(需软件判断具体引脚) |
Arduino 库支持 | 直接通过 attachInterrupt() | 需手动操作寄存器 |
适用场景 | 单个引脚的高速响应 | 多引脚监听(如键盘矩阵、传感器阵列) |
2.为什么需要引脚变化中断?
当需要监控多个引脚的状态变化(如同时检测多个按钮),但又不想占用稀缺的外部中断引脚时,引脚变化中断非常有用。例如:
- 键盘矩阵的行 / 列引脚监控。
- 多个传感器的状态变化检测(如红外对管阵列)。
注意:由于引脚变化中断是按组触发的,每次中断触发时需在服务程序中遍历组内所有引脚,判断具体是哪个引脚发生了变化,这会增加软件开销。因此,外部中断更适合对实时性要求极高的单个引脚场景,而引脚变化中断适用于多引脚但实时性要求稍低的场景。
外部中断 attachInterrupt()
外部中断是外部干扰出现时发生的系统中断。干扰可能来自用户或网络中的其他硬件设备。
ATmega168 / 328上有两个外部中断引脚,称为INT0和INT1。 INT0和INT1分别映射到引脚2和3。
核心函数
attachInterrupt()
void attachInterrupt(digitalPinToInterrupt(GPIO), ISR, mode);
void attachInterrupt(0, ISR, mode);//UNO 0 => GPIO2
参数:
ISR 对应函数
[!IMPORTANT]
ESP8266 在处理中断时,由于闪存读取的延迟问题,可能会让中断响应变得不稳定。为了保证中断能够及时响应,就需要把中断函数放到 RAM 里执行,此时就需要使用ICACHE_RAM_ATTR前缀。
普通 Arduino 板(如 Uno、Mega 等)普通的 Arduino 板(基于 AVR 架构)并不需要ICACHE_RAM_ATTR前缀。在这些平台上,中断函数的定义较为简单,像之前给出的代码示例,直接定义中断服务函数就行
//中断处理函数,ESP8266 这类特定芯片平台
ICACHE_RAM_ATTR void dataReadyISR() {if (LoadCell.update()) {newDataReady = 1;}
}//普通 Arduino 板
void dataReadyISR() {if (LoadCell.update()) {newDataReady = 1;}
}
-
中断服务程序的函数名,必须是 无参数、无返回值 的函数(如
void handleInterrupt()
)。 -
通常ISR需要越短小精悍越好!另外如果您的代码中有多个ISR函数,那么每次Arduino只能运行一个ISR函数,其它ISR函数只有在当前的ISR函数执行结束以后,才能按照其优先级别顺序执行。
-
ISR函数中
delay()
、millis()
、Serial.print()
等依赖硬件定时器的函数无法正常运行。 delayMicroseconds() 不需要任何计数器就可以运行(delayMicroseconds()
会直接根据 CPU 的时钟周期来进行计数,通过执行一定数量的空指令来达到指定的延迟时间), 运行是不会受到影响。 -
一般情况下,ISR函数与主程序之间传递数据是依靠全局变量来实现的。为了确保全局变量在ISR函数中可以正常的工作,应该将可能被ISR函数中使用的全局变量声明为volatile类型。
volatile – 易变变量
volatile这个关键字是变量修饰符,常用在变量类型的前面,该指令指示编译器从RAM而非存储寄存器(程序存储和操作变量的内存区域)中读取变量。ISR(中断服务程序)中所涉及的变量需要被声明为volatile易变变量。
mode触发方式
LOW
:低电平触发。CHANGE
:电平变化(上升或下降沿)触发。RISING
:上升沿(从低到高)触发。FALLING
:下降沿(从高到低)触发。HIGH
:高电平触发(仅适用于 Arduino Due)。
[!IMPORTANT]
长时间运行(> 1ms)的中断任务将导致程序不稳定或崩溃。如果中断被长时间运行的中断阻塞,WiFi和核心的其他部分可能会变得不稳定。如果有很多事情要做,可以设置一个全局易变变量,主循环
loop()
中检查来进行变量的值,进而运行需要长时间工作的程序。内存操作是危险的,应该在中断中避免。尽量减少对new或malloc的调用。因为如果内存被分割,可能需要很长的运行时间。出于同样的原因,realloc和free绝对不能被调用,也不能使用任何调用free或realloc本身的例程或对象。这意味着使用String、std:string 、std::vector和其他可调整大小的连续内存的类时必须非常小心(确保不改变字符串,不添加vector元素,等等)。
detachInterrupt()
- 功能:禁用指定中断。
- 参数:中断号(如
digitalPinToInterrupt(pin)
的返回值)。
interrupts()
、noInterrupts()
interrupts()
:启用全局中断。noInterrupts()
:禁用全局中断(慎用,可能影响系统功能)。