Arduinoのタイマー割り込み

本格的な組み込みプログラムではタイマー割り込みが必須になります。

Arduinoは割り込みをサポートしていて、ピンの信号変化で割り込み関数を呼び出すことが出来ますが、タイマー割り込みの扱いはどうなっているのか、

Arduino(1.8.9)でタイマー割り込みについて調べてみました。

Arduinoではタイマー割り込みの使用は非推奨

Arduinoユーザーにも、一定時間ごとに割り込み関数を呼び出すタイマー割り込みを使いたいと思う人はいて、Webを検索するとタイマー割り込み関連の記事が見つかりますが、古い情報が多いようです。
高校生ものづくりコンテストのQandAにも割り込み関連の質問がありました。

【質問内容】
Arduinoの使用している。TimerOne.hというヘッダファイルは
公式に認められているか?
【 回 答 】
持ち込みヘッダファイルについてはあくまでもレジスタ・ポート端子の定義、割込み(禁止・許
可)の定義について許可をします。
TimerOne.h、MsTimer2.h等については認めません。

第19回高校生ものづくりコンテスト全国大会(近畿大会)課題の質問・回答

タイマー割り込みをサポートするヘッダファイルTimerOne.h、 MsTimer2.hは今のArduinoでは廃止となっていて、タイマー割り込みの使用は非推奨になっているようです。

高校生ものづくりコンテストに持ち込み可能なヘッダファイル

高校生ものづくりコンテストのルールでは、次のように簡単なヘッダファイルの持ち込みは認められています。

(b)ヘッダファイル mono_con.h(制御用マイコンのレジスタ,ポート定義,割込み定義
を含む ヘッダファイル) の出力リスト(使用しない場合は提出しなくても良い)

しかし、TimerOne.h等にはレジスタ、ポート定義、割り込み定義以外の情報が含まれていると判断されたのでしょう。

ただ、開発環境として使用を認められているArduino IDEにはもっと複雑な情報を含むヘッダファイルが多く入っていますし、TimerOne.hが入っていた古いバージョンを使えばOKということになります。

MPLAB-Xを使えば、MCCで定義すると自動的に割り込み関連の関数やヘッダーが生成されます。
それを考えるとArduinoのTimerOne.hを認めないのは不公平に思えますが、決められたルールで運用する以上、やむを得ないことなのでしょう。

マイコンカーラリーのように、特定メーカーの特定CPUボードしか認めないことにすれば、厳密に公平な運用が可能となりますが、特定メーカーへの利益供与につながりますし、CPUと開発環境の革新が著しいこの分野でツールを固定することは役に立たないプログラミング教育に繋がります。

Arduino IDE vs MPLAB-X

マイコン制御プログラムの普及に大活躍したArduino IDE

Arduinoは元AtmelのCPUであるAVRを使って、Arduino IDEで数行のスケッチ(プログラム)を書けば、USBケーブルで接続したArduinoボードにプログラムを書き込んで動作するようにした、入門用マイコンです。

マイクロソフトを創業したビルゲイツは、最初に作ったパソコン用のBASICの大ヒットで成功への足がかりを作りました。

初期のPCでは複雑な準備をして、コンパイルに長い時間をかけないと動作するプログラムが作れませんでした。
「数行のプログラムを書けばプログラムが走り、結果が表示される。」ことが
BASICが大ヒットした理由ですが、Arduinoの大ヒットも「歴史は繰り返す」を見ているような気がします。

小学校プログラミング教育の教材としても最適なArduino

Arduinoはとにかく簡単にマイコン制御の勉強を始めることが出来るため、初心者向け入門用として優れているシステムです。
マイコンに関する予備知識が無い 小中学生でも、マイコンボードを使った制御プログラムを作って、動作を試してみることが出来ます。
文科省が推進している小学校プログラミング教育の教材としても、大いに活躍すると思います。

制御用マイコンとしてのArduinoの限界

Arduinoはマイコン入門用の教材として優れた特徴を備えていますし、数多くの拡張ボードが安価に提供されているため、Arduinoの機能が仕様を満たしているならば、Arduinoを使ってシステムを組むのは、開発効率においてもコスト面においても有利なので、検討する価値があります。

