この記事では、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()で止まらずに周期を作れます。

blink_millis.ino

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の正体もタイマ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もこのレジスタにあります。

ATmega328PのTCCR1Bレジスタ定義とTable 15-6クロック選択。CS12・CS11・CS10が1・0・0のときclkI/O/256(プリスケーラ256)
図:TCCR1Bの定義とTable 15-6「Clock Select Bit Description」。CS12・CS11・CS10=1・0・0でclkI/O/256(プリスケーラ256)。出典:ATmega328P Datasheet(Microchip, 7810D–AVR)15.11.2 TCCR1B, p.110 より原文引用(© Microchip Technology)

下の表は、今回使うビットを抜き出したものです。

レジスタ 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です。数え終わった合図(比較一致)で処理を呼ぶには、TIMSK1OCIE1Aを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になります。

timer1_register.ino

#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秒周期で点滅させてください。

解答・解説を見る
q1_millis.ino

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);

解説:WGM12TCCR1Bの3番ビットです。1にするとCTCモードになり、OCR1Aまで数えたら自動で0へ戻ります。

基礎 Q4

TCCR1Bで、プリスケーラを256にする1行を書いてください。

解答・解説を見る
TCCR1B |= _BV(CS12);

解説:分周比256はCS12・CS11・CS10を「1・0・0」にする設定です。CS11CS10は0のままでよいので、CS12だけを1にします。

基礎 Q5

比較一致Aの割り込みを許可する1行を書いてください。

解答・解説を見る
TIMSK1 |= _BV(OCIE1A);

解説:OCIE1ATIMSK1の1番ビットです。これを1にすると、OCR1Aまで数えたときにISR(TIMER1_COMPA_vect)が呼ばれます。全体の割り込みを有効にするsei()も必要です。

発展 Q6

1秒ごとにD13のLEDを反転させる割り込み処理ISR(TIMER1_COMPA_vect)を、レジスタ操作で書いてください。

解答・解説を見る
q6_isr.ino

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ごとに制御を計算する」というように、処理ごとに決まった周期が割り当てられ、タイマ割り込みがその起点になります。

cyclic_task.c

/* タイマ割り込みでフラグを立てるだけにする */
ISR(TIMER1_COMPA_vect) {
    g_10ms_flag = 1;
}

/* メインループはフラグを見て、対応する処理を呼ぶ */
if (g_10ms_flag) {
    g_10ms_flag = 0;
    read_inputs();      // 10ms周期の処理
}

ここでdelay()を使わない理由がはっきりします。delay()で止まると、その間ほかの周期処理が遅れ、決めた時間どおりに動くというリアルタイム性が崩れます。待っているだけの時間はCPUの無駄でもある。だから実務では、タイマ割り込みで時間を刻み、待たずに周期を回します。総合課題の「5分ごとに温度を測る」も、この周期処理の考え方で作ります。

最後につまずきポイントをおさらい

割り込みと共有する変数はvolatileにする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を、ライブラリに頼らずデータシートから読み取る練習へ進みます。