Arduino和计时器中断

哈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数据表的详细信息:


TCCR1A
7654321个0
0x80COM1A1COM1A0COM1B1COM1B0----WG11Wgm10
读写wwww[R[Rww
初始值00000000

TCCR1B
7654321个0
0x81ICNC1ICES1--WG13Wgm12CS12CS11CS10
读写ww[Rwwwww
初始值00000000

最重要的是TCCR1B的最后三个位:CS12,CS11和CS10。 它们确定计时器的时钟频率。 选择不同的组合,您可以命令计时器以不同的速度运行。 这是一个数据表,描述了选择位的作用:


CS12CS11CS10动作片
000无时钟源(定时器/计数器停止)
001个clk_io / 1(无除法)
01个0clk_io / 8(分频器)
01个1个clk_io / 64(分频器)
1个00clk_io / 256(分频器)
1个01个clk_io / 1024(分频器)
1个1个0T1引脚上的外部时钟源。 经济衰退时钟
1个1个1个T1引脚上的外部时钟源。 前置时钟

默认情况下,所有这些位都设置为零。


假设您希望Timer1以一个时钟频率运行,每个周期采样一个。 当它溢出时,您要调用中断例程,该例程将连接到引脚13的LED切换为开或关状态。 对于此示例,我们将编写Arduino代码,但是只要不使事情变得太复杂,我们都将使用avr-libc库的过程和函数。 纯AVR的支持者可以根据需要修改代码。


首先,初始化计时器:


// avr-libc library includes #include <avr/io.h> #include <avr/interrupt.h> #define LEDPIN 13 void setup() { pinMode(LEDPIN, OUTPUT); //  Timer1 cli(); //    TCCR1A = 0; //  TCCR1A   0 TCCR1B = 0; //   Timer1 overflow: TIMSK1 = (1 << TOIE1); //  CS10  ,      : TCCR1B |= (1 << CS10); sei(); //    } 

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); //  Timer1 cli(); //    TCCR1A = 0; //    0 TCCR1B = 0; OCR1A = 15624; //    TCCR1B |= (1 << WGM12); //   CTC  //   CS10  CS12    1024 TCCR1B |= (1 << CS10); TCCR1B |= (1 << CS12); TIMSK1 |= (1 << OCIE1A); //     sei(); //    } 

您还需要用同步中断替换溢出中断:


 ISR(TIMER1_COMPA_vect) { digitalWrite(LEDPIN, !digitalRead(LEDPIN)); } 

现在,LED将准确地打开和关闭一秒钟。 您可以在loop()循环中执行任何操作。 在更改定时器设置之前,该程序与中断无关。 对于使用具有不同模式和分频器设置的计时器没有任何限制。


这是一个完整的入门示例,您可以将其用作自己的项目的基础:


 // Arduino  CTC  // avr-libc library includes #include <avr/io.h> #include <avr/interrupt.h> #define LEDPIN 13 void setup() { pinMode(LEDPIN, OUTPUT); //  Timer1 cli(); //    TCCR1A = 0; //    0 TCCR1B = 0; OCR1A = 15624; //    TCCR1B |= (1 << WGM12); //  CTC  TCCR1B |= (1 << CS10); //      1024 TCCR1B |= (1 << CS12); TIMSK1 |= (1 << OCIE1A); //      sei(); //    } void loop() { //   } ISR(TIMER1_COMPA_vect) { digitalWrite(LEDPIN, !digitalRead(LEDPIN)); } 

请记住,您可以使用内置的ISR功能来扩展计时器功能。 例如,您需要每10秒轮询一次传感器。 但是没有计时器设置可以提供如此长的计数而没有溢出。 但是,您可以使用ISR每秒增加一次计数变量,然后在变量达到10时轮询传感器。使用上一个示例中的STS模式,中断看起来像这样:


 ISR(TIMER1_COMPA_vect) { seconds++; if(seconds == 10) { seconds = 0; readSensor(); } } 

由于将在ISR内部修改变量,因此必须将其声明为volatile 。 因此,在程序开始时描述变量时,您需要编写:


 volatile byte seconds; 

译者的后记


一次,本文为我节省了开发原型测量发生器的大量时间。 我希望它将对其他读者有用。

Source: https://habr.com/ru/post/zh-CN453276/


All Articles