ただし、手軽に使えることを優先したBASICが開発のプロからは歓迎されなかったように、使い易さと性能にはトレードオフの関係があるため、Arduinoで高性能なシステムを組もうとすればMPLAB-Xを使うより多くの労力が必要となる分岐点があります。

Webを検索すると、Arduinoに使われているAVRCPUの性能を限界まで引き出すために、AVRのデータシートを調べて周辺機能レジスタを直接書き換えるなど、苦労している人が見つかります。

デジモノ覚書-Arduino Uno – PWM周波数を”自由に”変更する

万事、塞翁が馬- mega のタイマー割込みについて調べる

周辺機能レジスタを直接いじってAVRの性能を引き出すことが出来る能力と時間があれば、MPLAB-XとPICやSTMCubeMXとstm32マイコンの組み合わせを使えば、はるかに高度な制御をおこなうシステムを簡単に作れるのに、勿体ないと思ったりします。

高校生全国ものづくりコンテスト電子回路部門の課題を解くのが、ちょうどArduinoの限界点だと思います。
つまり、工夫してArduinoの割り込みを使いこなせるレベルの能力があれば、MPLAB-XとPICマイコンの使い方を覚えた方が幸せになれるのではないか、といったところでしょうか。

企業人から見れば、工業高校生レベルなら、Arduinoを卒業して、MPLAB-XでCPUの周辺機能レジスタを設定する技術を身につけて欲しい、という勝手な要望もあったりします。

それでもArduinoが使い易い理由

高度な制御になると加速度的に扱いが難しくなるArduinoですが、それでもArduinoを使いたい理由は、CPUボードと周辺ボードの豊富さと入手しやすさにあるのではないでしょうか。

例えばAmazonで検索すると、Arduino用超音波センサモジュールが、ひとつあたり200円以下で見つかります。

また、アリエクスプレスのサイトでArduinoをキーワードにして検索すると、100均レベルの価格で多数のパーツを見つける事が出来ます。

個人でPCB基板を設計製作することが難しかった少し前までは、既製のボードを買って組み合わせるしか手段が無かったため、とにかくArduinoでシステムを組むのが制御システム製作の近道でした。

今は個人が自由にPCB基板を作れる時代

ところが、PCB基板作りの環境はこの10年で大きく変わりました。

ひとつは、高機能なオープンソースのKiCadを使って個人で基板の設計が出来るようになったことで、もうひとつは格安基板メーカーの出現で1,000円以下で100㎜x100㎜の基板10枚を注文できるようになったことです。

個人が最初にPCB基板を設計して発注するには多少のハードルがあるのは事実です。
ただ、そのハードルは思ったほど高いものではありません。
上で紹介したように、Arduinoの限界を引き出すためにAVRのデータシートを読み込んで、試行錯誤に時間をかけてプログラムを作っている人ならば、簡単に乗り越えることの出来るハードルです。

マイCPUボードを使ってロボットコンテストに取り組んでみませんか

Arduino用周辺ボードはPICでも使えますから、目的にあったCPUボードを自作してセンサーはArduino用センサーボードを使うことで、高機能なシステムを安く製作することが出来ます。

ものづくりの楽しみは、何かを作り上げた時の達成感です。
購入したArduinoだけで制御システムを組み上げるより、自分で設計して作った
CPUボードでシステムを製作した方が、はるかに大きな達成感と喜びを感じられることを保証します。

マイコン制御に興味がある人には、是非、CPUボードの製作まで手掛けて頂きたいと思います。

割り込み関数のデバッグ

制御プログラムに必須のタイマー割り込み

制御プログラムでは時間の管理が主要なテーマとなります。

ステッピングモーターをコントロールするのは、ステッピングモータの相電流を切り替える時間を制御することですし、ラジコンサーボのラダー角を制御するためには一定時間ごとにPWM信号を出力する必要があります。

そのため、タイマー割り込みを使うことで制御プログラムは簡潔に記述できます。

モーターのデジタルサーボコントロールは、正確に一定時間毎に繰り返してパラメータを調整するため、タイマー割り込み無しでは成り立ちません。

ものづくりコンテストの課題でも入門クラスはソフトウェアループを使ってなんとかこなせますが、上級クラスになるとタイマー割り込みを使わないと解けない課題が出題されるようになります。

MPLAB-Xを使えば割り込み関数が簡単に使える

