Arduino タイマ割り込みの使い方|delay卒業・分周から作る周期処理 練習問題7問
この記事では、Arduino Uno R3で決まった間隔で処理を動かすタイマを、delay()に頼らず自分で作ります。このドリルの中でも特に重要なテーマです。
Lチカではdelay(1000)で1秒待ちました。しかしdelay()の間、マイコンはほかに何もできません。温度を測りながら、ボタンも見て、通信もする、といった「複数の処理を並行して回す」には向きません。組込みの実務では、10msごと・100msごとに決まった処理を回す周期タスクが基本で、delay()で止まる書き方は使いません。
まずはmillis()を使ってdelay()なしの周期を作り、次にタイマのプリスケーラ(分周)から割り込みまでを自分で設定して、正確な1秒周期を作ります。シリアルのボーレートで出てきた「クロックを分周する」考え方を、今度は時間の制御に使います。
<自己紹介>
筆者は現役の車載組込みソフトエンジニアです。実務のソフトは、決まった周期で動くタスクの集まり。delay()で待つのではなく、タイマ割り込みで時間を刻むこの考え方は、そのまま業務のコードにつながります。分周やレジスタ名で最初につまずいた経験も踏まえて、データシートの読み方から説明します。
目次
タイマと実行周期で学ぶこと
実行周期とは、「決まった間隔で処理を繰り返す」ことです。1秒ごとに温度を測る、10msごとにボタンを見る、といった処理のもとになります。
delay()は待っている間ほかの処理ができないので、周期処理には使わないmillis()を使うと、delay()なしで「前回から一定時間たったか」で周期を作れる- 正確な周期は、タイマのプリスケーラ(分周)と比較一致割り込みで作る
- プリスケーラは、クロックを割ってタイマの歩幅を決める設定。ボーレートの分周と同じ考え方
本記事のコードは、Arduino Uno(ATmega328P)向けにPlatformIOでコンパイル確認しました。点滅やシリアル出力の実表示はまだ画像にしていないため、手元のUno R3で確認しながら進めてください。
準備するもの
| 使うもの | 用途 |
|---|---|
| Arduino Uno R3またはR3互換ボード | ATmega328PのTimer/Counter1を使うため。本記事はUno R4対象外 |
| USBケーブル | スケッチの書き込みと、シリアル出力の確認に使う |
| 内蔵LED(D13) | 周期どおりに点滅しているかを目で確認する |
先にLチカのドリルでDDRB・PORTBの操作を、シリアルのドリルで値の確認を済ませておくと、この記事のコードが読みやすくなります。
まずdelayをやめて、millisで周期を作る
millis()は、プログラムが動き始めてからの経過時間をミリ秒で返す関数です。「前回処理した時刻」を覚えておき、そこから決めた時間がたったかを毎回確認すれば、delay()で止まらずに周期を作れます。
unsigned long prev = 0;
const unsigned long interval = 1000; // 1000ms周期
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
unsigned long now = millis();
if (now - prev >= interval) {
prev = now;
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // 反転
}
// ここに別の処理を書いても、待たされずに動く
}
delay()版と違い、loop()は止まりません。ifを通らない間はすぐ下へ進むので、同じloop()の中に別の処理を並べられます。これが「複数の処理を並行して回す」の第一歩です。
millis()が時間を数えられるのは、Arduinoが裏でTimer0というタイマを一定間隔で割り込ませ、カウントを増やしているからです。つまり便利な関数の内側でも、これから自分で作るのと同じタイマ割り込みが動いています。
次にタイマを分周から自分で作る
millis()は手軽ですが、loop()の回り方に周期が左右され、正確な間隔はやや苦手です。そこで、マイコンのタイマを直接設定して、ハードウェアが正確に1秒を刻み、割り込みで処理を呼ぶ形にします。ここでも、いきなりTCCR1Bと書く前に、データシートで名前の出どころをたどります。
データシートからタイマを読み解く
使うのはATmega328Pのデータシート(Microchip、文書番号7810D–AVR)の「15. 16-bit Timer/Counter1」の章です。以下では、データシートの該当箇所を原文引用しながら、3つの手順で読み解きます。
① 何を分周するのかを決める
タイマは、クロックを数えて時間を測ります。Uno R3のクロックは16MHz、つまり1秒に16,000,000回進む速さ。これをそのまま数えると速すぎるので、プリスケーラ(分周器)でクロックを割って、タイマの歩幅を決めます。プリスケーラの選べる値は次のとおりです。
| プリスケーラ(分周比) | タイマクロック(16MHzを割った値) |
|---|---|
| 1(分周なし) | 16,000,000 Hz |
| 8 | 2,000,000 Hz |
| 64 | 250,000 Hz |
| 256 | 62,500 Hz |
| 1024 | 15,625 Hz |
今回は分周比256を選びます。タイマは1秒間に62,500回進む計算です。1秒の周期なら、62,500回数えたら割り込む設定にします。
② TCCR1Bで、モードとプリスケーラを設定する
タイマの動作モードとプリスケーラは、TCCR1Bというレジスタのビットで決めます。データシートの「15.11.2 TCCR1B」(p.110)には、プリスケーラを選ぶCS12・CS11・CS10(Clock Select)について原文でこう書かれています(英文は原文引用です)。
Bit 2:0 – CS12:0: Clock Select
The three clock select bits select the clock source to be used by the Timer/Counter.
(訳:ビット2:0 – CS12:0:クロック選択。この3つのクロック選択ビットで、タイマ・カウンタが使うクロック源を選ぶ。)出典:ATmega328P Datasheet(Microchip, 7810D–AVR)15.11.2 TCCR1B, p.110 より原文引用(© Microchip Technology)
同じページのTCCR1Bの定義図と、クロック選択の対応表(Table 15-6)は次のとおりです。分周比256はCS12・CS11・CS10 = 1・0・0(clkI/O/256)にあたり、CTCモードにするWGM12もこのレジスタにあります。

