組み込み用の代替ソフトウェア設計アプローチ

このトピックは、 「組み込み向けのソフトウェア設計への2つのアプローチ」という記事を読んだ後に書くことにしました。 「システムが大きくなり、時間的にも重要な多くの異なるアクションと反応が組み合わされる場合、リアルタイムOSを使用する以外の方法はありません。」 「どうですか?」と思いました。 もちろん、大きなプロセッサが使用されている大規模で負荷の高いリアルタイムシステムについて話している場合、OSはそれなしではできませんが、より控えめなマイクロコントローラーソリューションには代替手段があります。 結局、従来のスイッチケースを使用してタスクを実行でき、同時に必要な反応時間を提供できます。





個人的にRTOSを使用しない理由





同志oleklはRTOSについて話しましたが、私はこれに集中しません。 私が個人的に強調したいくつかのポイントに注目します-RTOSを使用しない理由:





スイッチケースアプローチ





用語では強力ではないので、そのような名前にします。

例を検討する方が便利です。 擬似コードを使用します。



デバイスには2つの温度センサーがあります。 最初のセンサーのポーリング時間は重要ではありません。「ポーリング、はい、大丈夫」、0.2ミリ秒とします。 設定温度のしきい値を超えた後、LEDを点灯します。 反対に、2番目のセンサーの測定値は私たちにとって非常に重要です。 できるだけ頻繁に問い合わせる必要があり、設定されたしきい値を超えると、ピンごとに「1」を与えて、個別の信号で冷却ファンをオンにします。 温度が別のしきい値まで低下したら、ファンをオフにします。 100ミリ秒ごとに、2番目のセンサーからの値をROMに書き込む必要があります。

実装には、ハードウェアタイマー割り込みが必要です。 確かに、この方法でのみ、割り当てられた時間内にタスクの履行を保証できます。 この場合、他の割り込みを使用する可能性は大幅に減少します。

ほとんどの周辺機器との作業は中断することなく行うことができ、非常に重要な通信割り込み(例:UART / SCI)は通常、タイマーよりも高い優先度を持ち、通常は受信/送信バイトを修正します。 時間がかかりません。

タイマーがタスクの時間をカウントするだけで、タスク自体が割り込みを禁止せずにバックグラウンド(またはスーパーサイクル)で実行されるアプローチでは、実行の必要な反応が保証されません。



まず、温度センサードライバーを作成します。 その主な機能は、SPIによって温度値を読み取ることです。



構造:

