2018年4月17日星期二

【很冷的知識】analogWrite怎麼做PWM輸出?改PWM頻率,定时器相關)


【很冷的知識】analogWrite怎麼做PWM輸出?改PWM頻率,定时器相關)








tsaiwn 高级会员 2015-3-17 12:12


43888




本帖最后由 tsaiwn 于 2015-3-17 16:39 编辑




大家都知道在 Arduino UNO 有六支 pin 可以使用 analogWrite( )做 PWM 輸出,

在板子上 pin 旁邊標示有 "~" 符號, analogWrite( ) 可以用來產生模擬電壓,

很多人一定很好奇那是怎麼做到的 ?




首先來看看 PWM 模擬電壓的原理, 這在官網上就有解說了:

http://arduino.cc/en/Tutorial/SecretsOfArduinoPWM

所謂的 PWM 全稱是 Pulse-Width Modulation (PWM), 其實這也沒啥學問,

就是對 GPIO 腳位不斷的切換 "有電" "沒電",

每秒鐘循環幾次即為其 Frequency(頻率),

每次"有電"時間佔一個循環的百分比稱為其佔空比(Duty cycle);

以下是官網上的模擬範例:

int pin = 13;

void setup() {

pinMode(pin, OUTPUT);

}

void loop(){

digitalWrite(pin, HIGH);

delayMicroseconds(100); // Approximately 10% duty cycle @ 1KHz

digitalWrite(pin, LOW);

delayMicroseconds(1000 - 100);

}

這個範例中, 一個循環是 1000 us = 1ms, 所以一秒循環 1000次, 因此 Frequency 是 1 KHz,

每個循環中, 有電的比率是 100/1000 * 100% = 10%, 所以 duty cycle (佔空比)為 10%;

這樣就可以模擬出 5Volt x 10% = 0.5 Volt 的電壓!

如果真的這樣做, 有好處也有壞處, 官網上已經說了:

好處是任一支 pin 都可這樣用, 包括 Pint 0 到 Pin 13, 以及 Pin A0 到 A5 共 20支 pin 都可以!

壞處卻更多, 首先就是頻率(Frequency)和佔空比(duty cycle)可能受中斷(Interrup)的影響變成不是很準確 !!

最大的壞處是, 在某支 pin 做 PWM 輸出期間都沒辦法做別的事情 !!




既然說這只是示範可以這樣做, 在 Arduino 當然不可能是這麼做,
那 Arduino 是怎麼做的呢?

就是透過 Timer 計時器直接控制 pin 做 PWM 輸出, Arduino UNO 的 MCU 有三個 timer,

其中 timer0 控制 pin 5, pin 6; timer1 控制 pin 9, pin 10; timer 2 控制 pin 11, pin 3;

所以, 我們可以對這些 pin 用 analogWrite(pin, val); 輸出 0 到 255 的 val 值到 pin ;

如果輸出 val 是 0, 它會偷偷直接改用 digitalWrite(pin, 0); 輸出,

如果 val 是 255, 也是會偷偷直接改用 digitalWrite(pin, 1); 輸出!

如果 val 是 1 到 254, 則會下命令請 pin 腳對應的 timer 計時器(定時器)幫忙!!

How ?

首先要知道 timer 的基本知識:

(1)每個 timer 一定有個 counter, 例如 timer0 的TCNT0, timer1 的TCNT1, timer2 的TCNT2;

該 counter 一定是每個 tick 會加 1, 每個 tick 通常是把 CPU 的 clock 拿來經過一個除頻電路,

然後給 timer 使用; Arduino UNO 採用 AVR ATmega328 MCU, 且 clock Rate 是 16MHz,

每個 timer 的除頻 Prescaler 是獨立設定的, 通常可以設 1, 2, 4, 8, 64, 256, or 1024 等,

這必須看 MCU 的 datasheet.