下の表は、今回使うビットを抜き出したものです。
| レジスタ | bit7 | bit6 | bit5 | bit4 | bit3 | bit2 | bit1 | bit0 |
|---|---|---|---|---|---|---|---|---|
TCCR1B |
ICNC1 | ICES1 | – | WGM13 | WGM12 | CS12 | CS11 | CS10 |
この表から、分周比256はCS12だけを1にすればよいと分かります。WGM12を1にするとCTCモードになり、次に決める上限値まで数えたら自動で0へ戻ります。
③ OCR1Aで上限(周期)を決め、TIMSK1で割り込みを許可する
数える上限はOCR1Aという16ビットのレジスタに入れます。データシートの「15.11.5 OCR1AH and OCR1AL」(p.111)には、こう書かれています(英文は原文引用です)。
The output compare registers contain a 16-bit value that is continuously compared with the counter value (TCNT1). A match can be used to generate an output compare interrupt, or to generate a waveform output on the OC1x pin.
(訳:出力比較レジスタは16ビットの値を保持し、カウンタ値(TCNT1)と絶えず比較される。一致は、出力比較割り込みの発生や、OC1xピンへの波形出力に使える。)出典:ATmega328P Datasheet(Microchip, 7810D–AVR)15.11.5 OCR1AH and OCR1AL, p.111 より原文引用(© Microchip Technology)
62,500回で1秒なので、0から数える分を引いてOCR1A = 62499です。数え終わった合図(比較一致)で処理を呼ぶには、TIMSK1のOCIE1Aを1にします。データシートの「15.11.8 TIMSK1」(p.112)には、こう書かれています(英文は原文引用です)。
Bit 1 – OCIE1A: Timer/Counter1, Output Compare A Match Interrupt Enable
When this bit is written to one, and the I-flag in the status register is set (interrupts globally enabled), the Timer/Counter1 output compare A match interrupt is enabled.
(訳:ビット1 – OCIE1A:タイマ・カウンタ1 出力比較A一致割り込み許可。このビットに1を書き、ステータスレジスタのIフラグがセット(割り込みが全体で有効)されていると、タイマ・カウンタ1の出力比較A一致割り込みが有効になる。)出典:ATmega328P Datasheet(Microchip, 7810D–AVR)15.11.8 TIMSK1, p.112 より原文引用(© Microchip Technology)
| レジスタ | bit7 | bit6 | bit5 | bit4 | bit3 | bit2 | bit1 | bit0 |
|---|---|---|---|---|---|---|---|---|
TIMSK1 |
– | – | ICIE1 | – | – | OCIE1B | OCIE1A | TOIE1 |
周期の計算はいつも同じ形です。上限+1 = タイマクロック × 作りたい秒数。1秒なら 62,500 × 1 = 62,500 で、上限は62,499。0.1秒なら 62,500 × 0.1 = 6,250 で、上限は6,249になります。
#include <Arduino.h>
#include <avr/interrupt.h>
void timer1_init_1hz(void) {
cli(); // 設定中は割り込みを止める
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0; // カウンタを0に
OCR1A = 62499; // 1秒ぶんの上限(62500-1)
TCCR1B |= _BV(WGM12); // CTCモード
TCCR1B |= _BV(CS12); // プリスケーラ256
TIMSK1 |= _BV(OCIE1A); // 比較一致Aの割り込みを許可
sei(); // 割り込みを再開
}
ISR(TIMER1_COMPA_vect) { // 1秒ごとに呼ばれる
PORTB ^= _BV(PORTB5); // D13のLEDを反転
}
void setup() {
DDRB |= _BV(DDB5); // D13(PB5)を出力に
timer1_init_1hz();
}
void loop() {
// 何も書かなくても、LEDは割り込みで正確に点滅する
}
cli()で割り込みを止めてから設定し、sei()で再開します。ISR(TIMER1_COMPA_vect)が、1秒ごとに自動で呼ばれる処理です。loop()が空でもLEDが点滅するのは、タイマがハードウェアとして動き、割り込みが処理を呼ぶからです。
UBRR0でクロックを割ってボーレートを作りました。タイマも同じで、プリスケーラでクロックを割って歩幅を決めています。1つの16MHzクロックを、用途に合わせて割って使う。これがマイコンの時間まわりの基本です。
練習問題(基礎5+発展2)
基礎5問は、millis版と、タイマの分周・上限・割り込み許可を確認する問題です。発展2問は、割り込み処理と周期の変更を扱います。まず自分で書いてから回答を開いてください。
基礎 Q1
delay()を使わず、millis()で内蔵LEDを1秒周期で点滅させてください。
解答・解説を見る
unsigned long prev = 0;
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
if (millis() - prev >= 1000) {
prev = millis();
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
}
解説:「前回の時刻」をprevに覚えておき、そこから1000ms以上たったら反転します。delay()と違ってloop()が止まらないので、ほかの処理も並べられます。
基礎 Q2
クロック16MHz・プリスケーラ256のとき、タイマクロックは何Hzですか。また、1秒周期にするためのOCR1Aの値も答えてください。
解答・解説を見る
解答:タイマクロック=16,000,000 ÷ 256 = 62,500 Hz。OCR1A=62,500 − 1 = 62,499。
解説:プリスケーラでクロックを割った値が、1秒あたりのカウント数です。0から数える分の1を引くので、上限は62,499になります。
基礎 Q3
TCCR1Bで、決めた上限まで数えたら0に戻すCTCモードにする1行を書いてください。
解答・解説を見る
TCCR1B |= _BV(WGM12);
解説:WGM12はTCCR1Bの3番ビットです。1にするとCTCモードになり、OCR1Aまで数えたら自動で0へ戻ります。
基礎 Q4
TCCR1Bで、プリスケーラを256にする1行を書いてください。
解答・解説を見る
TCCR1B |= _BV(CS12);
解説:分周比256はCS12・CS11・CS10を「1・0・0」にする設定です。CS11とCS10は0のままでよいので、CS12だけを1にします。
基礎 Q5
比較一致Aの割り込みを許可する1行を書いてください。
解答・解説を見る
TIMSK1 |= _BV(OCIE1A);
解説:OCIE1AはTIMSK1の1番ビットです。これを1にすると、OCR1Aまで数えたときにISR(TIMER1_COMPA_vect)が呼ばれます。全体の割り込みを有効にするsei()も必要です。
発展 Q6
1秒ごとにD13のLEDを反転させる割り込み処理ISR(TIMER1_COMPA_vect)を、レジスタ操作で書いてください。
解答・解説を見る
ISR(TIMER1_COMPA_vect) {
PORTB ^= _BV(PORTB5); // PB5(D13)を反転
}
解説:^=は排他的論理和で、そのビットを反転します。1なら0、0なら1になるので、呼ばれるたびに点灯と消灯が入れ替わります。割り込み処理は短く保ち、重い処理は入れないのが基本です。
発展 Q7
周期を100ms(0.1秒)に変えたいとき、プリスケーラ256のままならOCR1Aはいくつにしますか。
解答・解説を見る
解答:62,500 × 0.1 = 6,250 なので、OCR1A=6,249。
解説:「上限+1=タイマクロック×秒数」で計算します。6,249は16ビット(0〜65,535)に収まるので、プリスケーラ256のままで作れます。もっと長い周期で上限が65,535を超えるときは、プリスケーラを1024に上げてタイマの歩幅を大きくします。
例:実務で実際に書くコード
車載などの組込みソフトは、周期タスクの集まりでできています。「10msごとに入力を読む」「100msごとに制御を計算する」というように、処理ごとに決まった周期が割り当てられ、タイマ割り込みがその起点になります。
/* タイマ割り込みでフラグを立てるだけにする */
ISR(TIMER1_COMPA_vect) {
g_10ms_flag = 1;
}
/* メインループはフラグを見て、対応する処理を呼ぶ */
if (g_10ms_flag) {
g_10ms_flag = 0;
read_inputs(); // 10ms周期の処理
}
ここでdelay()を使わない理由がはっきりします。delay()で止まると、その間ほかの周期処理が遅れ、決めた時間どおりに動くというリアルタイム性が崩れます。待っているだけの時間はCPUの無駄でもある。だから実務では、タイマ割り込みで時間を刻み、待たずに周期を回します。総合課題の「5分ごとに温度を測る」も、この周期処理の考え方で作ります。
最後につまずきポイントをおさらい
ISRの中とloop()の両方で読み書きする変数には、volatileを付けます。付けないと、コンパイラが「この変数は変わらない」と判断してコードを省略し、割り込みでの変化がloop()に伝わらないことがあります。例:volatile uint8_t g_tick;
もう一つは、OCR1Aが16ビット(0〜65,535)までという上限です。プリスケーラ256で作れる最長は約1秒までなので、それより長い周期はプリスケーラを1024に上げるか、割り込みの中で回数を数えて対応します。TCCR1Bなどのレジスタ名はATmega328P向けで、Uno R4など別のマイコンでは変わる点も、Lチカ・シリアルと同じです。
よくある質問(FAQ)
Timer0・Timer1・Timer2はどう違いますか?
ATmega328Pには3つのタイマがあります。Timer0とTimer2は8ビット、Timer1は16ビット。millis()やdelay()は内部でTimer0を使っています。長い周期を正確に作りたいときは、数えられる範囲が広い16ビットのTimer1が扱いやすいです。
millisとタイマ割り込みは、どちらを使えばよいですか?
おおまかな周期でよければmillis()で十分です。正確な間隔や、loop()の混み具合に左右されたくない処理は、タイマ割り込みが向きます。まずmillis()で組み、精度が要る部分だけ割り込みにする、という使い分けが現実的です。
割り込みの中で長い処理をしてよいですか?
避けてください。割り込み処理が長いと、次の割り込みやほかの処理が遅れます。ISRではフラグを立てるなど短い処理だけにして、実際の重い処理はloop()側で行うのが基本です。
次に読む
正確な周期を作れるようになったら、次はその周期でセンサの値を読みます。温湿度センサDHT11を、ライブラリに頼らずデータシートから読み取る練習へ進みます。
- Arduino シリアルのドリル(#2):分周でボーレートを作る考え方を復習する
- C言語 ビット演算の練習問題:
_BV()と^=の意味を確認する - Arduino DHT11をライブラリなしで読むドリル(#4・作成中):周期タイマでセンサを読む