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的值,可以看到更加細膩。
夜晚的效果。