こんにちは。 この記事では、プロセス間データ交換とイベントとの同期の方法について説明します。
他の部分へのリンク:
FreeRTOS:はじめに。
FreeRTOS:ミューテックスとクリティカルセクション。
マルチスレッド環境をサポートする適切な手段がなければ、マルチスレッドOSは完全とは見なされません。 FreeRTOSには、これに必要なものがすべて揃っています。つまり、
- タスク間でデータを交換するためのキュー、またはISR。
- バイナリセマフォ、およびイベントとの同期のためのセマフォのカウント(割り込み)。
- リソース(ポートなど)を共有するためのミューテックス。
- スケジューラによって実行を中断できないコード領域を作成するための重要なセクション。
キュー
FreeRTOSを使用して記述されたプログラムは、一連の独立したタスク、またはデータを交換するための効率的でスレッドセーフなメカニズムを必要とするミニサブプログラムです; FreeRTOSの場合、これらはキューです。
キューは単純なFIFO(終了ではなくキューの先頭に書き込むこともできます)であり、サイズが既知の固定数の要素を格納できるバッファーです。 キューへの書き込みはデータをバッファにコピーするバイトで、読み取りはデータをコピーしてキューから削除することです。
実際、キューは、壊れたデータの読み取り/書き込みを恐れることなく、多くのライターやリーダーが持つことができる独立したオブジェクトです。 データを読み取るとき、オプションで、タスクが新しいデータを待つ時間を指定できます。 データを記録するときに、この時間を示すこともできますが、すでにキュー内の場所を待機しています。
FreeRTOSでキューを操作するための主な機能をさらに詳しく考えてみましょう。
キューを使用する前に、作成する必要があります。 キューのRAMメモリはFreeRTOSヒープから割り当てられ、そのサイズはデータサイズ+キュー構造のサイズに等しくなります。 コードでは、各キューはxQueueHandleなどのハンドルで表されます。
xQueueHandle xQueueCreate(unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxItemSize);
uxQueueLength-単位時間あたりにキューが保存できる要素の最大数。
uxItemSize-キューの各要素のサイズ。
return xQueueHandle、またはNULL-キューが作成された場合、対応するハンドルが返されます。 十分なメモリがない場合、NULLが返されます。
キューに書き込むには、特別な関数も使用されます。
portBASE_TYPE xQueueSendToFront(xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait); portBASE_TYPE xQueueSendToBack(xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait); // portBASE_TYPE xQueueSend(xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait);
xQueue-データが書き込まれるキューのハンドル。
pvItemToQueue-キューに入れるアイテムへのポインター。
xTicksToWait-キュー内の場所が表示されるためにタスクがロック状態にある必要がある時間。 portMAX_DELAYを指定して、タスクが無期限にロックされた状態になるようにできます。 キュー内の場所が表示されるまで。
return pdPASS、またはerrQUEUE_FULL-新しいアイテムがキューに正常に書き込まれた場合、関数はpdPASSを返します。十分なスペースがなく、時間xTicksToWaitが指定されている場合、タスクはブロックされた時間に移動してキュー内の場所を待ちます。
データを読み取るには、2つの関数が使用されます。主な違いは、xQueueReceiveがキューから要素を削除し、xQueuePeekは削除しないことです。
portBASE_TYPE xQueueReceive(xQueueHandle xQueue, const void * pvBuffer, portTickType xTicksToWait); portBASE_TYPE xQueuePeek(xQueueHandle xQueue, const void * pvBuffer, portTickType xTicksToWait);
xQueue-データが書き込まれるキューのハンドル。
pvBuffer-キューからのデータが読み込まれるメモリバッファへのポインタ。 バッファタイプ=キュー要素のタイプ。
xTicksToWait-データがキューに表示されるためにタスクがロック状態にある必要がある時間。 portMAX_DELAYを指定して、タスクが無期限にロックされた状態になるようにできます。 新しいデータがキューに表示されるまで。 次に、これを使用してゲートキーパータスクを作成する方法を説明します。
return pdPASS、またはerrQUEUE_EMPTY-新しい項目がキューから正常に読み取られた場合、関数はpdPASSを返します。キューが空でxTicksToWaitが指定されている場合、タスクはロックされた時間に移動してキュー内の新しいデータを待ちます。
キュー内のアイテムの数を表示するには、関数を使用できます
unsigned portBASE_TYPE uxQueueMessagesWaiting( xQueueHandle xQueue );
重要:上記の関数はISR(割り込み)で使用できません。また、最後のパラメーターを除いて、動作は以前の関数と同様の特別な接尾辞ISRを持つ特別なバージョンがあります。
portBASE_TYPE xQueueSendToFrontFromISR( xQueueHandle xQueue, void *pvItemToQueue, portBASE_TYPE *pxHigherPriorityTaskWoken ); portBASE_TYPE xQueueSendToBackFromISR(xQueueHandle xQueue, void *pvItemToQueue, portBASE_TYPE *pxHigherPriorityTaskWoken); portBASE_TYPE xQueueReceiveFromISR(xQueueHandle xQueue, const void *pvBuffer, portBASE_TYPE *pxHigherPriorityTaskWoken);
pxHigherPriorityTaskWoken-キューへの書き込みは、データを待機し、現在のタスクよりも高い優先度を持つタスクをロック解除できるため、強制コンテキスト切り替えを実行する必要があります(このためにはtaskYIELD()マクロを呼び出す必要があります)。 必要な場合、このパラメーターはpdTRUEと等しくなります 。
キューの効率的な使用に関するいくつかの言葉。 たとえば、UARTを検討します。一般的なアプローチでは、受信した各バイトはすぐにキューに書き込まれますが、それは価値がありません。 すでにかなり低い周波数では、非常に非効率的です。 ISRで基本的なデータ処理を実行してからキューに転送する方が効率的ですが、ISRコードはできるだけ短くする必要があることを理解することが重要です。
いわゆるゲートキーパータスクの例を考えてみてください(これを正しく翻訳する方法がわかりません。誰かから言われたら感謝します:))。
ゲートキーパータスクは、マルチスレッドプログラミングの主な問題を回避する簡単な方法です。優先順位を逆転させ、タスクをデッドロックに陥れます。
ゲートキーパータスクは、リソースに直接アクセスする唯一の方法です。他のすべてのタスクは、このタスクを介してリソースにアクセスする必要があります。
単純なスケルトンゲートキーパータスクを検討してください。 これは純粋に遠い例ですが、一般的な原則を理解するのに役立ちます。 たとえば、UARTを介していくつかのデータを安全に送信する必要があると仮定しましょう。
// Gatekeeper . void vGatekeeperTask( void *pvParameteres ) { char oneByte; // - , UART , . for( ;; ) { // . // portMAX_DELAY , . // , . xQueueReceive( xDataQueue, &oneByte, portMAX_DELAY); // , . vSendByteToUART( oneByte ); // . } }
したがって、UARTを使用してバイトを送信したいタスクは、たとえば次のようなユーティリティ関数のいずれかを使用できます。
void vUARTPutByte( char byte) { // , , . xQueueSend( xDataQueue, &byte, 0 ); }
また、キューのデータ型としてcharのみを制限する必要はありませんが、構造体を使用してパイプライン全体を整理することもできます。
バイナリセマフォ。
バイナリセマフォを使用すると、イベントが発生したとき(ボタンを押すなど)にタスクをロック解除できます。
典型的な作業シナリオ:特定の割り込みがISRに入ると、タスクの結果として、セマフォを放棄し 、待機中のセマフォがセマフォを取得し、操作のためにロック解除状態を離れます。 このメカニズムを次の図に示します。
すべてのタイプのセマフォを格納するには、 xSemaphoreHandleデータタイプが使用されます。
セマフォを操作するための関数を検討してください。
void vSemaphoreCreateBinary( xSemaphoreHandle xSemaphore );
xSemaphore-この関数はマクロを使用して実装されているため、ポインタではなくハンドルの値を渡す必要があります。 セマフォが正常に作成された場合、xSemaphoreはNULLではありません。
セマフォを「取得」するために、特別な関数が使用されます:
portBASE_TYPE xSemaphoreTake( xSemaphoreHandle xSemaphore, portTickType xTicksToWait );
xSemaphore-取得するセマフォハンドル。
xTicksToWait-セマフォが使用可能になるまで、タスクをロック状態にする必要がある時間。 portMAX_DELAYを指定して、タスクが無期限にロックされた状態になるようにできます。 セマフォが使用可能になるまで。
return pdPASS、またはpdFALSE -pdPASS-セマフォを受信した場合、pdFALSE-セマフォを利用できない場合
セマフォを「与える」ために、特別な関数も使用されます。 ISRバージョンを検討します ほとんどの場合、セマフォはISRと組み合わせて使用されます。
portBASE_TYPE xSemaphoreGiveFromISR( xSemaphoreHandle xSemaphore, portBASE_TYPE *pxHigherPriorityTaskWoken );
xSemaphore-渡されるセマフォハンドル。
pxHigherPriorityTaskWoken-セマフォの「転送」により、現在のタスクよりも優先度の高いセマフォデータを待機しているタスクのロックが解除される可能性があるため、強制コンテキストスイッチングを実行する必要があります(これを行うには、taskYIELD()マクロを呼び出します)。 必要な場合、このパラメーターはpdTRUEと等しくなります 。
return pdPASS、またはpdFALSE -pdPASS-セマフォが指定されている場合、pdFALSE-セマフォがすでに使用可能であるが、処理されていない場合
例として、短いプログラムコードを示します。ISRの場合は、疑似コードを作成しました。
xSemaphoreHandle xButtonSemaphore; void vButtonHandlerTask( void *pvParameteres ) { for( ;; ) { xSemaphoreTake( xButtonSemaphore, portMAX_DELAY ); // . } } void main() { // vInitSystem(); vSemaphoreCreateBinary( xButtonSemaphore ); if( xButtonSemaphore != NULL ) { // . , ! xTaskCreate( &vButtonHandlerTask, (signed char *)"GreenBlink", configMINIMAL_STACK_SIZE, NULL, 1, NULL ); // , .. . vTaskStartScheduler(); } // , , . . for( ;; ) { } } ISR_FUNCTION processButton() { portBASE_TYPE xTaskWoken; if( buttonOnPressed ) { xSemaphoreGiveFromISR( xButtonSemaphore, &xTaskWoken ); if( xTaskWoken == pdTRUE) { taskYIELD(); } } }
セマフォのカウント。
セマフォを使用するときに存在する典型的な状況を考えてみましょう。
1.中断を引き起こすイベントがありました。
2. ISRはセマフォを「与える」、つまり 待機中のタスクセマフォのロックを解除します。
3.待機中のタスクはセマフォを「取得」します。
4.目的のコードを実行すると、タスクは再びロック状態になり、新しいイベントを待ちます。
このアルゴリズムは正常に機能しますが、高周波数では機能しません。 高周波では、カウントセマフォを使用する必要があります。これは、原則として2つの場合に使用されます。
- また、イベントとの同期用ですが、高頻度です。
- リソース管理。 この場合、セマフォの数は利用可能なリソースの量を示します。
前述のように、xSemaphoreHandleデータ型はすべてのタイプのセマフォを格納するために使用されます。セマフォを使用する前に作成する必要があるため、カウントされたセマフォを作成するには特別な関数を使用する必要があります。
xSemaphoreHandle xSemaphoreCreateCounting( unsigned portBASE_TYPE uxMaxCount, unsigned portBASE_TYPE uxInitialCount );
uxMaxCount-カウンターが保存できるセマフォの最大数。 キューとの類推により、これはキューの長さです。
uxInitialCount-セマフォを作成した後のカウンター値。
returnはNULLではありません -セマフォが作成された場合、関数はNULL以外の値を返します。
それ以外の場合は、以前のものと同様の関数を使用して、セマフォをカウントします。