Arduinoのタイマー割り込み

Spread the love

Arduinoのタイマー割り込みを試してみました。Arduinoで、外部からの入力を得る場合など、loop()の中だけでの処理には限界があります。例えば、待ち時間を確保するためにdelay()関数を使うのが、手っ取り早い方法です。しかし、delay()関数はブロッキング関数ですので、待ち時間中は操作を受け付けません。これを解決するには、割り込み処理を使うのが得策です。

Arduinoには、様々な割り込みが用意されています。まずは割り込み処理の手始めとして、タイマー割り込みについて、調べてみました。以下は、Arduinoのタイマー割り込みについての備忘録です。

Arduinoのタイマーは三つある

Arduinoには三つのタイマーが搭載されています。なお、ここで言うArduinoは、ATmega328Pを搭載しているものを指します。たとえば、安価な互換機で使用されているATmega328PBには、5つのタイマーが搭載されています。しかし、ここでは、拡張されたタイマーについては扱いません。というか、まだ調べ切れていません。

Arduinoに搭載されているタイマーは、Timer0,Timer1,Timer2の三つです。以下に、三つのタイマーの主な役割をまとめてみました。

割り当て関数PWMピンカウンタービット数
Timer0millis(),delay()D6,D58
Timer1D9,D1016
Timer2tone()D11,D38

上の表からは、タイマ割り込みを使用すると、どこに影響するかが読み取れます。例えば、Timer0を使用すると、millis()やdelay()関数が正しく動作しなくなります。また、D6、D5ピンのPWM出力にも影響します。

カウンタビット数は、カウンターの最大値を表しています。8ビットなら、カウンターは255までカウントできます。しかし、Timer1なら、16ビットですので、65535まで、カウントできます。

Arduinoのタイマー割り込み発生条件

タイマー割り込みは、カウンター値が一定の条件を満たしたときに発生します。カウンターそれぞれで、三つの割り込みが使用できます。割り込みが発生すると、ISR(Interrupt Service Routine)が呼び出されます。ISRを定義する際に、どの割り込みを処理するのかを宣言するのが、ベクターです。下記の表の、xの部分がタイマー番号となります。

割り込み発生条件ベクター
カウンターがオーバーフローしたときTIMERx_OVF_vect
カウンター値がOCRxAと一致したときTIMERx_COMPA_vect
カウンター値がOCRxBと一致したときTIMERx_COMPB_vect

ISRの宣言は、こんな感じになります。

//  タイマー1 オーバーフロー割り込みルーチン
ISR(TIMER1_OVF_vect) {
  // ここに、割り込みルーチンでの処理を書く
}

Arduinoのタイマー割り込み発生頻度

タイマーで使用されるカウンターは、8ビットまたは16ビットです。8ビットカウンターなら、0からカウントを始め、最大値は255となります。そして、タイマーが最大値に至ると、オーバーフロー割り込みを発生させます。そして、また0からカウントアップをします。なお、実際には、モード設定により、割り込み発生タイミングと、カウンター最大値は変わります。ここでは、一例としてmode0(normalモード)の動作を説明しています。

カウンターのカウントアップは、MCUの動作クロックによって行われます。標準的なArduinoのクロック周波数は16MHzです。したがって、8ビットカウンターの場合、16Mhz/256=62.5kHzの頻度でオーバーフローが発生します。16ビットカウンタの場合は、16MHz/65536=244.1Hzとなります。

いずれにしても、ものすごい速さでオーバーフローします。COMPxAやCOMPxBの割り込みも、発生タイミングは異なるものの、発生頻度は同じです。その結果、8ビットカウンタを使用する、Timer0とTimer2では、一秒間に62500回も割り込みが発生します。これでは、割り込み周期が短すぎて使いずらいです。

割り込みルーチンは、次の割り込みが来る前に処理を終わらせなければいけません。ATmega328Pは1クロック当たり、一つの処理(機械語単位)をこなします。つまり、8ビットカウンタによる割り込みでは、最大で255の処理しかできません。しかし、実際には割り込みルーチンの呼び出しなどのオーバーヘッドがあります。したがって、割り込みルーチンでは簡単な処理しかできないと考えた方がよいでしょう。

プリスケーラーで割り込み頻度を下げる

Arduinoのタイマーは、ものすごい勢いでカウントしていきます。しかし、もっとゆっくりカウントさせることはできないでしょうか。

