适用于初学者的AVR教程。
目标
理解定时器的定义,学会使用定时器发生间隔1ms的中断(代码1、2)。通过定时器使LED进行Diming(PWM)。
使用到的硬件
- Arduino uno
- 面包板
- 杜邦线
- 电阻
- LED
电路图
Atmega328P的定时器有两种:8bit和16bit。本文使用的是8bit定时器timer0。该定时器有七种模式,最典型的应用是普通模式、CTC模式和快速PWM模式。
对于普通模式和CTC模式,示例代码使用的是Pin13上的LED,这意味着不需要任何接线。对于快速PWM模式。示例代码使用的是Pin6的PWM功能(OC0A)。该示例接线很简单,将电阻和LED简单串联即可。也可以使用AVR 编程 ATMEGA328P GPIO 基础应用中的三极管开关接法。
完整代码
1.普通模式:
1 |
|
2.CTC模式:
1 |
|
3.快速PWM模式(LED Diming):
1 |
|
Timer和PWM
假定读者对PWM调波和定时器有一定的了解,因为手头没有示波器可用,无法很具体的讲解,本文对此仅作简单描述。但是作为最基础的电气知识,即使是百度百科也写的很清楚,如果没有相对的知识储备,建议最少阅读一下百度百科:脉宽调制。
对于定时器的概念:定时器可以看作是一个带有比较逻辑结构的累加器。它的工作原理是不断的给一个数+1,然后将这个数与预设数作比较,当达该数到预设值时溢出并重置。
关于PWM,它经常被用于调节方波的占空比。所谓占空比指的是高电平占总带宽的比例,这里占指的是占用,空指的是空置。计算公式为:d=T_h/(T_l+T_h )。以8bit分辨率的PWM方波为例,高低电平分布为0x00->0xff。假设分布为0xf0:高高高高低低低低,那么它的占空比就是d=4/(4+4)=50%。控制占空比又有什么作用呢?以示例Diming为例,占空比越高LED的亮度就越大。当然,最典型的应用还是在电机上,比如vfc和foc。
Atmega328P的定时器有两种:8bit和16bit。二者最明显的区别在于当应用于PWM时,16bit产生的方波分辨率会更高。用上文提到的占空比公式可以计算出来,8bit定时器产生的最小占空比为1/8,而16bit的为1/16,两倍的分辨率。
那么是不是分辨率越高越好呢?
首先需要弄懂定时器到底是怎么工作的。如上图所示,定时器从某个值(通常为0)不断累加并进行判断是否达到了预设值,如果达到了就溢出重置初始值。具体在硬件上的累加可以理解为这是一个8bit存储单元,给定其初始值并不断累加,当达到预设值时产生溢出标志,并重置该值。
那么怎么计时呢?以Atmega328P为例,它的时钟源是由一个16MHZ的晶振提供的,这意味着它一秒可以计数16M次。那么定时器在不分频的情况下每加1一次,过去的时间就是(1/16M)s。对于分频,分频是将时钟源分成n等份。比如最常用的64分频下每秒可以计数250000次。那么假定在64分频下使用8bit分辨率的定时器,每分钟会溢出多少次呢?假设初始值为0,预设值为0xff(即255)。那么该定时器每秒钟溢出的次数为:16MHZ/(642^8),大约是98次。由此可以得出时钟带宽公式:f=f_clk/(n2^8 )。
现在回到那个问题上:分辨率是不是越高越好呢?根据带宽公式可以得出这样一个结论,当你的分辨率越高时你的时钟带宽就越小。所以根据不同的需求,分辨率不一定是越高越好。那么16bit定时器是不是一定比8bit的好呢?是的,因为可以通过改变预设值将16bit定时器应用为8bit定时器。
如何实现1ms精准定时及如何产生PWM方波
本文使用的是8bit定时器timer0。该定时器有七种模式,最典型的应用是普通模式、CTC模式和快速PWM模式。
普通模式和CTC模式可以用于精准定时。在上文中提到过,时钟分辨率是通过初始值和预设值来确定的,二者相减就是时钟分辨率。在Atmega328P上有如下的定义:
| 名称 | 定义 |
| :—-: | :—-: |
| BOTTOM | 底部值时0x00|
|MAX | 峰值0xff |
|TOP | 预设值 |
简单来说,对于CTC模式,底部值是BOTTOM,溢出值是TOP(即寄存器OCRnx内的值)。对于普通模式,底部值是寄存器TCNTn内储存的值,溢出值是MAX。二者在实际使用效果上没有区别。
快速PWM模式用于生成单斜率方波,对应的还有PWM相位矫正模式是双斜率的。二者的区别在于快速PWM模式在MAX/TOP溢出,而相位矫正模式则从MAX/TOP再减到BOTTOM。那么定时器是如何生成PWM方波、又是如何调节占空比的呢?
上图是快速PWM模式的时序图,每个周期被平均分为256份(8bit分辨率),OCnx为在该周期内高电平的份数。比如输出一个50%占空比的方波信号,OCnx值为127。该值储存在寄存器OCRnx内。n指的是Timer0/1/2,x指的是通道A/B。
关于PWM双通道,Atmega328P的定时器在使用到对比模块的模式下被设计为双通道模式,包括并不限于快速PWM和CTC模式。每个通道都有一个预设值寄存器OCRnA/B。在CTC模式下,可以通过分别赋予OCRnA/B不同的值来实现双通道独立计时。在快速PWM模式下则可以通过双通道设计,产生两个占空比不一样的方波。
那么该如何实现1ms精准定时、如何产生PWM方波呢?
上图是定时器0的各个模式的寄存器配置表、分频配置表及相关寄存器。使用定时器,首先要配置它的模式。然后再配置它的分频,接着再配置通道数和它们的预设值(比如OCRnx或TCNTn)。最后再根据需要来决定是否要配置中断。
以CTC模式为例,从上表中可以看到,CTC模式为模式3,其配置方法为将寄存器TCCR0A 上的WGM01位置1。使用64分频,即将寄存器TCCR0B上的CS01和CS00位置1。然后选用单通道、通道A,即将寄存器TCCR0A上的COM0A1位置1。
1 | TCCR0A = _BV(WGM01) | _BV(COM0A1); |
关于选通,以通道A为例。上图可以看到有四个选项。第一项是禁用该通道。第二项是更改Pin-OCnA的电平状态,一般在PWM模式下使用。第三项是常用的清零,即当溢出时,计数器值变为0x00。第四项是置位,此时类似于普通模式下的TCNTn,一般不用。
要产生1ms的标准时间,已知在64分频下一秒可以累加250000次,那么只要让定时器每累加250次溢出一次就可以得到精确的时间。需要注意的是溢出到0x00也记作一次累加,所以预设值OCR0A为249,即:OCR0A = 249
。
假设需要中断,那么将掩码寄存器和标志寄存器的A通道置1。
1 | TIMSK0 = _BV(OCIE0A); |
普通模式与CTC模式配置方法类似,此处不再详述。其与CTC模式的区别在于普通模式没有双通道。且其计数方式为从预赋值TCNTn到MAX。所以不需要配置OCR0A,而需要配置TCNT0。同样以64分频的1ms定时为例,TCNT0为5,即:TCNT0=5
。
注意:这里不需要减1,因为累加范围为5-255,刚好250次。
关于AVR的中断用法和中断向量表,请见AVR 编程 ATMEGA328P 中断基础。
PWM相对较难,其难点主要在于双通道选通。从模式配置表中可以看到有两个快速PWM模式,其区别在于TOP值和溢出值上。模式3的溢出值是MAX,即0xff。这意味着它的带宽是固定的8bit。模式7的溢出值是OCRA,这意味着它的带宽是不定的,且在该模式下由于没有更多的寄存器来调控占空比,它的占空比恒定位50%。
上图是PWM模式下通道A选通的选项。第一项还是禁用。第二项应用在模式7上,生成一个占空比为50%的方波信号,其带宽取决于OCRA。原理是第一个波段高电平,下一个波段反转电平。第三项、第四项用在模式3上,取决于你的方波信号是否需要反转(比如电机倒转)。
示例代码旨在实现LED的Diming,所以需要通过改变方波的占空比来达到更改LED亮度的目的。上文已经解释了模式3和模式7的区别,对于LED调光需要使用模式3,因为改变的是占空比而不是带宽。所以寄存器TCCR0A的WGM01和WGM00需要置1。关于选通,选择通道A禁用通道B,且不需要反转,即寄存器WGM01的COM0A1置1。分频与CTC一致,选择64分频。
1 | TCCR0A = (1 << COM0A1) | (1 << WGM01) | (1 << WGM00); |
上图是Arduino的引脚图,已知开启了Timer0的A通道作为PWM输出引脚,所以要在引脚图上找到OC0A引脚,即PD6。直接将LED和电阻串接到该引脚,LED的Diming电路就接好了。
对于Diming的算法,示例提供了一个很简单的动态占空比的算法。原理是亮度变量b(brightness)和控制变量c(control)相加得到即时亮度,当亮度和超过255或者低于0时,反转c的正负号。这样亮度就会在0->255之间变化。
注意:PWM模式是没有中断的,但是可以通过定时器的普通模式和CTC模式来模拟产生PWM方波。
关于本文
Arduino定时器功能挺难找到中文的讲解的,大部分写的都是一知半解(比如很难找到关于Toggle这个单词的解释)。有一篇德语的文章写的非常棒,作者还用示波器对比了定时器各个模式下的波形。如果想完整的了解Arduino的PWM功能,可以区看看:Timer und PWM – Teil 1 (8 Bit Timer0/2)。
本文还有许多需要完善的地方,因为关于定时器的东西比较多,讲解起来比较复杂。后续可能会在有时间的时候进行修补。再次推荐上面的链接。