typedef struct { unsigned char SpiCh; //   SPI (A, B, C) unsigned int SpiBrr; //  SPI- unsigned int Value; //    void (*ChipSelect) (unsigned char level_); // Callback    … // -  }TSensor;
      
      





温度プローブのポーリング機能:



 void SensorDriver(TSensor *p) { p->ChipSelect(0); //   p->Value = SpiRead(p->SpiCh, p->SpiBrr); //    SPI p->ChipSelect(1); //    }
      
      





ドライバーの準備ができました。 それを使用するには、初期化が必要です。 構造は#defineで完全に初期化できます。または、各フィールドを個別に初期化できます。 2つの温度センサーがあります。 2つの構造を作成します。

 TSensor Sensor1; TSensor Sensor2; void Init(void) { Sensor1.ChipSelect = &ChipSelectFunc1; //      Sensor1.SpiCh = 0; //  SPI Sensor1.SpiBrr = 1000; //  SPI Sensor2.ChipSelect = &ChipSelectFunc2; Sensor2.SpiCh = 0; Sensor2.SpiBrr = 1000; }
      
      







ドライバーの主な機能は、温度を読み取ることです。 このデータをどう処理するかは、ドライバーの外部で決定されます。



LEDを点灯します。



 void SensorLed(void) { if (Sensor1.Value >= SENSOR_LED_LIMIT) LedPin = 1; else If (Sensor1.Value < SENSOR_LED_LIMIT) LedPin = 0; }
      
      





個別の足でファンをオン/オフにします。



 void SensorCooler(void) { if (Sensor2.Value >= SENSOR_LED_LIMIT) CoolerPin = 1; else if (Sensor1.Value < SENSOR_LED_LIMIT) CoolerPin = 0; }
      
      





奇妙ですが、機能は驚くほど似ていることが判明しました:)

次のようにROMに書き込みます。

ROMドライバーの機能は、データが書き込まれるのを待っている間、1 kHzの周波数で周期的に実行され、「それらをどうするか」という命令とメモリ内のどのアドレスで実行されます。 つまり メモリの準備状況を確認し、プログラムのどこからでも命令とともにデータを送信するだけで十分です。



 void SensorValueRecord() { unsigned int Data = Sensor2.Value; //     unsigned int Address = 0; //    if (EepromReady()) //    { //  ,   ,     EepromFunction(Address, Data, WRITE); } }
      
      





データを送信し、メモリドライバーが動作を開始すると(SensorValueRecord関数よりも100倍高速に実行されます)、何をすべきかを既に認識しています。

私たちの機能は準備ができています。 次に、それらを適切に整理する必要があります。

これを行うには、10 kHz(100μs)の周波数でタイマーの割り込みを開始します。 これは、呼び出しタスクの最大保証頻度になります。 それで十分です。 タスクスケジューラの機能を作成します。ここで、実行するタスクを決定します。



 #define MAIN_HZ 10000 #define TASK0_FREQ 1000 #define TASK1_FREQ 50 #define TASK2_FREQ 10 //    void AlternativeTaskManager(void) { SensorDriver(&Sensor2); //      SensorCooler(); //     Task0_Execute(); //     } //  1 void Task0_Execute(void) { switch (TaskIndex0) { case 0: EepromDriver(&Eeprom); break; case 1: Task1_Execute(); break; case 2: Task2_Execute(); break; } //   if (++TaskIndex0 >= MAIN_HZ / TASK0_FREQ) TaskIndex0 = 0; } //    50  void Task1_Execute(void) { switch (TaskIndex1) { case 0: SensorDriver(&Sensor1); break; case 1: SensorLed(); break; } if (++TaskIndex1 >= TASK0_FREQ / TASK1_FREQ) TaskIndex1 = 0; } //    10  void Task2_Execute(void) { switch (TaskIndex2) { case 0: SensorValueRecord(); break; case 1: break; } if (++TaskIndex2 >= TASK0_FREQ / TASK2_FREQ) TaskIndex2 = 0; }
      
      







これで、タイマー割り込みでスケジューラーを実行することができ、完了です。



 interrupt void Timer1_Handler(void) { AlternativeTaskManager(); }
      
      





このシステムは、ギアを備えた一種のメカニズムのように見えます。最も重要なギアはモーターシャフトに直接あり、残りのギアをねじります。

タスクは「リング上で」実行されます。 実行の頻度は、呼び出しの場所によって異なります。 Task0_Execute関数は、タイマー割り込み(メインギア)で直接呼び出すため、10 kHzの周波数で実行されます。 その中で周波数分割が行われ、TaskIndex0を備えたスイッチケースを使用して、どのタスクが来たかが判断されます。 チャレンジ周波数は10kHz未満でなければなりません。

Task0_Executeサイクルのタスク頻度を1 kHzに設定します。つまり、1 kHzの頻度で10個のタスクを実行できます。



 #define MAIN_HZ 10000 #define TASK0_FREQ 1000 if (++TaskIndex0 >= MAIN_HZ / TASK0_FREQ)
      
      







システムのスイッチケース構造





Task1_ExecuteおよびTask2_Executeについても同様です。 それらを1 kHzの周波数で呼び出します。 最初のサイクルでは、タスクは50 Hzの周波数で実行され、2番目のサイクルでは10 Hzで実行されます。 合計でそれぞれ20および100のタスクがあることがわかります。

ディスパッチャのタスクを完了すると、プログラムはバックグラウンドに戻ります(バックグラウンドスーパーサイクル)。

時間的に重要ではない反応は、そこに置くことはかなり可能です。



 void main(void) { Init(); while (1) { DoSomething(); } }
      
      





DACがデバイスに追加され、LEDの点火とともに、4-20の信号を生成する必要がありますか? 質問なし。 DACドライバーを作成して実行します。 SensorLed関数に2行追加します。これにより、ドライバーに出力する値がわかり、ディスパッチャーがドライバー関数を呼び出します。



 void SensorLed(void) { if (Sensor1.Value >= SENSOR_LED_LIMIT) { LedPin = 1; Dac.Value = 20; //     } else If (Sensor1.Value < SENSOR_LED_LIMIT) { LedPin = 0; Dac.Value = 4; //     } }
      
      





 //    50  void Task1_Execute(void) { switch (TaskIndex1) { case 0: SensorDriver(&Sensor1); break; case 1: SensorLed(); break; case 2: DacDriver(&Dac) break; //    } if (++TaskIndex1 >= TASK0_FREQ / TASK1_FREQ) TaskIndex1 = 0; }
      
      







2行のインジケーターを追加しましたか? 問題ありません。 ドライバーを1 kHzの周波数で起動します。 文字をすばやく転送する必要があり、他の低速な機能では、表示する必要がある文字と行をドライバーに伝えます。



定格荷重





負荷を評価するには、最初のタイマーと同じ周波数で動作する2番目のハードウェアタイマーを有効にする必要があります。 タイマーの期間が近くないことを確認するのは良いことです。

タスクマネージャーを開始する前に、タイマーカウンターがリセットされ、作業後にその値が読み取られました。 定格負荷はタイマー周期に基づいています。 たとえば、最初のタイマーの周期は100です。つまり、カウンターは100にカウントされ、割り込みが発生します。 2番目のタイマーのカウンター(CpuTime)のカウントが100未満の場合は、問題ありません。 エンドツーエンドまたはそれ以上の場合、それは悪いです。タスクの反応時間は変動します。

 unsigned int CpuTime = 0; unsigned int CpuTimeMax = 0; interrupt void Timer1_Handler(void) { Timer2.TimerValue = 0; //   AlternativeTaskManager(); //  switch-case   CpuTime = Timer2.Value; //    =  if (CpuTime > CpuTimeMax ) //    CpuTime = CpuTimeMax; }
      
      







結果として何





RTOSと比較して個人的にどのような利点がありますか:

-ディスパッチャーが悲惨なときのリソースの消費。

-タスクの編成。単純ではありませんが、決定することになります。実行する機能はどこですか。 セマフォ、ミューテックスなどはありません。 RTOSの複数ページのマニュアルを読む必要はありません。 利点は言うまでもありませんが、私はそれに慣れています。

-コードは、あるコントローラーから別のコントローラーに簡単に転送できます。 主なことは、使用されるタイプを忘れないことです。



欠点:

-合併症ソフトウェア。 RTOSの場合、十分なリソースがあれば関数を作成してすぐに実行できるのであれば、スイッチケースの場合は、より厳密な最適化アプローチをとる必要があります。 これまたはそのアクションがシステム全体のパフォーマンスにどのように影響するかについて考える必要があります。 追加のアクションセットは、「ギアの動き」の違反につながる可能性があります。 システムが大きいほど、ソフトウェアは複雑になります。 オペレーティングシステムの機能を一度に実行できる場合は、ここでさらに詳細な手順(ステートマシン)に分解する必要があります。 たとえば、インジケータードライバーはすべての文字をすぐに転送するのではなく、1行ずつ転送します。

1)ストロボを設定し、一番上の行を左に進めます。

2)一番下の行を転送し、文字が表示されるようにストロボを削除して終了しました。

開発が少ない場合、このアプローチは開発の速度に影響します。

私はこのアプローチを数年間使用しています。 多くの開発、ライブラリがあります。 しかし、初心者にとっては難しいでしょう。



この記事では、RTOSを使用せずに組み込みシステム用のソフトウェアを設計するための代替アプローチを明らかにしようとしました。 誰かが何か役に立つものを手に入れたいと思います。



更新しました。 RTOSを使用するという考えを放棄しません。 RTOSが悪く、誰もがすぐにそれを落とす必要があると言っているのではありません。 私はこれについての私の態度を個人的に説明しましたが、誰にもそれを課しません。 この記事は、Embeddedのソフトウェア設計に関するトピックが原因で発生しました。著者は、代替手段がないことを示しました。 それどころか、私は代替案がまだ存在し、商業プロジェクトに存在することを示しました。



All Articles