こんにちは、ナナです。
それでは、3つ目のタスク間同期・通信機能の紹介です。そろそろ、タスク間でデータをやり取りする感覚が身に付いてきたのではないでしょうか。
メールボックスもデータキューと同様にタスク間で通信を行うための機構です。メールボックスとデータキューの違いも意識して解説していきます。
データキューについて知りたい方は、こちらの記事を参考にしてください。
本記事では次の疑問点を解消する内容となっています。
では、メールボックスの仕組みを学んでいきましょう。
メールボックスの概要
データキューは4Byte固定のデータをタスク間で通信する機能でした。メールボックスではサイズ制限のないデータ通信が可能です。
やってほしいことをメッセージに書いておいたよ!
メッセージに文字がびっしりなんだけど・・・指示多くない?
では、ITRON仕様を確認しましょう。
共有メモリに置かれたメッセージを受け渡すとされています。ここがデータキューとは違う点です。その仕組みを解き明かしていきましょう。
メールボックスで使用する代表的なサービスコール
メールボックスではよくこれらのサービスコールを使用します。
まずは、このサービスコールを覚えましょう。
メールボックスの生成方法(CRE_MBX)
メールボックスを生成するための静的APIを見てみましょう。
CRE_MBX(ID mbxid, { ATR mbxatr, PRI maxmpri, VP mprihd});
引数パラメータを解説しておきます。
ID | mbxid | 生成するメールボックスのID。整数として一意のIDを割り付ける。 |
ATR | mbxatr | メールボックスの属性。 |
PRI | maxpri | 送信されるメッセージの優先度の最大値。 |
VP | mprihd | 優先度別のメッセージキューヘッダ領域の番地。 NULLを指定すると自動メモリ割り付け。 |
属性には((TA_TFIFO || TA_TPRI) | (TA_MFIFO || TA_MPRI))が指定できます。TA_MFIFOとTA_MPRIという特殊なオプションがあります。TA_MPRIはメッセージに対して受信優先度を設定できるようになるオプションです。
これにより送信するデータに優先度をつけ、受信側に対してより優先度の高いメッセージから開封させることが可能になります。本記事では取り扱いしませんが、このような機能を持っていることは知っておくとよいでしょう。
maxpriはTA_MPRIを指定した場合に有効になる引数です。メッセージの優先度を何段階にするのかを指定することができます。
CRE_MBXによるメールボックスの生成定義
では、メールボックスを生成してみましょう。system.cfgにメールボックスを1つ生成定義してみてください。
追加するメールボックス生成情報
- IDは「MBXID_MBX1」を指定する。
- 属性は「TA_TFIFO | TA_MFIFO」を指定する。
- メッセージ優先度は「1」を指定する。
- メッセージキューヘッダ領域の番地はNULLを指定する。
system.cfg(一部抜粋)
//------------------------------------------------
// データキュー定義
//------------------------------------------------
CRE_DTQ(DTQID_DTQ1, {TA_TFIFO, 3, NULL});
//------------------------------------------------
// メールボックス定義
//------------------------------------------------
//------------------------------------------------
// メールボックス定義
//------------------------------------------------
CRE_MBX(MBXID_MBX1, {(TA_TFIFO | TA_MFIFO), 1, NULL});
メールボックスで送受信するデータの定義方法
メールボックスにおいて送受信するデータは特殊な形で定義が必要となります。
まずはITRON仕様の説明を見てみましょう。
皆さんはメールボックスで送受信するデータを定義する際に、この「メッセージヘッダ」と「メッセージパケット」を意識したデータ構造にする必要があります。
構造体で定義するメッセージヘッダとメッセージパケット
皆さんはメールボックスでやり取りしたいメッセージは構造体として定義を行います。この定義時に特別なルールに従う必要があります。
それは、「T_MSGという構造体型のメンバを先頭に配置する」というルールです。
// メッセージパケット定義例
typedef struct
{
T_MSG msg; // メッセージヘッダ 必ず先頭に配置する!
long data1; // アプリケーションのメッセージデータ1
long data2; // アプリケーションのメッセージデータ2
} S_MSGPK_SPK;
このようにT_MSG型のメンバを必ず先頭に配置するように定義を行います。
以降に続く構造体メンバは自由に配置して構いません。それが「アプリケーションのメッセージ」とされる部分になります。
メッセージの定義構成
このように定義したメールボックス用の構造体データを「メッセージパケット」と呼びます。
メッセージヘッダの役割
メッセージヘッダはカーネル側でメッセージパケットを管理するための情報を入れる領域です。ITRON仕様に「メッセージをリンクリストで管理する」とされていますね。
リンクリストとはメッセージ同士が順番に参照できるデータ構造です。
このようにメッセージ同士がつながっており、メッセージ同士をたどることが可能です。この情報を管理しているのが、T_MSGであるメッセージヘッダなのです。
メッセージヘッダを扱う際の注意
T_MSG型で配置したmsgの領域はカーネルが自動で書き換えを行うため、決して皆さんが値を変更してはなりません。
もし変更してしまうとリンクリストのデータ構造が破壊され、正しくメールボックス機能が動かなくなってしまいます。
メールボックスへのデータ送信方法(snd_mbx)
メールボックスへメッセージを送信する側のタスク処理を作ってみましょう。TASK1から送信することにします。
メールボックスへメッセージを送信するsnd_mbxの仕様
では、snd_mbxのITRON仕様を見てみましょう。
ER snd_mbx(ID mbxid, T_MSG * pk_msg);
ここで重要なのが「番地とするメッセージを送信する」とされていることです。
つまり、データのコピーを受け渡すのではなく、ポインタである番地を相手先に届けるということです。これはメールボックスを扱うときに気を付けなければならないポイントです。
メールボックスはデータのコピーではなくメッセージ領域へのポインタを送付する機能!
TASK1タスクからsnd_mbxを使ったミッション内容
snd_mbxを行うTASK1のミッション内容を決めます。次のミッションに従い処理を行いましょう。
TASK1のメッセージ送信のプログラム作成
では、TASK1からメッセージを送信してみましょう。次のプログラムをベースにミッション内容をプログラミングしてみてください。
main.c(一部抜粋)
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
while(1)
{
}
return;
}
main.c(一部抜粋)
// スピーカー制御用のメッセージパケット定義
typedef struct
{
T_MSG msg; // メッセージヘッダ
E_SPK_SCALE scale; // アプリケーションデータ
} S_MSGPK_SPK;
// グローバル変数としてメッセージパケットを作成
static S_MSGPK_SPK msgpkt;
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
E_SPK_SCALE scale;
while(1)
{
for (scale = E_SPK_SCALE_DO ; scale <= E_SPK_SCALE_MI ; scale++)
{
// ドレミを順に設定
msgpkt.scale = scale;
// メッセージの送信
snd_mbx(MBXID_MBX1, (T_MSG *)&msgpkt);
// 1000ミリ秒待ち
dly_tsk(1000);
}
}
return;
}
メッセージパケットをグローバル変数にした理由
この理由は別途解説します。続きを読んでください。
メッセージパケットへのポインタ設定時のキャストについて
snd_mbx(MBXID_MBX1, (T_MSG *)&msgpkt);
snd_mbx呼び出し時にT_MSG*型へキャストを行っています。これはメッセージパケットを送る際に必要なキャストです。
カーネルはT_MSG型のメンバによりリンクリストを構築するため、snd_mbxではメッセージヘッダへのポインタを必要としています。ここでポイントなのがメッセージパケットの先頭にメッセージヘッダを配置するというルールです。
メッセージヘッダとメッセージパケットの先頭番地は同じ場所を示しています。この構成により、メッセージヘッダの番地を送付することがメッセージパケットを送付することと等しくなるのです。
メールボックスを使ったタスクの待ち方(rcv_mbxの使い方)
メールボックスを使って受信待ちをするタスク処理を作ってみましょう。受信待ちをするタスクはMAINタスクとします。
データ受信待ちのサービスコールであるrcv_mbxの仕様
では、rcv_mbxのITRON仕様を見てみましょう。
ER rcv_mbx(ID mbxid, T_MSG ** ppk_msg);
rcv_mbxはタスクをメッセージ受信待ち状態に遷移させるサービスコールです。メールボックスにメッセージが入っていない時にタスクがrcv_mbxを呼び出すと待ち状態へ遷移します。
この流れはデータキューと一緒ですね。
メールボックスのデータの受け取り方
データキューとの違いはデータの受け取り方です。先頭メッセージパケットの先頭番地をpk_msgに格納するとされています。
よく見ると第2引数がダブルポインタになっていますね。
ダブルポインタに関して理解していない方は『C言語 「ポインタのポインタ」を図解【イメージで簡単理解!】』の記事を参照するとよいでしょう。
つまり、rcv_mbxを呼び出す側ではポインタ変数を作成し、rcv_mbxへはその番地を受け渡すことになります。rcv_mbxの中ではポインタ変数の中にメッセージパケットの番地を設定してあげるということになります。
これはダブルポインタを使う代表的なシーンですね。
このようにダブルポインタは要所で使われることがあるので、しっかりと習得しておくことをお勧めします。
メッセージヘッダへのキャストについて
rcv_mbxを使うときに特徴的なのが、ポインタ変数となるメッセージパケットをrcv_mbxへ受け渡す時にメッセージヘッダへのキャストすることです。
void TASK1(VP_INT exinf)
{
S_MSGPK_SPK *pMsg; // メッセージパケットのポインタ
// メッセージパケットをメッセージヘッダへキャストする
rcv_mbx(DTQID_DTQ1, (T_MSG**)&pMsg);
// 受信時処理・・・
}
メールボックスの受信処理はこのように少し変わった書き方が必要となります。
送信時と同じようにカーネルが必要としているのはメッセージヘッダのため、メッセージパケットを適切にキャストしてあげる必要があります。
MAINタスクからrcv_mbxを使ったミッション内容
rcv_mbxを行うMAINのミッション内容を決めます。次のミッションに従い処理を行いましょう。
それでは次のプログラムをベースとして受信を行うプログラムを作成してみましょう。
main.c(一部抜粋)
//------------------------------------------------
// 概 要:MAINタスク
//------------------------------------------------
void MAIN(VP_INT exinf)
{
while(1)
{
}
return;
}
main.c(一部抜粋)
//------------------------------------------------
// 概 要:MAINタスク
//------------------------------------------------
void MAIN(VP_INT exinf)
{
// メッセージパケットのポインタ定義
S_MSGPK_SPK * pMsg = NULL;
while(1)
{
// メールボックス受信
rcv_mbx(MBXID_MBX1, (T_MSG **)&pMsg);
// メッセージの音階データでスピーカー出力
Spk_start(pMsg->scale);
}
return;
}
メッセージを受信したらSpk_startを呼び出すだけですね。処理は簡単ですが、メッセージパケット変数の使い方を覚えましょう。
TASK2、CYC1、ALM1に関しては特にやるべきことはないので、次のようにしておきましょう。
//------------------------------------------------
// 概 要:練習用TASK2関数
//------------------------------------------------
void TASK2(VP_INT exinf)
{
while(1)
{
dly_tsk(1000);
}
return;
}
//------------------------------------------------
// 概 要:練習用周期ハンドラ関数
//------------------------------------------------
void CYC1(VP_INT exinf)
{
}
//------------------------------------------------
// 概 要:練習用アラームハンドラ関数
//------------------------------------------------
void ALM1(VP_INT exinf)
{
}
ここまでできたら、ビルドして動かしてみましょう。スピーカーから1秒毎にド・レ・ミが順に流れるはずです。これがメールボックスによるデータ送受信方法です。
メールボックスを使う際のデータ受け渡しの失敗事例
メールボックスを扱う上でよくやる失敗事例があります。それを紹介しましょう。
データキューの場合は、4Byteの情報をコピーして受け渡すため、snd_dtqが終われば送信データのメモリ領域を自由に更新することができます。
しかし、メールボックスではメッセージがポインタで送付されるため、受信する側がメッセージの参照を終えるまではメッセージのメモリ領域を保持し続ける必要があります。
これがメールボックスを扱う上で一番厄介な部分です。
メッセージはもちろん送る側にて準備する必要があり、受信側が無事にメッセージを開封するまではメッセージ内容を保持し続ける必要があります。
メッセージ受け渡しでよくやってしまう失敗
メールボックスを使い慣れていない方がよくやる失敗例が、送信側でメッセージパケットをローカル変数で確保するケースです。これは状況次第で不具合になるやばい設計例です。
送信側のsnd_mbxをサブ関数化した定義例
void subfunc(void)
{
// ローカル変数でメッセージを確保
S_MSGPK msgpkt;
msgpkt.data1 = 100;
// メッセージの送信
snd_mbx(MBXID_MBX1, (T_MSG *)&msgpkt);
// 送り終わったので関数終了
return;
}
このような送信用のsubfunc関数を作成し、ローカル変数でメッセージを送信するのは非常に危険です。
ローカル変数は関数から抜けた瞬間から不定のメモリ領域になります。受信側が開封する前に送信側がsubfunc関数の処理を終えてしまうとmsgpkt領域は不定値になります。
この問題の本質はメッセージを用意する送信側において、如何に受信側の参照が終わったことを知ることができるのか?という設計レベルの話なのです。
では、この問題を解決するためにはどうしたらよいのでしょう?
メールボックスのメッセージ受け渡しの設計例
メールボックスを利用したメッセージ受け渡しはいくつか設計パターンがあります。
- グローバル変数にメッセージを配置し、メモリ領域が破棄されないようにする方法。
- slp_tskとwup_tskを使った同期処理にてメモリ領域を破棄させない方法。
- ヒープメモリを利用し、送信側で確保・受信側で解放する受け渡し方法。
これら以外にも設計方法はあると思いますが、設計例として順に解説します。
1.グローバル変数にメッセージを配置する設計
この方式は本記事内のプログラムで実践している例ですね。これこそがメッセージ構造体をグローバル変数に用意した理由です。
グローバル変数にすることでメッセージパケットのメモリが勝手に破棄されることはありません。
ただし、この方法はメッセージを更新してよいタイミングがわからない欠点があります。これを解決するのであれば、メッセージ内に参照済みフラグを用意して受信側で開封したらフラグをONにするといった形で知らせるという解決策もあるでしょう。
プログラム例
// メッセージパケット定義
typedef struct
{
T_MSG msg; // メッセージヘッダ
int readflg; // 参照済みフラグ
} S_MSGPK;
// グローバル変数としてメッセージパケットを作成
static S_MSGPK msgpkt;
//------------------------------------------------
// 概 要:MAINタスク
//------------------------------------------------
void MAIN(VP_INT exinf)
{
S_MSGPK * pMsg = NULL;
while(1)
{
// メールボックス受信
rcv_mbx(MBXID_MBX1, (T_MSG **)&pMsg);
// 開封済みON
pMsg->readflg = 1;
}
return;
}
//------------------------------------------------
// 概 要:メールボックス送信用処理
//------------------------------------------------
void subfunc(void)
{
// 未開封で初期化
msgpkt.readflg = 0;
// メッセージの送信
snd_mbx(MBXID_MBX1, (T_MSG *)&msgpkt);
return;
}
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
while(1)
{
// メールボックス送信処理呼び出し
subfunc();
// 未開封の場合は待つ
while (msgpkt.readflg == 0)
{
// 100ミリ秒待ち
dly_tsk(100);
}
}
return;
}
この方法は静的メモリという固定メモリを使うため、柔軟性が欠ける方法です。また、受信側で処理が滞ると送信側が待たされることが欠点です。
2.slp_tskとwup_tskを使った開封確認を行う設計
この設計はメッセージ内に送信側のタスクIDを設定し、送信側はメッセージを送ったらslp_tskで眠り、受信側で開封したら送信タスクをwup_tskで起床させる方法です。
この方法を使うとローカル変数でメッセージパケットを作っても不正なメモリアクセスが発生しません。
// メッセージパケット定義
typedef struct
{
T_MSG msg; // メッセージヘッダ
ID taskID; // 送信側タスクID
} S_MSGPK;
//------------------------------------------------
// 概 要:MAINタスク
//------------------------------------------------
void MAIN(VP_INT exinf)
{
S_MSGPK * pMsg = NULL;
while(1)
{
// メールボックス受信
rcv_mbx(MBXID_MBX1, (T_MSG **)&pMsg);
// 開封が終わったとしてタスクを起床する
wup_tsk(pMsg->taskID);
}
return;
}
//------------------------------------------------
// 概 要:メールボックス送信用処理
//------------------------------------------------
void subfunc(void)
{
// ローカル変数としてメッセージパケットを作成
S_MSGPK msgpkt;
// 自タスクID取得
get_tid(&msgpkt.taskID);
// メッセージの送信
snd_mbx(MBXID_MBX1, (T_MSG *)&msgpkt);
// 受信側の開封まで眠る
slp_tsk();
}
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
while(1)
{
subfunc();
}
return;
}
subufunc関数ではローカル変数でメッセージパケットを定義しています。slp_tskで起床待ち状態に入ることで受信側の開封があるまで関数を抜けるのを待機しているということですね。
この方式の欠点は受信側が開封処理に手間取ると送信側が眠り続けることです。
3.ヒープメモリを使ってメッセージを受け渡しする設計
この方法はヒープメモリを使用しますが、1と2の欠点を補う方法です。今回のビュートローバーではヒープメモリを利用できるようにしていないためサンプルとしてプログラムを見てください。
// メッセージパケット定義
typedef struct
{
T_MSG msg; // メッセージヘッダ
long data1;
} S_MSGPK;
//------------------------------------------------
// 概 要:MAINタスク
//------------------------------------------------
void MAIN(VP_INT exinf)
{
S_MSGPK * pMsg = NULL;
while(1)
{
// メールボックス受信
rcv_mbx(MBXID_MBX1, (T_MSG **)&pMsg);
// 開封が終わったためメモリ解放
free(pMsg);
}
return;
}
//------------------------------------------------
// 概 要:メールボックス送信用処理
//------------------------------------------------
void subfunc(void)
{
// ヒープメモリ用のポインタ定義
S_MSGPK * pmsgpkt;
// ヒープメモリ上にメッセージパケットを確保
pmsgpkt = (S_MSGPK *)malloc(sizeof(S_MSGPK));
pmsgpkt->data1 = 100;
// メッセージの送信
snd_mbx(MBXID_MBX1, (T_MSG *)pmsgpkt);
}
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
while(1)
{
subfunc();
}
return;
}
送信側でヒープメモリ上にメッセージパケットを確保し、ポインタとして送信します。受信側ではメッセージを受け取って参照が終わったらヒープメモリを解放します。
この方法は送信側は受信側の都合を考えずにメッセージの送付が可能になります。
ただし、受信側がヒープメモリの解放を忘れると、メモリリークが発生しまくるので注意が必要です。送信側担当者と受信側担当者でしっかりと認識を合わせておく必要があります。
メールボックスによるタスク間同期・通信機能のまとめ
それではメールボックスの特徴をまとめます。
- メールボックスはオブジェクトとして生成が必要。
- メールボックスで送れるデータサイズは制限がない。
- メッセージの先頭には必ずメッセージヘッダを配置する必要がある。
- メッセージの情報は上書きがなく、管理情報を各メッセージヘッダで管理しているためデータキューのような個数制限がない。
- メッセージはポインタで送付されるため、受信側がメッセージを開封するまでメッセージのメモリ領域を変更しないように設計する必要がある。
メールボックスを使う際の一番大変なのが5番のデータ受け渡しのタイミング制御です。ここを如何にうまく設計するか工夫が求められます。