(2)每個 timer 通常提供許多 mode 運作模式, 例如 counter 溢出(Overflow)或Rollover歸零時產生中斷,

或 TCNT? 達到某個值時產生中斷等, Arduino ATmega328 的 timer 有 16種 mode, 許多 Mode 是與 PWM 有關;

要設定 timer 的 Mode 可以透過修改 timer 的控制暫存器, 例如 TCCR?A, TCCR?B, 注意以 ATmega328 為例, TCCR?A 和 TCCR?B 要合起來用, 此處的 A, B 與 channel A, channel B 無關!!




(3)每個 timer 通常有比較暫存器(Compare Register), 當 TCNT? 值與該些比較暫存器相同時可以做某事,

不一定是對 CPU 產生中斷! Arduino 每個 timer 有兩個比較暫存器, 分別命名 OCR?A 和 OCR?B,

其中 ? 是 0, 1, 2 分別對應到 timer0, timer1, 和 timer2 這三個計時器.


你可以先偷看 analogWrite( ) 的程序碼:

在你 Arduino IDE 下的 hardware\arduino\cores\arduino\wiring_analog.c

很簡單, 真正請 timer 幫忙只做三件事: a.找出對應的 port, b.設定控制暫存器, c.填入 analog的值到比較暫存器!

不過你會發現看不太懂, 因為還不知道硬體 timer 控制 PWM 運作方式與原理!

不想看 datasheet 可以參考這:

http://letsmakerobots.com/conten ... mers-and-interrupts




以Arduino UNO 的 timer1為例, 在 mode 5 (Fast PWM, 8 bit), 此時, TCNT1 從 0 數到 255, 然後又加 1 就變0, ...

通常從 255 (此 mode 的最大值)又加 1 變為 0 之時會產生 OVF 中斷(TIMSK1的TOIE1要 set), 不過這與 PWM 無關!

PWM 不是用 Interrupt 中斷請求做的, 不必麻煩 CPU, CPU 只要下命令給 timer, timer 就會照命令執行PWM工作 !!

PWM 是利用每個 timer 上的兩個"匹配符合輸出"暫存器(Compare Match Output) COM?A 和 COM?B;

(注意雖是 Compare Match Output, 但暫存器名稱是 COMxy 不是 CMOxy 喔 !)

在timer1 的 mode 5, 又稱 Fast PWM mode, (不過請注意 Arduino 的 init( ) 設定只有 timer0 用這, 另外 timer1 和 timer2 不是用這 mode),

這時可以把 1 到 254 之間的值放入 OCR1A 或 OCR1B 以便控制 pin 9 或 pin 10
的 PWM duty cycle, 1 到 254 分別對應到 (1+1)/256, .., (254+1)/256 的 duty cycle.

會 +1 是硬體電路設計上的關係, data sheet 上說:

Note that fast PWM holds the output high one cycle longer than the compare register value.

在 TCNT1 等於 0 之時, COM1A and/or COM1B 會輸出(當然要 TCCR1A 內的 COM1A1 and/or COM1B1 有set),

然後在 TCNT1 等於 OCR1A 則關閉 COM1A, 當 TCNT1 等於 OCR1B 則關閉 COM1B,

注意沒有立即關閉, 是延遲一個 tick 才關閉 ! 所以才會多加1, 因為一個循環是 256, 不是 255,如果不延遲加 1, 則輸出 val 是 254 時變成 254/256, 還差一點點, 所以犧牲 1/256, 就是沒有 1/256佔空比 !!




由於 Arduino 的 init( )把 timer1 的 Prescaler 設定為 64,

(參考在你 Arduino IDE 內的 hardware\arduino\cores\arduino\wiring.c )

且把 timer1 設定為 8-bit phase correct pwm mode, 所以其頻率是 490.196Hz, 不是 976.5625Hz;

