読者は、キューやフローなど、RTOSに関連する基本用語に精通していることを前提としています。 FreeRTOSについての詳細は、 qdxの投稿FreeRTOS:紹介およびFreeRTOS:プロセス間通信で読むことができます。
FreeRTOSを使用したマイクロコントローラーのプロジェクトに参加した人は、標準APIがかなり貧弱であるため、追加のコードを書く必要が生じるという事実に出くわすかもしれません。 私の場合、フロー間の相互作用のためのツール、つまりユニファイドメッセージングシステムの欠如がありました。 通常、スレッドと同期の間で情報を交換するには、1つまたは別の形式のキューが使用されます。 さらに、キューに含まれる情報のタイプは毎回異なり、コードの再利用の可能性を減らします。
統合メッセージフォームを使用すると、多くの場合、複数のスレッドを1つのワーカースレッドに結合して、キュー内の受信メッセージを処理できます。
この考え方はAndroidでHandlerクラスを使用するのと似ているため、名前(クラスフィールドと構造の名前を含む)はそこから恥知らずに借用されます。
このアプローチは、単一のスレッドを使用して複数のタイプのメッセージを処理し、キューからメッセージを抽出し、対応するハンドラーを呼び出し、次のメッセージに進むことに基づいています。
スレッドはキューでブロックされているため、メッセージがない場合、制御は他のスレッドに転送されます。 新しいメッセージがキューに入れられるとすぐに、スレッドのロックが解除され、メッセージが処理されます。 メッセージは、割り込みハンドラー、他のスレッド、他のハンドラー、または自分でキューに入れることができます。
他のスレッドと同様に、ワーカースレッド(またはルーパー)は、優先度の高い別のスレッドに置き換えることができます。 優先順位の異なる複数のルーパーを使用すると、最も重要なメッセージをタイムリーに処理できます。 理想的には、ハンドラごとに一意の優先度を持つ1つのスレッド(残念ながら、常に妥協点があります)。
なぜこれがすべて必要なのですか?
まず、このアプローチは柔軟性を提供します。 これにより、多くのイベントに応答する複雑なカプセル化オブジェクトを作成できます。 最近のプラクティスの例としては、RFIDリーダークラスがあります。これは、最初はコマンドラインでのみ機能すると想定されていました。 その結果、ハンドラーはステートマシンになり、フォルダー、タイマー、モーションセンサー、およびバッテリーレベルモニターからのメッセージがコマンドラインからのメッセージに追加されました。
チャート
実装例
単純なC ++プログラムの例で上記を考慮してください。 Threadクラスの説明は行いません。Threadの子孫は、スレッドの本体であるrun()メソッドをオーバーライドする必要があることに言及するだけで十分です。
各メッセージは構造です:
struct MESSAGE { /** Handler responsible for handling this message */ Handler *handler; /** What message is about */ char what; /** First argument */ char arg1; /** Second argument */ char arg2; /** Pointer to the allocated memory. Handler should cast to the proper type, * according to the message.what */ void *ptr; };
ルーパーストリームの実装例:
Looper::Looper(uint8_t messageQueueSize, const char *name, unsigned short stackDepth, char priority): Thread(name, stackDepth, priority) { messageQueue = xQueueCreate(messageQueueSize, sizeof(Message)); } void Looper::run() { Message msg; for (;;) { if (xQueueReceive(messageQueue, &msg, portMAX_DELAY)) { // Call handleMessage from the handler msg.handler->handleMessage(msg); } } } xQueueHandle Looper::getMessageQueue(){ return messageQueue; }
抽象ハンドラーの実装例(すべてのメソッドではありません):
Handler::Handler(Looper *looper) { messageQueue = looper->getMessageQueue(); } bool Handler::sendMessage(char what, char arg1, char arg2, void *ptr) { Message msg; msg.handler = this; msg.what = what; msg.arg1 = arg1; msg.arg2 = arg2; msg.ptr = ptr; return xQueueSend(messageQueue, &msg, 0); }
ハンドラーの実装例:
Looperが呼び出す1つの仮想メソッドをオーバーライドする必要があります。
void ExampleHandler::handleMessage(Message msg) { #ifdef DEBUG // , debugTx->putString("ExampleHandler.handleMessage("); debugTx->putInt(msg.what, 10); debugTx->putString(")\n"); #endif TxBuffer *responseTx; switch (msg.what) { case EVENT_RUN_SPI_TEST: responseTx = (TxBuffer*)msg.ptr; testSpi(); // responseTx->putString("Some response\n"); break; case EVENT_BLINK: // led->blink(msg.arg1, msg.arg2); break; } }
mainの実装例:
mainは、スレッド、ハンドラー、およびその他の初期化を作成するために使用されます。
int main( void ) { // Looper looper = Looper(10, "LPR", 500, configNORMAL_PRIORITY); // ExampleHandler exampleHandler = ExampleHandler(&looper); // CommandInterpreter interpreter = CommandInterpreter(); // . // Strings_SpiExampleCmd, // EVENT_RUN_SPI_TEST interpreter.registerCommand(Strings_SpiExampleCmd, Strings_SpiExampleCmdDesc, &exampleHandler, EVENT_RUN_SPI_TEST); interpreter.registerCommand(Strings_BlinkCmd, Strings_BlinkCmdDesc, &exampleHandler, EVENT_BLINK); vTaskStartScheduler(); /* Should never get here, stop execution and report error */ while(true) ledRGB.set(PINK); return 0; }
サンプルソース
おわりに
このアプローチを使用すると、いくつかの利点があります。
- 登録済みのハンドラーにメッセージを送信するボタンのコマンドラインインタープリターや割り込みハンドラーなど、再利用するためのいくつかのコンポーネントを作成できます。
- 各ハンドラーは、メッセージコードとともに個別のファイルに記述されます
- 既存のハンドラーを拡張したり、新しいハンドラーを追加したりするのは、新しいスレッドを作成するよりも簡単です
- メッセージは同じスレッドで実行されるため、競合する可能性はありません
- 単一のスレッドを使用すると、スタックあたりのメモリコストが大幅に削減されます。
- 開発プロセス中に、ハンドラーは複数のハンドラー(状態ごとに1つ)で構成される状態マシンに簡単に置き換えることができます。
- 1つのスレッドで複数のメッセージを処理するのにかかる時間は、コンテキストの切り替えがないために、各タイプのメッセージが個別のスレッドで処理された場合よりも短くなります。
メッセージハンドラー(ハンドラー)にはいくつかの制限があります。
- ハンドラーはストリームをブロックしないでください(ブロックが発生した場合、メッセージキュー全体が待機し、ストリームはアイドル状態になります)
- メッセージ処理に時間がかかりすぎないようにする
- メッセージ処理が擬似的に(タイムスライスごとに)行われるのではなく、順番に行われるため、イベントに対する反応時間を予測することはより困難です。
もちろん、すべてのスレッドが提案されたモデルを使用できるわけではありません。 ハードリアルタイムを確保する必要がある場合、同じスレッドに複数のハンドラーを配置することはできません(1つは可能です)。 ただし、実際には、他のすべてのフローは非常に単純であり、他のフローとの対話を実際に必要としないことが示されています。 これらは、(シリアルポートまたはUSBから)何かを読み取って責任のあるハンドラーにメッセージを送信するストリーム、または時間のかかる操作を実行するストリーム(表示)のいずれかです。 ファームウェアのメインロジックは、ハンドラーを使用して正常に記述できます。
ご清聴ありがとうございました。