PWM(Pulse Width Modulation)简介
PWM,也就是脉冲宽度调制,用于将一段信号编码为脉冲信号(一般是方波信号)。是在数字电路中 达到 模拟输出效果的一种手段。即:使用数字控制产生占空比不同的方波(一个不停在开与关之间切换的信号)来控制模拟输出。我们要在数字电路中输出模拟信号,就可以使用PWM技术实现。在嵌入式开发中,我们常用PWM来驱动LED的暗亮程度,电机的转速等。
原理
我们知道,在数字电路中,电压信号是离散的: 不是 0(0V) 就是 1(5V或者3.3V), 那么如何输出介于 0v 和 5V之间的某个电压值呢?
我们先来举个实际的例子,一看就懂,胜过千言万语。
如下图,要让让数字信号模拟出 3.7V 的电压输出。可以先假想:3.7V的电压输出是由多个周期时间为T ,电压值 都 为3.7V的信号 持续输出形成的。
根据PWM原理,我画出下面等效脉冲信号波形图(红色) 。PWM的理念是:连续的信号可以使用独立的方波信号模拟出来,手段就是调整输出的脉冲宽度,以达到同样的效果。你会怀疑:这真的可以达到同样的效果吗?
别急,让我们继续往后看。
如果一个周期T内的均值电压等于3.7V,那么,整体的输出就是3.7V,因为整体只不过是n个周期不断的重复而已。那么我们的主要问题就是如何让一个调制周期T时间内的均值电压等于3.7V。下面就开始计算。
设:脉冲信号的值随时间变化的函数为:
因为这里是数字电路的背景下的,所以v的值只能取 0v 或者 5v。
设:在一个周期T时间中,高电平持续时间占T的百分比为 D,则低电平持续时间在周期T中占的百分比为 1- D。
我们对 一个调制周期T内的电压值对时间积分,然后除以周期T,就得到了这个周期的输出电压均值。
由于这个积分图形是方波,所以很好计算(就是面积 除以T)。
可以看出,1个调制周期内,输出的电压均值只和D有关。也就是高电平信号占持续时间占这个周期的百分比决定这个周期内的输出电压。
上面说了,要让这个均值等于3.7v,则求出D为:0.74 。
那也就是说:如果在一个调制周期中,高电平持续时间占周期的百分比为74%,则整体输出的信号就是3.7V。这个百分比就是下面要说的占空比。
占空比(duty cycle)
有了前面的知识,相信你已经对占空比理解了,其实很好理解 ,占空比就是 在一个调制周期内,某个信号持续的时间占这个时间段的百分比。
下面给出占空比的公式
下面是一个占空比不断变化的图示
所以我们可以很自然的得出结论:
低占空比意味着输出的能量低,因为在一个周期内大部分时间信号处于关闭状态,如果pwm控制的负载为led,则具体表现例如led灯很暗。
高占空比意味着输出的能量高,在一个周期内,大部分时间信号处于on状态,具体表现为LED比较亮。
当占空比为100%时,表示 fully on,也就是在一个周期内,信号都处于on状态,具体表现为led亮度到达100%。
为0%时则表示 totally off,在一个周期内,一直处于off状态,具体表现为led熄灭。
现在一切都明了了:脉宽调制,脉宽调制,脉宽调制,这个宽,不是物体的宽度,而是高电平(有效电平)信号在一个调制周期中持续时间长短,它可以用占空比去衡量,占空比越大,脉冲宽度越宽。
占空比随时间变化的PWM调制
扩展:用PWM模拟出如下的正玄波(假设仅仅用3个周期去调制出这段正弦波)
道理和前面是一样的,只不过,因为从图中看出,模拟信号(黑色曲线)随着时间不断加强,因此,占空比要变化,也就是逐渐增加。下面3个调制周期中,占空比D逐渐增大。
PWM的频率 (PWM frequency)
pwm的频率决定了输出的数字信号on ,1 和 off,0 的切换速度。频率越高,切换就越快。频率的大小就是前面提到的调制周期T的倒数 : f = 1/T。
1秒内,0.5秒开,0.5秒灭,占空比是50%。那么,1毫秒内,0.5毫秒开,0.5毫秒灭,占空比也是50%,对于前者,频率就是1HZ,而后者,是1毫秒,频率就是1KHZ。
一般pwm频率都是因硬件设计而固定的,是由pwm发生器决定的。PWM频率越高,调制出来的输出曲线就更加的smooth,效果越好,完成一个调制周期的时间越短。这个和手机的ppi越高,显示越清晰是一个道理。当然我想PWM的频率越高,对硬件的要求就也越高。
下图中,右边的频率是左边的2倍,调制出的曲线更加圆滑,贴近理想波形。
使用Arduino来实战!
首先要确定你的Arduino 的哪些引脚支持PWM输出,数字引脚上标记了 ~ 符号的就是支持PWM的。Arduino主控芯片为ATmega168或者ATmega328的3, 5, 6, 9, 10, 和 11引脚支持PWM,Arduino Mega的 2~13 , 44~46引脚支持PWM,老板子ATmega8的9,10,11脚支持PWM。
Arduino的库中通过analogWrite函数来完成PWM输出。
analogWrite(pin,value)
作用:让一个支持PWM输出的引脚持续输出指定脉冲宽度的方波。
参数:
pin:PWM输出的引脚编号。
value:用于控制占空比,范围:0~255。值为0表示占空比为0,值为255表示占空比为100%,值为127表示占空比为50%。
当调用一次此函数后,引脚就会持续稳定地输出指定占空比的PWM方波,直到下一次对同一个引脚的新的调用来修改脉冲宽度的值,就会再持续输出新的脉冲宽度的PWM波。
Arduino板的PWM输出频率一般是490Hz,意味着一个调制周期的完成需要2ms的时间。在Uno或者与Uno相似的板子上,其5和6引脚PWM的频率约为980Hz(一个调制周期的完成需要1ms的时间)。
注意
1、analogWrite和analogRead没有任何关系,他们虽然都属于模拟驱动函数,但是他们使用的技术不同,一个是PWM,一个是A/D转换。
2、在调用analogWrite前,无需对引脚设置pinMode为输出,因为函数实现中已经完成了这个设置,见下面源代码。
3、从源代码中也可以发现,当value的值为0时(占空比为0),等价于持续输出低电平,当value值为255时(占空比为100%),等价于持续输出高电平。当value不是0也不是255时,是通过设置定时器/计数器的比较寄存器的值来完成的,可以看出PWM技术依赖单片机内部的Timer。正是这个原因,引脚5和6实际的占空比可能比设置的高,特别是在value值很小的时候,例如value值为0时,PWM的占空比实际却可能不为0而比0高一点,因为5和6的PWM生成器器依赖的Timer同时也被millis()和delay()函数使用。
void analogWrite(uint8_t pin, int val) { // We need to make sure the PWM output is enabled for those pins // that support it, as we turn it off when digitally reading or // writing with them. Also, make sure the pin is in output mode // for consistenty with Wiring, which doesn't require a pinMode // call for the analog output pins. pinMode(pin, OUTPUT); if (val == 0) { digitalWrite(pin, LOW); } else if (val == 255) { digitalWrite(pin, HIGH); } else { switch(digitalPinToTimer(pin)) { // XXX fix needed for atmega8 #if defined(TCCR0) && defined(COM00) && !defined(__AVR_ATmega8__) case TIMER0A: // connect pwm to pin on timer 0 sbi(TCCR0, COM00); OCR0 = val; // set pwm duty break; #endif //为了简化篇幅,省略部分代码,具体请查看库源代码 case NOT_ON_TIMER: default: if (val < 128) { digitalWrite(pin, LOW); } else { digitalWrite(pin, HIGH); } } } }
用USB逻辑分析仪来分析Arduino的PWM输出
void setup() { analogWrite(6,127); //让 6号引脚输出占空比为 127/255≈50% 的PWM信号 analogWrite(10,64); //让 10号引脚输出占空比为 64/255≈25% 的PWM信号 } void loop() { //nothing }
从上面的USB逻辑分析仪测量的结果可以得出这样几个事实:
1、官方给的数据是没问题的,普通的PWM引脚输出PWM的频率为490Hz,个别PWM引脚(如5和6)支持更高PWM输出频率,为980Hz。
2、analogWrite函数调用以后,那个引脚就会持续输出固定占空比的PWM信号,无需在loop函数里面循环调用来维持输出,上面的代码中我是在setup函数中调用的;输出的占空比由第二个参数指定,这个参数除以255就是占空比;
如果要修改这个引脚的PWM占空比,则用新的参数对此引脚再调用一次analogWrite即可。
一个例子
试验连线线图
const byte ledPin = 3; //pwm输出引脚 const byte button = 6; //按键引脚 byte pwmVal = 0; bool isKeyPressed(byte pin); void setup() { pinMode(button,INPUT_PULLUP); //配置为数字输入,且使能内部上拉电阻 Serial.begin(9600); } void loop() { if(isKeyPressed(button)) //如果检测到按键按下,就让pwmVal 增加2 { pwmVal+=2; //pwmVal 的类型为byte,到了256会自动溢出回0,所以为没做检查,不过不要过度依赖这个技巧啊,规范些好 } analogWrite(ledPin,pwmVal); Serial.println(map(pwmVal,0,255,0,5)); //使用map函数映射为 0~5v的电压信号 delay(30); } bool isKeyPressed(byte pin) //按键检测函数 { bool pre = false; if(digitalRead(pin)==LOW) { delay(10); if(digitalRead(pin)==LOW) { pre = true; for(int a = 5;digitalRead(pin)==LOW&&a;--a) { delay(5); } } } return pre; }
在IDE的串口绘图器中查看输出的波形。因为是手动按键来调整占空比的,所以波形不好看。用电位器调更加好。
我们去掉map函数,直接输出pwmVal的值,可以看到更加细腻。
夜晚的效果。