很多时候我们都会使用到微控制器,基于其具备的特点
正因为微控制器的特点,能处理多个输入,能做很多事情,因此微控制器将很忙碌。而忙碌的微控制器需要一种方式来管理外部事件,比如按钮按下,同时兼顾其他输入和输出定时处理。
中断是Arduino和其他微控制器的一个非常重要的基本特性,是能够保持对外部输入或内部计时事件的控制的一种方法。
中断是如何工作的
中断顾名思义,就是中断当前程序执行以便处理其他事情的方法。中断不是微控制器所独有的,它们已经在计算机和控制器中使用了几十年。当在键盘上输入、移动鼠标或在触摸屏上滑动时,都会触发中断和中断服务程序,从而对操作产生适当的响应。
中断的工作流程
中断对于监测间歇性发生的开关按下或警 触发等事件非常有用。当需要精确测量输入脉冲时,它们也是合适的选择。
微控制器和微处理器使用的中断有很多种,不同模型的中断特性各不相同。它们可以大致分为两类:
Arduino Uno 中断
Arduino程序执行流程
Arduino程序执行流程
Arduino Uno支持三种中断类型:
当中断产生时,需要一个中断服务程序(ISR,Interrupt Service Routine)对其进行处理,并且ISR只在中断发生时运行。
中断服务程序
中断服务程序(ISR)本质上是一个函数。但与常规Arduino函数不同:
为何需要使用中断
下面将通过2个按钮控制LED亮灭的示例来展现为何需要使用中断。
示例1:通过按钮控制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主板中断引脚
使用硬件中断主要包括两个步骤
attachInterrupt()函数
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的端口组
Arduino UNO支持PCI的端口组
如何使用PCI中断
PCI控制寄存器
Pin Change Mask
Pin Change Mask设置
PCI中断的ISR名称
PCI中断示例
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位。
计时器中的值以时钟频率或时钟频率的分数为单位递增。可以使用软件来设置中断触发的次数,也可以在计时器溢出时触发中断。
Arduino UNO有3个内部定时器
划分时钟频率
定时器由ATmega328内部的16mhz振荡器计时。
划分时钟频率
时钟一个周期为计时器的一个“tick”,其时间为62.5ns,这是一个非常短的时间,对于许多计时应用程序来说,它太短没有太多的实际用途。
为了降低时钟信 ,ATmega328有一个“预分频器”,本质上是一个时钟频率的分配器。预分频器可以将时钟划分为更易于管理的较低频率,从而选择许多常见的时间频率,最长可达64us。
预分频器
每个计时器有三个时钟选择位,通过对这三个时钟选择位的设置确定Prescaler的值,以及计时源。也可以将所有时钟选择位设置为零停用时钟源。
Timer0, 8位定时器
Timer1, 16位定时器
Timer2, 8位定时器
使用定时器中断
定时器中断可以在两种不同的模式下操作,
计算公式
定时器相关寄存器
定时器中断服务程序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() { } |
总结
对于需要精确定时或响应式用户界面的项目,中断是一种很好的方式。
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!