很多时候我们都会使用到微控制器,基于其具备的特点

  • 微控制器可以处理多个输入和输出。
  • 微控制器可以提供精确的定时脉冲。
  • 微控制器速度快。

正因为微控制器的特点,能处理多个输入,能做很多事情,因此微控制器将很忙碌。而忙碌的微控制器需要一种方式来管理外部事件,比如按钮按下,同时兼顾其他输入和输出定时处理。

中断是Arduino和其他微控制器的一个非常重要的基本特性,是能够保持对外部输入或内部计时事件的控制的一种方法。

中断是如何工作的

中断顾名思义,就是中断当前程序执行以便处理其他事情的方法。中断不是微控制器所独有的,它们已经在计算机和控制器中使用了几十年。当在键盘上输入、移动鼠标或在触摸屏上滑动时,都会触发中断和中断服务程序,从而对操作产生适当的响应。

中断的工作流程

  • 一个程序正在运行
  • 产生一个中断
  • 程序被暂停同时其相应数据被存放在一边,以便稍后恢复。
  • 运行与中断相关的代码。
  • 当中断代码完成时,程序从它暂停的地方恢复。

中断对于监测间歇性发生的开关按下或警报触发等事件非常有用。当需要精确测量输入脉冲时,它们也是合适的选择。

微控制器和微处理器使用的中断有很多种,不同模型的中断特性各不相同。它们可以大致分为两类:

  • 硬件中断——这些中断通常来自外部信号。
  • 软件中断——这些是内部信号,通常由计时器或软件相关事件控制。

Arduino Uno 中断

Arduino程序执行流程

Arduino程序执行流程

Arduino Uno支持三种中断类型:

  • 硬件中断——特定引脚上的外部中断信号。
  • 引脚改变中断(PCI,Pin Change Interrupts)——在任何引脚上的外部中断,以端口分组。
  • 定时器中断——内部定时器产生的中断,在软件中操作。

当中断产生时,需要一个中断服务程序(isrInterrupt Service Routine)对其进行处理,并且ISR只在中断发生时运行。

中断服务程序

中断服务程序(ISR)本质上是一个函数。但与常规Arduino函数不同:

  • 不能向ISR传递参数
  • 不能从ISR获得任何返回值
  • 应当尽可能的快
    • 不能使用delay()函数
    • 不能使用millis()函数
    • 不能使用Serial库,因此不能打印到串行监视器
    • 只使用全局变量,全局变量应该声明为volatile类型

为何需要使用中断

下面将通过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 UNO只有2个硬件中断引脚
    • Pin2:INT0
    • Pin3:INT1
  • 不同型号的arduino主板中断引脚和数量不一样

不同型号的arduino主板中断引脚

使用硬件中断主要包括两个步骤

  • 编写一个中断服务程序ISR(Interrupt Service Routine)
  • 将ISR函数附加到特定中断,并指定触发方式。

attachInterrupt()函数

  • 通常在setup()中调用attachInterrupt()函数

attachInterrupt()函数

  • 因为中断号不同于引脚号,最简单的方式是使用digitalPinToInterrupt()函数

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中断的引脚(同时即确定了所在端口组port)
  • 启用对应端口组port中断。
    • 需要使用PCI控制寄存器(PCICR,Pin Change Interrupt Control Register),PCICR寄存器低三位分别对应三个端口组,对应位设置为1即启用响应端口组,可同时启用多个端口组。

PCI控制寄存器

  • 启用对应引脚pin中断
    • 需要使用Pin Change Mask (PMSKx)来选择对应的引脚,有3个PMSK分别对应三个端口组,将对应位置为1即启用对应引脚中断。

Pin Change Mask

  • 可以在setup()函数中进行设置

Pin Change Mask设置

  • 编写中断服务程序ISR,如果使用同一端口组的多个引脚,则需要在ISR中进行对应识别。
    • 和硬件中断不同,PCI中断的ISR已经定义好名称,需要根据实际情况选择正确的ISR名称。

PCI中断的ISR名称

  • ISR中断服务程序规则和硬件中断一样,对于全局变量需要使用volatile修饰。

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位定时器, 使用 CS01, CS02和CS03.

Timer0, 8位定时器

  • Timer1, 16位定时器, 使用CS10, CS11和CS12.

Timer1, 16位定时器

  • Timer2, 8位定时器, 使用CS20, CS21和CS22.

Timer2, 8位定时器

使用定时器中断

定时器中断可以在两种不同的模式下操作,

  • 比较匹配模式,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()

{

}

总结

对于需要精确定时或响应式用户界面的项目,中断是一种很好的方式。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注