ArduinoでTimerを使った割込み処理を、ライブラリを使わないで行う方法をメモ書きしておきます。, 割込みにもいろいろ種類(方法)がありますが、今回はTimerを使った時間割込みを行ってみます。例えば、「1秒ごと」に「LEDを点滅」といったような処理です。, ある時間間隔(タイミング)で何かしらの処理を行います。普通にメインループの中で時間操作すれば処理できることもあるのですが、どうしても無駄時間発生するなど、いろいろ弊害がでることもあります。, 割込み処理では、処理発生のタイミングをTimerまかせにできるため、その間はほかの処理ができたり、かなり正確なタイミングを計ることができたり、と利点があります。, Timerを使用した割込み処理をおおざっぱに言うと、Timer(Arduino)が自動でステップカウントします。決められた数値までカウントされたら、関数が呼び出され処理します。と同時にカウントをリセットしてまた自動でカウント・・・。といった具合です。メインループで処理がされていても割り込んで関数が呼び出されます。処理する内容はその関数に記述するといった感じです。, レジスタ操作で割込み設定をするのですが今回の例のように単純な時間割込みだけならそれほど手順は多くないです。, ArduinoUNO(ATmega328/328P)にはTimerが3つ準備されてます。3つのTimerは以下のような構成です。, このような感じでArduinoでは時間に関する関数やPWMピンにTimerが既に使用されています。ですのでレジスタ触るときは既存の関数(特に時間関係の関数に)には何かしら影響がでるかも、と注意したほうがいいでしょう。, bit数はカウンタのMax値です。8bitでは0~255、16bitでは0~65535までカウントできます。, で今回は試しにTimer2を使用する前提でレジスタ(割込み方法)の使い方を見てみます。, 先ほど説明したようにTimerを使用した時間割込みの手順はそれほど多くないです。順を追ってみていきます。, まずはモードの選択です。Timer2の割込みモードに関しては「TCCRA2」「TCCRB2」の二つのレジスタでモード選択とカウント速度を設定することができます。, 単純な一定時間間隔の割込み処理であればmode2のCTCモードに設定します。ですので「TCCRA2」レジスタの「WGM21」bitのみを「1」にします。, などと記述すれば 「TCCRA2」レジスタ の「WMG21」ビットを「1」にできます。後者の表現が分かり易いためなのか?よく見かけますね。, CTCモードはカウンタの上限値(カウント数値)が直接指定できるため、時間割込みするときにはこのモードが便利です。なのでこれに設定しておけば問題ないかと・・。他のモードについてはまた機会があれば説明したいと思います。, 分周比の設定です。Timerでカウントしていくのですが、そのカウント速度(1カウントする時間間隔)の設定です。, 分周無しの場合、Arduinoのクロック周波数は16MHzのため、1カウントは1/16MHz=0.0625μsとなります。, 例えば分周比「8」とした場合は1カウントが1/(16MHz/8)=0.5μsとなります。, 分周比は「TCCR2B」レジスタの「CS20~CS22」ビットの組合わせで設定します。, 最後にいくつまでカウントしたら割込み処理を発生させるかを設定します。例えば「100」カウントで割込み発生させるならば「OCR2A」レジスタに直接数字を入れるだけです。, これだけで完了です。これで100カウント毎に割込み関数が呼び出されます。Timer2は8bitなので0~255までの数字しか指定できないので注意です・・。, となります。これでTimer割込みの設定は完了です。最初の2行は念のためレジスタをリセットしてます。基本この内容は1度実行するだけなので、setup()内に記述すればO.K。上記の設定例であれば100カウント(50μs)毎に関数が呼び出されます。, Timerを使用した時間割込みの設定はこれで完了です。説明の多さの割には記述は少ないです・・。で最後に割込み時の処理を記述します。, こうすれば50μs毎に、UNOであればオンボードのLEDが点滅します。早すぎて点灯してるようにしか見えませんが・・・・・。, ピンポイントにしぼった内容ですが、一定時間間隔で割込みを行うだけでよければこれだけで0.kです。Timer0やTimer2を使用する場合も基本的なことは同じです。ほかより詳しことはデータシートを参照して下さい。, Arduinoは結構前から触っていたのですが、なぜか今まで割込み処理をする機会がありませんでした。今回割込みを処理を行う必要があっていろいろ調べたのですが、Arduino(IDE)特有の便利関数(といっていいのか・・、)がTimer割込みに関してはほとんど準備されていない??調べてもあまり情報が出てこなかったです。, ライブラリもいくつかありましたが(自分には)ちょっと微妙な感じでだったので今回は直接レジスタ操作て割込み処理を行ってみたということです。, 以下はTimer1を使用して0.5秒置きにUnoのオンボードLED(13pin)を点滅するスケッチです。. Arduino環境ではTickerクラスを利用したタイマーと、より高精度の割り込みを利用したタイマー処理があり、割り込みを利用したタイマー処理を調べてみました。, ESP32には4つのタイマーがあり、自由に利用することができます。タイマーの原理としては、指定したクロック数が経過したらカウントして、特定カウントになったらタイマー割り込みをかけて、割り込み関数を呼び出すような動きになっています。, そのため、クロック数とカウント数の掛け算でタイマーの間隔を制御しています。あとの例では80クロック×1000000カウント=80Mクロックでタイマー割り込みが発生しています。タイマーは通常80MHzで動いていますので、1秒間隔での実行になります。, タイマーの動作周波数はCPU周波数に応じて変化します。CPU周波数と同一ではなく、ペリフェラル周波数と呼ばれる周辺機器の周波数です。, 上記の対応になっていて、ペリフェラル周波数の上限は80MHzで、それ以下の場合にはCPU周波数と同一です。, つまり、1カウントの時間をわかりやすい1マイクロ秒に設定したい場合には、ペリフェラル周波数に合わせて変える必要があります。変更していない場合、80Mクロックで設定していたタイマーを、ペリフェラル周波数40MHzで動かすと2秒になります。, 最小構成のLチカです。このよう処理は実際にはTickerクラスを利用したほうが良いです。, timerBegin()でタイマーを作成しています。一番最初の引数はタイマーのIDです。ESP32はタイマーが4つあり、0-3までのタイマーを利用できます。, 2つ目が何クロックでカウントをするかの数値になります。80の場合、80クロックで1カウントします。getApbFrequency()/1000000で、どのCPU周波数でも1マイクロ秒に固定化することができます。, 3つ目がカウンターをカウントアップする場合にはtrue。カウントダウンする場合にはfalseになります。通常はtrueのみしか使わないと思います。, 最初の引数は設定するタイマー。2つ目は割り込み時に呼ばれる割り込み関数。3つ目が割り込み検知方法です。, このオプションは関数を高速なIRAM上に読み込みことを保証させるもののようです。おのオプションが無いと、フラッシュ上に配置される可能性があり、その場合には低速動作になり、結果的にクラッシュする可能性があるみたいです。, 単純なプログラムの場合にはおそらく指定しなくても動きますが、基本的には割り込み関数は高速である必要があり、IRAM_ATTRをつけてください。, 3つ目の割り込み検知方法は、trueの場合にはエッジトリガー、falseの場合にはレベルトリガーになります。タイマーの場合にはカウントアップしていって、指定カウントに変わったところを検知するので、エッジトリガーのtrueを指定します。, 割り込みが発生したときの、トリガー条件を設定します。最初の引数は設定するタイマー。2つ目はカウント数。3つ目がautoreloadで、trueの場合には定期実行、falseの場合には1ショットの実行になります。, 上記でタイマーを有効化しています。timerAlarmDisable()という関数で停止させることもできます。, 実際のタイマー利用例です。マルチタスクや割り込みを使う場合には、通常のコールバックと違い意図しないタイミングで変数を更新される可能性などがあり、排他処理を行う必要があります。, ざっくり説明すると、他の人が使っていないのを確認してから、利用を宣言して、使い終わっていたら、開放する処理です。, 上記に詳しく書いてありますが、スケッチ例ではバイナリセマフォを利用しています。記事には書いていませんが、キューに似ているが、1つしか保存しない通知機能もあるのでどこかでマルチタスクでの注意点をまとめたいと思っています。, このスケッチでは、1秒間隔でシリアルに時間を出力し、GPIOがLOWになるとタイマーを停止する動きになります。M5StickCの場合にはGPIO37がHOMEボタンなので、書き換えてから実行してみてください。, この動き自体はGPIOにLOWになったら割り込み関数を実行したほうが適して、あまり良くないサンプルに見えます。, バイナリセマフォとは、一人だけセマフォを取得できる排他制御です。ただし、デフォルトは誰もセマフォを取得できず、誰かが許可した場合にだけ取得可能です。, 1マイクロ秒単位のタイマーを作成して、1000000カウント(1秒)したらonTimer()関数を割り込みで呼び出すタイマーを実行しています。, ここはセマフォの処理になります。0はタイムアウトの時間を表し、この場合にはセマフォを取得できなかった場合には即時終了します。portMAX_DELAYを指定すると、セマフォが取得できるまで、ブロッキングして待ちます。, セマフォが取得できた場合には、タイマー値を取得してからシリアルに出力しています。portENTER_CRITICAL()とportEXIT_CRITICAL()は割り込み禁止で、この関数に囲まれているクリティカルセクションと呼ばれる処理を実行中には、タイマー割り込みなどが発生しません。, 取得しようとしている変数が、タイマー割り込みで変更される可能性があるものなので、取得途中で変更されて困るものは、割り込み禁止を設定します。, volatile宣言がついています。割り込みで利用する変数にはすべてこの宣言を利用する必要があります。, この宣言があると、変数の置き場所がいろいろな場所から変更されても大丈夫な領域に確保されます。, 割り込み禁止も、ミューテックスと呼ばれる排他制御です。初期値は常にportMUX_INITIALIZER_UNLOCKEDで排他制御されていない状態です。, isrCounterが割り込み発生回数、lastIsrAtが最後に割り込みが発生したときの経過時間が入っている変数です。, 両方とも割り込みから変更される可能性があるのでvolatile宣言が付いています。, 割り込み関数では、通常と使える関数群が違いますので注意しましょう。_ISRが最後につく関数群がある場合には、そちらを利用しないと正しく動きません。ですが、単純な処理の場合には_ISRの割り込み中用の関数群ではなくても動いてしまうので、注意してください。複雑な割り込みが発生した場合など、意図しないタイミングでハングアップする可能性があります。, portENTER_CRITICAL_ISR()とportEXIT_CRITICAL_ISR()で囲んだ間で、タイマー変数の更新を行っています。この処理であれば、おそらく割り込み禁止をしなくても大丈夫そうですが、割込み禁止にしているようです。, xSemaphoreGiveFromISR(timerSemaphore, NULL)で、タイマー用のバイナリセマフォに許可を与えています。NULLに指定しているところには、本来以下のように値をいれた変数にする必要があります。, この関数を実行後にxHigherPriorityTaskWokenの中身を確認し、pdTRUEに変更された場合には、セマフォ取得待ちでブロックだった優先度の高いタスクが実行されたのがわかります。, ブロックしていると、かなりテクニカルな処理が必要になるので、なるべくノンブロックで動かすことをおすすめします。, 割り込みは非常に難しいです。今回の例ではありませんでしたが基本的にはマルチタスクを意識したコーディングが必要になります。, 使う関数群なども違いますので注意が必要です。このへんの処理はArduinoやESP32ではなく、内部で利用しているFreeRTOSの知識が必要になります。ただし日本語の情報も少なく、体系的に勉強するのは難易度が高そうです。, 管理者承認後にページに追加されます。公開されたくない相談はその旨本文に記載するかTwitterなどでDM投げてください。またスパム対策として、日本語が含まれない投稿は無視されますのでご注意ください。, What is and when to use IRAM_ATTR ? - ArduinoのTimerライブラリの紹介があります。Arduinoのタイマーライブラリ, (英語ページ) Help us understand the problem. "となると書かれていますが、, ビット演算のANDとOR  Timerは基本的には正確なタイミングでなんらかのシグナルを送りたい時に必要になるものです。arduinoではdelay()を使って時間の操作が行えますが、delayを使っているとそれが実行されている間に他の操作を行えないという弊害があり、そんな時にTimerを利用することができます。Timerの大まかなイメージとしてはTimerのカウンターが数字を0から「自動的に」カウントアップしていって、ある数値に到達した時に何かをしてもらい、そしてそのカウントしていた数値をまたリセットして(あるいはカウントダウンして)その後もこの動作が繰り返される、というようなものです。, Arduino UnoにはTimer0・Timer1・Timer2の3つのTimerがあります。, Arduino Unoは電源を繋ぐと16MHz(16*10^6Hz)の電気信号を中央の"T16.000"と書いてあるクリスタルから発振します。millis()などの関数も実際にはこの信号を元に作り出されています。Timerのbit数の違いによって、この16MHzの周期を分解する解像度が変わってきます。つまり、8bitの時は16MHzの信号をmax255(8bit = 2^8 = 256->0から255)のステップにして処理し、16bitの時には同じ考え方でmax65535のステップにします。1秒につき16*10^6回の波を送り出しているのを8bitと16bitで制御するので、1秒を1000ミリセカンド(ms)だとすると、それぞれのTimerが一つの周期にかかる時間はそれぞれ以下のようになります。, カウンターの描くグラフはノコギリ波のような形状になります。(ただしこれはカウンターの数値が描くグラフであって、何かこのような形状の電気信号が送られているわけではありません。自分は最初ここら辺のところがごっちゃになっていたので一応書いておきます。。。), これをみるとTimer0とTimer2に関しては構成は一緒でTimer1は比較的多数のレジスタがあることがわかります。ちなみにTCCR0AであればTimer0、TCCR1AであればTimer1、TCCR2AであればTimer2といったように、レジスタの名前にある数字でどのTimerに属するかを知ることができます。ちなみにTimer0とTimer2は8bit、Timer1は16bitのタイマーになります(違いについては上のノコギリのようなグラフを参照してください)。