割り込み関数のデバッグ

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

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

ステッピングモーターをコントロールするのは、ステッピングモータの相電流を切り替える時間を制御することですし、ラジコンサーボのラダー角を制御するためには一定時間ごとに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文はそのまま残しておいた方がベターです。

シリアルモニタを使うときの留意点

組み込みプログラムのデバッグには欠かせないシリアルモニタですが、使う上で注意しておくことがあります。

printf関数はdelay関数を挿入したのと同じ効果

シリアルポートからデータを出力する場合、一つのデータの出力を終えてから次のデータをシリアルポートに書き込むことの繰り返しになるため、
Arduinoのprintやprintf関数の実行には、出力する文字数に比例した待ち時間が必要となります。

待ち時間 = 1 / 通信レート(bps) * 10 * (文字数―1)

関数呼び出しに必要なオーバーヘッドは上の時間に比して短いので、通常はこの待ち時間が支配的となります。

通信速度が遅いほど、文字数が多いほど長く時間がかかると覚えておいてください。

例えば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文はそのまま残しておいた方がベターです。

PIC18F45K22ボードでシリアルモニタを使う

MPLAB-XのMCCを使って、PIC18F45K22 CPUボードでシリアルモニタを使うための設定方法を紹介します。

シリアルモニタについて

シリアルモニタはArduino IDEの用語で、基板上のプログラムでprint文を使ってUSBコネクタ経由でPCにデータを送って表示する機能です。

組み込みプログラミングで機器を制御する場合、短いプログラムを繰り返し使うことが多く、プログラム構造は簡単ですがCPUに接続された機器の特性を知ることが重要になります。

ものづくりコンテストの課題で使われるスイッチのチャタリング

「スイッチをONにしたらLEDを点灯、OFFにしたら消灯しなさい。」という初歩的な設問なら、条件文や単なる代入文を使って論理的に考えたプログラムで問題なく動作しますが、

「スイッチを一度ON-OFFしたらLEDを点灯、もう一度ON-OFFしたら消灯しなさい。」という設問に対して、スイッチのチャタリングを知らずに論理的に考えただけのプログラムを書くと、ON-OFFを繰り返す度に動作したりしなかったりするという不思議な現象に悩まされることになります。

チャタリングというのは、スイッチをON-OFFした直後のごく短い時間に信号が0と1の振動を繰り返すというもので、スイッチの種類によってチャタリングの時間が違ったり、チャタリングがあったりなかったりします。

チャタリングによるトラブルは、PCの画面でプログラムを穴が空くほど見つめて考えても解決できませんが、シリアルモニタを使ってスイッチの状態が変わった時に信号の状態を表示するようにすれば、
「あれっ?一度スイッチを操作しただけなのに、何度も信号が変化している。」と一発で気が付き、対策を考えることが出来ます。

また、シリアルモニタで信号の状態を観測することは、データシートには載っていないハードウェアの動作特性を知るために大変役に立ちます。

MALAB-Xでシリアルモニタを使うには

Arduino IDEのシリアルモニタは、シリアル通信を使ったprintfデバッグをIDEに組み込んだものです。
PICの開発環境MPLAB-Xにはシリアルモニタ機能が含まれていませんが、MCCを使ってprintfの出力をシリアルポートに出力するようにすれば 、Arduinoと同じ感覚でシリアルモニタが使えるようになります。

GUI画面で初期化設定が出来るMPLAB-XのMCC

PICの新しい開発環境MPLAB-Xの最大の特徴はMCC(MPLAB Code Configurator)が使えるようになったことです。
MCCを使うとGUI画面で設定項目を選んで値をセットするだけで、ハードウェアを初期化する関数を自動的に生成してくれます。
組み込みプログラムで難しいのは、データシートに記載された多くの情報を元にCPUのレジスタを設定しなければならない初期化プログラムです。

ものづくりコンテストに挑戦する場合、
Arduinoでは単純化した初期化関数をライブラリに用意することで初期化が簡単におこなえるようにしてあるため、初期化部分を覚えるのが簡単でした。
MCCが使えないMPLAB時代のPICのプログラム環境を使う場合は、覚えにくい初期化プログラムを丸暗記して書かなければならず、その部分に間違いがあればプログラムがまともに動作しなかったので、Arduinoに対して圧倒的に不利な条件で闘う必要がありました。

MCCを使うと、タイマーやPWMなどのPICに内蔵された高機能な機能を、課題に合わせて柔軟に設定できるので、Arduinoより高性能なCPU機能を使えるようになるため、高機能なプログラムを簡単に書けるようになります。

1.MCCでEUSART(シリアルポート)を有効にする

PIC18F45K22CPUのシリアルポートからデータを出力するにはプログラムの最初で初期化する必要があり、MCCを使えば自動的に初期化部分を含んだプログラムのテンプレートを作ってくれます。
MCCを起動し、左のDevice Resourcesで下図の赤丸で囲まれたEUSART2をダブルクリックして、上のProject Resourcesに追加します。

2.EUSARTのパラメータを設定する

次に、下の画面のとおりに設定をおこないます。
Mode asynchronaous → 非同期通信とはRS232C規格でも使われている標準的なシリアル通信プロトコルです。
Enable EUSART → このシリアルポートを有効にします
Enable Transmit → 送信を有効にします
Enable Receive → 受信を有効にします、シリアルモニタだけなら受信無効でもかまいません。
Baud Rate → とりあえず115200を選択します
Software Settings の Rediret STDIO to USART にチェックを入れます → これでprintf文の結果がEUSARTに出力されるようになります。

その他の項目は初期状態のままにしておいて下さい。

3.プログラムテンプレートを生成(再生成)する

設定が終わったらProject ResoucesタブでGenerateボタンをクリックすると初期化プログラムとmain.cが生成されます。
プログラムを書いている途中で設定を変更したい場合も再度MCCを開いて設定を変更した後にGenerateボタンをクリックします。
main.cにプログラムを書き込んでいる途中で、再度MCCのGenarateをおこなっても、編集した部分はそのまま保存されますので、デバッグ時にハードウェアの初期化を追加変更することも出来ます。

MCCが生成するプログラム

MCCで初期化プログラムを生成した後にProjectタブの内容を見るとmain.cとMCC Generated Filesが作られているのが確認できます。
生成されたファイルのmain.cを直接編集して課題のプログラムを書いていきます。
MCC Generated Filesフォルダにあるプログラムを編集することはありませんが、この中の関数を使う場合には関数名や使い方を確認するために開いてみることが出来ます。