Arduinoではライブラリ関数を使うことで簡単に割り込み関数を使うことが出来ますが、MCCを使えないMPLABでPICの割り込みを使うのは面倒でした。

しかし、MPLAB-XがサポートするMCCを使えば、GUI画面で割り込み要因の設定をすれば割り込み関連のライブラリが自動的に生成されるので、割り込みの扱いが簡単になりました。

Arduino IDEは「組み込みプログラムを簡単に」というコンセプトで作られているため、多数のサーボをコントロールするようなアプリケーションでは出来ることが限られてしまいます。
MCCを使うと、様々なPICのCPUを選んで、高機能なCPU周辺機能の能力を柔軟に使えるため、より高度な制御が可能になります。

Timer0 で 10ms周期の割り込みを使う例

左のウィンドウでTMR0を選び、表示される右のウィンドウでEnable TimerとEnable TImer Interrupgにチェックをいれて、タイマー周期(Timer Period)を設定すればOKです。
緑の楕円で囲んだ部分の設定により、タイマー割り込み周期の選択範囲が決まります。上の例では0.25msから約16msの間の周期を設定することが出来ます。

Timer1で0.1ms周期の割り込みを使う場合

左のウィンドウでTMR1を選び、表示される右のウィンドウでEnable TimerとEnable TImer Interrupgにチェックをいれて、タイマー周期(Timer Period)を設定すればOKです。
緑の楕円で囲んだ部分の設定により、タイマー割り込み周期の選択範囲が決まります。上の例では63nsから約4.096msの間の周期を設定することが出来ます。

Timer0の場合と設定画面が異なるのはタイマーのハード構成が異なるためです。

割り込み周期の短縮について

ハードウェア的には1us=0.001msの割り込み周期も設定できますが、関数内の処理に対して割り込み間隔が短いと割り込み関数の中で実行できる処理が限られるので、プログラムがハングアップしてしまいます。
クロック64MHzに設定した PIC18F45K22で、100us=0.1msの周期で関数呼び出しが無い場合、数十行程度のプログラムは問題なく動作します。
時間分解能を上げるため割り込み周期を短くしたい場合は、下で紹介する方法で割り込み関数内の処理がオーバーフローしていないかを確認して下さい。

制御プログラムはタイマー割り込み関数内に書くのがお勧め

ものづくりコンテストの課題では、main関数では設定だけをおこない空のループを回しておき、タイマー割り込み関数だけで処理を記述するのがお勧めです。

そうすることで、タイミングのコントロールが明確になり、効率よく正確なプログラムを作ることが出来るからです。

また、組み込みプログラミングにとって割り込みを活用する技術は必須なので、教育的効果を考えた場合、割り込みプログラミングから入るのがお勧めです。

割り込み関数の使用上の注意

割り込み関数はハードウェアによって周期的に呼び出されるので、次の項目について注意する点があります。

1.割り込み周期未満の時間で終わる処理を記述する

割り込み関数は一定時間ごとに強制的に呼び出されるので、割り込み周期未満の時間で確実に終わるようなプログラムにします。
例えば割り込み関数内で無限ループを使うと、プログラムはクラッシュします。

2.関数内で宣言した変数は保存されない

ソフトウェアループの中で使う変数は関数内で宣言しても構いません。
割り込み関数は繰り返し呼び出されるので、ソフトウェアループ内と同様のプログラムになりますが、関数内で宣言した変数は使えないことに留意して下さい。

割り込み関数のデバッグ

割り込み関数を使う上で最初に確認することは、「割り込み関数が呼び出されているか」と「割り込み関数内の処理時間が周期内に収まっているか」の二つです。

1.CPUボード上のLEDを使って確認

この二つを確認するために、ポートに接続されたLEDを割り込み関数の最初で点灯し、終わりで消灯する処理を追加します。
割り込み周期10ms以下の場合、人間の目で点滅を確認することは出来ませんが、LEDの明るさで割り込み関数が正常に動作していることをチェックできます。

LEDが点灯すれば割り込み関数が呼び出されていることを示し、LEDが暗いほど割り込み関数の処理時間が短いことを示します。

LEDが完全に消えていれば割り込み関数が呼び出されていないか、プログラムが異常終了している可能性があります。但し、割り込み処理時間が非常に短い場合も消えている様に見える場合があります。

