ストーリーの過程で、どの特定のケースで最初のケースを使用する価値があり、どのケースで2番目のケースがないとできないかも明確になると思います。
組み込みシステムの開発の世界を調べたいと思うすべての人にとって興味深いものになることを願っています。 既に埋め込みで犬を食べている人にとっては、おそらく新しいものは何もないでしょう。
かなりの理論(最初の一歩を踏み出した人向け)。
マイクロコントローラーは、実際にはプロセッサー、ビットメモリ、およびさまざまな周辺機器です。たとえば、アナログ-デジタルコンバーター、タイマー、イーサネット、USB、SPI-これらはすべて、コントローラーと解決するタスクに大きく依存します。
たとえば、ある種のセンサーをADC入力に接続できます。たとえば、温度センサーは、電力が供給されると、このADCによって測定された電圧に温度を変換します。
また、GPIO(汎用入出力)と呼ばれるコントローラーの出力に、たとえばLED(またはモーターのような強力なものをアンプを介して)を接続できます。
SPI、RS232、USBなどを介して コントローラーは、より複雑な方法で外界と通信できます-所定のプロトコルを使用してメッセージを送受信します。
90%のケースでは、ソフトウェアはCで書かれており、C ++またはアセンブラーを使用できる場合もあります。 より多くの場合、より高いレベルで何かを書く機会がありますが、これが周辺機器との直接的な作業に当てはまらず、可能な限り最高の速度が要求されない場合。
対処する必要があるものをよりよく想像するために、ここで作業しなければならない環境の例をいくつか示します:FLASHコントローラー(ハードディスクに類似)のサイズは16-256キロバイト、RAMのサイズは64-256キロバイトです! そして、このような環境では、アプリケーションだけでなく、マルチタスクを完全にサポートするリアルタイムオペレーティングシステムを起動することもできます。
以下の例は擬似コードであり、Cに非常によく似ています。実装の詳細がない場合、理解に不可欠ではありません。
したがって、「スーパーサイクル」アプローチ。
このアプローチのプログラムはシンプルに見えます。
int main() { while(1) { doSomething(); doSomethingElse(); doSomethingMore(); } }
コントローラーが必要なすべてを順次実行する無限ループ。
もちろん、組み込みシステムで最も興味深いのは、周辺機器(ADC、SPI、GPIOなど)との連携です。 コントローラは、2つの方法で外部周辺機器を操作できます:問い合わせまたは割り込みの使用。 前者の場合、たとえば、RS232コンソールからキャラクターを読みたい場合は、取得するまでキャラクターが存在するかどうかを定期的にチェックします。 2番目のケースでは、新しいシンボルが表示されたときに割り込みを生成するようにRS232コントローラーを構成します。
最初のアプローチのデモンストレーション。 たとえば、温度を監視し、設定された制限を超えた場合、LEDを点灯させます。 次のようになります。
int main() { init_adc(); init_gpio_as_out(); while (1) { int temperature = readTemperature(); if (temperature > TEMPERATURE_LIMIT) { turnLedOn(); } else { turnLedOff(); } }
これまでのところ、すべてがシンプルで明確でなければなりません。 (温度を読み取り、LEDを操作する機能は提供しません-これはこの記事の目的ではありません)。
しかし、特定の頻度で何かをする必要がある場合はどうでしょうか? 上記の例では、温度は可能な限り頻繁にチェックされます。 また、たとえば、1秒に1回LEDを点滅させる必要がある場合はどうでしょうか。 または、10ミリ秒の間隔でセンサーに厳密に問い合わせますか?
次に、タイマーが役立ちます(ほとんどすべてのマイクロコントローラーが持っています)。 特定の頻度で割り込みを生成するように動作します。 点滅するLEDは次のようになります。
volatile int interrupt_happened = 0; interrupt void timer_int_handler() { interrupt_happened = 1; clear_interrupt_condition(); } int main() { init_timer(1_SECOND_INTERVAL, timer_int_handler); while (1) { if (interrupt_happened) { ledToggle(); interrupt_happened = 0; } } }
割り込み処理の特性は、割り込みハンドラー(割り込みが発生したときにすぐに呼び出されるコード)をできるだけ短くすることです。 したがって、最も一般的な解決策は、ハンドラーにグローバルフラグ変数を設定し(はい、どこにもグローバル変数はありません)、メインループでそれを確認し、それが変更されたら、イベントを処理するために必要な基本的な作業を行います。
この最もグローバルな変数は、揮発性識別子で宣言する必要があります。そうしないと、オプティマイザーは、自分の観点からは使用されていないコードを単に破棄できます。
そして、2つのLEDを点滅させる必要がある場合、1つは1秒に1回、2つ目は3回点滅しますか? もちろん、2つのタイマーを使用することもできますが、このアプローチでは、長い間タイマーでは十分ではありません。 代わりに、タイマーをより高い周波数で動作させ、プログラムでは分周器を使用します。
volatile uint millisecond_counter = 0; interrupt void timer_int_handler() { ++millisecond_counter; clear_interrupt_condition(); } int main() { init_timer(1_MILLISECOND_INTERVAL, timer_int_handler); while (1) { uint timestart1 = millisecond_counter; uint timestart2 = millisecond_counter; if (millisecond_counter – timestart1 > 1000) // 1 second interval { led1Toggle(); timestart1 = millisecond_counter; } if (millisecond_counter – timestart2 > 333) // 1/3 second interval { led2Toggle(); timestart2 = millisecond_counter; } } }
符号なしタイプが使用されるため、ミリ秒カウンタのオーバーフローを監視する必要がないことに注意してください。
RS232インターフェイス(世界で最も一般的なソリューションが組み込まれています!)の上にデバッグコンソールが実装されていることを想像してください。 そして、そこにデバッグメッセージを出力します(COMポート経由でコントローラーをコンピューターに接続すると表示されます)。 同時に、厳密に指定された(同時に高い)周波数でコントローラーに接続されたセンサーに問い合わせる必要があります。
そして、ここで疑問が生じます-コンソールに文字列を出力するような平凡なことをどのように実装するのですか? のような明らかな解決策
void sendString(char * str) { foreach (ch in str) { put_ch(ch); } }
この場合は受け入れられません。 文字列を出力しますが、厳密に指定された周波数でセンサーに問い合わせるという要件に不可逆的に違反します。 しかし、すべてのアクションが順番に実行される1つの大きなサイクルでこれをすべて実行します。 また、コンソールは低速のデバイスであり、ライン出力は、連続したセンサーポーリングに必要な間隔よりもはるかに長くかかる場合があります。 以下の例は、その方法です!
int main { while (1) { … if (something) { send_string("something_happened"); } … if (10_millisecond_timeout()) { value = readADC(); } } }
別の例は、ソフトウェア過負荷保護を実装する場合です。 電流計を追加し、それをコントローラーのADCに接続し、安全リレーを入出力ピンの1つに制御します。 そしてもちろん、過負荷イベントの後はできるだけ早く保護が機能するようにします(そうでなければ、すべてが燃え尽きます)。 また、すべてのアクションが厳密に順番に実行される同じ一般的なサイクルがあります。 また、イベントに対する保証された反応時間は、サイクルの1回の繰り返しの実行時間よりも短くなることはありません。 そして、このサイクルに完了するのに長い時間が必要な操作がある場合、それがすべてです。他のすべてにシステムの反応時間を設定するのは彼らです。
そして、このサイクルのどこかで突然エラーが発生すると、システム全体が「落ち」ます。 過負荷への反応を含める(これは本当に許可したくないでしょうか?)。
理論的には、最初の問題で別のことができます。 たとえば、行を印刷する最も単純だが最長の機能を次のように置き換えます。
int position = 0; int send_string(char * str) { if (position < strlen(str) { put_ch(str[position]; ++position; return 1; } else { return 0; } }
そして、次のようなものでこの関数を呼び出すだけです:
int main { while (1) { … if (something) { do_print = 1; position = 0; } if (do_print) { do_print = send_string("something_happened"); } … if (10_millisecond_timeout()) { value = readADC(); } } }
その結果、行全体を印刷するのに必要な時間から1文字を印刷するのにかかる時間まで、1サイクルのサイクルにかかる時間を短縮しました。 しかし、このためには、プリミティブの代わりに、コンソールに一目で行出力機能をクリアする必要がありました。コードに2つのステートマシンを追加します-1つは印刷用(位置を記憶するため)、もう1つは印刷用です。次の数サイクルにわたって。 長期間有効なグローバル変数、状態を保存するダーティ関数など、コードを伴奏のないスパゲッティにすばやく簡単に変換できるすばらしいもの。
システムが同時に多数のセンサーからポーリングし、即時の反応を必要とするいくつかの重要なイベントに応答し、ユーザーまたはコンピューターから到着するコマンドを処理し、デバッグメッセージを表示し、多数のインジケーターまたはマニピュレーターを制御する必要があることを想像してください。 また、アクションごとに、反応時間と調査または制御の頻度に対する独自の制限が設定されます。 そして、これらすべてを1つの連続した一般的なサイクルに詰め込もうとします。
もちろん、これはすべて本物です。 しかし、執筆から少なくとも1年後にこのすべてに同行しなければならない人々にとって、私はen望しません。
「一般サイクル」設計の別の問題は、システム負荷の測定の複雑さです。 次のコードがあるとします:
interrupt void external_interrupt_handler() { interrupt_happened = 1; clear_interrupt_condition(); } int main() { while (1) { if (interrupt_happened) { doSomething(); interrupt_happened = 0; } } }
システムは、外部からの割り込みに何らかの形で応答します。 問題は、システムが1秒間に何回そのような中断を処理できるかということです。 1秒間に100個のイベントを処理するとき、プロセッサはどれくらい混雑しますか?
イベントの処理に費やされた時間と、変数「中断はありますか?」をポーリングするのにどれだけ時間がかかったかを測定することは非常に困難です。 結局のところ、すべてが1サイクルで行われます!
そして、ここで2番目のアプローチが助けになります。
リアルタイムオペレーティングシステムの適用
そのアプリケーションを説明する最も簡単な方法は、同じ例を使用することです。特定の周波数でセンサーを同時にポーリングし、長いデバッグ行をコンソールに出力します。
void SensorPollingTask() { while (1) { value = SensorRead(); if (value > LIMIT) { doSomething(); } taskDelay(10_MILLISECOND_DELAY); } } void DebugTask() { dbg_task_queue = os_queue_create(); while (1) { char * str = os_queue_read(dbg_task_queue); foreach (ch in str) { put_ch(ch); } } } void OtherTask() { other_task_init(); … while(1) { … // we want to do a dbg_printout here os_queue_put("Long Debug Output String"); … } } int main() { os_task_create(SensorPollingTask, HIGH_PRIORITY); os_task_create(DebugTask, LOW_PRIORITY); os_task_create(OtherTask, OTHER_PRIORITY); os_start_sheduler(); }
ご覧のとおり、main関数には1つのメイン無限ループがなくなりました。 代わりに、各タスクに個別の無限ループがあります。 (はい、os_start_sheduler()関数。制御を返すことはありません!)。 そして最も重要なこととして、これらのタスクには優先順位があります。 オペレーティングシステム自体が必要なものを提供します。つまり、優先度の高いタスクが最初に実行され、優先度の低いタスクが実行されるのは、時間がある場合のみです。
また、たとえば、スーパーサイクルを使用した設計の中断に対する反応時間が最悪の場合、サイクル全体の実行時間に等しい場合(もちろん中断はすぐに発生しますが、必ずしも必要なアクションがハンドラーで直接実行できるとは限りません)、反応時間リアルタイムOSの場合、タスク間の切り替え時間に等しくなります(これはすぐに起こると仮定するのに十分小さいです!)。 つまり 割り込みは1つのタスクで発生し、その完了後すぐに別のタスクに切り替え、割り込みから「トリガーされた」イベントを待機します。
interrupt void overcurrent_handler() { os_semaphore_give(overcurrent_semaphore); clear_interrupt_condition(); } void OvercurrentTask() { os_sem_create(overcurrent_semaphore); while (1) { os_semaphore_take(overcurrent_semaphore); DoOvercurrentActions(); } }
プロセッサ負荷の測定に関しては、OSのアプリケーションを使用したこのタスクは簡単になります。 デフォルトでは、各OSには最も食いしん坊な(ただし優先度が最も低い)アイドルタスクがあり、空の無限ループを実行し、他のすべてのタスクがアクティブでない場合にのみ制御を受け取ります。 また、通常はすでに実装されている、アイドルで費やされた時間の計算。 コンソールに表示するためだけに残ります。
また、突然エラーに「気付かない」場合、エラーが存在するタスクのみが「フォール」します(優先度の低いタスクもすべて実行される可能性があります)が、優先度の高いタスクは引き続き実行されます。例えば、過負荷に対する保護など、デバイスの少なくとも最低限の重要な機能を提供します。
要約すると、システムが非常にシンプルで、反応の時間までに要求が厳しくない場合、「スーパーサイクル」のモデルにするのは簡単です。 システムが大きくなり、多くの異なるアクションとリアクションを組み合わせた場合、これもまた時間的に重要であるため、リアルタイムOSを使用することに代わるものはありません。
さらに、OSを使用するプラスは、よりシンプルで理解しやすいコードです(スーパーサイクルでデザインを使用するときに必要なグローバル変数、ステートマシン、およびその他のゴミを回避し、タスクに従ってコードをグループ化できるため)。
欠点は、OSの使用です。より多くのスペース、メモリ、経験、知識が必要です(ただし、複雑なことは何もありませんが、マルチタスクは、順次実行されるコードよりも先験的に複雑で予測不可能です)。 マルチタスク環境での作業の原則、スレッドセーフコードの原則、データの同期などを十分に理解してください。
「遊ぶ」ために、無料のオープンソースプロジェクトであるFreeRTOSを利用できます。FreeRTOSは非常に安定しており、簡単に学習できます。 この特定のOSを使用する一般的ではない商用プロジェクト。