哈Ha! 我向您介绍E撰写的文章“ Timer interrupts” 。
前言
Arduino开发板可让您快速而最少地解决各种问题。 但是,在需要任意时间间隔的情况下(传感器的定期轮询,高精度PWM信号,长持续时间的脉冲),标准库延迟功能并不方便。 在操作期间,草图将被挂起,无法对其进行管理。
在类似情况下,最好使用内置的AVR计时器。 一篇成功的文章说,如何做到这一点而又不会迷失在数据表的技术荒野中,请您注意其翻译。

本文讨论了AVR和Arduino计时器,以及如何在Arduino项目和用户电路中使用它们。
什么是计时器?
就像微控制器中的日常生活一样,计时器是可以在您设置时立即发出信号的东西。 当这一时刻到来时,微控制器被打断,提醒他做一些事情,例如执行某段代码。
定时器像外部中断一样,独立于主程序工作。 无需循环或重复millis()延迟调用,您可以分配一个计时器来执行其工作,而代码可以执行其他操作。
因此,假设有一个设备需要执行某项操作,例如,每5秒闪烁一次LED。 如果不使用计时器,而是编写常规代码,则需要在LED点亮时设置一个变量,并不断检查其切换时刻是否到来。 对于计时器中断,您只需要配置中断,然后启动计时器即可。 无论主程序如何操作,LED都会准确地按时闪烁。
计时器如何工作?
它通过递增一个称为计数寄存器的变量来起作用。 计数寄存器可以根据其大小计数到某个值。 计时器将一遍又一遍地递增其计数器,直到达到最大值为止,此时计数器将溢出并复位为零。 计时器通常将标志位设置为使您知道发生了溢出。
您可以手动检查此标志,也可以进行计时器切换-设置该标志时会自动导致中断。 与其他任何中断一样,您可以分配一个中断服务程序 ( ISR )以在定时器溢出时执行指定的代码。 ISR本身将清除溢出标志,因此使用中断通常是最好的选择,因为它简单且速度快。
要以精确的时间间隔增加计数器值,必须将计时器连接到时钟源。 时钟源产生一个不断重复的信号。 每当计时器检测到此信号时,它会将计数器值增加一。 由于定时器运行在时钟源上,因此可测量的最小时间单位是周期。 如果连接1 MHz时钟信号,则计时器分辨率(或计时器周期)将为:
T = 1 / f(f是时钟频率)
T = 1/1 MHz = 1/10 ^ 6赫兹
T =(1 * 10 ^ -6)秒
因此,计时器的分辨率为百万分之一秒。 尽管您可以将外部时钟源用于计时器,但在大多数情况下,都使用芯片本身的内部源。
计时器类型
在8位AVR芯片上的标准Arduino板中,一次有多个计时器。 Atmega168和Atmega328芯片具有三个Timer0,Timer1和Timer2定时器。 它们还具有看门狗定时器,可用于防止故障或作为软件重置机制。 以下是每个计时器的一些功能。
定时器0:
Timer0是一个8位定时器,这意味着其计数寄存器最多可以存储255个数字(即无符号字节)。 标准Arduino临时函数(如delay()和millis())使用Timer0,因此,如果您在意后果,最好不要混淆它。
计时器1:
Timer1是一个16位定时器,最大计数值为65535(无符号整数)。 此计时器使用Arduino Servo库,如果在项目中使用它,请记住这一点。
计时器2:
Timer2是8位,与Timer0非常相似。 在Arduino tone()函数中使用。
Timer3,Timer4,Timer5:
ATmega1280和ATmega2560芯片(安装在Arduino Mega变体中)具有三个附加计时器。 它们都是16位的,其工作方式与Timer1类似。
注册配置
为了使用这些计时器,AVR具有设置寄存器。 计时器包含许多这样的寄存器。 其中两个-定时器/计数器控制寄存器包含设置变量,称为TCCRxA和TCCRxB,其中x是定时器的编号(TCCR1A和TCCR1B等)。 每个寄存器包含8位,每个位存储一个配置变量。 以下是Atmega328数据表的详细信息:
最重要的是TCCR1B的最后三个位:CS12,CS11和CS10。 它们确定计时器的时钟频率。 选择不同的组合,您可以命令计时器以不同的速度运行。 这是一个数据表,描述了选择位的作用:
默认情况下,所有这些位都设置为零。
假设您希望Timer1以一个时钟频率运行,每个周期采样一个。 当它溢出时,您要调用中断例程,该例程将连接到引脚13的LED切换为开或关状态。 对于此示例,我们将编写Arduino代码,但是只要不使事情变得太复杂,我们都将使用avr-libc库的过程和函数。 纯AVR的支持者可以根据需要修改代码。
首先,初始化计时器:
TIMSK1寄存器是定时器/计数器1中断屏蔽寄存器。 它控制计时器可能导致的中断。 将TOIE1位置1可使定时器在定时器溢出时中断。 稍后再详细介绍。
当您将CS10位置1时,计时器开始计数,一旦发生溢出中断,就会调用ISR(TIMER1_OVF_vect)。 这总是在定时器溢出时发生。
接下来,我们定义ISR中断函数:
ISR(TIMER1_OVF_vect) { digitalWrite(LEDPIN, !digitalRead(LEDPIN)); }
现在,我们可以定义loop()周期并切换LED,而无论主程序中发生了什么。 要关闭定时器,可随时将TCCR1B设置为0。
LED多久闪烁一次?
Timer1被设置为溢出中断,并且假设您使用的时钟频率为16 MHz的Atmega328。 由于计时器是16位的,因此它可以计入最大值(2 ^ 16-1)或65535。在16 MHz时,周期运行1 /(16 ∗ 10 ^ 6)秒或6.25e-8 s。 这意味着将在(65535 * 6.25e-8 s)中发生65535个采样,并且将在大约0.0041 s后调用ISR。 如此,每隔四千分之一秒。 看到闪烁太快了。
如果我们向LED施加覆盖率达到50%的非常快的PWM信号,则辉光将持续出现,但亮度不如平常。 这样的实验表明了微控制器的强大功能-即使是廉价的8位芯片也可以比我们检测到的信息处理速度更快。
定时器分频器和CTC模式
要控制周期,可以使用分频器,该分频器可以将时钟信号分为两个不同的度数并增加定时器周期。 例如,您希望LED以一秒的间隔闪烁。 TCCR1B寄存器中的三个CS位设置最合适的分辨率。 如果使用以下命令设置CS10和CS12位:
TCCR1B |= (1 << CS10); TCCR1B |= (1 << CS12);
那么时钟源的频率将被1024分频。这将使定时器分辨率为1 /(16 * 10 ^ 6/1024)或6.4e-5 s。 现在计时器将每隔(65535 * 6.4e-5s)或4.194s溢出一次。 太长了
但是还有另一种AVR计时器模式。 这被称为同时定时器复位或CTC。 计时器不会计数到溢出,而是将其计数器与先前存储在寄存器中的变量进行比较。 当计数与该变量匹配时,计时器可以设置标志或引起中断,就像发生溢出一样。
要使用CTC模式,您需要了解多少时间才能获得一秒的间隔。 假设分频比仍然是1024。
计算如下:
(target time) = (timer resolution) * (# timer counts + 1) (# timer counts + 1) = (target time) / (timer resolution) (# timer counts + 1) = (1 s) / (6.4e-5 s) (# timer counts + 1) = 15625 (# timer counts) = 15625 - 1 = 15624
您必须在采样数量上增加一个单位,因为在CTC模式下,当计数器与设置值匹配时,它将重置为零。 复位需要一个时钟周期,在计算中必须将其考虑在内。 在许多情况下,一个时期的错误不是很明显,但是在高精度任务中,它可能很关键。
setup()函数将如下所示:
void setup() { pinMode(LEDPIN, OUTPUT);
您还需要用同步中断替换溢出中断:
ISR(TIMER1_COMPA_vect) { digitalWrite(LEDPIN, !digitalRead(LEDPIN)); }
现在,LED将准确地打开和关闭一秒钟。 您可以在loop()循环中执行任何操作。 在更改定时器设置之前,该程序与中断无关。 对于使用具有不同模式和分频器设置的计时器没有任何限制。
这是一个完整的入门示例,您可以将其用作自己的项目的基础:
请记住,您可以使用内置的ISR功能来扩展计时器功能。 例如,您需要每10秒轮询一次传感器。 但是没有计时器设置可以提供如此长的计数而没有溢出。 但是,您可以使用ISR每秒增加一次计数变量,然后在变量达到10时轮询传感器。使用上一个示例中的STS模式,中断看起来像这样:
ISR(TIMER1_COMPA_vect) { seconds++; if(seconds == 10) { seconds = 0; readSensor(); } }
由于将在ISR内部修改变量,因此必须将其声明为volatile 。 因此,在程序开始时描述变量时,您需要编写:
volatile byte seconds;
译者的后记
一次,本文为我节省了开发原型测量发生器的大量时间。 我希望它将对其他读者有用。