极客工坊

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 94984|回复: 15

Arduino系列教程之 – PWM的秘密(下)(转)

[复制链接]
发表于 2011-9-23 11:32:08 | 显示全部楼层 |阅读模式
原文地址:http://www.diy-robots.com/?p=852

今天看了看之前的博客,赫然发现有个PWM的秘密(上),第二部分居然被我忘掉了,主要是这个部分实在不太好理解,可以认为是Arduino PWM的提高篇。说实话大部分兄弟平时应该是用不上的,不过可以先收起来,以后真遇到了能知道是怎么回事。

Atmega 168/328的时钟们
ATmega328P有三个时钟,Timer0,Timer1和Timer2。每个时钟都有两个比较寄存器,可以同时支持两路输出。其中比较寄存器用于控制PWM的占空比,具体的原理等会儿会介绍。大多数情况下,每个时钟的两路输出会有相同的频率,但是可以有不同的占空比(取决于那两个比较寄存器的设置)

每个时钟都有一个“预定标器”,它的作用是设置timer的时钟周期,这个周期一般是有Arduino的系统时钟除以一个预设的因子来实现的。这个因子一般是1,8,64,256或1024这样的数值。Arduino的系统时钟周期是16MHz,所以这些Timer的频率就是系统时钟除以这个预设值的标定值。需要注意的是,Timer2的时钟标定值是独立的,而Timer0和Timer1使用的是相同的。

这些时钟都可以有多种不同的运行模式。常见的模式包括“快速PWM”和“相位修正PWM”,这两种PWM的定义也会在后面解释。这些时钟可以从0计数到255,也可以计数到某个指定的值。例如16位的Timer1就可以支持计数到16位(2个字节)。

除了比较寄存器外,还有一些其他的寄存器用来控制时钟。例如TCCRnA和TCCRnB就是用来设置时钟的计数位数。这些寄存器包含了很多位(bit),它们分别的作用如下:
脉冲生成模式控制位(WGM):用来设置时钟的模式
时钟选择位(CS):设置时钟的预定标器
输出模式控制位(COMnA和COMnB):使能/禁用/反相 输出A和输出B
输出比较器(OCRnA和OCRnB):当计数器等于这两个值时,输出值根据不同的模式进行变化

不同时钟的这些设置位稍有不同,所以使用的时候需要查一下资料。其中Timer1是一个16位的时钟,Timer2可以使用不同的预定标器。

快速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,表示两路输出的占空比;
  1.   pinMode(3, OUTPUT);
  2.   pinMode(11, OUTPUT);
  3.   TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
  4.   TCCR2B = _BV(CS22);
  5.   OCR2A = 180;
  6.   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,表示相位修正模式,其他位设置和前面的例子相同:
  1.   pinMode(3, OUTPUT);
  2.   pinMode(11, OUTPUT);
  3.   TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM20);
  4.   TCCR2B = _BV(CS22);
  5.   OCR2A = 180;
  6.   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%

这里的频率计数又多除了一个2,原因上面解释过了。占空比的计算不用加1了,原因自己掰手指头算算就知道了

快速PWM下,修改时钟的计数上限
快速PWM和相位修正PWM都可以重新设置输出的频率,先看看快速PWM是如何设置的。在修改频率的模式下,时钟从0开始计数到OCRA而不是255,注意这个OCRA我们之前是用来做比较用的。这样一来,频率的设置就非常灵活了。对Timer1来说,OCRA可以设置到16位(应该是0~65535)

等等,OCRA用来设置总数了,那么谁用来做比较捏?好吧,灵活的代价就是这种模式下,只能输出一路PWM。即OCRA用来设置总数,OCRB用来设置比较器。
尽管如此,无孔不入的程序员们依然还是设置了一种特殊的模式,每次计数器数到头的时候,输出A做一次反相,这样能凑合输出一个占空比为50%的方波。

下面的例子中,我们依然使用Timer2,Pin3和Pin11。其中OCR2A用来设置周期和频率,OCR2B用来设置B的占空比,同时A输出50%的方波。具体的设置是:
WGM设置为111表示“OCRA控制计数上限的快速PWM”;
OCR2A设置为180,表示从0数到180;
OCR2B设置比较器为50;
COM2A设置为01,表示OCR2A“当数到头是反相”,用来输出50%的方波(其中WGM被设置到了两个变量里);
  1.   pinMode(3, OUTPUT);
  2.   pinMode(11, OUTPUT);
  3.   TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
  4.   TCCR2B = _BV(WGM22) | _BV(CS22);
  5.   OCR2A = 180;
  6.   OCR2B = 50;
复制代码
在Arduino Duemilanove开发板,上面这几行代码的结果为:

输出 A 频率: 16 MHz / 64 / (180+1) / 2 = 690.6Hz
输出 A 占空比: 50%
输出 B 频率: 16 MHz / 64 / (180+1) = 1381.2Hz
输出 B 占空比: (50+1) / (180+1) = 28.2%