所謂的 8-bit phase correct pwm mode, 意思是 TCNT? 從 0 數到 255, 接著又從 255 倒著數回 0,
那何時把 COM1A and/or COM1B 的輸出打開或關閉呢?
根據 datasheet, 在從 0 往上數, 碰到 OCR1A 時把 COM1A 關閉,

然後從 255 往回數, 數到 OCR1A 時把 COM1A 打開(有電); 對於 OCR1B 和 COM1B 也是這樣!

這使得 duty cycle (佔空比) 更準確, 也就是 val 1 ~ 254 分別對應到 1/255 到 254/255 的 duty cycle.

但是 Frequency 則不是除以 256, 是要除以 255 再除以 2, 於是: (注意是 255, 不是 256喔!)

Frequency = 16 MHz / 64 / 255 / 2 = 490.196Hz;


timer 2 也是在 init( )被設為 Prescaler 64 的 phase correct pwm (8-bit);

但是, timer0 雖然 Prescaler 也設 64, 但 PWM 是用 Fast PWM mode,

不使用 phase correct mode 是為了避免影響維護 millis( ) 的中斷 timer0 Overflow Interrupt,

即 ISR(TIMER0_OVF_vect) 這中斷處理程序, 否則 millis( ) 和 micros( ) 以及 delay() 都會受到影響 !!

因此 , timer0 控制的 PWM 其 Frequency 是 976.5625Hz,

16 MHz / 64 / 256 = 976.5625Hz

注意用 timer0 控制的pin 5, pin 6 之 PWM 的 duty cycle 無法是 1/256, 它是 0 再來就 2/256了!
結論:

timer0 控制 pin 5, pin 6, PWM 頻率 976.5625Hz, duty cycle可以 2/256 ~ 255/256 (對應 1 到254);

timer1 控制 pin 9, pin 10, PWM 頻率 490.196Hz, duty cycle 可以 1/255 ~ 254/255(對應 1 到254);

timer2 控制 pin 11, pin 3, PWM 頻率 與 duty cycle 跟 timer1 控制的相同 !!




Q: 那 PWM 的 Frequency 可不可以更改?

A: 可以, 偷改 timer 的 Prescaler 就可以達到更改 Frequency 的目的 !

但是, 千萬不要更改 timer0 的 Prescaler, 否則 millis( ) 和 micros( ) 以及 delay() 都會受到影響 !!!

以下是以 timer1 控制的 pin 9, pin 10 為例(注意兩個 pin 的頻率相同!)

在你的 setup( ) { 內, 寫如下兩句即可:

int fff = 3; // 可以是 1, 2, 3, 4, 5

TCCR1B = TCCR1B & 0xF8 | fff;

其中 fff 與對應頻率如下:

fff Prescaler Frequency

1 1 31372.549 Hz

2 8 3921.569

3 64 490.196 <--DEFAULT

4 256 122.549

5 1024 30.637 Hz




至於 timer2 控制的 pin 11 和 pin 3,

則在 setup( ) { 內寫:

TCCR2B = TCCR2B & 0xF8 | ?;

此處的 ? 可以有七種:

? Prescaler Frequency

1 1 31372.549 Hz

2 8 3921.569

3 32 980.392

4 64 490.196 <--DEFAULT

5 128 245.098

6 256 122.549

7 1024 30.637 Hz




如果你堅持要改 timer0 的 Prescaler, 以更改 pin 5, pin 6 的 PWM 頻率:

(注意 millis( ) 和 micros( ) 以及 delay() 都會受到影響 !! )

則在 setup( ) { 內寫:

TCCR0B = TCCR0B & 0xF8 | ?;

此處的 ? 可以有五種:

? Prescaler Frequency

1 1 362500 Hz

2 8 7812.5

3 64 976.5625 <--DEFAULT

4 256 244.140625

5 1024 61.03515625 Hz




參考:

http://playground.arduino.cc/Main/TimerPWMCheatsheet

http://www.atmel.com/Images/doc8161.pdf





没有评论:

发表评论