ゆっくりカウントさせるために、プリスケーラーが用意されています。プリスケーラーは、クロック周波数を分周する仕組みです。プリスケーラーは使用するタイマーによって、設定できる分周比が少し異なります。しかし、三つのタイマーすべてで、クロックを1/1~1/1024に分周できます。

8ビットカウンターを使用するTimer0とTimer2なら、16MHz/1024/256=16Hzとなります。つまり、1秒に16回までに、割り込み頻度を下げられます。16ビットカウンターを使用するTimer1なら、16MHz/1024/65536=0.238Hz となります。つまり、おおむね4秒に1回の割り込み頻度に抑えることが出来ます。

もちろん、割り込み処理が軽くて、もっと高頻度の割り込みが必要なら、プリスケーラーで調整できます。プリスケーラーの設定を、下記のリストにまとめておきました。

プリスケーラーの設定は、レジスタTCCRxBのCSx2,CSx1,CSx0の3ビットで行います。

Timer0プリスケーラー設定 (TCCR0Bレジスタ)  
  +------+------+------+--------------------------------------------+
  | CS02 | CS01 | CS00 |                 PRESCALING                 |
  +------+------+------+--------------------------------------------+
  |   0  |   0  |   0  |                  NO CLOCK                  |
  |   0  |   0  |   1  |                NO PRESCALING               |
  |   0  |   1  |   0  |                     1/8                    |
  |   0  |   1  |   1  |                    1/64                    |
  |   1  |   0  |   0  |                   1/256                    |
  |   1  |   0  |   1  |                  1/1024                    |
  |   1  |   1  |   0  |External Clock T0 pin. Clack on falling edge|
  |   1  |   1  |   1  |External Clock T0 pin. Clack on rising edge |
  +------+------+------+--------------------------------------------+
Timer1プリスケーラー設定 (TCCR1Bレジスタ)  
  +------+------+------+--------------------------------------------+
  | CS12 | CS11 | CS10 |                 PRESCALING                 |
  +------+------+------+--------------------------------------------+
  |   0  |   0  |   0  |                  NO CLOCK                  |
  |   0  |   0  |   1  |                NO PRESCALING               |
  |   0  |   1  |   0  |                     1/8                    |
  |   0  |   1  |   1  |                    1/64                    |
  |   1  |   0  |   0  |                   1/256                    |
  |   1  |   0  |   1  |                  1/1024                    |
  |   1  |   1  |   0  |External Clock T1 pin. Clack on falling edge|
  |   1  |   1  |   1  |External Clock T1 pin. Clack on rising edge |
  +------+------+------+--------------------------------------------+
Timer2プリスケーラー設定 (TCCR2Bレジスタ) 
  +------+------+------+----------------+
  | CS22 | CS21 | CS20 |   PRESCALING   |
  +------+------+------+----------------+
  |   0  |   0  |   0  |    NO CLOCK    |
  |   0  |   0  |   1  |  NO PRESCALING |
  |   0  |   1  |   0  |       1/8      |
  |   0  |   1  |   1  |      1/32      |
  |   1  |   0  |   0  |      1/64      |
  |   1  |   0  |   1  |     1/128      |
  |   1  |   1  |   0  |     1/256      |
  |   1  |   1  |   1  |    1/1024      |
  +------+------+------+----------------+

Timer0とTimer1では、外部クロックでタイマーを動かすことができるようになっています。なお、プリスケーラーの設定を、すべて0(NO CLOCK)にすると、タイマ割り込みを停止できます。デバッグ時などに使えるかもしれません。

一つだけ使えないベクターがある

割り込み処理ルーチンを宣言するためのベクターは、タイマーそれぞれに三つあります。つまり、三つのタイマーに、それぞれ三つのベクターがあります。したがって、3×3=9個の割り込みルーチンが使えると思っていました。もちろん、ATmega328P単体では9個の割り込みベクター=割り込みルーチンが使用できます。しかし、Arduinoとして使用する場合には、一つだけ使うことのできないベクター=割り込みがあります。

それは、TIMER0_OVF_vectです。つまり、Arduinoでは、Timer0のオーバーフロー割り込みを使用することはできません。使おうとすると、コンパイルエラーになります。

こんな感じです。

wiring.c.o (symbol from plugin): In function `__vector_16':
(.text+0x0): multiple definition of `__vector_16'
C:\Users\atata\AppData\Local\arduino\sketches\016C0A821F741E9D8F3486871B5BB8A6\sketch\timer_interrupt_TOV_test.ino.cpp.o (symbol from plugin):(.text+0x0): first defined here
collect2.exe: error: ld returned 1 exit status
exit status 1

