2017年9月6日 星期三

《高階》寫程式Arduino教學 - 02:Arduino 定時器 輸出/入捕獲&溢位中斷 操作 TCCR1A, TCCR1B, TCNT1, TIMSK1 暫存器

此為進階應用,入門者建議直接跳過。

Arduino 的 analog output 使用了 PWM 進行等效的類比平均電壓輸出,而我們直接使用 analoagWrite 函數時,事實上我們更改的只是 command 而已,並沒有辦法對 carrier 進行更改。這造成了我們輸出的 PWM 波都是屬於 "固定頻率"  而 "佔空比可變" 的模式,那如果我們想對 PWM 進行頻率的更改要怎麼辦呢? 此時我們就要修改內部的計數器,經由設計後就可以達成我們想要的 "任意佔空比" 且 "任意頻率"。

溢位中斷

首先是溢位中斷(Over flow, OVF)模式,使用了 timer1

溢位中斷是當 timer counter 溢位時產生中斷,對於一個 16 bits 的 timer 而言就是當 timer counter 在 65535 又加 1 的情況下發生。

/*
timer overflow interrupt. In this case timer1 is running in normal mode. 
The timer must be preloaded every time in the interrupt service routine.

實際作動 LED 亮 0.5sec 滅 0.5sec

改 timer 設定需注意
timer0 與delay()等函數連動 pin 5 6
timer1 與servo等函數連動 pin 9 10
timer2 與tone()函數連動 pin 3 11
*/
void setup()
{
  Serial.begin(115200);
  pinMode(13, OUTPUT);

  // initialize timer1 
  noInterrupts();           // disable all interrupts
  TCCR1A = 0;        // TCCR1A Reset
  TCCR1B = 0;        // TCCR1B Reset

  TCNT1 = 34286;            // preload timer 65536-16MHz/256/2Hz
  
  TCCR1B |= (1 << CS12);    // 256 prescaler 
  TIMSK1 |= (1 << TOIE1);   // enable timer overflow interrupt
  interrupts();             // enable all interrupts
}

ISR(TIMER1_OVF_vect)        // interrupt service routine that wraps a user defined function supplied by attachInterrupt
{
  TCNT1 = 34286;            // preload timer again
  digitalWrite(13,!digitalRead(13));
}

void loop()
{
  Serial.println(TCNT1);
}

首先我們看一下 TCCR1A, TCCR1B ,這兩個暫存器可以合起來看成一個 16bits 的暫存器 TCCR1 ,主要就是在設置 Timer 1 工作在什麼模式以及除頻的倍率。如下圖

COM1 用來設置工作模式
WGM1 用來設置當輸出捕獲發生時,對應的 I/O 腳位該如何反應(此部分較複雜,先行跳過)
ICNC1 用來設置輸入捕獲模式的濾波器是否啟動
ICES1 用來設置輸入捕獲模式中觸發條件
CS 用來設置除頻器的倍率




TCNT1 是由兩個 8bits 的暫存器所組成,總共 16 bits 來存放 timer1 的計數值,稱為 timer1 counter。

第 19 行的地方設置了 TCCR1B 的 CS12 為 1 。 為什麼能夠這樣寫呢? 而又為什麼要這樣寫

首先,這樣寫的原因在於我們可以很直觀的以程式碼來理解到底將這個暫存器設置了什麼。舉例來說,在上圖中的第二張圖我們發現到 TCCR1B 的第 2 個 bit , 名稱為 CS12,在回到程式內,之前我們在談 C 語言的 《筆記》C語言 - 07_2:Bitwise Operation、 ! 與 ~ 差別 就有出現第 19 行這樣的寫法,具體的行動就是想要設置某一個暫存器中的某一個 bit 為 0 或是 1 。

我們可以將設置 TCCR1B 的第 2 個 bit 很簡易的寫成

TCCR1B |= (1 << 2);

如次一來就可以將 TCCR1B 的第 2 個 bit 設置為 1 。但這樣有個缺點,就是我們無法從程式直接看出 bit2 是什麼東西,所以在 AVR 頭文件裡面額外將這些 "暫存器中的小名稱" 事先定義好他在哪一個 bit


在上圖中我們就發現 CS12 早已經被定義為 2 ,那我們就可以用 19 行那樣的寫法增加可讀性了。


TIMSK1 的用法在於設置 Timer1 是否 Enable。

ISR 稱為中斷服務程序,當中斷發生的時候(不管是什麼中斷)都會進入這個中斷服務程序,我們可以想像 ISR 是一個小房間,而且小房間內有一堆小洞。

