Arduino Uno输出自定义频率的PWM(详解)

Arduino Uno输出自定义频率的PWM(详解)_第1张图片

PWM

PWM(Pulse Width Modulation)是一种方波控制信号。方波高电平的宽度在一个周期里的占比被称为占空比(Duty Cycle)。改变PWM的占空比,可以改变输出信号的平均电压,实现模拟电压的输出。
https://www.arduino.cc/en/Tutorial/SecretsOfArduinoPWM (官网)

简单的说 ,就是在一些情况下,GPIO脚位不在的切换“有电”“没电”,
每秒种循环的几次即为其 Frequency(频率),
每次“有电”时间站一个循环的百分比称为其占空比

Arduino里的PWM

首先,Arduino Uno的5,6,9,10,3,11接口可以通过简单语句analogWrite(pin, dutyCycle)来实现一个指定占空比的PWM。其中pin的值选择(5,6,9,10,3,11),dutyCycle的值在0~255之间,0为占空比0%,255为占空比100%。但是这种方式PWM信号的频率是固定的默认值,大约1000Hz左右(16MHz/64/256)。

其次,手动切换高电平和低电平,再在中间加入delay函数,可以实现自定义频率的PWM:

void setup()
{
  pinMode(13, OUTPUT);
}

void loop()
{
  digitalWrite(13, HIGH);
  delayMicroseconds(100); // Approximately 10% duty cycle @ 1KHz
  digitalWrite(13, LOW);
  delayMicroseconds(1000 - 100); //修改这里的1000可以调整频率
}

这个例子中:一个循环是1000us = 1ms,所以一秒循环1000次,因此Frequency是1KHz,
每个循环中:有电的比率是100/1000 * 100% = 10%,所以duty cycle(占空比)为10%
这样就可以模拟出5V * 10%=0.5V的电压
好处是任何一个引脚都能通过这样输出PWM,
但是,这种操作需要CPU全神贯注的查数,任何其他的进程的干扰会导致输出的信号频率不准。

综上,需要底层的手段来控制Arduino实现PWM的频率调节。

调节Arduino里的时钟频率

Arduino Uno里有三个Timer:Timer0,Timer1,Timer2。 三个Timer都可以自定义调整频率,但是各有特点。Timer0负责控制delay等函数,动了Timer0的频率会导致计时函数不准;Timer1的计数器是16位的,和Timer0,Timer2的8位计数器不太一样;Timer2的频率可调的档位更多,因为它有7档预除数,下文会进一步解释。这里选择Timer2进行调节操作,先上代码:

void setup() {
  // put your setup code here, to run once:

  pinMode(3, OUTPUT); 
  pinMode(11, OUTPUT); 
  
  TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20); //Set Timer2 to varying top limit fast PWM mode
  TCCR2B = _BV(WGM22) | _BV(CS22) | _BV(CS21) | _BV(CS20);//another way to set prescaler CS2=fff
  
  OCR2A = 155; //Top value A
  OCR2B = 30; //Toggle value B, Output at pin 3
 
  //CS2  Divisor  Frequency
  //001    1        31372.55
  //010    8        3921.16
  //011    32       980.39
  //100    64       490.20   <--DEFAULT
  //101    128      245.10
  //110    256      122.55
  //111    1024     30.64
}

void loop() {
  // put your main code here, to run repeatedly:

}

由以上代码可见,需要的设置分为三个部分:pinMode,TCCR2A/B,OCR2A/B(这里的2是因为选择了Timer2)。

脉冲生成模式控制位(WGM):用来设置时钟的模式

时钟选择位(CS):设置时钟的预定标器

输出模式控制位(COMnA和COMnB):使能/禁用/反相 输出A和输出B

