本教程介绍如何设置基于定时器的中断。具体来说,它使用定时器比较中断定期闪烁 LED,这与流行的 Blink sketch 使用 delay() 形成对比。编程特定于 Arduino Uno、5V Nano 3.x 和克隆中使用的 16MHz ATmega328P。
我有一个疯狂的想法,即构建一个三单元 Raspberry Pi 集群,并使用自定义 8MHz、3.3V ATMega328P(与 Arduino Uno 相同的芯片,但速度和电压较低)在 Raspberry Pi 控制台端口之间切换,同时还做其他有用的东西,如测量温度和电源状态。我的第一步是找出控制台端口,这涉及到 SoftwareSerial 库。
我有一个 5V Arduino Uno,而 Raspberry Pi 当然是 3.3V。因此,为了避免烧坏我的 Pi,我剪了一根电线将软件串行传输和接收引脚连接在一起,创建了一个环回。这个理论是合理的,无论我在传输引脚上发出什么,都应该在接收引脚上返回。但是,它没有用。由于中断,它没有工作。
需要中断才能知道串行数据何时进入特定引脚,但由于关键时序,传输时中断被禁用。这种安排使得无法同时发送和接收。不用说,这对我连接到多个 Raspberry Pi 控制台端口的想法来说不是好兆头。
但是,从好的方面来说,它让我对 Arduino Uno 上的中断产生了兴趣。
眨眼草图。这可能是每个人的第一个 Arduino 项目。好吧,这里再次作为复习。
#define LED 9
void setup() {
pinMode(LED, OUTPUT);
}
void loop() {
digitalWrite(LED, LOW);
delay(500);
digitalWrite(LED, HIGH);
delay(500);
}
请注意熟悉的 delay() 子例程来控制打开和关闭时间。每个状态有 500mS,LED 将以稳定的 1Hz 脉冲。
在我的草图中,我将引脚 9 用于 LED。这需要在引脚 9 和地之间连接一个 LED 和限流电阻。我要大胆猜测,如果您正在阅读有关定时器中断的教程,那么您已经连接了几个 LED,我不会详细介绍。不过,使用外部 LED 很重要,因为稍后会同时使用内置 LED 来比较中断闪烁和 delay() 闪烁。
如果你想做的只是对着闪光灯发出呜呜声,那么这幅素描就很完美了。但是,如果您想同时做其他事情怎么办?也许测量热敏电阻两端的电压并计算温度。那样就好了。
问题是那两个 delay() 函数调用。每个人都花半秒钟什么都不做。您可以在其中一个延迟之前或之后插入代码。但是,如果该代码需要足够长的时间来执行,以至于它会错过眨眼的时间怎么办?
你可以缩短延迟。也许将其设为 490 而不是 500。也许这太短了,495 是更好的选择。在某些时候,您可能会举手说,“一定有更好的方法!”
有一个更好的办法。它被称为定时器中断。
查看本教程标题卡中的插图。
戴大礼帽的花花公子——他是个干扰者。你几乎可以想象他在一个古老的维多利亚火车站附近闲逛,说着类似“对不起,我的好先生们”之类的话。
现在长凳上的三位先生——他们是您的 loop() 函数中的进程。左边那个坐下,当 LED 熄灭时,他就是 LED。下一个人是 LED 灯。第三个人,那个戴着帽子遮住眼睛的人,他一定是延迟功能。
“请原谅,我的好先生们,”打断说。突然,他引起了替补席上三个人的注意。就连昏昏欲睡的家伙也竖起了耳朵。不管他们之前在做什么,他们都停下来让戴大礼帽的花花公子上台。
这正是中断所做的。它会停止 loop() 的正常执行并运行它自己的代码一段时间。
简洁很重要
如果戴礼帽的人是个体面的人,他会保持简短的打断。他可能会说一些重要的话,比如,‘老兄,你的火车就要出发了,’然后就上路了。或者他可能会一连几个小时在庞氏骗局的最新趋势上喋喋不休地自以为是。
前者是好中断的例子,后者是坏中断。当中断花费太多时间时,它会使所有其他进程等待。中断应该只做绝对必要的事情,并将控制权交还给主循环。
在 LED 闪烁的情况下,最少量的处理归结为改变输出引脚的状态,以便连接的 LED 打开或关闭。因此,在我们继续之前,请看一下以下代码行:
PORTB ^= B00100000; // Toggle bit 5, which maps to pin13.
它有什么作用?好吧,阅读评论,它说它切换了一个映射到引脚的位。该引脚恰好是引脚 13,这是 Arduino Uno 上的内置 LED。PORTB
是 Uno 上控制引脚 8 到 13 的寄存器。位 0 控制引脚 8,位 1 控制引脚 9,依此类推。第 5 位控制引脚 13,即内置 LED。
该^=
运算符是一个异或 (XOR)。XOR 可用于反转操作数之一为 1 的任何位。XOR 就像一个带有扭曲的常规 OR。两个零位作为输入给出一个零作为输出。零和一或一和零给出一的输出。但是,这里有一个转折……如果你有一个和一个,结果是零。
这就是为什么上面的代码行只会翻转 one 所在的位。如果寄存器 PORTB 的第 5 位的当前值为零,则该零与二进制值第 5 位中的 1 进行异或后B00100000
结果为 1。如果 PORTB 的第 5 位是 1,则 1 与 1 的异或结果为 0。每次应用 XOR 时,该位都会翻转。PORTB 中的任何其他位将与零进行异或,从而返回原始值并且不受影响。
使用 digitalRead() 确定引脚的当前状态然后使用 digitalWrite() 应用相反状态的练习现在在单个 XOR 中完成。快速高效,就像任何优秀的、正直的中断一样。
有了基础知识,就该开始设置中断了。这涉及两个部分。
第一个是中断服务程序(ISR)。它只是一个执行位翻转代码的子程序。ISR() 子例程存在于 setup() 和 loop() 例程之外,它看起来像这样:
ISR(TIMER1_COMPA_vect)
{
PORTB ^= B00100000; // Toggle bit 5, which maps to pin13.
}
在最简单的形式中,ISR() 接受一个参数。该参数TIMER1_COMPA_vect
是中断的向量(或源)。使用像 TIMER1_COMPA_vect 这样的名称,您可能会猜到它与 timer1 有关,并且可能正在进行一些比较。你是对的。
但我们还没有完成。到目前为止,我们所做的只是告诉 ATmega328P 当定时器比较中断发生时该做什么。我们实际上还没有设置任何定时器来产生中断,所以什么都不会发生。
设置的第二部分涉及告诉计时器何时引发中断。这涉及更多的控制寄存器和二进制值,但如果你已经做到了这一点,你会没事的。这是代码:
cli();
TCCR1A = B00000000;
TCCR1B = B00001100;
TIMSK1 = B00000010;
OCR1A = 31250;
sei();
cli() 和 sei() 指令是相关的。第一个清除中断标志,第二个设置它。中断标志允许中断发生。使用 cli() 忽略所有中断,本质上它是一个很大的请勿打扰标志。sei() 做相反的事情并允许中断。
最初使用 cli() 阻止中断的原因是因为需要设置四个寄存器并且所有四个都是相关的。在中断开始滚动之前只设置一两个将导致一些不可预测的行为。最好在设置过程中熄灭请勿打扰标志。
寄存器(TCCR1A、TCCR1B 和 TIMSK1)是指示 timer1 如何工作的标志的集合。在代码中,所有设置都使用二进制值,以便更容易查看正在设置的位以及它是 1 还是 0。
OCR1A 是要与定时器的当前计数进行比较的值。它以十进制表示法显示,以便于阅读。按照配置,计时器将从 0 开始并向上计数。当它达到存储在 OCR1A 中的值时,就会发生一些事情。如果您猜到某事是中断,您将赢得奖品。
定时器计数的速度以及当它达到 OCR1A 中的值时它做什么由 TCCR1A 和 TCCR1B 中的标志决定。这些定时器控制寄存器在 ATmega328P 数据表的第 15.1 节中有详细说明。但这里有一个简短的描述:
TCCR1A = B00000000
是最简单的。它设置全零,或所谓的“正常”模式。TCCR1B = B00001100
指示计数器用位 0..2 的值计数的速度。在这种情况下,那些较低位中的二进制 100 会将预分频器设置为 256(稍后会详细介绍)。TCCR1B = B00001100
,第 3 位中的 1 表示当达到 OCR1A 中存储的值时,计数器将重置为零。TIMSK1 = B00000010
确保当计数器达到存储在 OCR1A 中的值时将产生中断。最后设置的寄存器是OCR1A,比较寄存器,但是为什么设置为31250呢?答案在于以下等式:
interrupts_per_second = clock_speed / 预分频器 / OCR1A
也可以表示为
OCR1A = clock_speed / 预分频器 / interrupts_per_second
使用 16MHz Arduino Uno 和预分频器值为 256(记住 TCCRB1 的最低 3 位设置该值),等式变得更简单:
OCR1A = 16MHz / 256 / interrupts_per_second
OCR1A = 62500 / interrupts_per_second
我想每秒切换 LED 两次以获得 1Hz 闪烁率,因此我将 62500 除以 2 得到 31250。这就是 OCR1A 值的来源。
到目前为止,这都是一堆理论。让我们将所有代码放在草图中并证明它确实有效。这是它的样子:
#define LED 9
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(LED, OUTPUT);
cli();
TCCR1A = B00000000;
TCCR1B = B00001100;
TIMSK1 = B00000010;
OCR1A = 31250;
sei();
}
void loop() {
digitalWrite(LED, LOW);
delay(500);
digitalWrite(LED, HIGH);
delay(500);
}
ISR(TIMER1_COMPA_vect)
{
PORTB ^= B00100000;
}
请注意 delay() 闪烁的旧方法如何仍然包括在内,但现在草图的新中断代码部分也是如此。这就是使用两个 LED 的原因。外部 LED(引脚 9)将由 loop() 内的 digitalWrite() 和 delay() 控制,而内置 LED(引脚 13)由 ISR() 和 setup() 内的配置控制.
将草图加载到 Uno,您应该会看到两个 LED 灯同时闪烁。
现在,做一些改变。尝试以下任何或所有操作:
预测每种情况下会发生什么,然后上传草图以查看您是否正确。在您思考的同时,想象一下使用 8MHz 时钟晶体代替标准 16MHz 的 Arduino Uno 会产生什么效果。
作为最后的练习,这个练习需要一些时间,让原始草图运行几个小时甚至一整夜,以查看对 LED 同步的影响。(剧透:它们将明显不同步。)想出一些关于为什么会发生这种情况的理论。哪个LED的时序更准确?
一开始,闪烁的 LED 很有趣,但中断可以用于更多用途。例如,有些中断可以由输入引脚的变化触发。现在您已经知道如何编写用于打开或关闭 LED 的中断服务例程,请尝试使用分配给输入引脚的按钮而不是定时器来触发它。
作为介绍,本教程仅展示了一种在 ATmega328p 上配置中断的方法。还有更高级别的函数,如:attachInterrupt() 和 detachInterrupt,它们涵盖更广泛的 Arduino 模型并处理细节,因此您不一定需要阅读数据表和设置寄存器位。
无论您打算在哪里学习新知识,了解中断的工作原理都将使您能够构建更好、更高效的草图。因此,放弃 delay() 并开始使用更多中断。
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
全部0条评论
快来发表一下你的评论吧 !