瞬時押し、長押し、持続押し設定プログラミングしてみる
一つのボタン操作で複数の機能を持たせるために、長押しや持続押し設定などがあります。
ゲームでは、持続押しするとミサイルが一定間隔で連続発射したりしますね。
それをプログラミングしてみようと思います。
長押し設定する場合に問題となるのは、そのボタンで瞬時リリースも併用したい時です。
前項のプログラムでは、ボタンが押された後、チャタリング対策で 30 ms 後にON確定としました。
でも、これでは長押し設定と併用ができません。
ネットの情報に頼らずに、自分の頭で考えを絞り出した結果はこうなりました。
2秒長押しだけならば簡単で、時間計測を2秒に設定するだけですが、その前に瞬時押しリリースがあったとすると、HIGH レベルになって初めて瞬時ON確定とすることです。
これ、パソコンのマウスのクリックもそうですが、長押し対応のボタンを瞬時リリースしてみると分かるのですが、リリースした時点で確定していますね。
ということで、長押しは GPIO が LOW レベルになっている時間を計測して確定し、瞬時押しは GPIO が HIGH レベルになった時に確定というプログラムを組みます。
そこで、注意しなければいけないのは、長押ししている間は他のプログラムが動作していなければなりません。
M5Stack の3つのボタンでゲームをするとき、Aボタンを長押ししている間に他のボタンが操作できなくなるのは困りますし、ゲームが停止してしまうのもアホらしいです。
それと、当然かと思いますが、ボタンが沢山ある場合、ボタン毎に関数を作るのは効率が悪いので、一つの関数を使い回したいですよね。
そこで、M5Stack の3つのボタンを同時の操作できるように、自力で頭を振り絞ってプログラムを組んでみました。
以下は、瞬時押し、2秒長押し、持続押し対応プログラムです。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
/* * Button Operation Test Program of M5Stack or ESP32. * The MIT License (MIT) * Copyright (c) 2018 Mgo-tec. All rights reserved. * https://opensource.org/licenses/mit-license.php */ const uint8_t buttonA_GPIO = 39; const uint8_t buttonB_GPIO = 38; const uint8_t buttonC_GPIO = 37; uint32_t StartTimeA = 0; uint32_t StartTimeB = 0; uint32_t StartTimeC = 0; enum BTN_STATUS { Release, ON_start, MomentPress_Determined, LongPress_Determined, Continuous_Press } btnA_status = Release, btnB_status = Release, btnC_status = Release; //************************************************ void setup() { Serial.begin(115200); pinMode(buttonA_GPIO, INPUT); //GPIO #39 は内部プルアップ無し pinMode(buttonB_GPIO, INPUT); //GPIO #38 は内部プルアップ無し pinMode(buttonC_GPIO, INPUT); //GPIO #37 は内部プルアップ無し } //************************************************ void loop() { uint8_t btn_stateA = My_Button(buttonA_GPIO, &btnA_status, &StartTimeA, false, 10, 2000); if( btn_stateA == MomentPress_Determined ) Serial.println("Button A Momentary Pressed Determined!"); if( btn_stateA == LongPress_Determined ) Serial.println("--------------- Button A long Pressed Determined!"); uint8_t btn_stateB = My_Button(buttonB_GPIO, &btnB_status, &StartTimeB, false, 10, 2000); if( btn_stateB == MomentPress_Determined ) Serial.println("Button B Momentary Pressed Determined!"); if( btn_stateB == LongPress_Determined ) Serial.println("--------------- Button B long Pressed Determined!"); uint8_t btn_stateC = My_Button(buttonC_GPIO, &btnC_status, &StartTimeC, true, 10, 300); if( btn_stateC == MomentPress_Determined ) Serial.println("Button C Momentary Pressed Determined!"); if( btn_stateC == LongPress_Determined ) Serial.println("--------------- Button C long Pressed Determined!"); if( btn_stateC == Continuous_Press ) Serial.println("C Keep Pess"); } //************************************************ uint8_t My_Button(const uint8_t gpio, BTN_STATUS *button_status, uint32_t *start_time, boolean continuous_set, uint32_t chatter_time, uint32_t long_press_time){ uint8_t ret_state = Release; switch( digitalRead(gpio) ){ case LOW: // LOW = 0 switch ( *button_status ){ case Release: *button_status = ON_start; *start_time = millis(); //reset start_time break; case ON_start: if( continuous_set ){ if( Time_Mesure(*start_time) > long_press_time ){ *button_status = Continuous_Press; } }else{ if( Time_Mesure(*start_time) > long_press_time ){ *button_status = LongPress_Determined; ret_state = LongPress_Determined; } } break; case Continuous_Press: if( Time_Mesure(*start_time) > long_press_time ){ *start_time = millis(); //reset start_time return Continuous_Press; } break; default: break; } break; case HIGH: // HIGH = 1 if( *button_status == ON_start ){ if( Time_Mesure(*start_time) > chatter_time ){ ret_state = MomentPress_Determined; } } *button_status = Release; break; default: break; } return ret_state; } //****************************************** uint32_t Time_Mesure( uint32_t st_time ){ return millis() - st_time; }
Aボタンと B ボタンは2秒長押し対応で、Cボタンを持続押し設定としました。
今回は作っている最中にいろいろと新たな発見があり、とても勉強になりました。
まず、プログラムを見やすく、そして理解し易くするのにとっても有効な列挙型 ( enum ) というものを使ったことです。
これ、今までもたまに使っていましたが、このボタンスイッチプログラムを組む場合は最適で、とっても便利なんです。
ボタンの状態把握として列挙すると以下のようになります。
●ボタンリリース状態
●ボタン押されて時間計測始めた状態
●瞬時ON確定
●長押しON確定
●持続押し
この5種類の状態を判別する記号を作るのに、
int Release = 0;
int ON_start = 1;
...
や、
#define Release 0
#define ON_start 1
...
を使うこともできますが、このプログラムの場合、enum を使った方が遙かに使いやすく簡単です。
enum は宣言する時に、自動でその名前に 0から順に整数を割り当ててくれます。
switch 文と共にこの enum を使うと、分岐条件を自分の好きな名前にすることができて、プログラムがとても分かりやすくなります。
そして、enum のタグで関数の引数渡しや、ポインタ渡しもできるのです。
スゴイ便利ですよね。
今まで使ってこなかったのがアホらしくなりました。
ネットや Twitter の情報で、意味の無い整数の定義は enum を使った方が良いということは何となく知っていたのですが、このように自分で考えてプログラムを組んでみると、ホントにその通りでしたね。
ここでは enum について詳しくは述べませんが、Arduino IDE でも強力なアイテムになるので、今後はいろいろと使ってみたいと思います。
ということで、enum のおかげで、このソースコードを見れば説明不要と思いますので省略します。
今回はとっても勉強になりました。
といっても、まだまだ無駄が多かったり、使い方を誤ったりしているかもしれませんので、何かありましたらコメント投稿等でご連絡いただけると助かります。
コンパイル書き込み実行
では、最終的なプログラムを Arduino IDE で M5Stack にコンパイル書き込みしてみてください。
そして、シリアルモニターを 115200 bps で起動し、ボタンを押してみて下さい。
Aボタンと Bボタンは 2秒長押し対応で、Cボタンは持続押し対応です。
参考で以下の動画をご覧ください。
シリアルモニターにはこんな感じで表示されます。
3つのボタン同時押しでもちゃんと反応してくれるし、Cボタンを押したまま他のボタンを押してもちゃんと反応してくれるところがポイントです。
Cボタンを押しながら、Aボタンを長押ししても反応してくれます。
自分で作ってみて、なかなかイイ感じに仕上がったと思っています。
本当はダブルクリックも作ろうと思ったのですが、時間が無くて止めました。
でも、これを応用すれば、それほど難しくないと思います。
まとめ
どうでしょうか?
今回はボタンスイッチに特化していろいろ実験してみました。
チャタリングのハードウェア対策もいつかやってみたいと思っていますが、プログラミングだけで十分かなと思います。
今まではボタンプログラムは簡単だろうと思ってあえて作って来なかったのですが、ちゃんと作ろうと思うといろいろなノウハウがあるもんだなぁと実感しました。
SONY のPS4 や、任天堂の SWITCH などのゲーム機は、更に押し易く反応し易いようにいろいろと工夫しているのだろうと思いました。
これが出来るようになったので、TFT 液晶ディスプレイをボタンで動かしてみたいですね。
今回はここまでです。
ではまた・・・。
コメント
結構前の記事なのでもう解決済みかもしれませんが・・・。
ボタンの処理は遅延を最小限にするのに割り込み(ISR)を使われたほうが良いかと思います。
あと、どんなスイッチを使ってもほぼチャタリングは発生するので、バイナリーセマフォかミューテックスを使うのが常套手段かなと思います。
Lang-Shipさんが詳しくまとめられているので、参考になると思います。
https://lang-ship.com/blog/work/esp32-freertos-l06-semaphore-mutex/
PeakAliveさん
記事をご覧いただき、ありがとうございます。
チャタリングには割り込みとかバイナリセマフォとかミューテックスつかうのが常套手段というのは知らなかったです。
当時はFreeRTOSは全く無知でした。
しばらくプログラミングから離れておりますが、久々に勉強しなおそうかと思いました。
Lang-Shipさんは以前、大変お世話になったので、参考にさせて頂こうと思います。
情報ありがとうございました!!!
m(_ _)m