其中频率的计算用了180+1,依然是数数的问题;A输出的频率是B输出的一半,因为输出A每两个大周期才能循环一次。

相位修正PWM下,修改时钟的计数上限
类似的,相位修正模式下,也可以修改输出PWM的频率。代码几乎完全和上个例子一样,区别是WGM的值设置为101:
  1.   pinMode(3, OUTPUT);
  2.   pinMode(11, OUTPUT);
  3.   TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM20);
  4.   TCCR2B = _BV(WGM22) | _BV(CS22);
  5.   OCR2A = 180;
  6.   OCR2B = 50;
复制代码
在Arduino Duemilanove开发板,上面这几行代码的结果为:

输出 A 频率: 16 MHz / 64 / 180 / 2 / 2 = 347.2Hz
输出 A 占空比: 50%
输出 B 频率: 16 MHz / 64 / 180 / 2 = 694.4Hz
输出 B 占空比: 50 / 180 = 27.8%

跟之前的对比类似,相位修正模式下,一个大周期从0数到180,然后倒数到0,总共是360个时钟周期;而在快速PWM模式下,一个周期是从0数到180,实际上是181个时钟周期。这可能就是鬼子们说的“更加对称”的好处,好吧,可能老外们其实并不傻。

数不清楚这两者区别的同学,可以用OCRA=3为例:
快速PWM:0123-0123-0123….. 每个周期时钟数是4=3+1
相位修正:012321-012321-012321….每个周期时钟数是6=3*2

相应的占空比计算也有微小的区别,快速PWM模式下,高位的输出会多一个时钟周期。上面的这个例子,以比较器=1为例:
快速PWM:当计数器=1时反相,这时候已经经历了2个时钟周期,所以占空比是2/4
相位修正:计数器0到1时输出0,计数器1到0时输出1,占空比是1/3

一些其他的说明

前面的程序有一个非常疑惑的问题:Pin3和Pin11是怎么和Timer2对应上的呢?这个只能查表了,并不是任意对应的:
时钟输出 | Arduino输出Pin编号 | 芯片Pin | Pin name
OC0A 6 12 PD6
OC0B 5 11 PD5
OC1A 9 15 PB1
OC1B 10 16 PB2
OC2A 11 17 PB3
OC2B 3 5 PD3

一般来说,普通用户是不需要设置这些时钟参数。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之间是有一个跳跃

翻译了半天已经晕头转向了,最后再提醒一点,不是所有的参数配置都可以随意组合的。例如COM2A=01只有在WGM是111或者101时才有效,具体怎么用,还是去官网查表吧

原文链接:http://arduino.cc/en/Tutorial/SecretsOfArduinoPWM
回复

使用道具 举报

发表于 2012-1-13 15:40:55 | 显示全部楼层
学习了,很好。
回复 支持 反对

使用道具 举报

发表于 2012-1-18 09:35:50 | 显示全部楼层
为啥我想到了维多利亚的秘密.........
回复 支持 反对

使用道具 举报

发表于 2012-5-5 09:00:45 | 显示全部楼层
我也看过,那个博主挺敬业!!
回复 支持 反对

使用道具 举报

发表于 2012-5-17 10:24:14 | 显示全部楼层
这个真的不错,虽然是翻译的,但是很和我的口味
回复 支持 反对

使用道具 举报

发表于 2012-5-17 10:32:03 | 显示全部楼层
AVR关于定时器的一大堆寄存器最让人头痛了~~~
回复 支持 反对

使用道具 举报

发表于 2012-7-24 10:01:01 | 显示全部楼层
原本以为arduino只是简单的控制io口呢~原来arduino也可以像51那样设置寄存器,受教了~还是比51简单点~
回复 支持 反对

使用道具 举报

发表于 2013-12-5 13:13:34 | 显示全部楼层
本帖最后由 swfzz 于 2013-12-5 14:05 编辑

表示看不懂啊,首先:
TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
上面这段是位或得出来值?
然后COM2A,还有WGM是值是如何设置的啊?看了另一个程序也没有看到任何赋值的代码,系统给的?
比如我现在要2560的板子,给个不管是相位还是快速PWM,频率10K请问下应该咱整啊?

是不是控制频率由CS来控制?
回复 支持 反对

使用道具 举报

发表于 2014-12-17 11:43:22 | 显示全部楼层
那么如果要像地铁那种声音改怎么搞呢?就是像加到一定频率跳到更高的频率再加。简称就是VVVF了
回复 支持 反对

使用道具 举报

发表于 2015-1-4 14:43:35 | 显示全部楼层
学习拉 , 纯寄存器控制
回复 支持 反对

使用道具 举报

发表于 2015-1-7 19:48:34 | 显示全部楼层
如果是这样的话,拿Timer2的PWM输出为例,Pin3和Pin11必须同时作为PWM输出使用。
如果只想用一个口作为PWM输出,另外一个口作为普通的数字口是否可以呢?
回复 支持 反对

使用道具 举报

