PIC AVR 工作室別館 arduinoの館->TopPage->各種ライブラリ->タイマー割り込み

タイマー割り込みライブラリ

<補足情報>

現在、MsTimer2ライブラリの作者に、割り込み間隔の正確さについて確認を依頼中です。

現状のオーバーフロー割り込みではなくCTCモードで駆動して割り込みを発生させることにより割り込み間隔=1ミリ秒を保証できるものと考え、 公開中のライブラリを元に私が修正を入れたものを作者宛にメールし、確認をお願い中です。

そのライブラリソースを貼っておきます(右クリックでダウンロードしてください)。

なお、一応動作確認は行いましたが、この内容は無保証です。ご利用の場合はソースコードによく目を通して頂いた上で、at your own riskでお使いください。

<補足情報:ここまで>

arduinoの開発環境では、タイマー割り込みがContributed Librariesとして公開されています(バージョン0011公開時点)。

以下に、タイマー割り込みの概要と、arduinoでのタイマー割り込み使用方法、サンプルスケッチを纏めます。

タイマー割り込みとは

何かプログラム(スケッチ)を作っている時に、処理と処理の間に間隔を空けて処理させたいなぁということが 時々あると思います。なにしろ、arduinoの頭脳は1秒間に16000000回も命令を処理するので、例えばその速度でLEDを 点けたり消したりしても人間の目には点灯したままにしか見えないからです。

点けてから少し時間を置いて、消してからまた時間を置いて…という具合にすると人間の目には点滅して 見えるようになります。このように、処理と処理の間に少し間隔を空けたいという場合、遅延処理とかタイマー割り込みと いうものが利用されます。

タイマー割り込みとは、(細かい仕組みなどは後述しますが)大雑把に機能を要約すると 「一定の時間間隔で特定の関数(処理)を呼び出す」仕組みです。例えば、1/100秒毎とか1秒毎など定期的にある特定の処理を 行いたい場合などに使用します。

これとよく対比されるのは遅延処理とか時間稼ぎ処理とか言われる処理です。arduinoでは”標準機能”の中にこの遅延処理が 組み込まれています。delay()関数やdelayMicroseconds()関数などがそれにあたります。

これら遅延処理の関数をループ処理の中に挟み込んでおけば、一定の間隔を空けて処理を繰り返すことが出来ます。 その点ではタイマー割り込みと似ているのですが、動作タイミングの正確さという意味では少し違ってきます。 以下の図にその違いを記します。

遅延処理の場合

まずは遅延処理を利用した場合の処理フローです。

この図では、「処理A」と「遅延処理」をグルグルと繰り返すという処理内容になっています。 遅延処理の間、arduinoはこの1秒間「何もしないということ」をしています(くまのプーさんと一緒です)。 一般的にこの遅延機能は処理と処理の間に最低でも1秒間以上空ける必要があるときに用いられます。

タイマー割り込みの場合

次にタイマー割り込みを利用した場合の処理フローです。

この図では、「メイン処理」をぐるぐる回し続けながら、1秒毎にメイン処理を一旦中断して「処理A」に 制御を移し、処理Aが片付いたら中断したところからまた処理を再開するという流れになっています。

正確に1秒毎に処理Aが実行されるので、「正確な時間間隔」で処理するのに適しています。ちなみに、1秒毎に処理が 呼び出されるので、当然ながら処理Aに要する処理時間は割り込み間隔(この図なら1秒)以内に収まってないといけません。

(処理Aを行ってない間はメイン処理を行っているので、その点ではくまのプーさんとは異なりますね)

遅延処理とタイマー割り込みのまとめ

このような違いを考慮し、処理と処理の間に一定時間以上の間隔を空けたいのか、それとも正確な時間間隔で 定期的に処理を行いたいのかによってどちらを利用すればよいかを判断すればよいかと思います。大抵は遅延処理で 用が足りると思われます。簡単ですし。