LEDが点灯しっぱなしの時は、割り込み関数の処理時間が長すぎるなどの原因でプログラムが異常終了している可能性があります。

2.printfデバッグを使って確認

割り込み関数の中にprintf文を記述して、シリアルモニタで関数内の情報を確認します。
制御のタイミングに影響を与えないように、printf文は割り込み関数の最後に挿入するのがお勧めです。
また、printf文の実行で割り込み関数内の処理時間がオーバーフローしないように注意する必要があります。

下のソースプログラムが 割り込みを使うための初期化と、デバッグのためのLED点滅を追加した例です

/**
 MCCが挿入したコメント
*/

#include "mcc_generated_files/mcc.h"

/* 10ms毎に呼び出されるタイマ割り込み関数:関数名は任意
 *  mainの最初で登録することで周期的に呼ばれる
 */
void inttmr0(void)
{
    LATDbits.LATD2 = 1; // デバッグ用LED 点灯
    /** 10ms毎に繰り返す処理  **/
    LATDbits.LATD2 = 1; // デバッグ用LED 消灯
}

/* 0.1ms毎に呼び出されるタイマ割り込み関数:関数名は任意
 *  mainの最初で登録することで周期的に呼ばれる
 */
void inttmr1(void)
{

}

void main(void)
{
    // Initialize the device
    SYSTEM_Initialize();

    // タイマー0割り込み関数を登録 (MCCが生成したtmr0.c の関数を使う)
    TMR0_SetInterruptHandler(inttmr0); // この行はユーザーが追加する
    // タイマー1割り込み関数を登録 (MCCが生成したtmr1.c の関数を使う)
    TMR1_SetInterruptHandler(inttmr1); // ユーザーが追加
    // タイマー0をスタート (tmr0.c の関数を呼ぶ)
    TMR0_StartTimer();                 // ユーザーが追加
    // タイマー1をスタート (tmr1.c の関数を呼ぶ)
    TMR1_StartTimer();                 // ユーザーが追加
    
     // Enable the Global Interrupts
    INTERRUPT_GlobalInterruptEnable(); // この行のコメントを外し有効にする

    // Disable the Global Interrupts
    //INTERRUPT_GlobalInterruptDisable();

    // Enable the Peripheral Interrupts
    INTERRUPT_PeripheralInterruptEnable(); // コメントを外し有効にする

    // Disable the Peripheral Interrupts
    //INTERRUPT_PeripheralInterruptDisable();

    // メインループではなにもしない
    while (1)
    {
  
    }
}
/**
 End of File
*/

printf文の実行時間

割り込み関数内でprintfとシリアルモニタによるデバッグを使う場合は、printf関数の実行時間に注意する必要があります。

printf関数の実行には通信速度が遅いほど、文字数が多いほど長く時間がかかります。

例えば9600bpsで20文字のデータを出力するとprintf文の実行に

1/9600 * 10 * (20-1) ≒ 19.8ミリ秒

の時間が必要です。
これは忙しいプログラムとっては大きな影響を与える待ち時間かも知れませんし、10ミリ秒周期の割り込み関数の中にこのprintf文を書くと、プログラムがハングアップしてしまいます。

通信速度115200bpsなら、

1/115200 * 10 * (20-1) ≒ 1.6ミリ秒

と、10ミリ秒周期の割り込み関数の中でも使えるようになります。

さらに、クロック64MHzに設定したPIC18F45K22 にエラー率0%で設定できる2,000,000bpsを使うと待ち時間は

1/2000000 * 10 * (20-1) ≒ 0.095ミリ秒

となり、1ミリ秒周期の割り込み関数の中でも余裕で使えるようになります。

デバッグに使ったprintfの後始末

デバッグに使ったprintf文は、デバッグ完了時に#ifdefでマスクしたり、コメントアウトしてプログラムから除くのが常識と思われていますが、ものづくりコンテストの課題を解いている場合や、自分が使うプログラムならそのまま残しておいても構いません。
何故ならば、PCでシリアルモニタを使っている場合も、USBコネクタからケーブルを外してボード単独で動かす場合も、printf文がプログラムの動作に与える影響に変わりは無いからです。

むしろ、printf文のdelayが無くなることによりプログラムの動作が変わる可能性を考えればprintf文はそのまま残しておいた方がベターです。