Skip to main content

【Arduino】與PWM的模仿遊戲 (v0.1)

本筆記文某方面來說是實做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發生時的時間,實作步驟規畫如下:
  1. 撰寫generateHalfPWM()這個函數,在pin 10輸出1Hz、duty rate 50%的PWM(PWM的調控與參數說明可以參考【Arduino】Timers, Registers, and Fast PWM Mode
  2. 設定pin 2作為中斷腳位,利用pin 10輸出的PWM來觸發RISING與FALLING中斷
  3. 查看terminal確認時間區間是否正確
Arduino程式流程大致如下:
  1. 在一開始的setup()便要執行generateHalfPWM(),另外還要放入RISING觸發的中斷,並設置中斷函數為rising()
  2. 當輸入訊號拉升時,便會進入rising()函數,在函數中修改int.0的中斷函數與中斷條件為falling()以及FALLING,然後印出millis()取到的時間,接著便等待下一次輸入訊號狀態發生改變
  3. 當輸入訊號降低時,便會進入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),標示如下:
一旦知道了這幾個時間點,便可以由式duty rate = dutyEnd_time/cycleEnd_time計算出目前的duty rate,然後設定OCR1B。

Arduino程式建構(II)
由於這邊開始要控制PWM的register,注意!不能繼續使用剛剛的generateHalfPWM()作為模擬的PWM來源,而是要將Arduino的pin 2接上外部的PWM來源,pin 10則作為PWM的輸出,輸出與來源相同之duty rate的PWM訊號。

為了讓Arduino可以一邊擷取時間、計算DutyRate,一邊進行PWM腳位輸出的控制,實作步驟規劃如下:
  1. 移除generateHalfPWM(),該函數在此應用中無法作用
  2. 將pin 10接上PWM的外部來源,利用剛剛的Arduino程式確認外部來源有正常運作
  3. 改寫Arduino程式,進行時間與DutyRate的計算,並調播Register值
  4. 將pin 10接上LED或是示波器,查看duty rate是否有跟著變動
Arduino改寫流程大致如下:
  1. 在setup()中將PWM頻率調整為6Hz,0% duty rate
  2. RISING觸發時,若不是第一次RISING,便可以取得當下的時間點作為cycleEnd_time、計算dutyRate、修改OCR1B。另外,不論是否為第一次,在最後都會截取當下時間點作為下一次周期的cycleStart_time
  3. 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!讓我們繼續看下去!

Comments

Post a Comment

Popular posts from this blog

淺介I2C

I 2 C 起源 內部整合電路( Inter-Integrated Circuit, I 2 C, 讀做 I-square-C )是由飛利浦半導體公司開發的一種專用介面。 I 2 C 是以最少的連接線進行硬體佈線還要有靈活擴充的特性為目標而設計,最後出現了只有以序列資料線 SDA ( Serial DAta )及序列時脈線 SCL ( Serial CLock )來進行所有通訊的 I 2 C 介面, I 2 C 允許多主( master )多僕( slave )系統,其傳輸系統內每一個裝置都有唯一的地址可供辨識。資料的寫入和讀取都是由 master 主動發起, slave 無法主動向 master 回報,除非使用中斷腳通知 master 。 I 2 C 傳輸速度有慢(小於 100Kbps )、快( 400Kbps )及高速( 3.4Mbps )三種,每一種均可向下相容。 I 2 C 電路配置 如前所述 I 2 C 為兩線式,一為時脈線 SCL ,另一條為資料線 SDA ,硬體線路如圖 1 ,兩線皆為雙向性,且都需要透過高接電阻( pull-up, 對岸說的上拉電阻)接電。平常不使用時, SCL 與 SDA 的訊號都處於高電位。為了多裝置共線的功能,裝置的 SCL 和 SDA 腳位要為 開洩極( open-drain ) 或 開集極( open-collector ) 。一旦有一個腳位的開洩極導通接地,則整條線都為低電位,這種現象稱作 wired-AND 運作 ;如同邏輯 AND 運算,需要共接的腳位都是 1 (開洩極斷路),該條線的電位才是 1 。如果沒有開洩極的腳位,可以使用具內部高接電阻的腳位,當要輸出 1 時,則設定該腳位為高接型輸入腳;而輸出為 0 時,則改設定為輸出腳並輸出 0 的值。 圖 1. I 2 C 傳輸裝置接線 [1] I 2 C 通訊協定 為使說明部分更簡潔,首先介紹幾個名詞: 位元傳輸協定 當 master 要跟 slave 溝通時,會先有個起始條件( start condition )的訊號,結束時也會送出終止條件( stop condition )訊號。起始條件訊號

【Arduino】Timers, Registers, and Fast PWM Mode

由於Arduino預設的PWM控制方法僅有500Hz(好像還有另一個),想要知道怎樣才可以調整成其他頻率,以此做記錄。 先說明Timer設定方式、PWM Mode,之後會在Arduino上實做。 如果對_BV()沒有概念,可以參考 【Arduino I/O Ports】Control under avr-libc 這篇文章。

Emart Sunny Sale: 3D Shadow QR Code

  Emart是韓國連鎖超市,近期他們發現中午12:00到下午1:00的銷售量會明顯減少,為了提高銷售量他們製作出了帶有日晷概念的3D QR code。