イベントについて
ここまでのコードや説明で「イベントを発生する」「イベント発生フラグを立てる」といった説明していたので、それについて説明します。
イベント駆動型プログラム
まず、今回の受信処理プログラムはイベント駆動型プログラムで作成することにしました。イベント駆動型プログラムは下のようなアルゴリズムで動作するプログラムです。
while ( 1 ){
イベントが発生するまで待つ
イベントが発生したら、対応するアクションを行う。
}
今回イベント駆動型プログラムを採用したのは、赤外線受信処理とモーター制御(ソフトウェアPWM処理)を同時に制御する必要があるためです。2つの制御を並列的に処理するには手続き型プログラムよりイベント駆動型の方が処理しやすいと判断したからです。
イベントの種類
このプログラムにおいて、イベントは大きく分類すると2つの種類があります。
- プログラムの外から発生するイベント
- プログラムの内部で発生するイベント
プログラムの外から発生するイベント
1つ目の「プログラムの外から発生するイベント」とは、PICの割込みによって発生するイベントです。
これをさらに細かく分類すると2つあり、「TMR2の割込みイベント」と「赤外線モジュールの出力を受け取る入力端子のIOC割込みイベント」です。
前者のTMR2割込みは100usec毎に発生するように、PICの初期設定で設定しています。今回、PICのクロック周波数(FOSC)は8MHzにしていて命令クロックはその1/4なので、1命令は2MHz=500nsecで処理しています。100usecは200命令分の時間になります。(この200命令分というのは、後の説明でも出てくることなので、要注意です。)
後者のIOC割込みは、赤外線通信によって変化するので、TMR2割込みのような固定値ではありません。ただし、連続で発生する2つのIOC割込みの間隔は最短で約140usecになります。
High→LowやLow→Highなど変化するときにIOC割込みが発生するので、連続したIOC割込みの間隔は、「Low信号になっている期間」や「High信号になっている期間」になります。
ところでPICによる赤外線通信5の送信と受信のタイミングの違いで説明したように、赤外線受信モジュールの出力がHigh→Low→Highと変化する時のLowの期間は最短で140usecなので、これから上述の「最短で約140usec」が出てきます。
プログラムの内部で発生するイベント
2つ目の「プログラムの内部で発生するイベント」は、ここまでのコードでも出てきたEventRize()によってプログラム中で発生させるイベントです。(具体的には、IRR_HandlerForIRINChanged()のコードやIRR_ReceiveRun()のコードで使用しています。)
これは、プログラム処理をある程度の「かたまり」として分割するために発生させています。
イベントのコード
ここからは実際にコーディングをしていきます。
内部で発生するイベントを保持する
まず、「イベントが発生した」という情報を保持する仕組みを作ります。
これは「発生している/していない」を区別できればよいので1bitの情報として取り扱うことができます。1bitの情報を扱う手段はいろいろありますが、今回は構造体のビットフィールドを使う事にしました。
// EV_Flagのためのunion型
union EV_Flag_union
{
struct
{
uint8_t BIT0 : 1;
uint8_t BIT1 : 1;
uint8_t BIT2 : 1;
uint8_t BIT3 : 1;
uint8_t BIT4 : 1;
uint8_t BIT5 : 1;
uint8_t BIT6 : 1;
uint8_t BIT7 : 1;
};
uint8_t ALL;
};
// イベント発生のフラグを管理する変数
// 現状、イベント数が8個以下なので1byteサイズの変数にしているが、
// イベント数が増えた場合は 2byteサイズなどの大きい型にすること。
extern union EV_Flag_union EV_Flag;
これで、「EV_Flag.BIT2 == trueなら、イベント2が発生している。」や「EV_Flag.ALL == 0なら、イベントはまったく発生していない。」などと判定できるようになります。
今回の受信プログラムでは、内部で発生するイベント数が最終的に8個以下だったので上記のようにコーディングしています。イベント数が増え、9~16個になる場合は下のようにすると良いでしょう。
union EV_Flag_union
{
struct
{
uint8_t BIT0 : 1;
// 途中省略
uint8_t BIT7 : 1;
uint8_t BIT8 : 1;
uint8_t BIT9 : 1;
// 途中省略
uint8_t BIT15 : 1;
};
sturct
{
uint8_t ALL0;
uint8_t ALL1;
};
};
しかし「イベントN」ではあとからコードを見たときに意味が分かりにくく、メンテナンス性が悪くなります。そこで#defineを使って分かりやすくします。
//// イベントタイプ定義
// 1. 数値は重複してはならない。
// 2. 複数のイベントが同時に発生したときは、数値の小さい方が優先される。
// そのため、優先度や処理頻度の高いイベントの数値を小さくすること。
// 3. 数値の割り振りを変更した場合は、EventCheckAndAction()も変更する必要がある。
// 赤外線通信で、「ON信号とOFF信号を1回づつ受信した」イベント
#define Event_IRR_recivedOnAndOff 0
// モーターの制御状態が変わったイベント
#define Event_MTR_ChangeAnyStatus 1
// 赤外線通信で、データ受信が完了したイベント
#define Event_IRR_DataIsReceived 2
// モーター制御パラメーターが変化したイベント
#define Event_MRT_ControlParameter_Changed 3
// NRT_Counterが変化したイベント
#define Event_NRT_CounterChanged 4
// NRTカウンタが1秒経ったイベント
#define Event_NRT_1SecPassed 5
// NRTカウンタが6秒経ったイベント
#define Event_NRT_6SecPassed 6
// EV_Flag.BIT?? にアクセスするためのマクロ
#define EV_FlagBit(NUMBER) EV_Flag.BIT##NUMBER
// イベントを発生する(フラグを立てる)
#define EventRise(EventName) \
do \
{ \
EV_FlagBit(EventName) = true; \
} while (0)
これで、前述のEventRise(Event_IRR_recivedOnAndOff)が実現できました。
EventRise(Event_IRR_recivedOnAndOff)はEV_Flag.BIT0 = trueと同じになります。
EV_Flag.BIT0と書くよりはコードが読みやすくなるので、メンテナンス性がよくなります。
イベント発生チェック
イベントの発生をチェックするには、ビットフィールドをBIT0から順番にチェックすればよいです。
しかしこれも「if ( EV_Flag.BIT0){ }」のようなコードは分りにくくなるのでマクロを用意します。また、EV_Flagのビットフィールドをfalseにするためのマクロも作っておきます。
// イベント発生をチェックする
#define EventCheck(EventName) (EV_FlagBit(EventName))
// イベントを終わらせる(フラグを降ろす)
#define EventFall(EventName) \
do \
{ \
EV_FlagBit(EventName) = false; \
} while (0)
これで「if (EventCheck(Event_IRR_recivedOnAndOff))」とすれば、Event_IRR_recivedOnAndOffイベントが発生していることをチェックできます。
イベントアクション
イベント発生チェックとアクションを行うための関数を用意します。最初に全体のコードを示します。
/**
* イベント発生をチェックし、対応するアクションを実行する。
*/
void EventCheckAndAction(void)
{
// イベントがまったく発生していないなら、すぐに終了する。
if (EV_Flag.ALL == 0)
{
return;
}
// イベントに対するアクション
EventAction(Event_IRR_recivedOnAndOff, {
// 赤外線通信で、「ON信号とOFF信号を1回づつ受信した」イベントへのアクション
// 赤外線通信の受信処理
IRR_ReceiveRun();
});
EventAction(Event_MTR_ChangeAnyStatus, {
// モーターの制御状態が変わったイベントへのアクション
// モーターの制御値を更新する。
MTR_Update();
});
EventAction(Event_IRR_DataIsReceived, {
// 赤外線通信で、データ受信が完了したイベントへのアクション
ActionDataReceived();
});
EventAction(Event_MRT_ControlParameter_Changed, {
// モーター制御パラメーターが変化したイベントへのアクション
// モーターを制御する。
MTR_SetControlParameter_PostProcess();
});
EventAction(Event_NRT_CounterChanged, {
// NRT_Counterが変化したイベント
NRT_Check();
});
EventAction(Event_NRT_1SecPassed, {
// NRTカウンタが1秒経ったイベント
// モーターを止める
MTR_Stop();
});
EventAction(Event_NRT_6SecPassed, {
// NRTカウンタが6秒経ったイベント
// LEDを消す
OUT_SetLed(false);
});
}
EventActionは#defineで次のように定義しています。
// イベントが発生していたら、イベントフラグを降ろしアクションを行なう
#define EventAction(EventName, Action) \
do \
{ \
if (EventCheck(EventName)) \
{ \
EventFall(EventName); \
{ \
Action \
} \
return; \
} \
} while (0)
EventCheck()でイベントに対するビットフィールドをチェックし、真ならばそれをfalseにして、アクションを実行し、関数をreturnしているだけです。コーディング量を減らして、打ち間違いを減らすためにマクロにしているだけです。
個々のイベントに対するアクションが何をしているのかについてはあまり重要ではないので説明を省きます。
重要なことはこの関数がコールされるたびに、次のように動作するということです。
- 発生しているイベントの中で優先度の高いものが1つ選ばれる。
- 選ばれたイベントに対するアクションが行われる。
- 優先度の低いイベントは、とりあえず無視して関数を抜ける。再度この関数がコールされたときに(他に優先度の高いイベントが発生していなければ)そのイベントに対するアクションが行われる。
こうすることで関数およびアクションにかかる処理時間が長くならないようにしています。
TMR2などの割込みに対するイベントを適切に処理するために、EventCheckAndAction()は可能な限り高速で処理する必要があります。
また関数の最初にEV_Flag.ALL==0をチェックしているのもこのためです。まったくイベントが発生していない場合にすぐに関数を抜けることができます。
外から発生するイベント
次に、外から発生するイベント(割込みによって発生するイベント)に対してコーディングします。
外から発生するイベントはTMR2IFとIOCAF5を見ればよいので、内部で発生させるイベントのようなビットフィールドを用意する必要はありません。
また、これまでなんども「割込み」という言葉を使ってきましたが、割込み関数(__interrupt()修飾子のついた関数)は使いません。
実際のコードを見た方が説明が早いので、以下に記載します。
void main(void)
{
// initialize the device
SYSTEM_Initialize();
// When using interrupts, you need to set the Global and Peripheral Interrupt Enable bits
// Use the following macros to:
// Enable the Global Interrupts
// INTERRUPT_GlobalInterruptEnable();
// Enable the Peripheral Interrupts
// INTERRUPT_PeripheralInterruptEnable();
// Disable the Global Interrupts
// INTERRUPT_GlobalInterruptDisable();
// Disable the Peripheral Interrupts
// INTERRUPT_PeripheralInterruptDisable();
// イベントフラグを初期化
EventInit();
// 赤外線受信処理部を初期化する。
IRR_Initialize();
// モーター制御処理部を初期化する。
MTR_Initialize();
// NRTカウンタをリセットする。
NRT_Reset();
// 赤外線通信の受信開始
IRR_ReceiveStart();
// モーターOFF
OUT_SetMotor(0);
// LED OF
OUT_SetLed(false);
while (1)
{
// Add your application code
// TMR2割り込みイベント(100usec毎に発生)
if (TMR2IF)
{
TMR2IF = false;
// 赤外線受信処理のための割込みハンドラを実行する。
IRR_HandlerForTMR2();
// モーター制御のための割込みハンドラを実行する。
MTR_HandlerForTMR2();
// NRTカウンタのための割込みハンドラを実行する。
NRT_HandlerForTMR2();
continue;
}
// IRINピン(RA5ピン)割り込みイベント
// (赤外線受信モジュールの出力信号変化によって発生)
if (IOCAF5)
{
IOCAF5 = false;
// 赤外線受信処理のための割込みハンドラを実行する。
IRR_HandlerForIRINChanged();
continue;
}
// 上の2つのイベントが無いときにここに来る。
// その場合は、その他のイベントをチェックし、アクションを行う。
EventCheckAndAction();
}
}
このコードはmain()関数です。完成したコードのmain()をそのまま載せています。
41行目までは初期化処理です。最初にMCCが生成した初期設定関数SYSTEM_Initialize()をコールします。そのあと、赤外線通信などの自作の機能の初期化関数をコールします。
42行目からwhileループに入り、イベント駆動型の処理を始めます。
whileループの最初に、TMR2割込みの発生(TMR2IF)をチェックし、発生していたらそれに対するアクションを行います。TMR2割込みの場合は赤外線受信処理のためにIRR_HandlerForTMR2()をコールし、その他の処理のための関数もコールしています。
同様に、IOC割込みの発生チェック&アクションを行っていて、赤外線受信処理のためにIRR_HandlerForIRINChanged()をコールします。
2つの割込みが無かった場合は EventCheckAndAction()をコールして、内部で発生するイベントへのチェックとアクションを行い、continueでwhileループの最初に戻ります。
割込み関数を使わない理由
今回のプログラムを作成し始めた当初は割込み関数を使うつもりで設計していました。しかし途中で割込み関数を使わない設計に変えました。その理由を説明します。
割込み関数を使う上で注意するべきこととして、「割込み関数の中で複雑な処理を行うのは避けた方が良い」ということがあります。今回の例を使って説明すると、TMR2割込みとIOC割込みは発生原因が全く無関係なので同時に発生することがあるからです。
例えばIOC割込みが発生したときに、それに対するアクションの全て(計測した時間を赤外線通信フォーマットと比較して送信データを決定したり、データがすべて受信できた時にそれに応じてモーターを制御したり…などの全ての処理)を割込み関数の中で行ったとします。
その場合、IOC割込みの処理時間が長くなってしまいます。もし処理時間が100usec以上になってしまうと、その間はTMR2割込みに対する処理ができず、TMR2割込みを1回スキップしてしまうかもしれません。TMR2割込みは赤外線パルスのON/OFFの時間をただしく計測するためには必要なので、ここが正しく動作しなければ受信処理全体が正しく動作しません。
これを避けるには、割込み処理の中では単にフラグを設定する程度にしておき、フラグに応じてmain()処理の中で複雑な処理をすればるほうが良いことになります。つまり、IOC割込み関数の中ではEventRise(Event_IOC)だけを実行し、以降はEvent_IOCイベントに対するアクションとして処理をするのです。
しかしそれは、割込み関数を使わずに上述のmain()関数とアルゴリズム的には同一です。それならば、IOCAF5があるのに別途イベント用のビットフィールドを用意するのは無駄です。また、EventRize(Event_IOC)とするためだけに割込み関数を使うことも処理時間の無駄です。
このような無駄は減らした方が良いので、IOC割込み関数を使わない設計に切り替えました。
そうすると、TMR2割込みも同様の手法で設計できることに気づき、割込み関数は全く使わないようにしたのです。
処理速度を重視する理由
割込み関数を使わない理由と重複しますが、TMR2割込みは100usecに1回発生します。100usecに1回は必ず「if ( TMR2IF) 」の部分を通るようにしなければ、TMR2割込みがスキップされ適切に処理できなくなります。
つまり、main()関数内のwhile()の1回のループにかかる処理時間は必ず100usec未満で終わる必要があります。それを超えないようにプログラムを作成しなくてはなりません。これは設計上の制約であり、処理速度を重視する理由です。
そのために、main()のwhile()ではTMR2IFやIOCAF5に対するアクションを行ったときは、かならずループの先頭に戻り、EventCheckAndAction()を実行しません。EventCheckAndAction()の内部でも「1つのイベントアクションしか行わない」ようにしています。こうしてwhile()ループ1回当たりで実行されるアクションは必ず1回以下になるようにし、1回のループの処理時間を減らしています。
コメント