发表于 2015-3-16 20:59:52 | 显示全部楼层
弘毅大哥,为什么你的pwm库中关于timer2的寄存器,TIMER2A输出11脚的pwm波没有写进去,导致11脚没有pwm输出,我修改代码将TIMER2A插进去发现还是不行,连TIMER2B3引脚都没有了,求指教?修改代码如下,有关TIMER2A的我都插进去了
  1. switch(digitalPinToTimer(pin))
  2.                 {
  3.                         case TIMER0B:
  4.                         sbi(TCCR0A, COM0B1);
  5.                         regLoc8 = OCR0B_MEM;
  6.                         top = Timer0_GetTop();
  7.                         break;
  8.                         case TIMER1A:
  9.                         sbi(TCCR1A, COM1A1);
  10.                         regLoc16 = OCR1A_MEM;
  11.                         top = Timer1_GetTop();
  12.                         break;
  13.                         case TIMER1B:
  14.                         sbi(TCCR1A, COM1B1);
  15.                         regLoc16 = OCR1B_MEM;
  16.                         top = Timer1_GetTop();
  17.                         break;
  18.                 //        case TIMER2A:
  19.                 //        sbi(TCCR2A, COM2A1);
  20.                 //        regLoc8 = OCR2A_MEM;
  21.                 //        top = Timer2_GetTop();
  22.                 //        break;
  23.                         case TIMER2B:
  24.                         sbi(TCCR2A, COM2B1);
  25.                         regLoc8 = OCR2B_MEM;
  26.                         top = Timer2_GetTop();
  27.                         break;
  28.                         case NOT_ON_TIMER:
  29.                         default:
  30.                         if (val < 128)
  31.                         digitalWrite(pin, LOW);
  32.                         else
  33.                         digitalWrite(pin, HIGH);
  34.                         return;
  35.                 }
复制代码
回复 支持 反对

使用道具 举报

发表于 2016-11-23 16:51:38 | 显示全部楼层
弘毅大哥您好,有几个问题想请教您
(1)
  1. 这句话在库文件之前啥意思?
  2. (2),您发过修改库文件TIMER1的参数而输出PM波的帖子,您说TIMER0一般不要修改,那最多一个ARDUINO NANO只能输出4路控制频率PWM波,请问有办法能输出5路PWM波吗?对精度和频率有要求
  3. (3)
  4. 采用了弘毅大哥的pwm波的库,我设置四路pwm波输出,频率100Hz,代码如下
  5. [code]#include <PWM.h>

  6. int pwm1 = 9;  // led所连接到的引脚
  7. int pwm2 = 10;
  8. int pwm3 = 11;
  9. int pwm4 = 3;
  10. int speed1 = 33;         // led的亮度,也就是占空比,范围是0-255
  11. int speed2 = 33;
  12. int speed3 = 33;
  13. int speed4 = 33;
  14. int32_t frequency = 100; // 频率 (单位是Hz)

  15. void setup()
  16. {
  17. // 初始化除了0号计时器以外的其他计时器
  18. InitTimersSafe();
  19. // 设置指定引脚的频率
  20. bool success1 = SetPinFrequencySafe(pwm1, frequency);
  21. bool success2 = SetPinFrequencySafe(pwm2, frequency);
  22. bool success3 = SetPinFrequencySafe(pwm3, frequency);
  23. bool success4 = SetPinFrequencySafe(pwm4, frequency);
  24. }

  25. void loop()
  26. {
  27. pwmWrite(pwm1, speed1);
  28. pwmWrite(pwm2, speed2);
  29. pwmWrite(pwm3, speed3);
  30. pwmWrite(pwm4, speed4);
  31. delay(30);      
  32. }

复制代码

但是第三路11号引脚没有pwm波输出,其他三路pwm波都是正确的,3号引脚和11号引脚用的定时器2,不知道是代码哪里不对还是要修改库文件??
(4)我用ARDUINO官网下载的编译器编译您的那个程序#include <PWM.h>

int led = 9;                // led所连接到的引脚
int brightness = 100;         // led的亮度,也就是占空比,范围是0-255
int32_t frequency = 100; // 频率 (单位是Hz)

void setup()
{
  // 初始化除了0号计时器以外的其他计时器
  InitTimersSafe();

  // 设置指定引脚的频率
  bool success = SetPinFrequencySafe(led, frequency);
  
}

void loop()
{
  pwmWrite(led, brightness);
  delay(30);      
}
编译不l了,说没用声明,是要把库文件添加到某处吗?
谢谢



回复 支持 反对

使用道具 举报

发表于 2016-12-3 12:18:04 | 显示全部楼层
感谢分享,学习了。
回复 支持 反对

使用道具 举报

发表于 2017-4-13 17:59:26 | 显示全部楼层
频率计算公式 能发个完整的8 16位的吗? 谢谢
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则 需要先绑定手机号

Archiver|联系我们|极客工坊

GMT+8, 2024-3-29 21:00 , Processed in 0.068658 second(s), 32 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表