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

今天看了看之前的博客,赫然发现有个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,表示两路输出的占空比;

  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%

这里的频率计数又多除了一个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被设置到了两个变量里);

  pinMode(3, OUTPUT);
  pinMode(11, OUTPUT);
  TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
  TCCR2B = _BV(WGM22) | _BV(CS22);
  OCR2A = 180;
  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:

  pinMode(3, OUTPUT);
  pinMode(11, OUTPUT);
  TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM20);
  TCCR2B = _BV(WGM22) | _BV(CS22);
  OCR2A = 180;
  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



对 “Arduino系列教程之 – PWM的秘密(下)” 的 37 条 评论

  1. ccen3020 说:

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

    =======================
    记得我们做电路比赛的时候老师说过,我记得不太清了,说错见谅~

    MCU的PWM波是通过生成一个波形然后通过一个比较器来获得方波的

    普通的单片机(我们用的是C8051F120)是通过产生锯齿波比较获得PWM波

    而DSP(TMS320C28335)则能产生三角波来获得PWM波

    因为三角波的对称性比锯齿波好,所以能够减少谐波

    之类之类,具体原理我不太明白(当时大一……什么都没学呢),反正是这么说的恩

  2. wlreg 说:

    教程的大部分是看懂了,但是有一点地方没有看懂
    ——————————————————————
    pinMode(3, OUTPUT);
    pinMode(11, OUTPUT);
    TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
    TCCR2B = _BV(WGM22) | _BV(CS22);
    OCR2A = 180;
    OCR2B = 50;
    ——————————————————————
    前面的WGM都是在TCCR2A里设置的,但是上面这段WGM的第2位和第0位是在TCCR2A里设置,第1位是在TCCR2BA里设置,我就感觉有点凌乱了,那个WGM是不是在TCCR2A或者TCCR2B里设置都可以呢?WGM,COMnA,COMnB还有CSn是不是都是在TCCRnAhe TCCRnB里设置,而OCRnA和OCRnB是独立出来设置的呢?谢谢

    • 呵呵,你果然凌乱了。WGM20指的是 WGM2的第0位。
      不过WGM确实是被分到两个字节里设置的,不好理解。就像三个人去饭店吃饭,不巧的是桌子不够,正好其中有一个桌空一个位,另一个桌空两个位。这三个人只好将就一下分到两个桌吃饭。
      这种情况对习惯windows开发的人是不可思议的,但是在单片机里,寄存器非常少,只好这么处理了。

    • zxzxy1988 说:

      单片机分为8位,16位,32位的,你可以理解为,8位的单片机,每次可以取8个bit(1个char型字符),16位和32位同理,每次取16bit和32bit(大小分别是2个和4个char字符)。
      为了对很多外设进行控制,因此会有各种“控制寄存器”,比如这里的TCCR2BA等等。一般的,每个寄存器都会有一个名字,而且功能相近的寄存器,名字会一样(比如TCCR2A和TCCR2B)。
      每个“控制寄存器”里面,会分割成不同的小块儿,每一个小块儿分别管不同的功能。(因为单片机资源有限么,所以就合并功能,比如高位的4bit管一个事儿,低位的4bit管一个事儿),这每一个小块也有一个名字(比如WGM2, WGM1)
      再为了节省资源,比如有个寄存器占了4bit,第二个寄存器占了5bit,那么如果分别用两个字节,就分别有4bit和3bit的浪费,啥都干不了了。为了省钱么,在16位单片机中,就把连着的两个char数据拿来(因为一次就能拿2个char),一共16bit,用了4+5bit之后,还能剩下7bit干别的事儿。
      但是这种情况在8位单片机中不大常见,因为8位单片机一次只能取1个char,再修改的时候,需要再取出连着的另一个char再修改。假如在这个时候发生了什么不该发生的事儿(比如来了个中断,另一个寄存器来不及修改),可能就会影响整体的性能和稳定性。
      做了这么多年单片机,一个感觉就是…资源真得省着用啊,不然动不动就不够用!!

      • 是啊,8位的太老了。以后Android系统也许可以开放一些控制接口,直接控制硬件,那就爽了。
        内存和存储随便用,CPU到1G 。
        哈哈,到时候数组我创建两个,用一个,闲置一个,哼哼

  3. 求助老男孩 说:

    我想问下如果用Arduino直接输入PWM来控制电脑的4pin风扇,还需要对输出的信号做处理是吗?我直接输出值,风扇表示不理会啊

    • 不同的风扇驱动方式差别很大,如果是4针的话,很可能是一根地线,其他3根依次升高或者降低,直接用PWM肯定是不行的

      • 求助老男孩 说:

        那可行的办法就是PWM调制输出5-12V来调节,这样会有什么不良后果吗

      • 浪客颖 说:

        新一代的电脑CPU风扇是4pin的,我查了一下,应该是地线,12V供电,测速和PWM。

        PWM作为风扇转速的控制信号,并非直接用这个PWM信号去让12V电压时断时续,PWM信号中真正有用的是其中的直流分量,需要通过一个低通滤波器滤出PWM信号中的直流分量,再用这个直流分量去控制晶体管或IC,从而把12v电压转换成风扇需要的驱动电压,也就是说,PWM风扇最终也是通过改变电压来实现风扇转速的变化。

        4pin风扇中有IC,从而改变风扇电压,来改变转速。

        我想问问这样Arduino uno 是否就可以控制4pin风扇了:
        “风扇直接接12V电源,通过Arduino UNO输出pwm信号给4pin CPU风扇来控制呢?
        并且通过收集转速信号。”???

  4. 菜鸟小码农 说:

    如果使用计时器来做PWM是不是就无法通过上位机实时控制周期了啊。。。我用高低电平做的总是会多出1到2毫秒左右。。。想要做十分精确的实时控制PWM怎么办???求大大指点啊

  5. Janitor 说:

    感谢博主的经验分享。
    但是对于原文中以下部分我有一些疑惑。
    “这段代码看上去有点晕,其实很简单。_BV(n)的意思就是1< COM2A1,表示COM2A的第1位(靠,其实是第2位,不过程序员们是从0开始数数的)。所以_BV(COM2A1)表示COM2A = 10;类似的,_BV(WGM21) | _BV(WGM20) 表示 WGM2 = 011。”
    从ATmega328P datasheet 中的 Register summary中可以找到8位寄存器TCCR2A。COM2A1,COM2B1,WGM21,WGM20是TCCE2A中的位的名称,本身不是寄存器。
    Address Name Bit7 Bit6 Bit5 Bit4 Bit3Bit2 Bit1 Bit0
    (0xB0) TCCR2A COM2A1 COM2A0 COM2B1 COM2B0 – – WGM21 WGM20
    所以_BV(COM2A0)即1<<COM2A0即01000000,依次类推命令
    TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM21) |_BV(WGM20)
    可以转化为
    TCCR2A = 01000000 | 00100000 | 00000010 | 00000001

    TCCR2A = 01100011

    • 探索者 说:

      对于初学者,这个定时器的知识点也折腾了好久,看了博主的文章算有了初步认识,JANUTor 的解释 完全彻底的让我搞明白了。

    • sunny 说:

      1<<COM2A0即01000000这个是为什么呀

  6. 巧克力面包 说:

    arduino 可以产生连续变化频率的PWM波吗

  7. 流云神行 说:

    现在正在做入门的蓝牙控制小车,遇到一个问题,我用serialEvent函数接受蓝牙数据,loop函数为空。然后用timer2每100毫秒中断一次,在中断里面执行PWM控制小车,因为是两轮,所以PWM用的是TIMEER1的pin口,发现当我执行analog(ENA,0)的时候,车轮不定时转动一次,原因不知道。。。

  8. 比利丸 说:

    请教老男孩。作为一名接触Arduino才一个多学期得高中狗,有哪些基础资料或书籍可以帮助我去理解这一整页晦涩难懂的PWM知识,或者说我应该从哪儿下手学习0.0

  9. caida0204 说:

    请问可以查表的官网有链接吗

  10. 小白 说:

    还是一头雾水,我要12.5k的频率要怎么整?算半天算不到这个频率去,求大神赐教

    • 小白2 说:

      pinMode(11, OUTPUT);
      TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
      TCCR2B = _BV(WGM22) | _BV(CS22);
      OCR2A = 9;

      • 小白3 说:

        但是我不知道占空比怎么调

        • 小白4 说:

          终于整出来12.5k占空比怎么调了
          pinMode(3, OUTPUT);
          pinMode(11, OUTPUT);
          TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM20);
          TCCR2B = _BV(WGM22) | _BV(CS22);
          OCR2A = 10;
          OCR2B = 9; //占空比调这个,自己示波器慢慢实验

          • 小白5 说:

            但是这样浪费了一路11引脚,只有第三引脚可以用,有没有办法第11引脚也能使用吗?

          • 小白5 说:

            这个占空比精度也太差了,1到9总共只有9档可调,求更高精度的调节方法

  11. 小白6 说:

    找到问题了_BV(CS22)等于10进制4改3,分频比256就改为64或更小占空比可调范围就正大了

  12. 小白6 说:

    pinMode(3, OUTPUT);
    pinMode(11, OUTPUT);
    TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM20);
    TCCR2B = _BV(WGM22) | 2; //注意这边的2
    Serial.println(_BV(CS22), BIN);
    OCR2A = 80;
    OCR2B = 40; //这个数值不超过80,可以任意调占空比,分辨率就高了

  13. ziom 说:

    请教。我想输出pwm的频率为1hz。怎样写?在arduino的pin9输出

发表评论

可以使用下列 XHTML 标签:<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>