本筆記文某方面來說是實做Arduino PWM控制之一的筆記(BUT!最後使用的方法跟PWM完全無關阿XDD),主要目標是要讓Arduino模仿來源PWM的frequency與duty輸出一模一樣PWM訊號,本篇筆記將會描述幾種可以達成目標的做法以及主要的思路,如有哪邊考慮不周全,還請各位大大高手們指教。
PWM的來源訊號是一個透過555晶片來產生方波,另外還有一個旋鈕式的可變電阻來控制duty,這部分的實作需要另外參考網路上的文章,目前還不清楚是怎麼實做的。總而言之,透過這樣的架構,就可以輸出固定頻率的PWM,並且轉動可變電阻的旋鈕來調整duty。
在目前的專案中,Arduino在某個模式下需要模仿PWM輸入訊號來將訊號輸出(可以想像成是切換成手動控制模式,不過仍由Arduino負責輸出),大概的流程會像下圖這個樣子:
對了,據說手邊這個手工打造的555模組輸出頻率是6Hz(雖然最後其實是有一點誤差)。
OK,條件說明完成,接下來開始實做吧~如果對這項功能有興趣的童鞋,請跟著我們一起看下去!
followDuty v0.1
目前已知的是頻率固定,所以Arduino要做的事是要跟著輸入的duty不同來進行調整,這邊使用調控register的方式來微調PWM輸出(複習筆記:【Arduino】Control I/O pin by avr-libc 說明register的簡單概念、【Arduino】Timers, Registers, and Fast PWM Mode Arduino register控制PWM輸出)。目標是維持以6Hz進行輸出,只要調整duty的方式來實做,換句話說就是OCR1A不變,僅調整OCR1B。
透過millis()函數取得時間點
說到計算duty,最重要的就是時間了,只要知道duty維持的時間以及一個cycle的所需時間,就可以反推duty。在Aruino中我們可以使用millis()這個內建的函數,雖然millis()回傳的是Arduino從開始運行程式後經過了多少ms,但是只要透過簡單的減法就可以計算出我們所需要的時間區段。
使用中斷(interrupt)觸發截取電壓變化時間點的事件
雖然我們可以在loop()內加入digitalRead()讀取輸入訊號,然後在電壓發生變化時記錄當下的時間,但是如果loop()內Arduino要做的工作越多,可能電壓已經發生變化了,但是Arduino卻還在執行其它的工作,因此讀取到的時間就會有更大的誤差。
這時候可以使用中斷(Interrupt, 在某些文件中又稱int.)幫助我們在電壓一發生變化的時候就執行擷取時間的動作,中斷的概念就像是如果xxx事件發生了,Arduino會將手邊的工作儲存起來,轉而優先執行中斷指定函數內的工作,
Arduino Uno有兩個中斷腳位,分別是使用int.0的pin2以及使用int.1的pin3,為pin腳加入中斷功能的語法為attachInterrupt(interrupt, ISR, mode),要填入的參數說明如下表:
舉例來說如果是attachInterrupt(0, detectRising, Rising),表示當pin2電壓由low轉high時,將會執行detectRising()這個自訂的中斷函數。
注意!中斷函數不接受任何參數或進行任何的數值回傳;另外要記得在中斷函數中有使用到的變數,在宣告時需加入volatile這個關鍵字,讓Arduino直接將數值儲存在RAM內而不是register,避免數值不同步的狀況發生(但是不確定兩者的差別是什麼,以後有機會了解再補上)。
Arduino程式建構(I)
首先確認程式透過中斷以及millis()函式,可以取得RISING以及FALLING發生時的時間,實作步驟規畫如下:
- 撰寫generateHalfPWM()這個函數,在pin 10輸出1Hz、duty rate 50%的PWM(PWM的調控與參數說明可以參考【Arduino】Timers, Registers, and Fast PWM Mode)
- 設定pin 2作為中斷腳位,利用pin 10輸出的PWM來觸發RISING與FALLING中斷
- 查看terminal確認時間區間是否正確
Arduino程式流程大致如下:
- 在一開始的setup()便要執行generateHalfPWM(),另外還要放入RISING觸發的中斷,並設置中斷函數為rising()
- 當輸入訊號拉升時,便會進入rising()函數,在函數中修改int.0的中斷函數與中斷條件為falling()以及FALLING,然後印出millis()取到的時間,接著便等待下一次輸入訊號狀態發生改變
- 當輸入訊號降低時,便會進入falling()函數,在函數中修改int.0的中斷函數與中斷條件為rising()以及RISING,然後印出millis()取到的時間,接著當輸入訊號改變狀態時又會回到步驟2,如此周而復始的印出正緣觸發以及負緣觸發的時間
與流程相應的Arduino source code:
// HARDWARE PIN // simulate PWM source from pin 10 // duty read from pin D2 const int interrupt0 = 0; // pin D2 volatile int cycleStart_time = 0; volatile int dutyEnd_time = 0; volatile int cycleEnd_time = 0; volatile float dutyRate = 0.0; void setup() { Serial.begin(9600); generateHalfPWM(); attachInterrupt(interrupt0, rising, RISING); } void loop() { } void rising(){ attachInterrupt(interrupt0, falling, FALLING); Serial.print("R: "); Serial.print(millis()); } void falling(){ attachInterrupt(interrupt0, rising, RISING); Serial.print("\tF: "); Serial.println(millis()); } void generateHalfPWM(){ // pwm pin settings pinMode(9, OUTPUT); pinMode(10, OUTPUT); // real pwm output // frequency 1Hz, duty 50% // pwm output @ pin d10 TCCR1A = _BV(COM1A0) | _BV(COM1B1) | _BV(WGM11) | _BV(WGM10); TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS12) | _BV(CS10) ; // prescaler 1024 OCR1A = 15625; // 1Hz OCR1B = 7812; // 50% duty }
運行程式之前,記得將pin 10(PWM輸出)與pin 2相接,不然會收到不正確的terminal訊息。
下圖為terminal的訊息:
由上圖可知第二個PWM周期的
- RISING中斷觸發的時間約為5192ms
- FALLING中斷觸發的時間約為5693ms
- 下一次RISING觸發的時間約為6193ms
duty的計算如下
- duty = (5192 - 4692) / (5693-4692)
= 50% (大約)
所以使用中斷進行上緣與下緣觸發是可以正常運作的,時間點也順利取得,接下來要進行duty計算的部分。
計算Duty Rate並設置PWM控制Register
為了要計算Duty Rate,必須要把需要的變數準備好,這些變數分別為duty維持的時間(dutyEnd_time)、一個cycle所需的時間(cycleEnd_time),另外還有用來作時間差基準點的cycle起始時間點(cycleStart_time),標示如下:
Arduino程式建構(II)
由於這邊開始要控制PWM的register,注意!不能繼續使用剛剛的generateHalfPWM()作為模擬的PWM來源,而是要將Arduino的pin 2接上外部的PWM來源,pin 10則作為PWM的輸出,輸出與來源相同之duty rate的PWM訊號。
為了讓Arduino可以一邊擷取時間、計算DutyRate,一邊進行PWM腳位輸出的控制,實作步驟規劃如下:
- 移除generateHalfPWM(),該函數在此應用中無法作用
- 將pin 10接上PWM的外部來源,利用剛剛的Arduino程式確認外部來源有正常運作
- 改寫Arduino程式,進行時間與DutyRate的計算,並調播Register值
- 將pin 10接上LED或是示波器,查看duty rate是否有跟著變動
Arduino改寫流程大致如下:
- 在setup()中將PWM頻率調整為6Hz,0% duty rate
- RISING觸發時,若不是第一次RISING,便可以取得當下的時間點作為cycleEnd_time、計算dutyRate、修改OCR1B。另外,不論是否為第一次,在最後都會截取當下時間點作為下一次周期的cycleStart_time
- FALLING觸發時,取得當下的時間點並減去cycleStart_time,計算出dutyEnd_time
接下來透過下面Arduino程式,便可以自動計算出dutyRate,並修改OCR1B的數值:
// HARDWARE PIN // duty read from pin 2 // pwm output from pin 10 // 16000000 / 1024/ 6 = 2604.16667, @6Hz const int timerCounter =2604; const int interrupt0 = 0; // pin D2 volatile int cycleStart_time = 0; volatile int dutyEnd_time = 0; volatile int cycleEnd_time = 0; // in 6Hz, cycleEnd_time = 167 volatile int temp_time = 0; volatile float dutyRate = 0.0; void setup() { // pwm pin settings pinMode(9, OUTPUT); pinMode(10, OUTPUT); // real pwm output // frequency 6Hz, duty 0% // pwm output @ pin d9 TCCR1A = _BV(COM1A0) | _BV(COM1B1) | _BV(WGM11) | _BV(WGM10); TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS12) | _BV(CS10) ; // prescaler 1024 OCR1A = timerCounter; // 6Hz OCR1B = 0; // 0% duty attachInterrupt(interrupt0, rising, RISING); } void loop() { } void rising(){ attachInterrupt(interrupt0, falling, FALLING); // if not the fitst loop if (cycleStart_time != 0){ cycleEnd_time = millis() - cycleStart_time; // set parameter according last cycle dutyRate = (float) dutyEnd_time / cycleEnd_time; OCR1B = timerCounter * (dutyRate); } cycleStart_time = millis(); } void falling(){ attachInterrupt(interrupt0, rising, RISING); dutyEnd_time = millis() - cycleStart_time; }
可以將pin 10接上LED或示波器查看PWM輸出是否有跟著來源信號一起變化。
To be Continued...
在接上示波器後,發現Arduino模仿的PWM輸出不如預期的穩定,外部來源的PWM是一直保持穩定的輸出,但是Arduino pin 10出來的卻會微微晃動,而且跟來源的周期似乎有一點誤差,所以便有了followDuty v0.2!讓我們繼續看下去!
期待作者繼續發布 followDuty v0.2 好文章
ReplyDelete