タイマー割り込みを用いる最も典型的な応用例は「時計」でしょう。1秒毎に割り込みが発生する ように設定しておいて、割り込みが発生する毎に秒針を1秒進めるといった具合です。ちなみにその場合、メイン処理側では 時刻をLCDに表示したり、モーターで秒針を進めたり、アラーム設定時刻の判定をしていたり…というような 時間がかかる処理を行うようにしています。

なお、割り込み間隔の精度ですが、これはarduinoボード上に使用するクリスタルの精度がそのまま現れます。 互換ボードなどでセラロックを使っていたとすれば、精度は少し低くなってしまうので、時計のような用途には 向かないでしょう。超高精度オシレータを登載しているarduinoも見たこと無いですが…

タイマー割り込みライブラリMsTimer2

arduinoのタイマー割り込みライブラリは「MsTimer2」という名前で公開されています。まずはライブラリを arduino-IDEに組み込んでください。(タイマー割り込みに関しては、目下のところこれ以外のライブラリは 公開されていないようです)

公式ページのここでダウンロードできます。

仕組み

MsTimer2のタイマー割り込みは、AVR AT-MEGA168チップに内蔵されている3つのタイマー機能 のうち、タイマー2というモノを利用して提供されます。

難しく言うと、AT-MEGA168のタイマー2は、CPUのクロックか入力ピンからのクロック入力(を適当に分周したクロック) を自動的にカウントし、指定されたカウント値に達したらハードウェア割り込みを発生させるという機能を持っています。

でもそういったIC内部のそういう細かい制御を自分で行うのは面倒だということで、ライブラリがオブラートに包んで 苦い薬も飲みやすくしてくれています。

MsTimer2を利用すると、1/1000秒単位で割り込み間隔を指定することが出来、その指定した時間が経過する毎に 任意のユーザー関数を呼び出してくれるようになっています。例えば、50ミリ秒と指定しておけば、50ミリ秒毎に 正確に任意の関数を呼び出してくれます。

なお、タイマー2といえばデジタル3番ピンやデジタル11番ピンでのアナログ出力(=PWM出力)でも 用いられるタイマーですので、これらと同時に使うことは出来ません。

(もしかしたら使えるかもしれませんが、その場合どういう動作になるかはライブラリのソースプログラムを 読み解かないとよく解りません。既存機能との相性問題というところなどがContributed Librariesということ なのかもしれません。)

使い方

MsTimer2の機能には、「割り込み間隔をミリ秒単位で指定する機能」、「割り込み発生時に呼び出す先の関数を指定する機能」、 「タイマーをスタートさせる機能」、「タイマーを停止させる機能」があります。

割り込み間隔と割り込み先を指定するのは以下の書式で行います。

MsTimer2::set(ミリ秒, 関数名)

ミリ秒に指定出来る範囲は、1ミリ秒~4294967295ミリ秒までです。最長約50日間隔で割り込みを発生することが出来ます(笑)。 ちなみに、ミリ秒指定は内部ではunsigned long型で定義されています。定数で長ーーーーい時間間隔を指定する場合、定数値の後ろに 適宜ULをつけないとコンパイル時にエラーが出るはずです。まぁ、そんなに長い間隔で使うタイマー割り込みって、用途を思いつきませんが…。

カウントの開始と中断はそれぞれ

MsTimer2::start()
MsTimer2::stop()

にて行います。startでカウント開始、stopでカウント中断です。両関数とも引数は要りません。一度setup()関数内でスタート したら、あとはカウントしっぱなしという使い方が多いと思います。

サンプルスケッチ

