AVR开发 Arduino方法(三) 定时/计数器子系统
Arduino UNO R3的主处理器ATMega328P拥有3个定时/计数器,它们分别是Timer0,Timer1和Timer2;它们都通过对来自内部或外部的脉冲信号进行计数的方式完成基本的定时/计数功能以及一些其他的功能。
Timer0和Timer2是8位定时/计时器,Timer1是16位定时/计数器;下面以Timer2为例讨论定时/计数器子系统的典型应用,这些内容同样适用于Timer0和Timer1。
1. 精准延时
在前面的例子中,已经使用了一些与精准延时相关的Arduino库函数:
delay(ms):延迟一段时间
ms:延迟的时长,单位是毫秒
请注意,上面的Arduino库函数使用了Timer0的中断,因此不要在任何中断服务程序中调用它,否则程序可能无法正常工作。
如果需要在延迟的同时,Arduino可以执行其他程序,这需要借助第三方库MsTimer2;你可以在http://playground.arduino.cc/Main/MsTimer2下载到它,把下载到的MsTimer2.zip解压到Arduino IDE安装目录下的libraries文件夹里就可以使用了,它包含了一个示例:
1 // FlashLed.ino 2 #include <MsTimer2.h> 3 4 void flash() { 5 static boolean output = HIGH; 6 7 digitalWrite(13, output); 8 output = !output; 9 } 10 11 void setup() { 12 pinMode(13, OUTPUT); 13 14 MsTimer2::set(500, flash); 15 MsTimer2::start(); 16 } 17 18 void loop() { 19 }
这个示例的效果与Blink示例基本相同,MsTimer2库提供了3个库函数:
MsTimer2::set(interval, function):每隔一段时间执行指定函数
interval:间隔时长,单位是毫秒
function:指定执行函数的名称
MsTimer2::start():开始每隔一段时间执行指定函数
MsTimer2::stop():结束每隔一段时间执行指定函数
Timer2有2个控制寄存器:TCCR2A和TCCR2B,它们的结构如下图所示:
COM2A1 |
COM2A0 |
COM2B1 |
COM2B0 |
|
|
WGM21 |
WGM20 |
FOC2A |
FOC2B |
|
|
WGM22 |
CS22 |
CS21 |
CS20 |
其中WGM2[2:0]位用于设置模式,CS2[2:0]位用于设置时钟源,如下表所示:
WGM2[2:0] |
模式 |
计数上限 |
|
CS2[2:0] |
时钟源 |
000 |
正常 |
0xFF |
|
000 |
无 |
001 |
相位校正脉宽调制 |
0xFF |
|
001 |
系统时钟 |
010 |
比较匹配时清零 |
OCR2A |
|
010 |
系统时钟8分频 |
011 |
快速脉宽调制 |
0xFF |
|
011 |
系统时钟32分频 |
100 |
(保留) |
|
|
100 |
系统时钟64分频 |
101 |
相位校正脉宽调制 |
OCR2A |
|
101 |
系统时钟128分频 |
110 |
(保留) |
|
|
110 |
系统时钟256分频 |
111 |
快速脉宽调制 |
OCR2A |
|
111 |
系统时钟1024分频 |
其余位暂且设置为0。
精准延时可以采用正常模式。正常模式下,Timer2不断从0x00计数到0xFF;当定时计数器寄存器TCNT2每一次返回0x00时,若Timer2的中断屏蔽寄存器TIMSK2的溢出中断使能位TOIE2为1,则产生中断,它的结构如下图所示:
|
|
|
|
|
OCIE0B |
OCIE0A |
TOIE0 |
通过直接访问寄存器改写以上程序为:
1 // FlashLed_reg.ino 2 void flash() { 3 PORTB ^= (1 << PB5); 4 } 5 void setup() { 6 DDRB |= (1 << PB5); 7 8 // 正常模式,系统时钟256分频,计数初值为6 9 TCCR2A = 0x00; 10 TCCR2B = 0x06; 11 TCNT2 = 0x06; 12 13 TIMSK2 |= (1 << TOIE2); 14 sei(); 15 } 16 17 void loop() { 18 } 19 20 ISR(TIMER2_OVF_vect) { 21 static volatile int iTimes = 0; 22 23 // 中断125次为500毫秒 24 if (++iTimes == 125) { 25 flash(); 26 iTimes = 0; 27 } 28 // 重新赋初值6 29 TCNT2 = 0x06; 30 }
程序中的“中断125次为500毫秒”是这样计算出来的:Arduino UNO R3开发板使用16MHz的系统时钟,Timer2使用系统时钟256分频,每记256-6=250个脉冲溢出一次,则每秒溢出16000000÷256÷250=250次,因此每溢出125次为500毫秒。
2. 脉宽调制
脉宽调制的一个典型应用是控制直流电机速度。将直流电机两极分别连接到直流电源的正负两极上,电机会以最快速度运行;要调整电机速度,一个很容易想到的方法是调整直流电源的功率,但在数字系统中还有一个更简单的方式:使用高低电平宽度不一样的脉冲信号快速开关直流电机;因为惯性的作用,电机不会以最快的速度运行。一般来说,高电平在一个脉冲周期中所占宽度更宽时,直流电机速度越快;我们把高电平所占一个脉冲周期的宽度称为占空比。
如图所示连接电路,11(PB3/OC2A)引脚通过三极管间接控制直流电机:
用Arduino库函数输出一个脉宽调制信号十分简单,下面的示例使得直流电机由慢到快,又由快到慢反复运行:
1 // FadingMotor.ino 2 int motor = 11; 3 int speed = 0; 4 int fadeAmount = 5; 5 6 void setup() { 7 pinMode(motor, OUTPUT); 8 } 9 10 void loop() { 11 analogWrite(motor, speed); 12 13 speed = speed + fadeAmount; 14 if (speed <= 0 || speed >= 255) { 15 fadeAmount = -fadeAmount; 16 } 17 18 delay(30); 19 }
与脉宽调制相关的Arduino库函数有:
analogWrite(pin, value):在指定引脚上输出一个指定占空比脉宽调制信号
pin:指定引脚
value:脉宽调制信号的占空比;0为0%,255为100%
Timer2拥有2个输出比较寄存器OCR2A和OCR2B,它们通过与TCNT2寄存器发生比较匹配时对引脚置位、清零或取反来完成脉宽调制信号的输出。TCCR2A寄存器中的COM2A[1:0]位用于设置OCR2A寄存器发生比较匹配时的行为,如下表所示:
COM2A[1:0] |
行为 |
00 |
正常的端口操作,OC2A未连接 |
01 |
WGM22=0,正常的端口操作,OC2A未连接 WGM22=1,发生比较匹配时OC2A取反 |
10 |
发生比较匹配时OC2A清零,计数到下限时OC2A置位 |
11 |
发生比较匹配时OC2A置位,计数到下限时OC2A清零 |
通过直接访问寄存器改写以上程序为:
1 // FadingMotor_reg.ino 2 int speed = 0; 3 int fadeAmount = 5; 4 5 void setup() { 6 DDRB |= (1 << PB3); 7 8 // 快速脉宽调制,OC2A比较匹配置位,下限清零,系统时钟 9 TCCR2A = 0x83; 10 TCCR2B = 0x01; 11 } 12 13 void loop() { 14 OCR2A = speed; 15 16 speed = speed + fadeAmount; 17 if (speed <= 0 || speed >= 255) { 18 fadeAmount = -fadeAmount; 19 } 20 21 delay(30); 22 }
3. 输入捕获*
输入捕获用来计算外部输入信号的周期,它是Timer1的特有功能,Arduino官网提供了一个输入捕获的示例:
1 // PulseIn.ino 2 const int pin = 8; 3 unsigned long duration; 4 5 void setup() { 6 pinMode(pin, INPUT); 7 } 8 9 void loop() { 10 duration = pulseIn(pin, HIGH); 11 }
与输入捕获相关的Arduino库函数有:
pulseIn(pin, value):计算指定引脚输入外部信号的周期
pin:指定引脚
value:捕获脉冲的类型,LOW(低电平,0V)或HIGH(高电平,5V)
函数返回外部输入信号的周期,是一个unsigned long类型的整数,单位为微秒
Timer1的控制寄存器与Timer2的不尽相同,请参阅ATMega328P芯片手册16-bit Timer/Counter1 with PWM章的Register Description小节进行设置。通过直接访问寄存器改写以上程序为:
1 // PulseIn_reg.ino 2 unsigned long duration; 3 unsigned long timer1_pulse_in(); 4 5 void setup() { 6 DDRB &= ~(1 << PB0); 7 PORTB &= ~(1 << PB0); 8 9 // 正常模式,系统时钟8分频 10 // 噪声抑制关闭,下降沿触发输入捕获 11 TCCR1A = 0x00; 12 TCCR1B = 0x42; 13 TCCR1C = 0x00; 14 TIMSK1 |= (1 << ICIE1) | (1 << TOIE1); 15 sei(); 16 } 17 18 void loop() { 19 duration = timer1_pulse_in(); 20 } 21 22 volatile int iOvf = 0; 23 volatile bool isCap = false; 24 volatile uint16_t iCap = 0; 25 volatile uint16_t iLastCap = 0; 26 27 unsigned long timer1_pulse_in() { 28 unsigned long duration; 29 30 while (!isCap); 31 duration = 0xffff - iCap 32 + 0xffff * (iOvf - 1) 33 + iLastCap; 34 35 isCap = false; 36 return duration; 37 } 38 39 ISR(TIMER1_CAP_vect) { 40 iCap = iLastCap; 41 iLastCap = (ICR1H << 8) | ICR1L; 42 43 isCap = true; 44 } 45 46 ISR(TIMER1_OVF_vect) { 47 iOvf += 1; 48 }
事实上,本章仅仅讨论了定时/计数器子系统的典型应用,这仅仅是定时/计数器子系统强大功能的冰山一角,由于篇幅关系没有提及的部分,另请参阅ATMega328P芯片手册。