なぜこれがすべて必要なのですか?
単純な一連のアクションから離れる必要性に休んでいるやかんが、「これを行う方法」などの質問をするとき、ハブでは、雇用者の国に応じて「google有限状態マシン」と30%が「有限状態マシンを使用する」と言う可能性が高いプロ。 次の質問は「どうやって?」です。 googleに送信。 LEDを点滅させて額から汗を拭き取ったばかりのケトルがあり、学校でドイツ語を教え、このグーグルでブルドーザーとして働いていた一生を費やし、 Wikipediaのような公式と有限状態機械に関する記事があり、前置詞のみが明確です。
私はティーポットでもありますが、30年前にブルドーザーの前にプログラマーとして働き、マイクロコントローラーのプログラミングを習得しながら多くの熊手を踏んだので、初心者向けにシンプルな言語でこの記事を書くことにしました。
したがって、タスクは、たとえば、クリック、長押しなどのボタンクリックを理解するようにArduinに教えることです。 初心者はループ()関数でこれを実行し、それを誇りに思うことができますが、複数のボタンがある場合はどうでしょうか? しかし、別のコードの実行に干渉する必要がない場合はどうでしょうか? 後でボタンのトピックに戻らないように、Arduino用のSmartButtonライブラリを作成しました。 ここで、それがどのように機能するかを書きます。
私たちが生きることを妨げるものとその対処方法は?
delay()関数は私たちの生活を妨げます。MCAの助けを借りてそれを克服し、独自のSmartDelayクラスを作成しました。 私はすでにSmartDelayから生成された新しいクラス(SmartDelayMs)を作成しました。すべてが同じですが、ミリ秒単位です。 この記事では、このライブラリを使用せず、自慢します。
delay()関数を使用するのがいかに悪いか、そしてそれなしで生きる方法についての話で読者を煩わさないために、Arduino IDEでの非ブロッキング遅延の古い記事Replacing delay()を最初に読むことをお勧めします。 コードを作成する際に、ここで少し下の要点を繰り返します。
理論のビット
いずれにしても、あなたにはオブジェクトがあります 。 それらをボタン、ディスプレイ、LEDストリップ、ロボット、タンクの水位計と呼ぶことができます。 コードが1つのスレッドで順番に実行されるのではなく、一部のイベントで実行される場合、オブジェクトの状態を保存します。 好きな名前を付けることができますが、それは事実です。 ボタンの場合、たとえば、「押された」状態と「解放された」状態があります。 たとえば、ロボットの場合:立ち、まっすぐに進み、回転します。 これらの条件の数は有限です。 まあ、私たちの場合、はい。 「クリック」、「クリック」、「保留」の状態を追加します。 区別する必要がある5つの状態が既にあります。 さらに、最後の3つだけに興味があります。 ボタンはこれら5つの状態にあります。 ひどい外部の世界では、一般的にボタンに依存しないイベントが発生します。指で指を離したり、放したり、異なる時間保持したりするなどです。 それらをイベントと呼びましょう。
したがって、有限数の状態とイベントが作用するボタンオブジェクトがあります。 これは有限状態マシン ( KA )です。 KA理論では、可能なイベントのリストは辞書と呼ばれ、イベントは単語と呼ばれます。 申し訳ありませんが、30年前にこれを経験しましたが、用語をよく覚えていません。 用語は必要ありませんよね?
この記事では、イベントに応じて宇宙船を状態から状態に変換するすばらしいコードを作成します。 実際には、有限オートマトン ( MCA )のマシンです 。
デバイス全体を1つの巨大な宇宙船に詰め込む必要はありません。デバイスを個別のオブジェクトに分割することができます。各オブジェクトは、独自のMCAによって処理され、個別のコードファイルに配置されます。 多くのオブジェクトを既製のarduinoライブラリとしてGitHubに配置し、それらの実装を詳しく調べることなく後で使用できます。
コードが移植性と再利用を必要としない場合は、すべてを1つの大きなファイルにスカルプトすることができます。これはまさに初心者が行うことです。
練習する
MCAの設計の基礎は表です。
- すべての可能なイベントを水平に書き込みます。
- 垂直方向にすべての状態。
- アクションセル内。 新しい状態への移行もアクションです。
ボタンの場合、次のようになります。
ステータス\イベント | 押された | リリース済み | 押された20ms | 250msが押されました | 1秒押す | 押された3秒 |
---|---|---|---|---|---|---|
押されていない | ||||||
クリックした | ||||||
押された | ||||||
開催 | ||||||
長すぎる |
なぜそんなに難しいの? 考えられるすべての状態を記述し、考えられるすべてのイベントを考慮する必要があります。
イベント:
- 押された-これは、連絡先が閉じていることがわかったときです。
- リリース済み-これは、連絡先が開いていることが判明したときです。
- 20msを押す-連絡先のチャタリングを考慮に入れ、しばらくの間は開始を無視する必要があります。その後、クリックと見なされます。
- 250msが押されている-クリックには250msで十分です。クリックにかかる時間はそれだけです。
- 1を押した-1を超える番号は既に保留中です。
- 3秒を押す-3秒以上保持するとエラーと見なされます。
都合の良いときに自分の時間を設定できます。 後で別の場所の番号を変更しないために、すぐにそれらを文字に置き換えることをお勧めします:)
#define SmartButton_debounce 20 #define SmartButton_hold 250 #define SmartButton_long 1000 #define SmartButton_idle 3000
ステータス:
- 押されていない-誰もボタンに触れていないことを報告します。
- おしゃべり-連絡先のおしゃべりが終わるのを待っています。
- クリック-クリックがありました。
- Pressed-ボタンが押されています。
- ホールド-ボタンを押したままにします。
- 保持が長すぎる-何かがおかしかったので、この場合はクリックを放棄することを提案します。
したがって、ロシア語をC ++に翻訳します
// byte btPin; // enum event {Press, Release, WaitDebounce, WaitHold, WaitLongHold, WaitIdle}; // enum state {Idle, PreClick, Click, Hold, LongHold, ForcedIdle}; // enum state btState = Idle; // unsigned long pressTimeStamp;
イベントと状態の両方が、byte型またはint型の変数全体としてではなく、enumとして示されるという事実に注意を喚起したいと思います。 このような記録は、メモリマイクロコントローラーで非常に高価なビットを保存しないため、専門家にはあまり愛されていませんが、一方で、非常に明確です。 列挙型を使用する場合、各定数がエンコードされる数値を気にしません。数値ではなく単語を使用します。
enum Name {定数、定数、定数};
そこで、中括弧で囲まれたものからの固定値のセットを持つ、データ型 enum Nameを作成します。 実際、これはもちろん数値整数型ですが、定数の値には自動的に番号が付けられます。
値を手動で指定することもできます。
enum Name {ConstantA = 0、ConstantB = 25};
この新しい型の変数を宣言するには、次のように記述できます。
enum Name Variable;
ボタンを使用した例:
- 列挙型イベントmyEvent;
- 列挙状態currentState;
すでにテーブルを埋めましょう。 アイコンで->新しい状態への移行を示します。
ステータス\イベント | 押された | リリース済み | 押された20ms | 250msが押されました | 1秒押す | 押された3秒 |
---|---|---|---|---|---|---|
押されていない | ->バウンス | ->押されていない | ||||
ガラガラ | ->押されていない | ->クリック | ||||
クリックした | ->押されていない | ->押された | ||||
押された | ->押されていない | ->保持 | ||||
開催 | ->押されていない | ->長すぎる | ||||
長すぎる | ->押されていない |
テーブルを実装するために、関数をvoid doEvent(enum event e)にします。 この関数はイベントを受け取り、表に説明されているとおりにアクションを実行します。
イベントはどこから来ますか?
少しの間居心地の良いボタンを離れ、loop()関数のひどい外の世界に飛び込む時です。
void loop() { // unsigned long mls = millis(); // // if (digitalRead(btPin)) doEvent(Press) else doEvent(Release) // if (mls - pressTimeStamp > SmartButton_debounce) doEvent(WaitDebounce); // if (mls - pressTimeStamp > SmartButton_hold) doEvent(WaitHold); // if (mls - pressTimeStamp > SmartButton_long) doEvent(WaitLongHold); // if (mls - pressTimeStamp > SmartButton_idle) doEvent(WaitIdle); }
そのため、イベントジェネレータを手元に用意して、ICAの実装を開始できます。
実際、コントローラーの足元の信号レベルを時間内に上げるだけでなく、イベントを生成できます。 イベントは、あるオブジェクトから別のオブジェクトに送信できます。 たとえば、1つのボタンを押して別のボタンを自動的に離したい場合。 これを行うには、Releaseパラメーターを指定してdoEvent()を呼び出します。 MCAはボタンだけではありません。 温度のしきい値を超えると、MCAのイベントとなり、たとえば、温室内でファンがオンになります。 ファン故障センサーは、温室の状態を「緊急」などに変更します。
コードでテーブルを実装する3つのアプローチ
プランA:if {} elsif {} else {}
これは初心者にとって最初に起こることです。 実際、これは、テーブルに入力されたセルがほとんどなく、アクションが状態の変化にのみ縮小される場合に適したオプションです。
void doAction(enum event e) { if (e == Press && btState == Idle) { // btState=PreClick; // pressTimeStamp=millis(); // } if (e == Release) { // btState=Idle; // } if (e == WaitDebounce && btState == PreClick) { // btState=Click; // , } // }
長所:
- arduinoを初めて手にした初心者に書いてください。
- 頭に浮かぶ最初のもの。
短所:
- コードを読むのが難しい。
- テーブルで何かが変更された場合、コードを変更することは困難です。
- テーブル内のほとんどのセルがいっぱいの場合は地獄。
プランB:関数ポインターテーブル
もう1つの極端な例は、遷移または関数の表です。 このアプローチについては、記事「 Arduinoのボタンプレスを処理する」で説明しました。 クロスOOPとICA。 。 簡単に言うと、 異なるテーブルセルごとに個別の関数を作成し、それらへのポインタのテーブルを作成します。
// , typedef void (*MKA)(enum event e); // MKA action[6][6]={ {&toDebounce,$toIdle,NULL,NULL,NULL,NULL}, {NULL,&toIdle,&toClick,NULL,NULL,NULL}, {NULL,&toIdle,NULL,&toHold,NULL,NULL}, {NULL,&toIdle,NULL,NULL,&toLongHold,NULL}, {NULL,&toIdle,NULL,NULL,NULL,&toVeryLongHold}, {NULL,&toIdle,NULL,NULL,NULL,NULL} }; // doEvent void doEvent(enum event e) { if (action[btState][e] == NULL) return; (*(action[btState][e]))(e); } // // " " void toIdle(enum event e) { btState=Idle; } // void toDebounce(enum event e) { btState=PreClick; pressTimeStamp=millis(); } //
typedefを使用すると、データ型を定義できます。多くの場合、データ型を使用すると便利です。 たとえば、enumイベントは次のように定義できます。
typedef enum event BottonEvent;
その後、プログラムに書き込むことができます。
BottonEvent myEvent;
enumイベント型の単一の引数を取り、何も返さない関数へのポインターを定義しました。
typedef void (*MKA)(enum event e);
そこで、新しいMKAデータ型を作成しました。
&アイコンは、「アドレス」または「ポインター」としてロシア語に翻訳されます。 したがって、MKAは関数値ではなく、それへのポインターです。
MKA action[6][6]; // action
すぐに、アクションを実行する関数へのポインターで埋めました。 アクションがない場合、「ポインタをどこにも設定しない」NULLを設定します。
doEvent関数は、座標 "現在の状態" x "発生したイベント"のテーブルからポインターをチェックし、このための関数へのポインターがある場合、 "event"パラメーターで呼び出します。
if (action[btState][e] == NULL) return; // - (*(action[btState][e]))(e); //
Cでのポインターとアドレス演算の詳細については、「ダミーのC言語」などの書籍でGoogleを参照できます。
長所:
- 非常に読みやすいコード。 笑わないで。 一度ポインタを使って配列を味わうだけで十分で、シンプルで美しいコードが得られます。 あなたはタブレットとしてタブレットを持っています、セルは別々に描かれています-美しさ。
- 書きやすい。
- ロジックの変更は非常に簡単です。
短所:
- メモリを食べる。 各ポインターは2〜4バイトです。 6x6 = 36セルは72〜144バイトのメモリを占有します。これは各ボタン用ですが、4つある場合はどうでしょうか。 一般的なArduinoチップのメモリは2048バイトしかないことを思い出させてください。
マイナス記号は非常に太字であることが判明したため、Arduinoのキーストロークの処理を書いた後です。 クロスOOPとICA。 そして、ビジネスにおけるコードの非常に最初のアプリケーションは、それをすべてオフにしました。 その結果、「スイッチ{スイッチ{}}」の中間地点にたどり着きました。
プランB:ゴールデンミーンまたはスイッチ{スイッチ{}}
最も一般的な中程度のオプションは、ネストされたswitchステートメントを使用することです。 switchステートメントのケースとしての各イベントに対して、現在の状態でswitchを記述します。 長いことが判明しましたが、多かれ少なかれ明確です。
void doEvent(enum event e) { switch (e) { case Press: switch (btState) { case Idle: btState=PreClick; pressTimeStamp=millis(); break; } break; case Release: btState=Idle; break; // ... ... } }
上記のプランBで説明したように、スマートコンパイラは引き続き変換テーブルを作成しますが、このテーブルはコード内でフラッシュされ、変数のメモリを占有しません。
長所:
- クイックコード。
- メモリを節約します。
- 論理は明確です。 読むことができます。
- ロジックを簡単に変更できます。
短所:
- 多数の状態とイベントを含むいくつかの画面にあるフットクロス。これを個別のファイルに表示することは困難です。
実際、Plan-BとPlan-Aを使用できます。つまり、イベントの場合は切り替えますが、状態の場合は切り替えが可能です。 私の好みでは、後で何かを変更する必要がある場合、スイッチはより明確で便利です。
ボタンのクリックに応じてコードを埋め込む場所
タブレットでは、ボタンイベントハンドラーに焦点を当てて、他のMCAの存在を考慮しませんでした。 実際には、ボタンが機能していることを喜ぶだけでなく、他のアクションも実行する必要があります。
適切なコードを挿入するための2つの重要な場所があります。
case Release: switch(btState) { case Click: // . . break;
case WaitDebounce: switch (st) { case PreClick: btState=Click; // . "" . break;
これらのイベントが処理されるoffClick()やonClick()などの独自の関数を作成して、美しさを損なわないようにするのが最善です。別のMCAに渡す可能性があります:D
ICAボタンのロジック全体をC ++クラスでラップしました。 これは便利です。メインコードの記述を妨げず、すべてのボタンが独立して機能し、変数の名前を作成する必要がないためです。 これだけがまったく異なる話であり、MCAをクラスにフォーマットし、それらからArduinoのライブラリを作成する方法を書くことができます。 コメント希望に書いてください。
結果は何ですか?
結果として、理論的には、この記事を読んだ後、お気に入りのArduinoスケッチにある読めないバグのあるたわごとを、MCAを使用した美しい構造化されたものに置き換えたいと思うはずです。
ループ()関数では、イベントを生成するだけですが、単一の遅延はありません。 したがって、上記のコードの後、他の何かを書くことができます。他のMCAのイベントを生成し、遅延を含まない独自のコードをさらに実行します()。
この記事が、MCAとは何か、またMCAをスケッチに実装する方法を理解するのに役立つことを本当に願っています。