このアプローチの概要として、Michael J. Pontの著書「Time-triggered Embedded Systemsのパターン」が興味のある人に使用されました-www.safetty.net/publications/pttes
ここで概念の概要を説明します。
このコンセプトは、次のアイデアに基づいています。
- システムには単一の定期的な中断があります-ティック。
- タスクには優先順位はありません。
- 次のタスクの制御は、現在実行中のタスクの完了後にのみ転送されます。
この一連の原則は、協調タスクスケジューラ(協調スケジューラ)とも呼ばれます。 クラシックRTOSは、プリエンプティブスケジューラと呼ばれるものを使用します。
著者は、実装の単純さ、非常に低いオーバーヘッドコスト、そしてどんなに奇妙に思えても信頼性という利点を挙げています。
欠点は、より徹底的な設計の必要性です。 たとえば、要件の1つは、タスクの実行時間ができるだけ短いこと、理想的には中断期間よりも大幅に短い場合です。
この原則を示す擬似コード。
void main(void) { scheduler_init(); add_task(Function_A, 2); add_task(Function_B, 10); add_task(Function_C, 15); scheduler_start(); while(1) { dispatch_tasks(); } }
今のところ、すべてが明確になっているはずです-シェダーを初期化し、ティックで指定された頻度で実行される3つのタスクを追加し、シェダーを開始し、タスクマネージャーの無限のサイクルに進みます。
タスクのコンテキストを記述する構造:
typedef struct { void (* pTask)(void); uint32 Period; uint32 PeriodCur; uint8 RunMe; } task_descriptor_t;
実際、RTOSと比較して、「オーバーヘッド」ははるかに少なく-関数へのポインター、起動の頻度、現在の値-起動を待つティックの数、およびタスクを起動する回数です。
task_descriptor_t all_task_list[MAX_TASKS];
タスクリストは、あらかじめ決められた長さの規則的な配列です。
シェダラー自身がハングアップしてタイマーを中断し、特定の周波数、たとえば1ミリ秒(同じティック)で発生するように構成されます。
void scheduler_update(void) interrupt { foreach (task in all_task_list) { task.PeriodCur--; if (task.PeriodCur == 0) { task.PeriodCur = task.Period; task.RunMe++; } } }
ハンドラーでは、タスクのリスト全体を調べて、起動前に残っているティックの現在の値をデクリメントし、0に達すると、それを上書きして起動カウンターをインクリメントします。
そして最後に-ディスパッチャー。 無限ループで回転します。
void dispatch_tasks(void) { foreach (task in all_task_list) { if (task.RunMe > 0) { task.pTask(); task.RunMe--; } } }
それでもタスクのリストを調べて、タスクの開始カウンターが0より大きい場合は、その関数を直接呼び出してこのタスクを開始し、開始カウンターをデクリメントします。
実際にすべて!
実際、実装は途方もなく簡単です(したがって、どこにでも簡単に移植できます)。 実際、無限ループよりも優れています。 実際、セマフォ、キュー、クリティカルセクションなどの同期ツールは必要ありません。 実際、コンテキストの切り替えは必要ありません。
しかし、私はそれが好きではありません。 そして、ここに理由があります。
- システムに1つだけの割り込みが必要であるということは、周辺機器とのすべての作業がポーリングモードで行われなければならないことを意味します。 それはその制限を課します。 また、システムの反応時間を短縮します。
- タスクリストを確認します。 つまり コントロールがリストの最後のタスクに到達する前に、最悪の場合、リストの前のタスクがすべて呼び出されます。 外部イベントに対する反応時間は、やはり最も予測可能なものからはほど遠いものです。
- タスクの1つに何かが発生した場合、リストの最後のタスクに到達しない可能性があります。 協調モードがあり、各タスク自体がディスパッチャに制御を返す必要があります!
- 単一タスクの実行時間の制限。 それから、単純な線形コードの代わりに、多かれ少なかれ長時間のアクションの場合、ステートマシンをフェンスする必要があります。
最後の段落については、同じ本で素晴らしい例が見つかりました。 そこにある棚に重点を置いていることに加えて、本の3分の2は周辺機器とプロトコルを操作する組み込みシステムの基本についての物語に捧げられています。 この本のSPIを使用した例を次に示します。
/*------------------------------------------------------------------*- SPI_X25_Write_Byte() Store a byte of data on the EEPROM. -*------------------------------------------------------------------*/ void SPI_X25_Write_Byte(const tWord ADDRESS, const tByte DATA) { // 0. We check the status register SPI_X25_Read_Status_Register(); // 1. Pin /CS is pulled low to select the device SPI_CS = 0; // 2. The 'Write Enable' instruction is sent (0x06) SPI_Exchange_Bytes(0x06); // 3. The /CS must now be pulled high SPI_CS = 1; // 4. Wait (briefly) SPI_Delay_T0(); // 5. Pin /CS is pulled low to select the device SPI_CS = 0; // 6. The 'Write' instruction is sent (0x02) SPI_Exchange_Bytes(0x02); // 7. The address we wish to read from is sent. // NOTE: we send a 16-bit address: // - depending on the size of the device, some bits may be ignored. SPI_Exchange_Bytes((ADDRESS >> 8) & 0x00FF); // Send MSB SPI_Exchange_Bytes(ADDRESS & 0x00FF); // Send LSB // 8. The data to be written is shifted out on MOSI SPI_Exchange_Bytes(DATA); // 9. Pull the /CS pin high to complete the operation SPI_CS = 1; }
シンプルで明確な線形コードのようです。 ただし、上記の設計に適用しようとすると、次の問題が発生します。
- SPI_Exchange_Bytes()は、内部で周辺機器の準備を待機するブロックサイクルを使用します-ポーリングしかありません、覚えていますか? (コードは提供しませんが、すでに多くのコードがありますが、コードがそこにあると信じています)。 そして以来 周辺機器は突然故障する可能性があり、待機サイクルごとにタイムアウトが設定されます。 この関数では5ミリ秒に相当します! その結果、単純な関数では、SPIバイト交換関数への呼び出しが5つありますが、最悪の場合、それぞれ5ミリ秒かかる場合があります。 1ティックの時間(1ミリ秒)よりもかなり短い時間でタスクが完了するという要件を覚えていますか? では、SPI EEPROMを操作するための単純で明確なコードの代わりに、1回の呼び出しで複数のバイトが転送されないように複雑なステートマシンを記述する必要があります。 それでも、好ましくない状況ではSPI_Exchange_Bytes()を1回呼び出しても5ミリ秒かかる可能性があり、遅延の可能性をさらに減らすには、さらに単純な関数SPI_Exchange_Bytes()を書き直して、5ミリ秒で1つではなくタイムアウトが呼び出されるようにしますが、呼び出しごとに100μsの小片で? 私が言いたいのは、「彼らは本気ですか?」
- 私の実際のプロジェクトでは、1 MBのデータをSPI経由でFLASHに転送する必要があります。 では、ミリ秒に1回タスクの呼び出しが発生し、1回の呼び出しで1バイトを超える情報を送信できない場合、1メガバイトを転送するのにどれくらいの時間を計算するのでしょうか。 もちろん、1バイトではなく数バイトを転送することでこれを回避できますが、コードはさらに複雑になります-合計時間が300μsを超えないようにする必要があります タスクのチャレンジがティックよりも短いという要件はまだ有効です!
- SPIからの割り込みを有効にできる場合、タスクはさらに簡素化されるという事実は言うまでもありません-バッファーへの送信用のデータブロックを書き込み、最初のバイトを送信し、割り込みハンドラーでバッファーから次のバイトを送信して送信を完了します。 しかし、システムの2番目の割り込みは、Time Triggered Designの基盤を破壊するため、忘れてください。
まあ、何らかの理由で著者が、自分の設計に適応した、SPIを使用した上記の例のコードの例を提供しなかったことは、どういうわけか奇妙に思えます。 おびえた、私は推測する。
いわゆるいわゆる もう1つの割り込みが許可され、その状況で別の優先度の高いタスクを実行できるハイブリッドシェダー。 それでも、これは一般的な本質を変えるものではありません。
本の第2部では、複数のマイクロコントローラーのシステムに設計を適合させる方法について説明します。 主なアイデアは、マスターマイクロコントローラーがタイマーをティックの割り込みソースとして使用し、他のすべてが外部割り込みをティック割り込みとして使用することです。マスターは、残りのマイクロコントローラーの外部割り込み入力に接続されたGPIO出力の1つを介して割り込みハンドラーから送信します。 したがって、すべてのマイクロコントローラーの同期が達成されます。 このアイデアは興味深いものですが、残念ながら上記の問題を特に解決するものではありません。
一般に、このアプローチにはおそらく生命に対する権利があります。 99%の時間を何もしない小さなマイクロコントローラーが、特定の時間の即時応答を必要としないいくつかの外部イベントを処理する場所。 一方、ここではスーパーサイクルは正常に機能しますが、いくつかの割り込みを使用できます。
しかし、より多くのイベントがあり、周辺機器を操作するために割り込みを使用する方がはるかに簡単であり、反応時間と安定性が多かれ少なかれ重要であり、コントローラーのパフォーマンスを最大限に使用する必要がある状況では、私は依然としてRTOSの使用を支持しています。 システムに一定の予測不能性と同期ツールの適切な使用の必要性があるようにしましょう-タスクの相互のより厳密な分離とシェダラーの特性からの利点はまだ大きいようです。