Arduinoのタイマー割り込み

Arduinoのタイマー割り込みを試してみました。Arduinoで、外部からの入力を得る場合など、loop()の中だけでの処理には限界があります。例えば、待ち時間を確保するためにdelay()関数を使うのが、手っ取り早い方法です。しかし、delay()関数はブロッキング関数ですので、待ち時間中は操作を受け付けません。これを解決するには、割り込み処理を使うのが得策です。
Arduinoには、様々な割り込みが用意されています。まずは割り込み処理の手始めとして、タイマー割り込みについて、調べてみました。以下は、Arduinoのタイマー割り込みについての備忘録です。
Arduinoのタイマーは三つある
Arduinoには三つのタイマーが搭載されています。なお、ここで言うArduinoは、ATmega328Pを搭載しているものを指します。たとえば、安価な互換機で使用されているATmega328PBには、5つのタイマーが搭載されています。しかし、ここでは、拡張されたタイマーについては扱いません。というか、まだ調べ切れていません。
Arduinoに搭載されているタイマーは、Timer0,Timer1,Timer2の三つです。以下に、三つのタイマーの主な役割をまとめてみました。
割り当て関数 | PWMピン | カウンタービット数 | |
Timer0 | millis(),delay() | D6,D5 | 8 |
Timer1 | D9,D10 | 16 | |
Timer2 | tone() | D11,D3 | 8 |
上の表からは、タイマ割り込みを使用すると、どこに影響するかが読み取れます。例えば、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を覗いてみたらこんな感じでした。

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