很多时候我们都会使用到微控制器,基于其具备的特点
- 微控制器可以处理多个输入和输出。
- 微控制器可以提供精确的定时脉冲。
- 微控制器速度快。
正因为微控制器的特点,能处理多个输入,能做很多事情,因此微控制器将很忙碌。而忙碌的微控制器需要一种方式来管理外部事件,比如按钮按下,同时兼顾其他输入和输出定时处理。
中断是Arduino和其他微控制器的一个非常重要的基本特性,是能够保持对外部输入或内部计时事件的控制的一种方法。
中断是如何工作的
中断顾名思义,就是中断当前程序执行以便处理其他事情的方法。中断不是微控制器所独有的,它们已经在计算机和控制器中使用了几十年。当在键盘上输入、移动鼠标或在触摸屏上滑动时,都会触发中断和中断服务程序,从而对操作产生适当的响应。
中断的工作流程
- 一个程序正在运行
- 产生一个中断
- 程序被暂停同时其相应数据被存放在一边,以便稍后恢复。
- 运行与中断相关的代码。
- 当中断代码完成时,程序从它暂停的地方恢复。
中断对于监测间歇性发生的开关按下或警报触发等事件非常有用。当需要精确测量输入脉冲时,它们也是合适的选择。
微控制器和微处理器使用的中断有很多种,不同模型的中断特性各不相同。它们可以大致分为两类:
- 硬件中断——这些中断通常来自外部信号。
- 软件中断——这些是内部信号,通常由计时器或软件相关事件控制。
Arduino Uno 中断
Arduino程序执行流程
Arduino Uno支持三种中断类型:
- 硬件中断——特定引脚上的外部中断信号。
- 引脚改变中断(PCI,Pin Change Interrupts)——在任何引脚上的外部中断,以端口分组。
- 定时器中断——内部定时器产生的中断,在软件中操作。
当中断产生时,需要一个中断服务程序(isr,Interrupt Service Routine)对其进行处理,并且ISR只在中断发生时运行。
中断服务程序(ISR)本质上是一个函数。但与常规Arduino函数不同:
- 不能向ISR传递参数
- 不能从ISR获得任何返回值
- 应当尽可能的快
- 不能使用delay()函数
- 不能使用millis()函数
- 不能使用Serial库,因此不能打印到串行监视器
- 只使用全局变量,全局变量应该声明为volatile类型
为何需要使用中断
下面将通过2个按钮控制LED亮灭的示例来展现为何需要使用中断。
示例1:通过按钮控制led亮灭。
代码
// 定义LED和按钮引脚 const byte ledPin = 13; const byte buttonPin = 2; // 定义布尔变量用于记录开关状态 // volatile为类型修饰符,常用于中断ISR全局变量 volatile bool togglestate = false; // 检测按钮状态,并控制LED亮灭 void checkSwitch() { if (digitalRead(buttonPin) == LOW) { delay(200); // 如果按钮按下,则翻转开关状态变量的值 toggleState = !toggleState; // 控制led digitalWrite(ledPin,toggleState); } } void setup() { // 设置LED引脚为输出模式 pinMode(ledPin, OUTPUT); // 设置按钮引脚为上拉输入模式 pinMode(buttonPin, INPUT_PULLUP); } void loop() { // 调用按钮检测函数 checkSwitch(); } |
上传程序,按钮能完美控制led的亮灭。
示例2:在示例1的基础上增加了一个延时,再来看看按钮是否能有效的控制led的亮灭。
代码
const byte ledPin = 13; const byte buttonPin = 2; volatile bool toggleState = false; void checkSwitch() { if (digitalRead(buttonPin) == LOW) { delay(200); toggleState = !toggleState; digitalWrite(ledPin, toggleState); } } void setup() { pinMode(ledPin, OUTPUT); pinMode(buttonPin, INPUT_PULLUP); Serial.begin(9600); } void loop() { checkSwitch(); // 增加一个5秒的延时 Serial.println("延时开始"); delay(5000); Serial.println("延时结束"); Serial.println("………….."); } |
上传程序,可以发现一个问题就是:按钮好像失效了,只能偶然在延时的间隙能幸运触发按钮翻转led的状态。为了解决这个问题,最好的方式是使用中断。
硬件中断
硬件中断是外部中断,在大多数Arduino型号上都限于特定的引脚。这些引脚被配置为输入,可以通过操纵它们的逻辑状态触发硬件中断。
Arduino硬件中断引脚
- Arduino UNO只有2个硬件中断引脚
- Pin2:INT0
- Pin3:INT1
- 不同型号的arduino主板中断引脚和数量不一样
使用硬件中断主要包括两个步骤
- 编写一个中断服务程序ISR(Interrupt Service Routine)
- 将ISR函数附加到特定中断,并指定触发方式。
attachInterrupt()函数
- 通常在setup()中调用attachInterrupt()函数
- 因为中断号不同于引脚号,最简单的方式是使用digitalPinToInterrupt()函数
示例3:使用硬件中断重写示例2项目,验证是否能有效触发按钮事件,控制led亮灭
const byte ledPin = 13; const byte buttonPin = 2; volatile bool toggleState = false; // ISR中相比前面示例移除了200ms延时,因为在ISR中不能使用delay()函数 void checkSwitch() { if (digitalRead(buttonPin) == LOW) { toggleState = !toggleState; digitalWrite(ledPin, toggleState); } } void setup() { pinMode(ledPin, OUTPUT); pinMode(buttonPin, INPUT_PULLUP); // 附件中断处理函数,并指定中断触发模式为FALLING,即按下按钮时触发中断 attachInterrupt(digitalPinToInterrupt(buttonPin),checkSwitch, FALLING); } void loop() { Serial.println("延时开始"); delay(5000); Serial.println("延时结束"); Serial.println("………….."); } |
注意,volatile修饰的布尔变量,它的值是在中断服务例程中被操作的。如果没有volatile,Arduino IDE编译器可能会尝试过度优化代码并删除变量。
结论:通过上述3个示例的对比,可以了解为什么需要使用中断。
PCI中断(Pin Change Interrupts)
引脚变更中断(PCI)是硬件中断的另一种形式。它不局限于特定的引脚,所有的引脚都可以用于引脚变更中断。
PCI中断是以端口形式分组的,同一端口的所有引脚产生相同的引脚变更中断,因此如果同端口下多个引脚都会产生中断,则需要自行在中断服务程序中进行引脚识别,以便响应正确的中断事件和正确的处理方式。
PCI中断的模式为CHANGE,因此对于按钮来说,按下和释放按钮会产生2次PCI中断。
Arduino UNO支持PCI的端口组
如何使用PCI中断
- 确定需要使用PCI中断的引脚(同时即确定了所在端口组port)
- 启用对应端口组port中断。
- 需要使用PCI控制寄存器(PCICR,Pin Change Interrupt Control Register),PCICR寄存器低三位分别对应三个端口组,对应位设置为1即启用响应端口组,可同时启用多个端口组。
- 启用对应引脚pin中断
- 需要使用Pin Change Mask (PMSKx)来选择对应的引脚,有3个PMSK分别对应三个端口组,将对应位置为1即启用对应引脚中断。
- 可以在setup()函数中进行设置
- 编写中断服务程序ISR,如果使用同一端口组的多个引脚,则需要在ISR中进行对应识别。
- 和硬件中断不同,PCI中断的ISR已经定义好名称,需要根据实际情况选择正确的ISR名称。
- ISR中断服务程序规则和硬件中断一样,对于全局变量需要使用volatile修饰。
PCI中断示例
示例1:使用单个PCI引脚中断控制led亮灭。
代码
const byte ledPin = 13; const byte buttonPin = 7; volatile bool togglestate = false; void setup() { pinMode(ledPin, OUTPUT); pinMode(buttonPin, INPUT_PULLUP); // 启用端口组Port D(bit2–PORTD,bit1–PORTC,bit0–PORTB) PCICR |= B00000100; // 启用D7引脚,PCMSK0,PCMSK1,PCMSK2分别对应PORTB,PORTC,PORTD PCMSK2 |= B10000000; } void loop() { // No code in Loop } // PCI中断的ISR程序名称已经预先定义,需要选择正确的名称。 ISR (PCINT2_vect) { togglestate = !togglestate; digitalWrite(ledPin, togglestate); } |
示例2:使用同一端口组的多个PCI引脚中断
代码
// 定义引脚 const byte ledPin1 = 11; const byte ledPin2 = 13; const byte buttonPin1 = 2; const byte buttonPin2 = 7; // 定义状态变量,使用volatile修饰 volatile bool D2_state = LOW; volatile bool D7_state = LOW; void setup() { pinMode(ledPin1, OUTPUT); pinMode(ledPin2, OUTPUT); pinMode(buttonPin1, INPUT_PULLUP); pinMode(buttonPin2, INPUT_PULLUP); // 启用Port D端口组 PCICR |= B00000100; // 启用D2 和 D7引脚 PCMSK2 |= B10000100; } void loop() { // Loop code } // 选择正确的ISR名称 ISR (PCINT2_vect) { // D2 按钮PCI中断处理 if (digitalRead(buttonPin1) == LOW) { //D2 引脚在按钮按下(下降沿)时触发 ISR D2_state = !D2_state; digitalWrite(ledPin1, D2_state); } // D7 按钮PCI中断处理 if (digitalRead(buttonPin2) == LOW) { //D7引脚在按钮按下(下降沿)时触发 ISR D7_state = !D7_state; digitalWrite(ledPin2, D7_state); } } |
注意:如前面所述,因为按钮按下(H–>L)和释放(L–>H)会触发两次PCI中断,因此需要在ISR程序中进行判断。
定时器中断(Timer Interrupts)
定时器中断不使用外部信号,而时由软件中生成的中断,计时是基于Arduino Uno的16 MHz时钟振荡器。如Servo和Tone库,在内部使用了Timer Interrupts,因此在编写代码时应注意避免发生冲突。
Arduino UNO有3个内部定时器,其位数有所不同,位数决定了定时器的最大计数数,8位定时器为256位,16位定时器为65,536位。
计时器中的值以时钟频率或时钟频率的分数为单位递增。可以使用软件来设置中断触发的次数,也可以在计时器溢出时触发中断。
划分时钟频率
定时器由ATmega328内部的16mhz振荡器计时。
时钟一个周期为计时器的一个“tick”,其时间为62.5ns,这是一个非常短的时间,对于许多计时应用程序来说,它太短没有太多的实际用途。
为了降低时钟信号,ATmega328有一个“预分频器”,本质上是一个时钟频率的分配器。预分频器可以将时钟划分为更易于管理的较低频率,从而选择许多常见的时间频率,最长可达64us。
每个计时器有三个时钟选择位,通过对这三个时钟选择位的设置确定Prescaler的值,以及计时源。也可以将所有时钟选择位设置为零停用时钟源。
- Timer0, 8位定时器, 使用 CS01, CS02和CS03.
- Timer1, 16位定时器, 使用CS10, CS11和CS12.
- Timer2, 8位定时器, 使用CS20, CS21和CS22.
使用定时器中断
定时器中断可以在两种不同的模式下操作,
- 比较匹配模式,Compare Match Mode:将一个计数器值放入比较匹配寄存器。当定时器计数器与寄存器中的值相匹配时,产生定时器中断。
- 溢出模式,Overflow Mode:当定时器技术器达到它的最大计数值时,产生一个中断,计数器重置为零并重新开始计数。
- 通过将比较匹配寄存器与预分频器相结合,可以获得在计时器的范围内想要的任何计时周期(8位计时器最多只能除以255)。
定时器相关寄存器
- Timer/Counter control registers (TCCR1A/B) 是8位寄存器。
- 所有中断都用定时器中断掩码寄存器(TIMSK1)单独屏蔽。
定时器中断服务程序ISR
- 每个定时器都有两个isr相关联,一个用于比较匹配模式,另一个用于溢出模式。
示例:配置一个2hz输出的定时器,用其控制LED闪烁(每秒2次)
代码
#define ledPin 13 // 定义变量存储比较寄存器值 int timer1_compare_match; ISR(TIMER1_COMPA_vect) // 使用timer1的比较匹配模式ISR { // 预加载比较匹配寄存器值,该值根据所需的频率,通过上述公式可以计算得出 TCNT1 = timer1_compare_match; // 翻转LED状态,^按位异或,相同为0,相异为1 digitalWrite(ledPin, digitalRead(ledPin) ^ 1); } void setup() { pinMode(ledPin, OUTPUT); // 禁用所有中断,避免在配置定时器中断时产生中断 noInterrupts(); // 初始化Timer1 TCCR1A = 0; TCCR1B = 0; // 期望2hz,如果256的预分频器,则计算得出比较匹配寄存器的值为。 // [16000000/(256*2)]-1=31249 timer1_compare_match = 31249; // 预加载比较匹配寄存器值 TCNT1 = timer1_compare_match; // 设置预分频器为256, TCCR1B |= (1 << CS12); // 启用定时器中断timer1比较匹配模式 TIMSK1 |= (1 << OCIE1A); // 启用所有中断 interrupts(); } void loop() { } |
总结
对于需要精确定时或响应式用户界面的项目,中断是一种很好的方式。