输出比较器(OCRnA和OCRnB):当计数器等于这两个值时,输出值根据不同的模式进行变化

  • pinMode:
    Timer2所控制的管脚是pin11和pin3(Timer0控制5,6;Timer1控制9,10–这是chip的 datasheet上规定),所以把这两个管脚设为输出。

  • OCR2A/B:
    这部分的解释需要提到Timer的结构和原理。
    每个Timer里都有一个计数器和两个比较寄存器。Timer2里计数器从0数到255(8位)然后归0继续从头数;Timer2的两个比较寄存器分别为OCR2A和OCR2B。

    比较寄存器就是你设置一个小于255的数,比如155。当计数器数到0时输出为高电平,数到155的时候改变输出为低电平。这样就实现了占空比的调节。在普通模式下(Fast
    PWM),OCR2A控制pin11的占空比,OCR2B控制pin3的占空比。如下图所示

Arduino Uno输出自定义频率的PWM(详解)_第2张图片

  • TCCR2A/B:
    理解了Timer的原理,下面来讨论这个PWM的频率。Arduino Uno的芯片ATmega328,晶振频率为16MHz。Timer计数器的频率会在这个基础上除以一个预除数,Timer2可选择的预除数有(1,8,32,64,128,256,1024)。也就是说,如果预除数设为64(默认),计数器计数的频率是16MHz/64 。又因为计数器要数256下才会完成一个PWM周期,所以输出PWM的频率16MHz/64/256(Frequency = 16 MHz / 64 / 256 = 976.5625Hz), 约等于1000Hz。若果要获得最低的输出频率,预除数要选1024,得到的PWM是61Hz。

    TCCR2A/B就是来控制Timer2计数器的模式与预除数的大小的,由于是分位赋值,看起来怪怪的,我来解释一下。先说CS2位,这个就是来控制Timer2计数器预除数的: _BV(CS22) | _BV(CS21) | _BV(CS20)的三部分由逻辑按位或“|”链接;每个BV是按位赋注(bit value)的意思;_BV(CS22 )= 在CS2里,1<<2(把1左移2位) = 00000100;得到三部分分别是00000100,00000010,00000001;按位或最终得到0111;查代码里的表得到对应的预除数是1024。

  • 模式选择:
    现在的问题是,我需要的是100Hz,不是1024预除数下的61Hz,如何实现?这就需要控制计数器模式来微调频率。请看下图:

Arduino Uno输出自定义频率的PWM(详解)_第3张图片

这张图中的模式可以在原有的fast PWM基础上提高频率,得到图中OCnB所示的信号。这个模式叫做Varying the timer top limit: fast PWM。比较寄存器OCR2A在这里不再控制管脚11的占空比,而是设定一个计数器的上限:计数器不用数到255而是达到OCR2A就可以归零。OCR2B依然控制管脚3的占空比。

为了让pin11有活干,这里设置TCCR2A里的COM2A位=01(表示数到极限就把pin11的电平反转,本应用不需要),COM2B位=10(表示pin3输出非反转PWM)。

那么是如何选择模式的?剩下的WGM2位就是确定模式的。在fast PWM模式下,WGM2位是011,Varying the timer top limit: fast PWM模式下,WGM2位是111。所以需要_BV(WGM22) | _BV(WGM21) | _BV(WGM20)。处于我所不理解的原因,这个赋值可以被分为两部分分别写在TCCR2A和TCCR2B里。有明白的高手麻烦留个言解释一下。

频率计算

到这里,所有设置已经解释完。下面来计算一下100Hz输出的PWM具体参数应该设为多少。

pin3的输出频率=16MHz / 1024/ (OCR2A + 1),因此100Hz对应的OCR2A=155。(+1是因为fast PWM是从0开始数到上限值)
占空比 = (OCR2B+1)/ (OCR2A+1),所以:

占空比 OCR2B值
20% 30
25% 38
33% 51
50% 77
100% 155

常见的模式(“快速PWM”和“相位修正PWM”)

快速PWM

对于快速PWM来说,时钟都是从0计数到255。当计数器=0时,输出高电平1,当计数器等于比较寄存器时,输出低电平0。所以输出比较器越大,占空比越高。这就是传说中的快速PWM模式。后面的例子会解释如何用OCRnA和OCRnB设置两路输出的占空比。很明显这种情况下,这两路输出的周期是相同的,只是占空比不同。

快速PWM的例子