Compilation error: exit status 1

どうやら、Arduinoのランタイムで、ベクターTIMER0_OVF_vectのISRが定義済みのようです。エラーメッセージにあった、wiring.cを覗いてみたらこんな感じでした。

TIMER0_OVF_vectが使用的ない理由

がっつりmillis()関数で参照されるカンターの、カウントアップ処理をやっています。このルーチンを消去して、無理やりTIMER0_OVF_vectを使用できるかもしれません。しかし、millis()関数を使用するスケッチは、それなりにあると思います。消し去ってしまうと、影響が大きそうです。したがって、Timer0のオーバーフロー割り込みの使用は、あきらめることにしました。

なお、ベクターの宣言を、TIM0_OVF_vectまたはTimer0_OVF_vectにすることで、コンパイルエラーは回避できます。しかし、割り込み処理ルーチンが呼び出されることはありません。これについては、不確かな情報が多く、混乱しました。

タイマーモード

Arduinoのタイマー割り込みを使用する場合には、タイマーモードを設定しなければなりません。しかし、ATmega328Pにはたくさんのタイマーモードが存在します。詳細については、データシートの参照をお勧めします。しかし、一般的な利用であれば、nomalモードとCTCモードの二つで事足りるでしょう。

nomailモードでは、カウンターは0からカウントを始め、Timer0とTimer2では0xFFでオーバーフローします。Timer1の場合、カウンターは16ビットですので、0xFFFFでオーバーフローします。オーバーフローと同時にTIMERx_OVF_vectのISRが実行されます。

また、カウンターの値が、OCRxAと一致したときに、TIMERx_COMPA_vectのISRが実行されます。さらに、カウンターの値が、OCRxBと一致したときには、TIMERx_COMPB_vectのISRが実行されます。

CTCモードは、nomalモードとカウンターリセットのタイミングが異なります。CTCモードでは、OCRxBがカウンターの上限となります。例えば、OCRxBにあらかじめ0x7Fを代入しておいた場合、カウンターは0x7Fになった時点でリセットされます。カウンターリセットにより、カウンターは0に戻り、TIMERx_COMPB_vectのISRが実行されます。

また、CTCモードでは、オーバーフロー割り込みの利用に注意が必要です。8ビットタイマーでは、OCRxAが0xFF以外の時に、オーバーフロー割り込みは実行されません。同様に、16ビットタイマーでは、OCRxAが0xFFFF以外の場合、オーバーフロー割り込みは発生しません。

また、「OCRxA < OCRxB」の場合、TIMERxB_COMPB_vectのISRは実行されないので注意してください。

Arduinoのタイマー割り込みを使うサンプルスケッチ

Arduinoのタイマー割り込みをテストする目的で、サンプルスケッチを作ってみました。このサンプルでは、Timer2で、D13ピンに接続したLEDをスムースブリンクさせています。また、Timer1の16ビットカウンタを有効に使って、D12ピンのLEDを、1秒に1回点滅させています。

Timer0では、タイマー割り込みと、変数を使ったカウンタを組み合わせたました。これにより、D11に接続したLEDを1秒に3回点滅させています。

LEDの点滅は、全て割り込みで処理されています。したがって、loop()の中は空っぽです。

なお、使用する変数について注意事項があります。割り込みルーチン内では、グローバル変数を使用します。そのため、変数定義は、setup()の前に記述しなければなりません。また、グローバル変数は、コンパイラの最適化対象外となるよう、volatile修飾をつけておきましょう。

#define LED_2 (1 << 5)  // D13:portB bit5
#define LED_1 (1 << 4)  // D12:portB bit4
#define LED_0 (1 << 3)  // D11:portB bit3

volatile bool count_dir;
volatile bool blank_sts;
volatile uint8_t blank_time;
volatile uint8_t blank_count;
volatile uint8_t counter_0;