而 ISR 後面的 TIMER1_OVF_vect 稱為中斷服務向量。當溢位中斷發生時我們可以想像 MCU 會插一支小旗子在小房間裡面的某一個名為 TIMER1_OVF_vect 的洞裡。如此一來當中斷發生的時候我們進入 ISR 這個房間裡,只要看到小棋子在哪個洞就知道是哪一個中斷被觸發了。

輸出捕獲

溢位中斷是當 timer counter 溢位時產生中斷,對於一個 16 bits 的 timer 而言就是當 timer counter 在 65535 又加 1 的情況下發生。而輸出捕獲中斷,我們可以任意選擇 0 ~65535 其中的數字,當 timer counter 等於這個數字時,就會觸發輸出捕獲中斷函式。

其中的 "輸出" 二字需要對應到 "輸入" 捕獲模式來理解。

以往我們計算產線上有幾個產品通過光閘,可以使用一個 digitalRead 的功能,搭配上一個變量來進行儲存,例如當 digitalRead 為 HIGH 時代表有產品通過光閘,則變量的値加 1 。

這造成一個困擾的點,也就是我們必須無時無刻確保程式有在進行 digitalRead 這句程式碼,以免漏算了。然而輸入捕獲模式就可以彌補這個缺點,例如當我們設定 timer1 為輸入捕獲模式時,他會對應到 MCU 上面的某一隻腳位(例如 ICP1 pin8 ) ,那麼只要 pin8 有發生電位的變化(例如 HIGH 變 LOW) 就會觸發輸入捕獲中斷。(詳細腳位在哪要看接線圖)

而輸出捕獲中斷則不然,他的觸發條件是依據 timer counter 計數然後去比對該數值有沒有到達使用者設定的値。若比對到了,則會觸發輸出捕獲中斷函式,除此之外這個中斷很特別,他會連到外部的某個 I/O 腳位(例如OC1A pin9),當輸出捕獲中斷發生時該腳位的電位將會產生變化(也可以設定成不要變化該腳位狀態)。

了解兩者差異後我們實作一下輸出捕獲中斷,由於 timer 經由除頻器後,計數的速度為 16Mhz / 256 (也就是每 1/(16M/256)計數一次),故我們設計在 62500 的時候觸發輸出捕獲模式,並且進入 timer1輸出捕獲中斷函式。

在此需要注意的是,當輸出捕獲比對成功並且進入中斷後 CTC(Clear 
Timer on Compare match) 模式會讓 timer counter 自動清零。

/*
Change the Timer behaviour through the timer register TCCRx
輸出捕獲模式(比對成功時會清除timer value)
 * 
TCNTx - Timer/Counter register. store the value of timer
OCRx - Output compare register
ICRx - Input Capture register
TIMSKx - Timer/Counter interrput Mask register. To enable or disable timer interrupts.
TIFRx - Timer/Counter interrput Flag register. Set to 1 if interrupt is occurs


 */



void Timer1Init()
{
  noInterrupts();
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0; //timer1 counter 歸零

  OCR1A = 62500; // 設置Counter peak = 65536  Output Compare Register
  TCCR1B |= (1 << WGM12); // CTC Mode(Clear timer on compare match)
  TCCR1B |= (1 << CS12);  // 256 prescalar 指定CS12 = 1
  TIMSK1 |= (1 << OCIE1A); // Enable timer compare interrupt
  interrupts();
}

ISR(TIMER1_COMPA_vect)
{
  digitalWrite(13,!digitalRead(13));
  //Serial.println("00000000000000000000");
}

void setup()
{
 Serial.begin(115200);
 pinMode(13,OUTPUT);
 Timer1Init(); 
}

void loop()
{
  Serial.println(TCNT1);
  //Serial.println(TIFR1);
}


TCCR1B |= (1 << WGM12); 此處是設置 timer1 為輸出捕獲模式中的 CTC 模式。

前面我們有提到輸出捕獲模式除了可以觸發輸出捕獲中斷函式以外,還可以反應在某一個固定的腳位上。查看此圖 我們可以發現在 pin9 有一個 OC1A ,這就代表著我們可以設定當輸出捕獲中斷觸發時,反應在 pin9 上面。

當然我們也可以設定成不要反應在 OC1A 上面,具體該如何作動由暫存器TCCR1 中的 COM1A 決定


我們在程式中沒有特別去設定 COM1A 的値,所以是操作在 Normal port operation 模式的,也就是 OC1A/OC1B disconnected ,不會反應在腳位上。

參考連結
Atmel AVR Microcontroller Primer: Programming and Interfacing
Could someone explain this weird looking code, used to setup timers?
how to access "Input Capture Interrupt", Atmega328, USB Boarduino