下面这个例子以Timer2为例,把Pin3和Pin11作为快速PWM的两个输出管脚。其中:
WGM的设置为011,表示选择了快速PWM模式;
COM2A和COM2B设置为10,表示A和B输出都是非反转的PWM;
CS的设置为100,表示时钟周期是系统时钟的1/64;
OCR2A和OCR2B分别是180和50,表示两路输出的占空比;

pinMode(3, OUTPUT);  
pinMode(11, OUTPUT);  
TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);  
TCCR2B = _BV(CS22);  
OCR2A = 180;  
OCR2B = 50;  

这段代码看上去有点晕,其实很简单。_BV(n)的意思就是1< COM2A1,表示COM2A的第1位(靠,其实是第2位,不过程序员们是从0开始数数的)。所以_BV(COM2A1)表示COM2A = 10;
类似的,_BV(WGM21) | _BV(WGM20) 表示 WGM2 = 011。

在Arduino Duemilanove开发板,上面这几行代码的结果为:
输出 A 频率: 16 MHz / 64 / 256 = 976.5625Hz
输出 A 占空比: (180+1) / 256 = 70.7%
输出 B 频率: 16 MHz / 64 / 256 = 976.5625Hz
输出 B 占空比: (50+1) / 256 = 19.9%
频率的计算里都除以了256,这是因为除以64是得到了时钟的计数周期,而256个计数周期是一个循环,所以PWM的周期指的是这个循环。

另外,占空比的计算都加了1,这个还是因为无聊的程序员们都从0开始计数。

相位修正PWM

另外一种PWM模式是相位修正模式,也有人把它叫做“双斜率PWM”。这种模式下,计数器从0数到255,然后从255再倒数到0。当计数器在上升过程中遇到比较器的时候,输出0;在下降过程中遇到比较器的时候,输出1。说实话,我觉得这种模式除了频率降低了一倍之外,没看出和快速PWM有什么区别。可能是在集成电路的底层级别上有区别吧。原文说“它具有更加对称的输出”,好吧,也许老外都比较傻吧。

相位修正PWM的例子

下面的例子还是以Timer2为例,设置Pin3和Pin11为输出管脚。其中WGM设置为001,表示相位修正模式,其他位设置和前面的例子相同:

pinMode(3, OUTPUT);  
pinMode(11, OUTPUT);  
TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM20);  
TCCR2B = _BV(CS22);  
OCR2A = 180;  
OCR2B = 50;  

在Arduino Duemilanove开发板,上面这几行代码的结果为:
输出 A 频率: 16 MHz / 64 / 255 / 2 = 490.196Hz
输出 A 占空比: 180 / 255 = 70.6%
输出 B 频率: 16 MHz / 64 / 255 / 2 = 490.196Hz
输出 B 占空比: 50 / 255 = 19.6%

一般来说,普通用户是不需要设置这些时钟参数。Arduino默认有一些设置,所有的时钟周期都是系统周期的1/64。Timer0默认是快速PWM,而Timer1和Timer2默认是相位修正PWM。具体的设置可以查看Arduino源代码中writing.c的设置。

需要特别特别注意的是,Arduino的开发系统中,millis()和delay()这两个函数是基于Timer0时钟的,所以如果你修改了Timer0的时钟周期,这两个函数也会受到影响。直接的效果就是delay(1000)不再是标准的1秒,也许会变成1/64秒,这个需要特别注意

在程序中使用analogWrite(pin, duty_cycle)函数的时候,就启动了PWM模式;当调用digitalWrite()函数时则取消了PWM模式。请参考wiring_analog.c和 wiring_digital.c文件。

还有一件很有意思的现象,对于快速PWM模式,如果我们设置analogWrite(5, 0),实际上应该有1/256的占空比,事实上你会发现输出的是永远低电平的0。这个实际上是在Arduino系统中强制设定的,如果发现输入的是0,那么就关闭PWM。随之而来的问题是,如果我们设置analogWrite(5, 1),那么占空比是多少呢?答案是2/256,也就是说0和1之间是有一个跳跃

你可能感兴趣的:(C/C++,Arduino,Maker,Arduino,PWM,定时器)