void setup() {
  // put your setup code here, to run once:

  // timer0のセッティング()
  TCCR0A = 0;                                              // コントロールレジスタAクリア
  TCCR0B = 0;                                              // コントロールレジスタBクリア
  TCCR0A |= (1 << WGM01) | (1 << WGM00);                   // mode3 fast PWMモード設定
  TCCR0B |= (0 << WGM02);                                  //
  TCCR0B |= (1 << CS02) | (0 << CS01) | (1 << CS00);       // プリスケーラー 1/1024
  TIMSK0 |= (0 << OCIE0B) | (1 << OCIE0A) | (0 << TOIE0);  // OCR0A一致割り込み有効化

  counter_0 = 0;  // 分周カウンターリセット

  // timer1のセッティング
  TCCR1A = 0;                                              // コントロールレジスタAクリア
  TCCR1B = 0;                                              // コントロールレジスタBクリア
  TCCR1A |= (0 << WGM11) | (0 << WGM10);                   // mode4 CTCモード設定
  TCCR1B |= (0 << WGM13) | (1 << WGM12);                   //
  TCCR1B |= (1 << CS12) | (0 << CS11) | (1 << CS10);       // プリスケーラー 1/1024
  TIMSK1 |= (1 << OCIE1B) | (0 << OCIE1A) | (0 << TOIE1);  // OCR1B一致割り込み
  OCR1A = 15625;                                           // カウンタ上限値 1秒
  OCR1B = 7813;                                            // カウンタ中間 0.5秒

  // timer2のセッティング
  TCCR2A = 0;                                             // コントロールレジスタAクリア
  TCCR2B = 0;                                             // コントロールレジスタBクリア
  TCCR2A |= (1 << WGM21) | (0 << WGM20);                  // CTCモード設定
  TCCR2B |= (0 << WGM22);                                 //
  TCCR2B |= (1 << CS22) | (1 << CS21) | (1 << CS20);      // プリスケーラー 1/1024
  TIMSK2 = (1 << OCIE2B) | (1 << OCIE2A) | (0 << TOIE2);  // OCR2A,OCR2B一致割り込み
  OCR2A = 200;                                            // カウンタ上限値セット
  OCR2B = 0;                                              // 初期デューティー比設定

  count_dir = true;   // カウンタ方向 true:+(照度up) false:-(照度down)
  blank_time = 180;   // LED消灯期間長さ設定
  blank_count = 0;    // LED消灯サイクルカウンタ
  blank_sts = false;  // LED消灯状態 (true:消灯)

  /*
    ピンモードセット
    D11,D12,D13(port-B bit3,4,5)をOUTPUTに設定する
  */
  DDRB |= LED_0 | LED_1 | LED_2;  //D11,D12,D13 pin set as OUTPUT
}

void loop() {
}

/*
タイマー0 OCR0Aがカウンタ値と一致したら呼ばれる割り込みルーチン
*/
ISR(TIMER0_COMPA_vect) {
  if (counter_0 <= 1) PORTB |= LED_0;
  else if (counter_0 == 10) PORTB &= ~LED_0;
  if (counter_0++ >= 20) counter_0 = 0;
}

/*
タイマー1 OCR1Aがカウンタ値と一致したら呼ばれる割り込みルーチン
*/
ISR(TIMER1_COMPA_vect) {
  PORTB |= LED_1;
}

/*
タイマー1 OCR1Bがカウンタ値と一致したら呼ばれる割り込みルーチン
*/
ISR(TIMER1_COMPB_vect) {
  PORTB &= ~LED_1;
}

/*
タイマー2のOCR2A(TOP)一致で呼ばれる割り込みルーチン
*/
ISR(TIMER2_COMPA_vect) {
  if (count_dir) OCR2B++;
  if (!blank_sts) PORTB |= LED_2;  // set portB bit5 as D13
}

/*
タイマー2のOCR2B(中間)一致で呼ばれる割り込みルーチン
*/
ISR(TIMER2_COMPB_vect) {
  if (blank_sts) {
    if (++blank_count >= blank_time) {
      blank_sts = false;
      count_dir = true;
    }
  } else {
    PORTB &= ~LED_2;  // reset portB bit5 as D13 LED消灯
    if (count_dir == true) {
      if (OCR2B >= (OCR2A - 1)) count_dir = false;
    } else {
      if (--OCR2B == 0) {
        blank_sts = true;
        blank_count = 0;
      }
    }
  }
}

このサンプルを実行すると、こんな感じです。Arduinoのタイマー割り込みだけで、LEDを点滅させています。

Arduinoのタイマー割り込みで、LEDを点滅させてみました
サンプルスケッチ実行結果

Arduinoのタイマー割り込みは多才です。今回は、ISRの中でポート操作を行って、LEDを点滅させました。しかし、タイマーの状態を直接ピンに出力する方法もあります。これを使えば、制約は多くなりますが、割り込み処理を記述せずに、ピンをON/OFFすることが出来ます。

コメントを残す

This site uses Akismet to reduce spam. Learn how your comment data is processed.