以下に、MsTimer2を使ったサンプルスケッチを記します。といっても、ライブラリのダウンロードしたzipファイルに 入っていたサンプルスケッチを殆どそのままです。(#include文のところの不等号がブラウザ上で上手く表示されない ので全角不等号に置換してあります。半角に戻して使用してください)

#include <MsTimer2.h>
void flash() {
  static boolean output = HIGH;
  digitalWrite(3, output);
  output = !output;
}
void setup() {
  pinMode(3, OUTPUT);
  MsTimer2::set(500, flash);
  MsTimer2::start();
}
void loop() {
}

0.5秒毎にLEDを点けたり消したりするスケッチです。簡単に解説します。

まずloop()関数ですが、ご覧の通り何もしてませんね。空回しという状態です。先ほどの図では「メイン処理」が このloop()関数に相当しています。普通はこのloop()関数内で主だった処理を行いながら、時々割り込みが入るという使い方をします。

次にsetuo()関数ですが、デジタル3番ピン(※)を出力に設定し、MsTimer2::set(500, flash); で500ミリ秒(=0.5秒) 毎にflash()関数を呼び出すように設定しています。その次のMsTimer2::start(); でタイマーがカウントを始めます。 (※:私のreduino-nanoはLEDが3番ピンなので…普通のarduinoであれば13番に設定してください)

そして、flash()関数です。outputという変数を定義していますが、static属性なので実行時の最初に1回だけHIGHで 初期化され、その後は割り込みの都度output = !output; でHIGHとLOWを繰り返します。それをデジタル出力ピンに 出力しています。

何もしていないloop()関数をグルグルと処理中に0.5秒が経過すると、一旦loop()関数の処理を中断してflash()関数を頭から 処理します。flash()関数の処理が終ったら、loop()関数の中断していた場所に戻って、処理を再開します。

最後にスケッチ先頭の#include文。MsTimer2.hを取り込むことでタイマー割り込み機能が使えるようになります。

結果、LEDが0.5秒毎に点いたり消えたりという動作になります。

試しに、タイマー割り込みではなく遅延処理を使ったLED点灯のスケッチも見てみましょう。 公式サイトのチュートリアルページに こういうスケッチがあります。(少し端折ってます)

int ledPin = 13;
void setup()
{
  pinMode(ledPin, OUTPUT);
}
void loop()
{
  digitalWrite(ledPin, HIGH);
  delay(1000);
  digitalWrite(ledPin, LOW);
  delay(1000);
}

続に言うLEDピコピコというヤツです。1秒毎に点けて、消して、を繰り返します。delay()関数で1秒の待ち時間を作り出しています。

ぜひこれを実行して、先ほどのタイマー割り込みのスケッチと比べてみてください。タイマー割り込み の方はずっと動かしっぱなしにしておいてもLED点滅のと時計の秒針がシンクロし続けるのに対し、 遅延処理を使ったLEDピコピコは、時計の秒針よりもarduinoの点滅の方が少しずつ遅れていくはずです。1秒+αの時間が かかっているからです。

タイマー割り込みライブラリでの注意点

メイン処理側と割り込み処理関数の間で”変数を共用”する場合には注意が必要になります。

例えば、「時計を作ろう」というケースを想定してみてください。割り込みが発生するごとに、割り込み関数の中では 秒針を1進めておき、メイン処理側ではその秒数を読み出してLCDなどに表示しようということになるかと思います。

つまり、割り込み処理内で秒数をカウントアップしつつ、メイン処理側で秒数を参照するという形になります。 両方の処理で変数を共用することになります。

こういう場合、メイン側と割り込み関数側で共用する変数は、「volatile」属性というものを付けて宣言する必要が あります。volatile属性についてはarduinoの範疇というよりgccの範疇になっちゃうので端折りますが、 変数の定義個所で、例えば以下のように宣言してください。一つの呪文やおまじないだと思って頂いていいです。

volatile int Second;

ご興味のある方は、公式ページ(英文)の こちらをご覧ください。

補足:ライブラリの内部処理

最後に、ちょっとマニアック領域に入ってみます。

本当に1ミリ秒は正確に1ミリ秒なのか? 端数はないのか? その1ミリ秒が如何にして正確に実現されているのかが 気になってしまい、ライブラリソースの中身を少し眺めてみました。その内容を備忘録的にまとめておきたいと思います。

まず前提条件。AT-MEGA168のタイマー2は8ビット幅(0~255)のカウンターを持ったタイマーです。一方、 arduinoに組み込まれているクリスタルは16MHzです。

さて、arduinoの頭脳であるAT-MEGA168のタイマーにはCTC(クリアタイマ・オン・コンペアマッチ)という モードがあります。カウンターが指定の値になったら自動的にカウンタを0に戻してまた数えなおすという機能です(この時割り込みも発生する)。 カウンタの値が8ビット幅の中で任意の値になったらまた0からカウントしなおすということが自動で行えます。 この比較する数の大きさを調節することで、割り込み間隔を長くしたり短くしたり任意に調整することが出来るわけです。

また、タイマーに入力するクロックは直接CPUのカウンターを使うだけでなく、「プリスケーラ」という 一種のカウンタ回路を通すことによってCPUクロックの周波数より1/8、1/64、1/256、1/1024のいずれかに 速度を落としてから使うことも出来ます(少ないビット幅のカウンタでも長い間隔の割り込みが実現できるような工夫です)。 MEGA168版のarduinoでは1/64にセットさるようにライブラリ内で組まれています。そして、 CTCで適用されるカウンタの上限値は250(ギリギリ8ビット幅以内!)に設定されています。

CPUクロックが16000000Hzですから、タイマー2は1/64のプリスケーラを通して250000Hz毎に カウントされます。そして250カウントでカウンタクリア、及び割り込みが生じるので、丁度1000Hz(1/1000秒)毎に 割り込みが生じることになります。これはクリスタルの精度1クロック単位で正確に発生します。

ライブラリの内部では、この1/1000秒毎の割り込み毎にミリ秒カウンタを1つカウントアップするような 処理が行われています。

そして、MsTimer2::set(ミリ秒,関数); にて指定したミリ秒に達すると、ようやく指定の関数を呼び出しに行きます。 つまり、内部では常に正確に1/1000秒毎に割り込みが生じていて、指定したミリ秒分経過すると初めてユーザー関数が 呼び出されるわけです。

というわけで、このタイマー割り込みライブラリを使用すれば、CPUの1クロック単位で正確な間隔で割り込みを 発生させることが出来るわけです。

ただしもう少し厳密に言うと、実際は割り込み時の内部処理によるオーバーヘッドなどがあるので、毎回毎回若干の ズレは生じているはずです。ただ「その若干のズレ」は累積していくことは無く、毎回毎回リセットされることになります。

なので、この割り込みライブラリを時計などに応用した場合、どうせ人間の目には何万分の1秒といった個々の誤差は 判別できないし、誤差は累積していかないので、時計としての機能は万全、クリスタルの精度1クロック単位で正確と言って 良いでしょう。

MsTimer2、安心して使ってください!

なお、MEGA8と違ってMEGA168はデータシート上20MHzまで上げることが出来るのですが、 なぜ16MHzの仕様を踏襲しているのかについても考えてみました。

このMsTimer2ライブラリもそうなのですが、標準機能であるdelay()関数やmillis()関数もMEGA168内蔵の タイマーを利用して1/1000秒を取り出しています(これらはタイマー0を使っている)。

タイマー0やタイマー2は共に8ビット幅のカウンターを持ったタイマーです。タイマー1は16ビット幅です。

20MHzを使用すると、どのプリスケラーを選択しても8ビットカウンタではピッタリ1/1000秒を得ることが 出来なくなります(お暇な方は計算してみてください。端数が出てしまいます)。

というわけで、少しばかりクロックを速くするよりも、タイマー関係のリソースを幅広く便利に応用できる16MHz の方がユーザの利便性が高いということなのだと思います。

さて、AVRといえばxMEGAシリーズは32MHz動作が可能です。今後arduinoにxMEGAシリーズが使われるかは 判りませんが、その時には16MHzから一気に32MHzになるのでしょうか???気になるところです。 入出力ピンが3.3Vになっちゃうんですよねぇ、xMEGAシリーズは。