使用 Arduino 中断 – 硬件、引脚变化和定时器
查看原文
今天我们将学习中断,这是Arduino和其他微控制器的一个非常重要的基本功能。虽然我们将专注于Arduino Uno,但这里介绍的概念与其他板同样有效。
介绍
当我们设计一个项目时,我们通常基于微控制器。这样做有很多很好的理由,其中包括:
- 微控制器可以处理多个输入和输出。
- 微控制器可以提供精密定时脉冲。
- 微控制器速度很快。
因为它们可以处理多个输入,并且因为它们可以做很多事情,所以微控制器可能会变得非常繁忙。繁忙的微控制器需要一种方法来管理外部事件,例如按下按钮,同时兼顾其他输入和输出时序过程。
控制外部输入或内部定时事件的一种方法是使用中断。
中断的工作原理
中断顾名思义,是一种中断程序执行以处理其他事情的方法。
中断绝不是微控制器所独有的,它们已经在计算机和控制器中使用了几十年。当您在键盘上键入、移动鼠标或在触摸屏上滑动时,您正在创建中断,这些中断使服务正常工作,从而为您的操作创建适当的响应。
在其基本形式中,中断的工作方式如下:
- 程序正在运行。
- 发生中断。
- 程序将暂停,其数据被放在一边,以便以后可以恢复。
- 运行与中断相关的代码。
- 中断代码完成后,程序将从中断的位置继续。
中断非常适合监发生的事件,例如按下开关或警报触发。当您需要精确测量输入脉冲时,它们也是正确的选择。
微控制器和微处理器使用的中断类型有很多种,中断功能因型号而异。它们都可以大致分为两类:
- 硬件中断 – 这些通常来自外部信号。
- 软件中断 – 这些是内部信号,通常由计时器或软件相关事件控制。
使用中断会让你成为一个更好的编码员,一旦你熟悉了它们,它们就不难使用。今天,我们将看到如何使用Arduino Uno的中断。
Arduino Uno 中断
Arduino Uno支持三种类型的中断:
- 硬件中断 – 特定引脚上的外部中断信号。
- 引脚更改引发中断 – 任何引脚上的外部中断,分组到端口中。
- 定时器中断 – 内部定时器生成的中断,在软件中操作。
我们将在稍后详细讨论这些内容,但现在,我们只是说它们都以基本相同的方式工作。发生中断事件时,微控制器会运行您放置在“中断服务例程”或 ISR 函数中的一些代码。
中断适合的位置
让我们看看所有这些如何融入您的Arduino程序。
我们可以使用一个简单的流程图来可视化Arduino程序,如下所示:
程序首先包括库(如果需要)并定义全局变量和对象。
在设置功能中,我们设置 PinModes,启动对象和设备,并运行要在微控制器启动时执行的任何一次性代码。
然后我们继续循环。在循环中,我们按顺序运行代码正文。一旦我们到达循环的底部,我们就会从顶部重新开始。我们一直处于循环中,直到微控制器重置,此时我们又回到了起点。
现在查看相同的流程图,只是这次使用中断:
程序的执行与原始示例相同,程序在运行“启动”和“设置”过程后仍保留在循环中。但除此之外,我们还有另一个标有“ISR”的框。这是中断服务例程,它将在发生中断事件时运行。
因此,在我们的流程图中,我们可以看到代码执行分支出循环并分支到 ISR。ISR 代码完成后,代码执行将返回到循环中,位于它分支的同一位置。
ISR 仅在发生中断时运行。
也可以有一个只有ISR的程序,并且不使用循环。在这种情况下,在发生中断之前,不会运行任何内容。稍后您将看到几个示例。
中断服务例程
中断服务例程 (ISR) 本质上是一个函数。但是,与常规的Arduino函数不同,您不能将参数传递给它,也不能从中返回任何值。
实际上,ISR 函数有许多限制,其中大多数是由于同一件事——它们需要快速。非常非常快。
想想看,你实际上是在中断一个正在工作的微控制器,而很多工作都涉及计时。您无法长时间中断它,因此在 ISR 中无法执行许多操作:
- 你不能使用delay( ) 函数。
- 不仅如此,您也不能使用millis( ) 函数。
- 没有串行库,因此无法打印到串行监视器。
- 仅使用全局变量,这些变量应声明为volatile。
尽管有这些限制,ISR 可以执行非常有用的工作,例如更改一个或多个全局变量的值,然后可以在循环中读取这些变量。
只要记住尽快完成有用的工作!
为什么要使用中断?
为了说明使用中断的价值,我们将运行一个非常简单的实验,除了Arduino之外,实际上只需要一个按钮开关。我也在使用 LED,但由于它连接到引脚 13,您可以放弃它,而只依靠内置 LED。
在我们的实验中,我们将让按钮充当切换开关,在每次按下按钮时交替打开和关闭 LED。这是一个非常常见的应用程序,我相信你已经看过它,并且可能以前已经为它编码过。
以下是我们将如何连接这一切:
我为我的红色 LED 使用了 220 欧姆的压降电阻,但您可以使用 150 到 470 欧姆之间的任何值,您可以使用您喜欢的任何颜色的 LED。或者只需取消 LED 并使用板载 LED,因为它也连接到引脚 13。
现在这是我们同样简单的程序:
/*
LED Toggle
led-toggle.ino
Use pushbutton switch to toggle LED
DroneBot Workshop 2022
https://dronebotworkshop.com
*/
// Define LED and switch connections
const byte ledPin = 13;
const byte buttonPin = 2;
// Boolean to represent toggle state
volatile bool toggleState = false;
void checkSwitch() {
// Check status of switch
// Toggle LED if button pressed
if (digitalRead(buttonPin) == LOW) {
// Switch was pressed
// Slight delay to debounce
delay(200);
// Change state of toggle
toggleState = !toggleState;
// Indicate state on LED
digitalWrite(ledPin,toggleState);
}
}
void setup() {
// Set LED pin as output
pinMode(ledPin, OUTPUT);
// Set switch pin as INPUT with pullup
pinMode(buttonPin, INPUT_PULLUP);
}
void loop() {
// Check switch
checkSwitch();
}
我们首先为开关和LED引脚声明几个常量字节。
我们还定义了一个名为togglestate 的布尔值,我们将用它来表示切换开关的当前状态。这被初始化为假。
接下来,我们定义一个名为checkSwitch 的函数。这是一个非常简单的功能,可以检查按钮的状态,如果按下按钮,则会反转当前切换值,然后使用它来更改 LED 的状态。
在setup()
中,我们只需将LED引脚设置为输出,将开关引脚设置为输入即可。我们使用内部上拉电阻作为输入。
我们在循环中所做的只是调用checkSwitch函数,因此我们始终查询开关状态。
加载程序并进行测试。每次按下开关时,LED 都应打开或关闭。
修改(即破坏)我们的程序
我们的开关似乎工作得很好,如果您只想拥有一个拨动开关,那么您没有理由需要更进一步。
但是,如果开关是更大设计的一个组件呢?假设一个温度计使用它从摄氏度切换到华氏度。建造起来有多容易?
从表面上看,这似乎很简单,只需将DHT22或类似传感器的代码添加到切换代码中,然后使用切换值来确定温度单位。
但是,在实践中,这可能是一个不同的事!为了说明这一点,让我们对代码进行一个小的修改:
/*
LED Toggle with Delay
led-toggle-delay.ino
Use pushbutton switch to toggle LED
DroneBot Workshop 2022
https://dronebotworkshop.com
*/
// Define LED and switch connections
const byte ledPin = 13;
const byte buttonPin = 2;
// Boolean to represent toggle state
volatile bool toggleState = false;
void checkSwitch() {
// Check status of switch
// Toggle LED if button pressed
if (digitalRead(buttonPin) == LOW) {
// Switch was pressed
// Slight delay to debounce
delay(200);
// Change state of toggle
toggleState = !toggleState;
// Indicate state on LED
digitalWrite(ledPin, toggleState);
}
}
void setup() {
// Set LED pin as output
pinMode(ledPin, OUTPUT);
// Set switch pin as INPUT with pullup
pinMode(buttonPin, INPUT_PULLUP);
// Setup Serial Monitor
Serial.begin(9600);
}
void loop() {
// Check switch
checkSwitch();
// Add a 5-second time delay
Serial.println("Delay Started");
delay(5000);
Serial.println("Delay Finished");
Serial.println("..............");
}
您会注意到修改后的程序中有两个差异:
- 我们设置了串行监视器,并在循环中打印到它。
- 我们在循环底部添加了 5 秒的延迟。
现在,5秒的延迟诚然是一件愚蠢的事情,添加到程序中,但我把它放在那里是为了说明一个观点。它可以很容易地读取DHT22,然后在传感器稳定时延迟2秒。你明白了,我正在添加一个需要一些时间才能完成的过程到我们的循环中。
将程序加载到 Arduino,然后重试切换。我想你会注意到一个不同。
你可能认为你破坏了它,但实际上它只是退化了。查看串行监视器并观察,尝试在延迟事件之间的短暂时间内按下按钮。您可能会很幸运并实际触发切换。
当然,我想说明的一点是,如果你在循环中还有其他任何可能占用超过几毫秒时间的东西,那么轮询循环内的开关并不是从中获取读数的最佳方式。
更好的解决方案是使用硬件中断。
硬件中断
硬件中断是外部中断,在大多数Arduino型号上仅限于特定的引脚。这些引脚配置为输入,可以通过操纵其逻辑状态来触发硬件中断。
这些可能是微控制器实验者最常用的中断类型,我们在DroneBot研讨会上也多次使用它们。
Arduino 硬件中断引脚
在Arduino Uno上,只有两个引脚支持硬件中断:
- 引脚 2 – 中断向量 0
- 引脚 3 – 中断向量 1。
并非所有 Arduino 板都限制为 2 个硬件中断引脚,一些 Arduino 板具有更多。下图显示了几个常见 Arduino 板上的硬件中断数:
使用硬件中断
使用硬件中断实际上非常简单,因为您实际上只需要做两件事:
- 编写一个函数以用作中断服务例程。
- 将函数附加到要使用的特定中断,并指定如何触发它。
ISR 函数应遵守有关速度和使用全局变量的规则,但除此之外,它只是一个函数。它可以具有所需的任何有效名称,但不能具有输入参数。
附加中断功能
要将函数“粘合”到特定中断,您将使用attachInterrupt函数。此函数具有以下语法和参数:
- 中断向量 – 您希望使用的中断。请注意,这是内部中断向量编号,而不是引脚编号。
- ISR – 要粘附到中断的中断服务例程函数的名称。
- 模式 – 您希望如何触发中断。
对于模式,有四个选项:
- RISING(上升) – 当输入从低到高时触发。
- FALLING(下降)– 当输入从高到低时触发。
- LOW(低) – 当输入保持低电平时触发。
- CHANGE– 每当输入状态从高到低或从低到高时触发。
您通常会在设置函数中使用 attachInterrupt。
数字引脚到中断功能
attachInterrupt 函数中的中断矢量参数与引脚编号不同,并且在 Arduino 板之间可能会有所不同。获取此数字的(更好)方法是使用digitalPinToInterrupt函数。
该函数的名称也是其描述,它接受引脚编号并返回中断向量编号。
您可以直接在attachInterrupt 函数中使用digitalPinToInterrupt。
attachInterrupt(digitalPinToInterrupt(pin),ISR,Mode)
这是硬件中断编码的首选方式,因为它使代码在电路板之间可移植。
重写硬件中断程序
现在我们进一步了解了如何使用硬件中断,让我们修改切换和延迟程序以使用它们。
以下是使用硬件中断的程序的更新版本:
/*
LED Toggle with Delay & Interrupt
led-toggle-interrupt.ino
Use pushbutton switch to toggle LED with interrupt
DroneBot Workshop 2022
https://dronebotworkshop.com
*/
// Define LED and switch connections
const byte ledPin = 13;
const byte buttonPin = 2;
// Boolean to represent toggle state
volatile bool toggleState = false;
void checkSwitch() {
// Check status of switch
// Toggle LED if button pressed
if (digitalRead(buttonPin) == LOW) {
// Switch was pressed
// Change state of toggle
toggleState = !toggleState;
// Indicate state on LED
digitalWrite(ledPin, toggleState);
}
}
void setup() {
// Set LED pin as output
pinMode(ledPin, OUTPUT);
// Set switch pin as INPUT with pullup
pinMode(buttonPin, INPUT_PULLUP);
// Attach Interrupt to Interrupt Service Routine
attachInterrupt(digitalPinToInterrupt(buttonPin),checkSwitch, FALLING);
}
void loop() {
// 5-second time delay
Serial.println("Delay Started");
delay(5000);
Serial.println("Delay Finished");
Serial.println("..............");
}
我们同样地从声明 LED 和开关的引脚号开始。我们还使用相同的布尔值进行切换。
请注意,布尔值是可变的。这很重要,因为它的值是在中断服务例程中操作的。如果没有 volatile 语句,Arduino IDE 编译器可能会尝试过度优化代码并删除变量。
我们的checkSwitch 功能几乎与以前相同,唯一的区别是我们删除了延迟功能。这是因为我们将使用checkSwitch作为中断服务例程,并且我们不能在 ISR 中使用延迟。
在setup()
中,我们执行通常的*pinMode
命令,初始化串行监视器,然后运行attachInterrupt
以将checkSwitch
*功能附加到引脚2上的硬件中断。我们使用下降模式,因为我们希望在按下(并接地)开关时捕获。
我们在循环中所拥有的只是延迟,现在它将连续运行。现在,任何开关活动都将由中断处理。
加载程序并使用按钮播放,同时观察 LED 和串行监视器。您应该看到,尽管循环始终处于延迟状态,但切换开关仍有效。
如您所见,硬件中断是捕获开关输入的一种更有效的方法。
引脚变化引发中断
引脚更改引发中断是硬件中断的另一种形式。与我们刚刚使用的中断不同,它们不限于特定的引脚,所有引脚都可用于引脚更改引发中断。
问题是引脚更改引发中断被分组到端口中,并且同一端口上的所有引脚创建相同的引脚更改引发中断。如果您只使用一个引脚,这很好,否则,您需要找出导致中断的引脚。
引脚更改引发中断被限制为仅监视逻辑状态的更改。因此,按下开关将产生两个中断,一个在按下开关时,另一个在松开开关时。您必须自己弄清楚中断是由高电平还是低电平输入引起的。
引脚更改引发端口
Arduino Uno(和其他板卡)所基于的ATmega328芯片上的24个引脚几乎每个引脚都支持引脚切换中断。这包括用于16MHz晶体振荡器的两个引脚。
在Arduino Uno上,有20个引脚可用于引脚更改引发中断,它们分为三个端口。
引脚 8 到 13 位于端口 B 上。
针脚 A0 至 A5 位于端口 C 上。
引脚 0 到 7 位于端口 D 上。
使用引脚更改引发中断
要使用引脚更改引发中断,您需要执行以下操作:
- 确定要使用的引脚。还需要告诉您使用哪些端口。
- 启用所需的端口。
- 启用该端口内必须为中断启用的引脚。
- 编辑相应的中断服务例程。如果在同一端口上使用多个引脚,则 ISR 需要能够确定哪个引脚导致了中断。
让我们更详细地看一下这些步骤。对于我们的示例,我们将只使用单个引脚,稍后我们将运行一个在同一端口上使用两个引脚的程序。
选择端口
第一步是启用相应的端口,您将根据引脚编号确定该端口。要启用该端口,您将使用*引脚更改中断控制寄存器(Pin Change Interrupt Control Register)*或 PCICR。
PCICR 有三个感兴趣的位:位 0、位 1 和位 2。每个位都与其中一个端口相关联,将其设置为 1 将启用该端口。
在setup()
程序中,您将向 PCICR 写入二进制数,以将相应的位设置为 1。当然,您可以启用多个端口。
启用/禁用端口上的引脚
启用端口后,您需要启用要用于引脚更改引发中断的引脚。您可以通过修改所选端口的引脚更改引发掩码来执行此操作。
有三个引脚更改引发掩码,每个模板可以启用或禁用 8 个引脚。要启用引脚,请为其写入“1”。您可以根据需要启用任意数量的引脚,请记住,您需要找到一种方法来区分中断服务例程中的引脚。
中断服务例程
与硬件中断不同,您不只是创建一个中断服务例程并为其指定任何随机名称。使用引脚更改引发中断时,已经为您定义了 ISR,因此您需要为您的端口使用正确的 ISR。
有三个端口,因此有三个 ISR,其名称如下所示:
有关中断服务例程的所有相同规则适用于与引脚更改引发中断一起使用的 ISR。保持简短并使用易失性的全局变量。
试验引脚更改引发中断
为了运行接下来的几个示例,我们需要在Arduino中添加另一个按钮和LED。最终的连接如下所示:
所以我们现在有:
- 引脚 D2 上的按钮。
- 引脚 D7 上的按钮。
- 引脚 D11 上的 LED。
- 引脚 D13 上的 LED。
请注意,D2 和 D7 上的两个按钮开关位于同一端口 D 上。
引脚更改引发中断示例 1 – 简单中断
我们将运行的第一个示例将说明如何为引脚更改引发中断编写代码。在这个实验中,我们将只使用一个开关和一个LED。请注意,我们的开关位于引脚 D7 上,它不是硬件中断引脚。这并不重要,因为我们将通过换针中断来感应它!
每次在引脚 D7 上遇到中断时,我们都会切换 LED 的状态。要记住的一件重要事情是,我们将感知输入状态的变化,因此按钮将产生两个中断 - 一个在按下它时,另一个在释放它时。
这将使我们的 LED 运行得很像刚刚与开关串联的!
这是我们的程序:
/*
Pin Change Interrupt Test
pin-change-test.ino
Demonstrates use of Pin Change Interrupt
Input on D7, LED on D13
DroneBot Workshop 2022
https://dronebotworkshop.com
*/
// LED and switch
const byte ledPin = 13;
const byte buttonPin = 7;
// Boolean to represent toggle state
volatile bool togglestate = false;
void setup() {
// Set LED as output
pinMode(ledPin, OUTPUT);
// Set switch as input with pullup
pinMode(buttonPin, INPUT_PULLUP);
// Enable PCIE2 Bit3 = 1 (Port D)
PCICR |= B00000100;
// Select PCINT23 Bit7 = 1 (Pin D7)
PCMSK2 |= B10000000;
}
void loop() {
// No code in Loop
}
ISR (PCINT2_vect)
{
// Interrupt for Port D
// Invert toggle state
togglestate = !togglestate;
// Indicate state on LED
digitalWrite(ledPin, togglestate);
}
我们首先定义开关和LED的引脚。
接下来,我们创建一个易失性布尔值来表示切换状态。
在设置中,我们使用pinMode函数来定义我们的输入和输出。
然后我们设置了引脚更改引发中断。我们修改PCICR寄存器,让它知道我们要使用端口D,并修改端口D的掩码PCMSK2,将引脚D7设置为中断输入。
我们在 Loop 中没有任何代码,因为我们使用中断服务例程执行所有操作。
接下来是中断服务例程,当我们使用端口 D 时,我们使用ISR (PCINT2_vect)。在 ISR 中,我们翻转toggleState变量的状态并使用它来驱动 LED。
加载代码并尝试一下。按住开关时 LED 应亮起,松开开关时 LED 应熄灭。当然,使用微控制器确实是矫枉过正,但它确实演示了引脚更改引发中断!
引脚更改引发中断示例 2 – 同一端口上的多个中断
在前面的示例中,我们只能对引脚更改引发中断进行操作,因为端口上只有一个引脚启用了中断。毫无疑问,是什么导致了中断。
但是,当您在同一端口上为引脚更改引发中断启用两个或多个引脚时,如何处理它?您需要确定谁导致了中断,以便您可以采取相应的措施。
在此示例中,我们将有两个按钮和两个 LED。每个按钮将充当其各自 LED 的切换开关。这意味着当我们得到中断时,我们需要知道两件事:
- 什么引脚导致中断?
- 是低还是高?
我们将在程序中回答这两个问题:
/*
Multiple Pin Change Interrupt Demo
pin-change-multiple-test.ino
Demonstrates Pin Change Interrupts with two on same port
Inputs on D2 & D7, LEDs on D11 and D13
DroneBot Workshop 2022
https://dronebotworkshop.com
*/
// LEDs and switchs
const byte ledPin1 = 11;
const byte ledPin2 = 13;
const byte buttonPin1 = 2;
const byte buttonPin2 = 7;
// Booleans for input states
volatile bool D2_state = LOW;
volatile bool D7_state = LOW;
void setup() {
// Set LEDs as outputs
pinMode(ledPin1, OUTPUT);
pinMode(ledPin2, OUTPUT);
// Set switches as inputs with pullups
pinMode(buttonPin1, INPUT_PULLUP);
pinMode(buttonPin2, INPUT_PULLUP);
// Enable PCIE2 Bit3 = 1 (Port D)
PCICR |= B00000100;
// Enable PCINT18 & PCINT23 (Pins D2 & D7)
PCMSK2 |= B10000100;
}
void loop() {
// Loop code
}
ISR (PCINT2_vect)
{
// Port D Interrupt occured
// Check if this was D2
if (digitalRead(buttonPin1) == LOW) {
//Pin D2 triggered the ISR on a Falling pulse
D2_state = !D2_state;
//Set LED 1 to state of D2_state boolean
digitalWrite(ledPin1, D2_state);
}
// Check if this was D7
if (digitalRead(buttonPin2) == LOW) {
//Pin D7 triggered the ISR on a Falling pulse
D7_state = !D7_state;
//Set LED 2 to state of D7_state boolean
digitalWrite(ledPin2, D7_state);
}
}
程序如您所料开始,我们为 LED 和开关定义了一堆变量以及两个布尔值,每个切换状态一个。
在设置中,我们设置开关和LED的引脚模式,再次使用内部上拉电阻作为输入。
然后,我们启用端口D,就像我们在前面的程序中所做的那样,通过写入PCICR寄存器的“1”到位2。
接下来,我们为 PCMSK2 掩码的第 7 位和第 2 位写入“1”,让它知道引脚 D7 和 D2 将被视为引脚更改引发中断。
同样,循环中没有代码,一切都在中断服务例程中完成。
该例程是 ISR (PCINT2_vect),即端口 D 的 ISR,这与我们在上一个程序中使用的 ISR 相同。只有这一次,我们需要弄清楚是引脚D2还是D7导致了中断。
我们在此程序中寻找低条件,因为我们想在按下开关时切换按钮,而不是在松开开关时切换按钮。此外,我们使用几个if语句和一个数字读取来检查每个输入并确定它当前是否为LOW。如果是,那么我们切换相应的布尔值并使用它来控制 LED。
加载程序并对其进行测试。连接到引脚 D2 的按钮应控制引脚 D11 上的 LED,D7 上的按钮应与引脚 D13 上的 LED 配合使用。您应该能够独立切换它们中的每一个。
当扫描一组或一组开关时,或者当您需要外部中断但没有备用硬件中断引脚时,引脚更改引发中断非常有用。
定时器中断
定时器中断不使用外部信号。相反,这些中断是在软件中生成的,它们的时序基于Arduino Uno的16 MHz时钟振荡器。
您可能一直在使用定时器中断而没有意识到这一点,因为一些流行的库(如伺服和音调库)在内部使用定时器中断。请记住,如果您使用的是使用计时器的库,则需要知道这一点,以便不会编写冲突的代码。
Arduino Uno 计时器
Arduino Uno有三个内部定时器,Timer0,Timer1和Timer2。
这些计时器并不相同,因为 Timer1 是 16 位计时器,而其他两个计时器只是 8 位计时器。位数决定了计时器可以计数的最大数字,8 位计时器为 256,16 位计时器为 65,536。
这些计时器中的值按时钟频率或时钟频率的一小部分递增。您可以使用软件来确定要设置的中断触发的计数,也可以在计时器溢出时触发中断。
对时钟频率进行分频
定时器由 ATmega328 内部的 16 MHz 振荡器计时。
时钟的每个周期都是一个计时器“滴答”,因此在 16 MHz 时,“滴答”的周期为 62.5 纳秒。这是一个相当短的周期,对于许多定时应用来说,它太短而没有太多的实际用途。
为了减慢时钟信号,ATmega328 有一个“预分频器”,本质上是时钟频率的分频器。预分频器可以将时钟划分为更易于管理的较低频率,您可以从许多常见分频中进行选择,以创建长达64us的脉冲。
每个定时器有三个时钟选择位,这些位的值可以确定预分频器设置以及定时源。您也可以通过将所有时钟选择位设置为零来完全停止时钟。
Timer0 是一个 8 位定时器,使用位 CS01、CS02 和 CS03。
Timer1 是 16 位定时器,使用位 CS10、CS11 和 CS12。
Timer2 是另一个 8 位定时器,使用 CS20、CS21 和 CS22 位。
使用计时器中断
定时器中断可以在几种不同的模式下操作,包括比较匹配模式和溢出模式。
在比较匹配模式下,将计数器值放入比较匹配寄存器中。当计时器计数器与该寄存器中的值匹配时,将生成计时器中断。
在溢出模式下,当计时器达到其计数结束时,将生成计时器中断,将生成中断,计时器重置为零并再次开始计数。
通过将比较匹配寄存器与预分频器结合使用,您可以很好地获得您想要的任何定时周期,假设它在您的计时器范围内(8 位定时器最多只能除以 255)。
确定定时器输出频率和周期的公式如下:
*时钟/(预分频器 x (比较匹配寄存器 + 1)) = 频率*
如果您知道所需的频率并想要确定“比较匹配寄存器”值,则可以按如下方式重写公式:
*[时钟/(预分频器 x 频率)] -1 = 比较匹配寄存器*
使用上述公式,我们可以计算出要获得 1 Hz 输出,我们可以使用 1024 的预分频器和 15,624 的比较匹配寄存器。当此值超过 255 时,我们将只能使用 Timer1,即 16 位计时器。
计时器中断服务例程
与引脚更改引发中断一样,已经为您确定了定时器中断的中断服务例程的名称。
每个计时器都有两个与之关联的 ISR,一个用于比较匹配模式,另一个用于溢出模式。
在上图中,单词“TIMER”后面的“x”应替换为计时器编号,例如 0、1 或 2。
简单计时器示例
这个非常简单的定时器示例为2 HZ输出或每秒两次设置定时器。然后我们使用计时器来控制LED,所以本质上我们正在构建闪烁程序!
我们将在引脚 13 上使用 Arduino Uno 和 LED(带下降电阻)。如果您愿意,您可以抓住以前的实验之一,或者只是使用 Arduino 板上的内置 LED。
以下是我们将使用的程序:
/*
Arduino Timer Interrupt Flash Demo
timer-int-flash.ino
Flash LED using Timer1
DroneBot Workshop 2022
https://dronebotworkshop.com
*/
// Define the LED pin
#define ledPin 13
// Define timer compare match register value
int timer1_compare_match;
ISR(TIMER1_COMPA_vect)
// Interrupt Service Routine for compare mode
{
// Preload timer with compare match value
TCNT1 = timer1_compare_match;
// Write opposite value to LED
digitalWrite(ledPin, digitalRead(ledPin) ^ 1);
}
void setup()
{
// Set LED as output
pinMode(ledPin, OUTPUT);
// Disable all interrupts
noInterrupts();
// Initialize Timer1
TCCR1A = 0;
TCCR1B = 0;
// Set timer1_compare_match to the correct compare match register value
// 256 prescaler & 31246 compare match = 2Hz
timer1_compare_match = 31249;
// Preload timer with compare match value
TCNT1 = timer1_compare_match;
// Set prescaler to 256
TCCR1B |= (1 << CS12);
// Enable timer interrupt for compare mode
TIMSK1 |= (1 << OCIE1A);
// Enable all interrupts
interrupts();
}
void loop()
{
}
在此程序中,我们使用比较匹配模式,因此在定义LED引脚后,我们还创建一个整数来保存比较匹配寄存器值。
接下来是我们的中断服务例程。由于我们在 Timer1 上使用比较匹配模式,我们将使用ISR(TIMER1_COMPA_vect)。
在 ISR 中,我们做两件事:
- 使用比较匹配值预加载计时器,以再次启动循环
- 翻转 LED 的值。
在设置中,我们将 LED 引脚设置为输出。然后,我们暂时禁用所有中断,以防止在我们仍在设置时出现一个中断。
我们使用两个命令初始化 Timer1,然后设置我们的比较匹配值。由于我们希望实现 2 Hz,因此如果我们使用 31246 的预分频器,我们计算出 256 是一个很好的值。
然后,我们使用比较匹配值预加载计时器,将预分频器设置为 256,并在比较匹配模式下启用中断。
最后,我们重新启用所有中断。
加载程序并观察 LED 闪烁。在这种快感消失后,尝试不同的预分频器和比较匹配值。
结论
中断是构建需要精确计时或响应式用户界面的项目的好方法。虽然我们大多数人都涉足硬件中断,但许多人回避引脚更改引发或定时器中断,这是一种耻辱,因为它们非常有用,而且一旦您了解寄存器的使用,二进制编码就不是那么困难了。
我鼓励您将中断纳入您的下一个设计中。请记住,在某些情况下,被打断并不粗鲁!
资源
代码示例–本文中使用的所有代码都很好地打包在一个ZIP文件中。
附加中断 – 与硬件中断一起使用的附加中断函数的参考。